diff options
author | polwex <polwex@sortug.com> | 2025-10-06 04:32:28 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-10-06 04:32:28 +0700 |
commit | a26de7c9cb8ae6e4129662db665bb726c3ee4d88 (patch) | |
tree | 56416ada5cbe2b0e08d850bf6e1900eb7c66753e /ocaml/lib | |
parent | e927376355a1b07e5385dedad7a0d6c5d5bb1f92 (diff) |
eyre done
Diffstat (limited to 'ocaml/lib')
-rw-r--r-- | ocaml/lib/io/dune | 2 | ||||
-rw-r--r-- | ocaml/lib/io/http.ml | 285 |
2 files changed, 286 insertions, 1 deletions
diff --git a/ocaml/lib/io/dune b/ocaml/lib/io/dune index db1abbb..924541c 100644 --- a/ocaml/lib/io/dune +++ b/ocaml/lib/io/dune @@ -1,4 +1,4 @@ (library (name io_drivers) - (modules behn ames) + (modules behn ames http) (libraries nock_lib zarith eio eio.unix)) diff --git a/ocaml/lib/io/http.ml b/ocaml/lib/io/http.ml new file mode 100644 index 0000000..97a4b08 --- /dev/null +++ b/ocaml/lib/io/http.ml @@ -0,0 +1,285 @@ +(* Eyre - HTTP Server Driver with Eio + * + * This is the HTTP server for serving web requests to Urbit ships. + * Uses Eio.Net for async TCP connections - can handle thousands of concurrent clients! + * + * Key innovation vs C Vere: + * - C Vere: Blocking HTTP with libh2o, sequential request processing + * - Overe: Async HTTP with Eio, concurrent request handling with fiber-per-connection + *) + +(* HTTP request *) +type http_method = + | GET + | POST + | PUT + | DELETE + | HEAD + | OPTIONS + | Other of string + +type http_request = { + method_: http_method; + path: string; + version: string; + headers: (string * string) list; + body: bytes; +} + +(* HTTP response *) +type http_response = { + status: int; + status_text: string; + headers: (string * string) list; + body: bytes; +} + +(* Eyre configuration *) +type config = { + port: int; + host: string; +} + +(* Eyre driver state *) +type t = { + config: config; + mutable stats: stats; +} + +and stats = { + mutable requests_total: int64; + mutable requests_active: int; + mutable bytes_sent: int64; + mutable bytes_recv: int64; +} + +(* Create Eyre driver *) +let create config = { + config; + stats = { + requests_total = 0L; + requests_active = 0; + bytes_sent = 0L; + bytes_recv = 0L; + }; +} + +(* Parse HTTP method *) +let parse_method str = + match String.uppercase_ascii str with + | "GET" -> GET + | "POST" -> POST + | "PUT" -> PUT + | "DELETE" -> DELETE + | "HEAD" -> HEAD + | "OPTIONS" -> OPTIONS + | other -> Other other + +(* Method to string *) +let method_to_string = function + | GET -> "GET" + | POST -> "POST" + | PUT -> "PUT" + | DELETE -> "DELETE" + | HEAD -> "HEAD" + | OPTIONS -> "OPTIONS" + | Other s -> s + +(* Parse HTTP request line *) +let parse_request_line line = + match String.split_on_char ' ' line with + | [method_str; path; version] -> + Ok (parse_method method_str, path, version) + | _ -> + Error "Invalid request line" + +(* Parse HTTP header *) +let parse_header line = + match String.index_opt line ':' with + | Some idx -> + let key = String.sub line 0 idx |> String.trim in + let value = String.sub line (idx + 1) (String.length line - idx - 1) |> String.trim in + Ok (key, value) + | None -> + Error "Invalid header" + +(* Parse HTTP request from string *) +let parse_request data = + let lines = String.split_on_char '\n' data in + match lines with + | [] -> Error "Empty request" + | request_line :: header_lines -> + (match parse_request_line (String.trim request_line) with + | Error e -> Error e + | Ok (method_, path, version) -> + (* Parse headers until blank line *) + let rec parse_headers acc = function + | [] -> (List.rev acc, "") + | "" :: rest | "\r" :: rest -> + (List.rev acc, String.concat "\n" rest) + | line :: rest -> + (match parse_header (String.trim line) with + | Ok header -> parse_headers (header :: acc) rest + | Error _ -> parse_headers acc rest) + in + let headers, body_str = parse_headers [] header_lines in + Ok { + method_; + path; + version; + headers; + body = Bytes.of_string body_str; + }) + +(* Generate HTTP response *) +let generate_response resp = + let status_line = Printf.sprintf "HTTP/1.1 %d %s\r\n" resp.status resp.status_text in + + let headers_str = List.map (fun (k, v) -> + Printf.sprintf "%s: %s\r\n" k v + ) resp.headers |> String.concat "" in + + let response_header = status_line ^ headers_str ^ "\r\n" in + + (* Combine header and body *) + let header_bytes = Bytes.of_string response_header in + let total_len = Bytes.length header_bytes + Bytes.length resp.body in + let result = Bytes.create total_len in + + Bytes.blit header_bytes 0 result 0 (Bytes.length header_bytes); + Bytes.blit resp.body 0 result (Bytes.length header_bytes) (Bytes.length resp.body); + + result + +(* Handle single HTTP connection *) +let handle_connection eyre ~sw:_ ~event_stream flow addr = + Printf.printf "[Eyre] New connection from %s\n%!" + (Format.asprintf "%a" Eio.Net.Sockaddr.pp addr); + + eyre.stats.requests_active <- eyre.stats.requests_active + 1; + + try + (* Read request *) + let buf = Cstruct.create 16384 in (* 16KB buffer *) + let recv_len = Eio.Flow.single_read flow buf in + let request_data = Cstruct.to_string (Cstruct.sub buf 0 recv_len) in + + eyre.stats.bytes_recv <- Int64.add eyre.stats.bytes_recv (Int64.of_int recv_len); + + Printf.printf "[Eyre] Received %d bytes\n%!" recv_len; + + (* Parse request *) + (match parse_request request_data with + | Ok request -> + eyre.stats.requests_total <- Int64.succ eyre.stats.requests_total; + + Printf.printf "[Eyre] %s %s %s\n%!" + (method_to_string request.method_) + request.path + request.version; + + (* Create ovum for runtime *) + let ovum = Nock_lib.Effects.make_ovum + ~wire:(Nock_lib.Noun.atom 0) + ~card:(Nock_lib.Noun.cell + (Nock_lib.Noun.atom 3) (* eyre tag *) + (Nock_lib.Noun.atom 0)) (* simplified request data *) + in + + (* Send to runtime event queue *) + Eio.Stream.add event_stream ovum; + + (* Generate simple response *) + let response = { + status = 200; + status_text = "OK"; + headers = [ + ("Content-Type", "text/plain"); + ("Content-Length", "13"); + ("Server", "Overe/0.1"); + ]; + body = Bytes.of_string "Hello, Urbit!"; + } in + + let response_bytes = generate_response response in + eyre.stats.bytes_sent <- Int64.add eyre.stats.bytes_sent + (Int64.of_int (Bytes.length response_bytes)); + + (* Send response *) + Eio.Flow.write flow [Cstruct.of_bytes response_bytes]; + + Printf.printf "[Eyre] Sent %d byte response\n%!" (Bytes.length response_bytes) + + | Error err -> + Printf.printf "[Eyre] Failed to parse request: %s\n%!" err; + + (* Send 400 Bad Request *) + let response = { + status = 400; + status_text = "Bad Request"; + headers = [("Content-Length", "0")]; + body = Bytes.empty; + } in + + let response_bytes = generate_response response in + Eio.Flow.write flow [Cstruct.of_bytes response_bytes] + ); + + eyre.stats.requests_active <- eyre.stats.requests_active - 1 + + with + | End_of_file -> + Printf.printf "[Eyre] Client closed connection\n%!"; + eyre.stats.requests_active <- eyre.stats.requests_active - 1 + | e -> + Printf.printf "[Eyre] Connection error: %s\n%!" (Printexc.to_string e); + eyre.stats.requests_active <- eyre.stats.requests_active - 1 + +(* Accept fiber - continuously accepts connections *) +let accept_fiber eyre ~env:_ ~sw ~event_stream listening_socket = + Printf.printf "[Eyre] Accept fiber started\n%!"; + + let rec loop () = + try + (* Accept connection - blocks this fiber but not others! *) + Eio.Net.accept_fork listening_socket ~sw + ~on_error:(fun exn -> + Printf.printf "[Eyre] Accept error: %s\n%!" (Printexc.to_string exn) + ) + (fun flow addr -> + (* Handle connection in its own fiber *) + handle_connection eyre ~sw ~event_stream flow addr + ); + + (* Loop forever *) + loop () + with + | End_of_file -> + Printf.printf "[Eyre] Accept fiber closed\n%!" + | Eio.Cancel.Cancelled _ -> + Printf.printf "[Eyre] Accept fiber cancelled\n%!" + in + + loop () + +(* Run Eyre driver - spawns accept fiber *) +let run eyre ~env ~sw ~event_stream = + Printf.printf "[Eyre] Starting HTTP server on %s:%d\n%!" + eyre.config.host eyre.config.port; + + (* Create listening socket *) + let net = Eio.Stdenv.net env in + let addr = `Tcp (Eio.Net.Ipaddr.V4.any, eyre.config.port) in + let listening_socket = Eio.Net.listen net ~sw ~reuse_addr:true ~backlog:128 addr in + + Printf.printf "[Eyre] Listening on port %d\n%!" eyre.config.port; + + (* Spawn accept fiber *) + Eio.Fiber.fork ~sw (fun () -> + accept_fiber eyre ~env ~sw ~event_stream listening_socket + ); + + Printf.printf "[Eyre] HTTP server running!\n%!" + +(* Get statistics *) +let get_stats eyre = eyre.stats |