Skip to content

Commit

Permalink
feat: update http/1.1 compliance
Browse files Browse the repository at this point in the history
  • Loading branch information
leostera committed Jan 2, 2024
1 parent 46de860 commit 53b7080
Show file tree
Hide file tree
Showing 5 changed files with 173 additions and 543 deletions.
105 changes: 52 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,46 +21,46 @@ test-bed][bandit-tests], [h2spec][h2spec], and [Autobahn][autobahn]

### HTTP/1.1

- [ ] invalid requests
- [ ] returns a 400 if the request cannot be parsed
- [ ] returns a 400 if the request has an invalid http version
- [ ] keepalive requests
- [ ] closes connection after max_requests is reached
- [ ] idle keepalive connections are closed after read_timeout
- [ ] unread content length bodies are read before starting a new request
- [ ] unread chunked bodies are read before starting a new request
- [ ] origin-form request target (RFC9112§3.2.1)
- [ ] derives scheme from underlying transport
- [ ] derives host from host header
- [ ] returns 400 if no host header set in HTTP/1.1
- [ ] sets a blank host if no host header set in HTTP/1.0
- [ ] derives port from host header
- [ ] derives host from host header with ipv6 host
- [ ] derives host and port from host header with ipv6 host
- [x] invalid requests
- [x] returns a 400 if the request cannot be parsed
- [x] returns a 400 if the request has an invalid http version
- [x] keepalive requests
- [x] closes connection after max_requests is reached
- [x] idle keepalive connections are closed after read_timeout
- [x] unread content length bodies are read before starting a new request
- [x] unread chunked bodies are read before starting a new request
- [x] origin-form request target (RFC9112§3.2.1)
- [x] derives scheme from underlying transport
- [x] derives host from host header
- [x] returns 400 if no host header set in HTTP/1.1
- [x] sets a blank host if no host header set in HTTP/1.0
- [x] derives port from host header
- [x] derives host from host header with ipv6 host
- [x] derives host and port from host header with ipv6 host
- [ ] returns 400 if port cannot be parsed from host header
- [ ] derives port from schema default if no port specified in host header
- [ ] derives port from schema default if no host header set in HTTP/1.0
- [ ] sets path and query string properly when no query string is present
- [ ] sets path and query string properly when query string is present
- [ ] ignores fragment when no query string is present
- [ ] ignores fragment when query string is present
- [ ] handles query strings with question mark characters in them
- [ ] returns 400 if a non-absolute path is send
- [ ] returns 400 if path has no leading slash
- [ ] absolute-form request target (RFC9112§3.2.2)
- [ ] uses request-line scheme even if it does not match the transport
- [ ] derives host from the URI, even if it differs from host header
- [ ] derives ipv6 host from the URI, even if it differs from host header
- [ ] does not require a host header set in HTTP/1.1 (RFC9112§3.2.2)
- [ ] derives port from the URI, even if it differs from host header
- [ ] derives port from schema default if no port specified in the URI
- [ ] sets path and query string properly when no query string is present
- [ ] sets path and query string properly when query string is present
- [ ] ignores fragment when no query string is present
- [ ] ignores fragment when query string is present
- [ ] handles query strings with question mark characters in them
- [ ] authority-form request target (RFC9112§3.2.3)
- [ ] returns 400 for authority-form / CONNECT requests
- [x] derives port from schema default if no port specified in host header
- [x] derives port from schema default if no host header set in HTTP/1.0
- [x] sets path and query string properly when no query string is present
- [x] sets path and query string properly when query string is present
- [x] ignores fragment when no query string is present
- [x] ignores fragment when query string is present
- [x] handles query strings with question mark characters in them
- [x] returns 400 if a non-absolute path is send
- [x] returns 400 if path has no leading slash
- [x] absolute-form request target (RFC9112§3.2.2)
- [x] uses request-line scheme even if it does not match the transport
- [x] derives host from the URI, even if it differs from host header
- [x] derives ipv6 host from the URI, even if it differs from host header
- [x] does not require a host header set in HTTP/1.1 (RFC9112§3.2.2)
- [x] derives port from the URI, even if it differs from host header
- [x] derives port from schema default if no port specified in the URI
- [x] sets path and query string properly when no query string is present
- [x] sets path and query string properly when query string is present
- [x] ignores fragment when no query string is present
- [x] ignores fragment when query string is present
- [x] handles query strings with question mark characters in them
- [x] authority-form request target (RFC9112§3.2.3)
- [x] returns 400 for authority-form / CONNECT requests
- [ ] asterisk-form request target (RFC9112§3.2.4)
- [ ] parse global OPTIONS path correctly
- [ ] request line limits
Expand Down Expand Up @@ -96,8 +96,8 @@ test-bed][bandit-tests], [h2spec][h2spec], and [Autobahn][autobahn]
- [ ] writes out a response with a valid date header
- [ ] returns user-defined date header instead of internal version
- [ ] response body
- [ ] writes out a response with deflate encoding if so negotiated
- [ ] writes out a response with gzip encoding if so negotiated
- [x] writes out a response with deflate encoding if so negotiated
- [x] writes out a response with gzip encoding if so negotiated
- [ ] writes out a response with x-gzip encoding if so negotiated
- [x] uses the first matching encoding in accept-encoding
- [x] falls back to no encoding if no encodings provided
Expand All @@ -114,18 +114,17 @@ test-bed][bandit-tests], [h2spec][h2spec], and [Autobahn][autobahn]
- [x] writes out a response with zero content-length for 200 responses
- [x] writes out a response with zero content-length for 301 responses
- [x] writes out a response with zero content-length for 401 responses
- [ ] writes out a chunked response
- [ ] does not write out a body for a chunked response to a HEAD request
- [ ] writes out a chunked iolist response
- [ ] returns socket errors on chunk calls
- [ ] writes out a sent file for the entire file with content length
- [ ] writes out headers but not body for files requested via HEAD request
- [ ] does not write out a content-length header or body for files on a 204
- [ ] does not write out a content-length header or body for files on a 304
- [ ] writes out a sent file for parts of a file with content length
- [ ] sending informational responses
- [ ] does not send informational responses to HTTP/1.0 clients
- [ ] reading HTTP version
- [x] writes out a chunked response
- [x] does not write out a body for a chunked response to a HEAD request
- [x] returns socket errors on chunk calls
- [x] writes out a sent file for the entire file with content length
- [x] writes out headers but not body for files requested via HEAD request
- [x] does not write out a content-length header or body for files on a 204
- [x] does not write out a content-length header or body for files on a 304
- [x] writes out a sent file for parts of a file with content length
- [x] sending informational responses
- [x] does not send informational responses to HTTP/1.0 clients
- [x] reading HTTP version
- [ ] reading peer data

### HTTP/2
Expand Down
120 changes: 81 additions & 39 deletions nomad/adapter.ml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ let deflate_string str =
Buffer.contents r

let gzip_string str =
let time () = Int32.of_float (Unix.gettimeofday ()) in
let time () = 2112l in
let i = De.bigstring_create De.io_buffer_size in
let o = De.bigstring_create De.io_buffer_size in
let w = De.Lz77.make_window ~bits:15 in
let w = De.Lz77.make_window ~bits:16 in
let q = De.Queue.create 0x1000 in
let r = Buffer.create 0x1000 in
let p = ref 0 in
Expand Down Expand Up @@ -78,49 +78,91 @@ let maybe_compress (req : Request.t) buf =
|> Option.value ~default:[] |> List.map String.trim
in
let accepts_deflate = List.mem "deflate" accepted_encodings in
let accepts_gzip =
List.mem "gzip" accepted_encodings || List.mem "x-gzip" accepted_encodings
in
let accepts_gzip = List.mem "gzip" accepted_encodings in
let accepts_x_gzip = List.mem "x-gzip" accepted_encodings in
if accepts_deflate then (Some (deflate buf), Some "deflate")
else if accepts_gzip then (Some (gzip buf), Some "gzip")
else if accepts_x_gzip then (Some (gzip buf), Some "x-gzip")
else (Some buf, None))

let send conn (req : Request.t) (res : Response.t) =
let body, encoding =
if
has_custom_content_encoding res
|| has_strong_etag res || has_no_transform res
then (res.body, None)
else
match res.body with
| Some body -> maybe_compress req body
| None -> (None, None)
in
let headers =
match encoding with
| Some encoding -> Http.Header.add res.headers "content-encoding" encoding
| None -> res.headers
in
let headers = Http.Header.add headers "vary" "accept-encoding" in
if req.version = `HTTP_1_0 && res.status = `Continue then ()
else
let body, encoding =
if
has_custom_content_encoding res
|| has_strong_etag res || has_no_transform res
then (res.body, None)
else
match res.body with
| Some body -> maybe_compress req body
| None -> (None, None)
in
let headers =
match encoding with
| Some encoding -> Http.Header.add res.headers "content-encoding" encoding
| None -> res.headers
in
let headers = Http.Header.add headers "vary" "accept-encoding" in

let content_length =
Option.map IO.Buffer.filled body |> Option.value ~default:0
in
let headers =
if res.status = `No_content then
Http.Header.remove headers "content-length"
else if res.status != `Not_modified && content_length > 0 then
Http.Header.replace headers "content-length"
(Int.to_string content_length)
else headers
in
let body =
if
req.meth = `HEAD || res.status = `No_content
|| res.status = `Not_modified
then None
else body
in

let buf = Response.to_buffer { res with headers; body } in
Logger.debug (fun f -> f "res: %S" (IO.Buffer.to_string buf));
let _ = Atacama.Connection.send conn buf in
()

let content_length =
Option.map IO.Buffer.filled body |> Option.value ~default:0
let send_chunk conn (req : Request.t) buf =
if req.meth != `HEAD then (
let chunk =
Format.sprintf "%x\r\n%s\r\n" (IO.Buffer.filled buf)
(IO.Buffer.to_string buf)
in
Logger.debug (fun f -> f "sending chunk: %S" chunk);
let chunk = IO.Buffer.of_string chunk in
let _ = Atacama.Connection.send conn chunk in
())

let send_file conn (req : Request.t) (res : Response.t) ?off ?len ~path () =
let len =
match len with
| Some len -> len
| None ->
let stat = File.stat path in
stat.st_size
in
let headers =
if res.status != `No_content && res.status != `Not_modified && content_length > 0 then
Http.Header.replace headers "content-length" (Int.to_string content_length)
else headers
Http.Header.replace res.headers "content-length" (Int.to_string len)
in
let body = if req.meth = `HEAD || res.status = `No_content || res.status = `Not_modified then None else body in

let buf = Response.to_buffer { res with headers; body } in
Logger.debug (fun f -> f "res: %S" (IO.Buffer.to_string buf));
let _ = Atacama.Connection.send conn buf in
()

let send_chunk conn (_req : Request.t) buf =
let chunk = Format.sprintf "%d\r\n%s\r\n" (IO.Buffer.filled buf) (IO.Buffer.to_string buf) in
Logger.debug (fun f-> f "sending chunk: %S" chunk);
let chunk = IO.Buffer.of_string chunk in
let _ = Atacama.Connection.send conn chunk in
()
let res = { res with headers; body = None } in
let _ = send conn req res in
if res.status != `No_content then
let _ = Atacama.Connection.send_file conn ?off ~len (File.open_read path) in
()

let close conn (req: Request.t) (res: Response.t) =
(if req.meth = `HEAD then ()
else if res.status = `No_content then ()
else
let _ =
Atacama.Connection.send conn (IO.Buffer.of_string "0\r\n\r\n")
in
());

8 changes: 4 additions & 4 deletions nomad/http1.ml
Original file line number Diff line number Diff line change
Expand Up @@ -167,10 +167,12 @@ let make_uri state (req : Trail.Request.t) =
if Uri.port uri |> Option.value ~default:0 < 0 then raise_notrace Bad_port;

let path = Uri.path req.uri in
let query = Uri.query req.uri in
if not (String.starts_with ~prefix:"/" path) then
raise_notrace Path_missing_leading_slash;

let uri = Uri.with_path uri path in
let uri = Uri.with_query uri query in
Logger.error (fun f -> f "parse uri: %a" Uri.pp uri);
uri

Expand All @@ -194,9 +196,7 @@ let handle_request state conn req body =
| Handler.Close _conn when is_keep_alive ->
Logger.debug (fun f -> f "connection is keep alive, continuing");
Continue { state with sniffed_data = None }
| Handler.Close conn ->
let _ = Atacama.Connection.send conn (IO.Buffer.of_string "0\r\n\r\n") in
Close state
| Handler.Close _conn -> Close state
| Handler.Upgrade (`websocket (upgrade_opts, handler)) ->
let state = Ws.make ~upgrade_opts ~handler ~req ~conn () in
let state = Ws.handshake conn state in
Expand All @@ -218,7 +218,7 @@ let run_handler state conn req body =
handle_request state conn { req with uri } body
| `HTTP_1_1, Some _host, uri ->
handle_request state conn { req with uri } body
| `HTTP_1_0, None, uri -> handle_request state conn { req with uri } body
| `HTTP_1_0, _, uri -> handle_request state conn { req with uri } body
| _ -> bad_request conn state

let handle_data data conn state =
Expand Down
Loading

0 comments on commit 53b7080

Please sign in to comment.