From 1866c3e048d6bd3d62e90bf6518840d57aeb5b4a Mon Sep 17 00:00:00 2001 From: Laytan Laats Date: Mon, 6 May 2024 15:06:04 +0200 Subject: [PATCH] wip --- client/client.odin | 15 +++++---- client/dns/dns.odin | 50 ++++++++++++++++++++++------ client/test_client.odin | 72 +++++++++++++++++++++++++---------------- responses.odin | 1 + 4 files changed, 95 insertions(+), 43 deletions(-) diff --git a/client/client.odin b/client/client.odin index 1d1985d2..919e09c0 100644 --- a/client/client.odin +++ b/client/client.odin @@ -66,6 +66,9 @@ Connection_State :: enum { Failed, } +// TODO: should response `Set-Cookie` headers automatically be stored on the connection and passed +// along next requests? + Connection :: struct { allocator: mem.Allocator, @@ -106,9 +109,6 @@ Response :: struct { status: http.Status, cookies: [dynamic]http.Cookie, body: http.Body, - - // Below is for internal use only: - using _: http.Has_Body, } @@ -181,8 +181,8 @@ connection_init :: proc(conn: ^Connection, client: ^Client, target: string, sche return true } -connection_make :: proc(client: ^Client, host: string, scheme := Scheme.From_Target, allocator := context.allocator) -> (conn: Connection) { - connection_init(&conn, client, host, scheme, allocator) +connection_make :: proc(client: ^Client, target: string, scheme := Scheme.From_Target, allocator := context.allocator) -> (conn: Connection) { + connection_init(&conn, client, target, scheme, allocator) return } @@ -395,9 +395,10 @@ callback_and_process_next :: proc(r: ^Request, err: net.Network_Error) { log.infof("request done: %v %v", r.res.status, err) + r.on_response(r, r.user_data, err) + r.conn.request = r.next connection_process(r.conn) - r.on_response(r, r.user_data, err) } @(private="file") @@ -719,7 +720,7 @@ prepare_request :: proc(r: ^Request) { // Escape newlines in headers, if we don't, an attacker can find an endpoint // that returns a header with user input, and inject headers into the response. esc_value, was_allocation := strings.replace_all(value, "\n", "\\n", r.allocator) - defer if was_allocation do delete(esc_value) + defer if was_allocation do delete(esc_value, r.allocator) ws(&r.buf, esc_value) ws(&r.buf, "\r\n") diff --git a/client/dns/dns.odin b/client/dns/dns.odin index d701d361..e2799167 100644 --- a/client/dns/dns.odin +++ b/client/dns/dns.odin @@ -23,8 +23,11 @@ Client :: struct { io: ^nbio.IO, // Hosts/Name servers configuration. - using config: net.DNS_Configuration, + name_servers: []net.Endpoint, hosts: []net.DNS_Host_Entry, + // init_cb: proc(^Client, rawptr), + // init_ud: rawptr, + // init_state: int, // Cache. cache: map[string]Cache_Entry, @@ -50,7 +53,7 @@ Callback :: struct { ud: rawptr, } -// TODO: provide a callback, OR allow `resolve` before these things are done with some sort of request queue. +// TODO: callback. init :: proc(c: ^Client, allocator := context.allocator) { c.allocator = allocator c.cache.allocator = allocator @@ -59,6 +62,33 @@ init :: proc(c: ^Client, allocator := context.allocator) { load_hosts(c) } +// Waits until all requests are done and frees all related resources. +destroy :: proc { + destroy_cb, + destroy_no_cb, +} + +destroy_no_cb :: proc(c: ^Client) { + destroy_cb(c, nil, proc(_: rawptr) {}) +} + +destroy_cb :: proc(c: ^Client, user: rawptr, cb: proc(user: rawptr)) { + cache_clear(c) + + // Try to clear again next tick, we don't want to interrupt in progress requests. + if len(c.cache) > 0 { + nbio.next_tick(c.io, c, user, cb, destroy_cb) + } else { + delete(c.cache) + delete(c.name_servers, c.allocator) + for h in c.hosts { + delete(h.name, c.allocator) + } + delete(c.hosts, c.allocator) + cb(user) + } +} + // Removes any cache entries that aren't currently being resolved. cache_clear :: proc(c: ^Client) { for hostname, entry in c.cache { @@ -358,14 +388,15 @@ resolve :: proc(c: ^Client, hostname: string, user: rawptr, cb: On_Resolve) { // Loads the name servers from the OS, this is called implicitly during `init`. @(private) load_name_servers :: proc(c: ^Client) { - if c.resolv_conf == "" { - log.error("empty resolv_conf file path") + resolv_conf := net.DEFAULT_DNS_CONFIGURATION.resolv_conf + if resolv_conf == "" { + log.error("empty resolv_conf file path") // TODO: this is not an error on Windows. return } - fd, err := nbio.open(c.io, c.resolv_conf) + fd, err := nbio.open(c.io, resolv_conf) if err != os.ERROR_NONE { - log.errorf("error opening %q: %v", c.resolv_conf, err) + log.errorf("error opening %q: %v", resolv_conf, err) return } @@ -388,14 +419,15 @@ load_name_servers :: proc(c: ^Client) { // Loads the hosts file from the OS, this is implicitly called during `init`. @(private) load_hosts :: proc(c: ^Client) { - if c.hosts_file == "" { + hosts_file := net.DEFAULT_DNS_CONFIGURATION.hosts_file + if hosts_file == "" { log.error("empty hosts file") return } - fd, err := nbio.open(c.io, c.hosts_file) + fd, err := nbio.open(c.io, hosts_file) if err != os.ERROR_NONE { - log.errorf("error opening %q: %v", c.hosts_file, err) + log.errorf("error opening %q: %v", hosts_file, err) return } diff --git a/client/test_client.odin b/client/test_client.odin index 650513be..c0133cc1 100644 --- a/client/test_client.odin +++ b/client/test_client.odin @@ -19,35 +19,53 @@ test_client :: proc(t: ^testing.T) { nbio.init(&io) defer nbio.destroy(&io) - client: dns.Client - client.io = &io - client.config = net.dns_configuration // TODO: make default if not set. - dns.init(&client) + // client: dns.Client + // client.io = &io + // client.config = net.dns_configuration // TODO: make default if not set. + // dns.init(&client) + // + // nbio.timeout(client.io, time.Second, &client, proc(client: ^dns.Client, _: Maybe(time.Time)) { + // log.info("Resolving") + // + // dns.resolve(client, "github.com", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("github.com", recs, err) + // }) + // dns.resolve(client, "laytanlaats.com", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("laytanlaats.com", recs, err) + // }) + // dns.resolve(client, "laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("laytan.dev", recs, err) + // }) + // dns.resolve(client, "laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("laytan.dev", recs, err) + // }) + // dns.resolve(client, "odin-http.laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("odin-http.laytan.dev", recs, err) + // }) + // dns.resolve(client, "odin-http.laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("odin-http.laytan.dev", recs, err) + // }) + // dns.resolve(client, "github.com", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { + // log.info("github.com", recs, err) + // }) + // }) + + client := client_make(&io) + conn := connection_make(&client, "https://github.com") - nbio.timeout(client.io, time.Second, &client, proc(client: ^dns.Client, _: Maybe(time.Time)) { - log.info("Resolving") + req := request_make(&conn, "/laytan") + request(&req, nil, proc(r: ^Request, _: rawptr, err: net.Network_Error) { + log.infof("%v %v %#v %#v", r.res.status, err, r.res.headers._kv, r.res.cookies) + }) + + req = request_make(&conn, "/laytan/odin-http") + request(&req, nil, proc(r: ^Request, _: rawptr, err: net.Network_Error) { + log.infof("%v %v %#v %#v", r.res.status, err, r.res.headers._kv, r.res.cookies) + }) - dns.resolve(client, "github.com", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("github.com", recs, err) - }) - dns.resolve(client, "laytanlaats.com", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("laytanlaats.com", recs, err) - }) - dns.resolve(client, "laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("laytan.dev", recs, err) - }) - dns.resolve(client, "laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("laytan.dev", recs, err) - }) - dns.resolve(client, "odin-http.laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("odin-http.laytan.dev", recs, err) - }) - dns.resolve(client, "odin-http.laytan.dev", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("odin-http.laytan.dev", recs, err) - }) - dns.resolve(client, "github.com", nil, proc(_: rawptr, recs: dns.Record, err: net.Network_Error) { - log.info("github.com", recs, err) - }) + req = request_make(&conn, "/laytan/Odin") + request(&req, nil, proc(r: ^Request, _: rawptr, err: net.Network_Error) { + log.infof("%v %v %#v %#v", r.res.status, err, r.res.headers._kv, r.res.cookies) }) // log.info(net.resolve("github.com")) diff --git a/responses.odin b/responses.odin index 9b2ac2a6..aa2cb1d8 100644 --- a/responses.odin +++ b/responses.odin @@ -42,6 +42,7 @@ If any other error occurs, a 500 is sent and the error is logged. */ respond_file :: proc(r: ^Response, path: string, content_type: Maybe(Mime_Type) = nil, loc := #caller_location) { // PERF: we are still putting the content into the body buffer, we could stream it. + // PERF: could even use `splice`/`sendfile` on Linux https://tinselcity.github.io/Sendfile-W-Iouring/ assert_has_td(loc) assert(!r.sent, "response has already been sent", loc)