diff --git a/src/ewe.gleam b/src/ewe.gleam index c252aa8..f476c91 100644 --- a/src/ewe.gleam +++ b/src/ewe.gleam @@ -1,7 +1,8 @@ import ewe/internal/connection import ewe/internal/file import ewe/internal/handler as handler_ -import ewe/internal/http1 +import ewe/internal/http1/body as http1_body +import ewe/internal/http1/encoder import gleam/bytes_tree import gleam/erlang/process import gleam/http @@ -405,10 +406,10 @@ pub type BodyError { InvalidBody } -fn from_internal_http1_body_error(error: http1.BodyError) -> BodyError { +fn from_internal_http1_body_error(error: http1_body.BodyError) -> BodyError { case error { - http1.BodyTooLarge -> BodyTooLarge - http1.InvalidBody -> InvalidBody + http1_body.BodyTooLarge -> BodyTooLarge + http1_body.InvalidBody -> InvalidBody } } @@ -422,7 +423,7 @@ pub fn read_body( case req.body { connection.Http1(connection) -> { use #(body, trailers) <- result.try( - http1.read_body(connection, limit) + http1_body.read_body(connection, limit) |> result.map_error(from_internal_http1_body_error), ) @@ -452,12 +453,12 @@ pub fn read_body_chunk( ) -> Result(ReadEvent, BodyError) { case req.body { connection.Http1(connection) -> { - case http1.read_body_chunk(connection, max_chunk_bytes:, limit:) { - Ok(http1.Chunk(data, connection)) -> { + case http1_body.read_body_chunk(connection, max_chunk_bytes:, limit:) { + Ok(http1_body.Chunk(data, connection)) -> { let body = connection.Http1(connection) Ok(Chunk(data, request.set_body(req, body))) } - Ok(http1.Done(trailers)) -> { + Ok(http1_body.Done(trailers)) -> { let headers = list.append(req.headers, trailers) Ok(Done(request.Request(..req, headers:, body: Nil))) } @@ -488,7 +489,7 @@ pub fn stream_response( pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { case writer { connection.Http1Writer(writer) -> - connection.Http1Writer(http1.send_chunk(writer, chunk)) + connection.Http1Writer(encoder.send_chunk(writer, chunk)) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } @@ -496,7 +497,7 @@ pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { /// Sends `chunk` as the final response body chunk and closes the stream. pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { case writer { - connection.Http1Writer(writer) -> http1.finish_chunk(writer, chunk) + connection.Http1Writer(writer) -> encoder.finish_chunk(writer, chunk) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } @@ -505,7 +506,7 @@ pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { /// there's one last chunk to send. pub fn finish_response(writer: ResponseWriter) -> Nil { case writer { - connection.Http1Writer(writer) -> http1.finish_response(writer) + connection.Http1Writer(writer) -> encoder.finish_response(writer) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } diff --git a/src/ewe/internal/connection.gleam b/src/ewe/internal/connection.gleam index f14f723..fbb9342 100644 --- a/src/ewe/internal/connection.gleam +++ b/src/ewe/internal/connection.gleam @@ -1,31 +1,15 @@ +import ewe/internal/http1/connection as http1 import gleam/bytes_tree import gleam/erlang/process -import glisten/socket -import glisten/transport +import gleam/option +import glisten +import glisten/internal/handler pub type Connection { - Http1(Http1Connection) + Http1(http1.Connection) Http2 } -pub type Http1Connection { - Http1Connection( - transport: transport.Transport, - socket: socket.Socket, - self: process.Subject(Http1Signal), - buffer: BitArray, - framing: Framing, - read: Int, - chunk_remaining: Int, - ) -} - -pub type Framing { - Fixed(length: Int) - Chunked - NoBody -} - pub type Body { Bytes(bytes_tree.BytesTree) Text(String) @@ -47,30 +31,14 @@ pub type File { FileMetadata(path: String, offset: Int, length: Int) } -pub type Message { - Timeout -} - -pub type Http1Signal { - BodyDrained(leftover: BitArray) - BodyAbandoned - BodyProgress(buffer: BitArray, read: Int, chunk_remaining: Int) - StreamFinished(keep_alive: Bool) -} - pub type ResponseWriter { - Http1Writer(Http1ResponseWriter) + Http1Writer(http1.ResponseWriter) Http2Writer } -pub type Http1ResponseWriter { - Http1ResponseWriter( - transport: transport.Transport, - socket: socket.Socket, - self: process.Subject(Http1Signal), - chunked: Bool, - keep_alive: Bool, - ) +pub type SseConnection { + Http1Sse(http1.SseConnection) + Http2Sse } pub type Outcome { @@ -78,11 +46,25 @@ pub type Outcome { StoppedAbnormal(reason: String) } -pub type SseConnection { - Http1Sse(Http1SseConnection) - Http2Sse +pub type Message { + Timeout +} + +pub const idle_timeout = 10_000 + +pub fn start_idle_timer( + connection: glisten.Connection(Message), +) -> option.Option(process.Timer) { + process.send_after(connection.subject, idle_timeout, handler.User(Timeout)) + |> option.Some } -pub type Http1SseConnection { - Http1SseConnection(transport: transport.Transport, socket: socket.Socket) +pub fn cancel_idle_timer(timer: option.Option(process.Timer)) -> Nil { + case timer { + option.Some(timer) -> { + let _cancelled = process.cancel_timer(timer) + Nil + } + option.None -> Nil + } } diff --git a/src/ewe/internal/handler.gleam b/src/ewe/internal/handler.gleam index 5caa0d9..01ddc52 100644 --- a/src/ewe/internal/handler.gleam +++ b/src/ewe/internal/handler.gleam @@ -6,22 +6,16 @@ import gleam/http/request import gleam/http/response import gleam/option import glisten -import glisten/internal/handler import logging +/// The connection's protocol, still undecided until the HTTP/2 preface has been +/// ruled in or out. pub type State { - Initialised( - handler: fn(request.Request(connection.Connection)) -> - response.Response(connection.Body), - buffer: BitArray, - idle_timer: option.Option(process.Timer), - ) + Initialised(http1.State) Http1(http1.State) Http2 } -pub const idle_timeout = 10_000 - pub fn on_init( handler: fn(request.Request(connection.Connection)) -> response.Response(connection.Body), @@ -30,17 +24,14 @@ pub fn on_init( State, option.Option(process.Selector(connection.Message)), ) { - let timer = - process.send_after( - connection.subject, - idle_timeout, - handler.User(connection.Timeout), + let state = + http1.State( + handler:, + buffer: <<>>, + idle_timer: connection.start_idle_timer(connection), ) - #( - Initialised(handler:, buffer: <<>>, idle_timer: option.Some(timer)), - option.None, - ) + #(Initialised(state), option.None) } } @@ -50,58 +41,48 @@ pub fn loop( connection: glisten.Connection(connection.Message), ) -> glisten.Next(State, glisten.Message(connection.Message)) { case state, message { - Initialised(handler:, buffer:, idle_timer:), glisten.Packet(data) -> { - case idle_timer { - option.Some(timer) -> process.cancel_timer(timer) - option.None -> process.TimerNotFound - } + Initialised(state), glisten.Packet(data) -> { + connection.cancel_idle_timer(state.idle_timer) + let buffer = <> - let buffer = <> case sniff_preface(buffer) { - NeedMoreData -> { - let timer = - process.send_after( - connection.subject, - idle_timeout, - handler.User(connection.Timeout), - ) - - Initialised(handler:, buffer:, idle_timer: option.Some(timer)) + NeedMoreData -> + http1.State( + ..state, + buffer:, + idle_timer: connection.start_idle_timer(connection), + ) + |> Initialised |> glisten.continue - } Http2Preface(_remaining) -> glisten.continue(Http2) - NotHttp2(buffer:) -> { - let next = - http1.State(handler:, buffer:, idle_timer: option.None) - |> http1.handle_message(connection) - - case next { - http1.Continue(state) -> glisten.continue(Http1(state)) - http1.Close -> glisten.stop() - http1.CloseAbnormal(reason:) -> glisten.stop_abnormal(reason) - } - } + NotHttp2(buffer:) -> + http1.State(..state, buffer:, idle_timer: option.None) + |> http1.handle_message(connection) + |> to_glisten_next } } - Http1(state), glisten.Packet(data) -> { - let next = - http1.State(..state, buffer: <>) - |> http1.handle_message(connection) - - case next { - http1.Continue(state) -> glisten.continue(Http1(state)) - http1.Close -> glisten.stop() - http1.CloseAbnormal(reason:) -> glisten.stop_abnormal(reason) - } - } - Http2(..), glisten.Packet(_) -> todo as "HTTP/2 is not implemented yet!" - _, glisten.User(connection.Timeout) -> { + Http1(state), glisten.Packet(data) -> + http1.State(..state, buffer: <>) + |> http1.handle_message(connection) + |> to_glisten_next + Http2(..), glisten.Packet(_data) -> todo as "HTTP/2 is not implemented yet!" + _state, glisten.User(connection.Timeout) -> { logging.log(logging.Debug, "Connection idled for too long, closing.") glisten.stop() } } } +fn to_glisten_next( + next: http1.Next, +) -> glisten.Next(State, glisten.Message(connection.Message)) { + case next { + http1.Continue(state) -> glisten.continue(Http1(state)) + http1.Close -> glisten.stop() + http1.CloseAbnormal(reason:) -> glisten.stop_abnormal(reason) + } +} + pub type Sniff { NeedMoreData Http2Preface(remaining: BitArray) diff --git a/src/ewe/internal/http1.gleam b/src/ewe/internal/http1.gleam index 6feb660..eb12433 100644 --- a/src/ewe/internal/http1.gleam +++ b/src/ewe/internal/http1.gleam @@ -1,19 +1,17 @@ -import ewe/internal/clock import ewe/internal/connection import ewe/internal/file -import gleam/bit_array +import ewe/internal/http1/body +import ewe/internal/http1/connection as http1 +import ewe/internal/http1/encoder +import ewe/internal/http1/parser import gleam/bytes_tree import gleam/erlang/process import gleam/http import gleam/http/request import gleam/http/response -import gleam/int -import gleam/list import gleam/option import gleam/result -import gleam/string import glisten -import glisten/internal/handler import glisten/socket import glisten/transport import logging @@ -27,8 +25,6 @@ pub type State { ) } -pub const idle_timeout = 10_000 - pub type Next { Continue(State) Close @@ -36,7 +32,8 @@ pub type Next { } type Sent { - Sent(keep_alive: Bool) + SentKeepAlive + SentClose SentAbnormal(reason: String) } @@ -44,22 +41,14 @@ pub fn handle_message( state: State, connection: glisten.Connection(connection.Message), ) -> Next { - case state.idle_timer { - option.Some(timer) -> process.cancel_timer(timer) - option.None -> process.TimerNotFound - } - - case parse(state.buffer) { - Ok(Complete(head, metadata, remaining)) -> { - let scheme = case connection.transport { - transport.Tcp -> http.Http - transport.Ssl -> http.Https - } + connection.cancel_idle_timer(state.idle_timer) + case parser.parse(state.buffer) { + Ok(parser.Complete(head, metadata, remaining)) -> { let self = process.new_subject() let body_connection = - connection.Http1Connection( + http1.Connection( transport: connection.transport, socket: connection.socket, self:, @@ -69,142 +58,53 @@ pub fn handle_message( chunk_remaining: 0, ) - let request = - request.Request( - method: head.method, - headers: head.headers, - body: connection.Http1(body_connection), - scheme:, - host: head.host, - port: head.port, - path: head.path, - query: head.query, - ) - - let response = state.handler(request) + let response = + state.handler(to_request(head, connection, body_connection)) let drained = drain_messages(self) - - let #(buffer, body_drained) = resolve_body(body_connection, drained.body) - - let metadata = - Metadata(..metadata, keep_alive: metadata.keep_alive && body_drained) + let ResolvedBody(buffer, body_keep_alive) = + resolve_body(body_connection, drained.body) + let keep_alive = + http1.and_keep_alive(metadata.keep_alive, body_keep_alive) let sent = case - encode_response(response, head.method, head.version, metadata) + encoder.encode_response(response, head.method, head.version, keep_alive) { - Ok(Encoded(bytes:, keep_alive:, remainder:)) -> { - use Nil <- result.try(transport.send( - connection.transport, - connection.socket, - bytes, - )) - - case remainder { - NoRemainder -> Ok(Sent(keep_alive)) - RemainderFile(data) -> - case file.send(connection.transport, connection.socket, data) { - Ok(Nil) -> Ok(Sent(keep_alive)) - Error(reason) -> Error(reason) - } - RemainderStream(Stream(handler: stream_handler, chunked:)) -> { - connection.Http1Writer(connection.Http1ResponseWriter( - transport: connection.transport, - socket: connection.socket, - self:, - chunked:, - keep_alive:, - )) - |> stream_handler - - let stream_drained = drain_messages(self) - let stream_keep_alive = case stream_drained.stream { - option.Some(connection.StreamFinished(keep_alive:)) -> - keep_alive - option.None -> { - case chunked { - True -> { - let _ = - transport.send( - connection.transport, - connection.socket, - bytes_tree.from_bit_array(<<"0\r\n\r\n":utf8>>), - ) - Nil - } - False -> Nil - } - False - } - option.Some(connection.BodyDrained(..)) - | option.Some(connection.BodyAbandoned) - | option.Some(connection.BodyProgress(..)) -> - panic as "drain_messages stores body messages in the other field" - } - - Ok(Sent(keep_alive && stream_keep_alive)) - } - RemainderSse(handler: sse_handler) -> { - let sse_connection = - connection.Http1Sse(connection.Http1SseConnection( - transport: connection.transport, - socket: connection.socket, - )) - - case sse_handler(sse_connection) { - connection.Stopped -> Ok(Sent(False)) - connection.StoppedAbnormal(reason:) -> Ok(SentAbnormal(reason)) - } - } - } - } - Error(UnsafeHeader(name)) -> { + Ok(encoded) -> + send_response(encoded, connection.transport, connection.socket, self) + Error(encoder.UnsafeHeader(name)) -> { logging.log( logging.Error, "Handler produced an unsafe response header: " <> name, ) - use Nil <- result.try(transport.send( + transport.send( connection.transport, connection.socket, - internal_server_error(), - )) - - Ok(Sent(False)) + encoder.internal_server_error(), + ) + |> result.replace(SentClose) } } case sent { - Ok(Sent(True)) -> { - let timer = - process.send_after( - connection.subject, - idle_timeout, - handler.User(connection.Timeout), - ) - - State(..state, buffer:, idle_timer: option.Some(timer)) - |> Continue + Ok(SentKeepAlive) -> { + let idle_timer = connection.start_idle_timer(connection) + Continue(State(..state, buffer:, idle_timer:)) } - Ok(Sent(False)) -> Close + Ok(SentClose) -> Close Ok(SentAbnormal(reason)) -> CloseAbnormal(reason) Error(_reason) -> Close } } - Ok(Incomplete) -> { - let timer = - process.send_after( - connection.subject, - idle_timeout, - handler.User(connection.Timeout), - ) - - Continue(State(..state, idle_timer: option.Some(timer))) - } + Ok(parser.Incomplete) -> + State(..state, idle_timer: connection.start_idle_timer(connection)) + |> Continue Error(error) -> { logging.log( logging.Error, - "Failed to parse HTTP/1.x request: " <> error_to_string(error), + "Failed to parser.parse HTTP/1.x request: " + <> parser.error_to_string(error), ) Close @@ -212,1398 +112,164 @@ pub fn handle_message( } } -pub type BodyError { - BodyTooLarge - InvalidBody -} - -pub type Connection = - connection.Http1Connection - -const body_read_timeout = 10_000 - -const auto_drain_limit = 1_048_576 - -pub fn read_body( - conn: Connection, - limit: Int, -) -> Result(#(BitArray, List(#(String, String))), BodyError) { - let connection.Http1Connection( - transport:, - socket:, - self:, - buffer:, - framing:, - .., - ) = conn - - case framing, consume_body(transport, socket, buffer, framing, limit) { - _framing, Ok(#(body, trailers, leftover)) -> { - process.send(self, connection.BodyDrained(leftover:)) - Ok(#(body, trailers)) - } - connection.Fixed(_length), Error(BodyTooLarge) -> Error(BodyTooLarge) - _framing, Error(error) -> { - process.send(self, connection.BodyAbandoned) - Error(error) - } - } -} - -pub type ChunkRead { - Chunk(data: BitArray, connection: Connection) - Done(trailers: List(#(String, String))) -} - -pub fn read_body_chunk( - conn: Connection, - max_chunk_bytes max_chunk_bytes: Int, - limit limit: Int, -) -> Result(ChunkRead, BodyError) { - let connection.Http1Connection(self:, buffer:, read:, chunk_remaining:, ..) = - conn - - case pull_chunk(conn, max_chunk_bytes, limit) { - Ok(PulledChunk(data, next)) -> { - connection.BodyProgress(buffer:, read:, chunk_remaining:) - |> process.send(self, _) - - Ok(Chunk(data, next)) - } - Ok(PulledDone(trailers, leftover)) -> { - process.send(self, connection.BodyDrained(leftover:)) - - Ok(Done(trailers)) - } - Error(error) -> { - process.send(self, connection.BodyAbandoned) - - Error(error) - } - } -} - -const auto_drain_chunk_bytes = 65_536 - -fn resolve_body( - conn: Connection, - drained: option.Option(connection.Http1Signal), -) -> #(BitArray, Bool) { - case drained { - option.Some(connection.BodyDrained(leftover)) -> #(leftover, True) - option.Some(connection.BodyAbandoned) -> #(<<>>, False) - option.Some(connection.BodyProgress(buffer:, read:, chunk_remaining:)) -> - connection.Http1Connection(..conn, buffer:, read:, chunk_remaining:) - |> drain_remaining - option.None -> drain_remaining(conn) - option.Some(connection.StreamFinished(..)) -> - panic as "drain_messages stores stream messages in the other field" - } -} - -type Drained { - Drained( - body: option.Option(connection.Http1Signal), - stream: option.Option(connection.Http1Signal), +fn to_request( + head: parser.Head, + connection: glisten.Connection(connection.Message), + body: http1.Connection, +) -> request.Request(connection.Connection) { + let scheme = case connection.transport { + transport.Tcp -> http.Http + transport.Ssl -> http.Https + } + + request.Request( + method: head.method, + headers: head.headers, + body: connection.Http1(body), + scheme:, + host: head.host, + port: head.port, + path: head.path, + query: head.query, ) } -fn drain_messages(self: process.Subject(connection.Http1Signal)) -> Drained { - do_drain_messages(self, Drained(body: option.None, stream: option.None)) -} - -fn do_drain_messages( - self: process.Subject(connection.Http1Signal), - acc: Drained, -) -> Drained { - case process.receive(self, 0) { - Ok(connection.StreamFinished(..) as message) -> - do_drain_messages(self, Drained(..acc, stream: option.Some(message))) - Ok(message) -> - do_drain_messages(self, Drained(..acc, body: option.Some(message))) - Error(Nil) -> acc - } -} - -fn drain_remaining(conn: Connection) -> #(BitArray, Bool) { - let connection.Http1Connection(read:, ..) = conn - do_drain_remaining(conn, read + auto_drain_limit) -} - -fn do_drain_remaining(conn: Connection, limit: Int) -> #(BitArray, Bool) { - case pull_chunk(conn, auto_drain_chunk_bytes, limit) { - Ok(PulledChunk(_data, next)) -> do_drain_remaining(next, limit) - Ok(PulledDone(_trailers, leftover)) -> #(leftover, True) - Error(_reason) -> #(<<>>, False) - } -} - -fn consume_body( - transport: transport.Transport, - socket: socket.Socket, - buffer: BitArray, - framing: connection.Framing, - limit: Int, -) -> Result(#(BitArray, List(#(String, String)), BitArray), BodyError) { - case framing { - connection.NoBody -> Ok(#(<<>>, [], buffer)) - connection.Fixed(length) if length > limit -> Error(BodyTooLarge) - connection.Fixed(length) -> - read_fixed(transport, socket, buffer, length) |> to_body_result - connection.Chunked -> - read_chunked(transport, socket, buffer, limit, bytes_tree.new(), 0) - |> to_body_result - } -} - -fn to_body_result(result: Result(a, ParseError)) -> Result(a, BodyError) { - case result { - Ok(value) -> Ok(value) - Error(ChunkTooLarge) -> Error(BodyTooLarge) - Error(_other) -> Error(InvalidBody) - } -} - -fn read_fixed( - transport: transport.Transport, - socket: socket.Socket, - buffer: BitArray, - length: Int, -) -> Result(#(BitArray, List(#(String, String)), BitArray), ParseError) { - case buffer { - <> -> Ok(#(body, [], leftover)) - _ -> { - case - transport.receive_timeout( - transport, - socket, - length - bit_array.byte_size(buffer), - body_read_timeout, - ) - { - Ok(more) -> Ok(#(<>, [], <<>>)) - Error(_reason) -> Error(BodyReadFailed) - } - } - } -} - -fn read_chunked( +fn send_response( + encoded: encoder.Encoded, transport: transport.Transport, socket: socket.Socket, - buffer: BitArray, - limit: Int, - acc: bytes_tree.BytesTree, - total: Int, -) -> Result(#(BitArray, List(#(String, String)), BitArray), ParseError) { - use #(size, remaining) <- result.try(pull_until( - transport, - socket, - buffer, - parse_chunk_line, - )) - - case size { - 0 -> { - use #(trailers, _state, remaining) <- result.try({ - use buffer <- pull_until(transport, socket, remaining) - parse_headers(buffer, [], 0, initial_header_state()) - }) - - Ok(#(bytes_tree.to_bit_array(acc), trailers, remaining)) - } - size -> { - let total = total + size - case total > limit { - True -> Error(ChunkTooLarge) - False -> { - use #(data, remaining) <- result.try({ - use buffer <- pull_until(transport, socket, remaining) - take_chunk_prefix(buffer, size, True) - }) - - read_chunked( - transport, - socket, - remaining, - limit, - bytes_tree.append(acc, data), - total, - ) - } - } + self: process.Subject(http1.Signal), +) -> Result(Sent, socket.SocketReason) { + let encoder.Encoded(head:, keep_alive:, remainder:) = encoded + + case remainder { + encoder.NoRemainder -> { + use Nil <- result.try(transport.send(transport, socket, head)) + Ok(to_sent(keep_alive)) } - } -} - -type Pulled { - PulledChunk(data: BitArray, connection: Connection) - PulledDone(trailers: List(#(String, String)), leftover: BitArray) -} - -fn pull_chunk( - conn: Connection, - max_chunk_bytes: Int, - limit: Int, -) -> Result(Pulled, BodyError) { - let connection.Http1Connection(buffer:, framing:, read:, chunk_remaining:, ..) = - conn - - case framing { - connection.NoBody -> Ok(PulledDone([], buffer)) - connection.Fixed(length) if length > limit -> Error(BodyTooLarge) - connection.Fixed(length) -> - pull_fixed_chunk(conn, length, read, max_chunk_bytes) |> to_body_result - connection.Chunked -> - pull_chunked_chunk(conn, limit, read, chunk_remaining, max_chunk_bytes) - |> to_body_result - } -} - -fn pull_fixed_chunk( - conn: Connection, - length: Int, - read: Int, - max_chunk_bytes: Int, -) -> Result(Pulled, ParseError) { - let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn - - case length - read { - 0 -> Ok(PulledDone([], buffer)) - remaining -> { - let want = int.min(remaining, max_chunk_bytes) - use #(data, _trailers, leftover) <- result.try(read_fixed( + encoder.RemainderInline(body) -> { + use Nil <- result.try(transport.send( transport, socket, - buffer, - want, + bytes_tree.append_tree(head, body), )) - let conn = - connection.Http1Connection(..conn, buffer: leftover, read: read + want) - Ok(PulledChunk(data, conn)) + Ok(to_sent(keep_alive)) } - } -} - -fn pull_chunked_chunk( - conn: Connection, - limit: Int, - read: Int, - chunk_remaining: Int, - max_chunk_bytes: Int, -) -> Result(Pulled, ParseError) { - let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn - - case chunk_remaining { - 0 -> { - use #(size, buffer) <- result.try(pull_until( - transport, - socket, - buffer, - parse_chunk_line, - )) - - case size { - 0 -> { - use #(trailers, _state, buffer) <- result.try({ - use buffer <- pull_until(transport, socket, buffer) - parse_headers(buffer, [], 0, initial_header_state()) - }) - - Ok(PulledDone(trailers, buffer)) - } - size if read + size > limit -> Error(ChunkTooLarge) - size -> - connection.Http1Connection(..conn, buffer:) - |> take_chunk_slice(read, size, max_chunk_bytes) - } + encoder.RemainderFile(data) -> { + use Nil <- result.try(transport.send(transport, socket, head)) + use Nil <- result.try(file.send(transport, socket, data)) + Ok(to_sent(keep_alive)) } - remaining -> take_chunk_slice(conn, read, remaining, max_chunk_bytes) - } -} - -fn take_chunk_slice( - conn: Connection, - read: Int, - chunk_remaining: Int, - max_chunk_bytes: Int, -) -> Result(Pulled, ParseError) { - let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn - - let want = int.min(chunk_remaining, max_chunk_bytes) - let final_slice = want == chunk_remaining - - use #(data, buffer) <- result.try({ - use buffer <- pull_until(transport, socket, buffer) - take_chunk_prefix(buffer, want, final_slice) - }) - - let conn = - connection.Http1Connection( - ..conn, - buffer:, - read: read + want, - chunk_remaining: chunk_remaining - want, - ) - Ok(PulledChunk(data, conn)) -} - -fn pull_until( - transport: transport.Transport, - socket: socket.Socket, - buffer: BitArray, - step: fn(BitArray) -> Step(a), -) -> Result(a, ParseError) { - case step(buffer) { - StepDone(value) -> Ok(value) - ParseError(error) -> Error(error) - More -> - case transport.receive_timeout(transport, socket, 0, body_read_timeout) { - Ok(more) -> - pull_until(transport, socket, <>, step) - Error(_reason) -> Error(BodyReadFailed) - } - } -} - -fn parse_chunk_line(buffer: BitArray) -> Step(#(Int, BitArray)) { - use #(line, remaining) <- try_step(extract_line( - buffer, - max_chunk_size_line, - ChunkSizeLineTooLong, - BadChunkSize, - )) - use size <- try_step(parse_chunk_size(line)) - StepDone(#(size, remaining)) -} - -fn parse_chunk_size(line: BitArray) -> Step(Int) { - case parse_hex_digits(line, 0, False) { - Ok(size) -> StepDone(size) - Error(Nil) -> ParseError(BadChunkSize) - } -} - -fn parse_hex_digits(bits: BitArray, acc: Int, any: Bool) -> Result(Int, Nil) { - case bits { - <> if byte >= 48 && byte <= 57 -> - parse_hex_digits(remaining, acc * 16 + { byte - 48 }, True) - <> if byte >= 97 && byte <= 102 -> - parse_hex_digits(remaining, acc * 16 + { byte - 87 }, True) - <> if byte >= 65 && byte <= 70 -> - parse_hex_digits(remaining, acc * 16 + { byte - 55 }, True) - _bits if any -> Ok(acc) - _bits -> Error(Nil) - } -} - -fn take_chunk_prefix( - buffer: BitArray, - want: Int, - final_slice: Bool, -) -> Step(#(BitArray, BitArray)) { - case final_slice { - True -> - case buffer { - <> -> - StepDone(#(data, remaining)) - _buffer -> - case bit_array.byte_size(buffer) < want + 2 { - True -> More - False -> ParseError(BadChunkFraming) - } - } - False -> - case buffer { - <> -> - StepDone(#(data, remaining)) - _buffer -> More - } - } -} - -pub type EncodeError { - UnsafeHeader(name: String) -} - -type EncodeState { - EncodeState(tree: bytes_tree.BytesTree, force_close: Bool) -} - -fn initial_encode_state() -> EncodeState { - EncodeState(tree: bytes_tree.new(), force_close: False) -} - -pub type Stream { - Stream(handler: fn(connection.ResponseWriter) -> Nil, chunked: Bool) -} - -pub type Remainder { - NoRemainder - RemainderFile(connection.File) - RemainderStream(Stream) - RemainderSse(handler: fn(connection.SseConnection) -> connection.Outcome) -} - -pub type Encoded { - Encoded(bytes: bytes_tree.BytesTree, keep_alive: Bool, remainder: Remainder) -} - -fn build_head( - state: EncodeState, - status: Int, - keep_alive: Bool, -) -> bytes_tree.BytesTree { - append_date(state.tree) - |> append_connection(keep_alive) - |> bytes_tree.prepend(status_line(status)) -} - -fn close_delimited_head( - state: EncodeState, - status: Int, -) -> bytes_tree.BytesTree { - build_head(state, status, False) - |> bytes_tree.append(<<"\r\n":utf8>>) -} - -pub fn encode_response( - response: response.Response(connection.Body), - method: http.Method, - version: Version, - metadata: Metadata, -) -> Result(Encoded, EncodeError) { - use state <- result.try(encode_headers(response.headers)) - let keep_alive = metadata.keep_alive && !state.force_close - - case response.body { - connection.Streaming(connection.StreamingMetadata(handler)) -> - Ok(encode_stream(state, response, keep_alive, version, method, handler)) - connection.Sse(connection.SseMetadata(handler)) -> - Ok(encode_sse(state, response, method, handler)) - _other_body -> { - let length = body_length(response.body) - let head = - build_head(state, response.status, keep_alive) - |> bytes_tree.append_string("content-length: " <> int.to_string(length)) - |> bytes_tree.append(<<"\r\n\r\n":utf8>>) - - case method, response.body { - http.Head, _body -> Ok(Encoded(head, keep_alive, NoRemainder)) - _method, connection.File(data) -> - Ok(Encoded(head, keep_alive, RemainderFile(data))) - _method, connection.Bytes(tree) -> - Ok(Encoded( - bytes_tree.append_tree(head, tree), - keep_alive, - NoRemainder, - )) - _method, connection.Text(text) -> - Ok(Encoded( - bytes_tree.append_string(head, text), - keep_alive, - NoRemainder, - )) - _method, connection.Empty -> Ok(Encoded(head, keep_alive, NoRemainder)) - _method, connection.Streaming(..) -> - panic as "the Streaming body is handled above" - _method, connection.Sse(..) -> panic as "the Sse body is handled above" - } - } - } -} - -fn encode_stream( - state: EncodeState, - response: response.Response(connection.Body), - keep_alive: Bool, - version: Version, - method: http.Method, - handler: fn(connection.ResponseWriter) -> Nil, -) -> Encoded { - case version { - Http11 -> { - let head = - build_head(state, response.status, keep_alive) - |> bytes_tree.append_string("transfer-encoding: chunked") - |> bytes_tree.append(<<"\r\n\r\n":utf8>>) - - case method { - http.Head -> Encoded(head, keep_alive, NoRemainder) - _method -> - Encoded(head, keep_alive, RemainderStream(Stream(handler, True))) - } - } - Http10 -> { - let head = close_delimited_head(state, response.status) - - case method { - http.Head -> Encoded(head, False, NoRemainder) - _method -> Encoded(head, False, RemainderStream(Stream(handler, False))) - } - } - } -} - -fn encode_sse( - state: EncodeState, - response: response.Response(connection.Body), - method: http.Method, - handler: fn(connection.SseConnection) -> connection.Outcome, -) -> Encoded { - let head = close_delimited_head(state, response.status) - - case method { - http.Head -> Encoded(head, False, NoRemainder) - _method -> Encoded(head, False, RemainderSse(handler)) - } -} - -pub type ResponseWriter = - connection.Http1ResponseWriter - -fn chunk_frame(chunk: BitArray) -> bytes_tree.BytesTree { - bytes_tree.new() - |> bytes_tree.append_string(int.to_base16(bit_array.byte_size(chunk))) - |> bytes_tree.append(<<"\r\n":utf8>>) - |> bytes_tree.append(chunk) - |> bytes_tree.append(<<"\r\n":utf8>>) -} - -pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { - let bytes = case writer.chunked { - True -> chunk_frame(chunk) - False -> bytes_tree.from_bit_array(chunk) - } - let _ = transport.send(writer.transport, writer.socket, bytes) - writer -} - -pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { - let bytes = case writer.chunked { - True -> bytes_tree.append(chunk_frame(chunk), <<"0\r\n\r\n":utf8>>) - False -> bytes_tree.from_bit_array(chunk) - } - let _ = transport.send(writer.transport, writer.socket, bytes) - finish(writer) -} - -pub fn finish_response(writer: ResponseWriter) -> Nil { - case writer.chunked { - True -> { - let _ = - transport.send( - writer.transport, - writer.socket, - bytes_tree.from_bit_array(<<"0\r\n\r\n":utf8>>), - ) - Nil - } - False -> Nil - } - finish(writer) -} - -fn finish(writer: ResponseWriter) -> Nil { - process.send( - writer.self, - connection.StreamFinished(keep_alive: writer.keep_alive), - ) -} - -fn encode_headers( - headers: List(#(String, String)), -) -> Result(EncodeState, EncodeError) { - use state, #(name, value) <- list.try_fold(headers, initial_encode_state()) - case name { - "content-length" | "transfer-encoding" | "date" -> Ok(state) - "connection" -> - case find_unsafe_header_byte(value) { - Error(Nil) -> { - let lowered = value |> bit_array.from_string |> lowercase_ascii - let force_close = - state.force_close || has_token(lowered, <<"close":utf8>>) - Ok(EncodeState(..state, force_close:)) - } - Ok(_position) -> Error(UnsafeHeader(name)) - } - _name -> - case find_unsafe_header_byte(name), find_unsafe_header_byte(value) { - Error(Nil), Error(Nil) -> { - let tree = - bytes_tree.append_string(state.tree, name) - |> bytes_tree.append(<<": ":utf8>>) - |> bytes_tree.append_string(value) - |> bytes_tree.append(<<"\r\n":utf8>>) - - Ok(EncodeState(..state, tree:)) - } - _other, _other -> Error(UnsafeHeader(name)) - } - } -} - -fn append_date(tree: bytes_tree.BytesTree) -> bytes_tree.BytesTree { - bytes_tree.append_string(tree, "date: ") - |> bytes_tree.append(clock.get()) - |> bytes_tree.append(<<"\r\n":utf8>>) -} - -fn append_connection( - tree: bytes_tree.BytesTree, - keep_alive: Bool, -) -> bytes_tree.BytesTree { - let value = case keep_alive { - True -> <<"keep-alive":utf8>> - False -> <<"close":utf8>> - } - - bytes_tree.append(tree, <<"connection: ":utf8>>) - |> bytes_tree.append(value) - |> bytes_tree.append(<<"\r\n":utf8>>) -} - -fn status_line(status: Int) -> BitArray { - case status { - 100 -> <<"HTTP/1.1 100 Continue\r\n":utf8>> - 101 -> <<"HTTP/1.1 101 Switching Protocols\r\n":utf8>> - 102 -> <<"HTTP/1.1 102 Processing\r\n":utf8>> - 103 -> <<"HTTP/1.1 103 Early Hints\r\n":utf8>> - 200 -> <<"HTTP/1.1 200 OK\r\n":utf8>> - 201 -> <<"HTTP/1.1 201 Created\r\n":utf8>> - 202 -> <<"HTTP/1.1 202 Accepted\r\n":utf8>> - 203 -> <<"HTTP/1.1 203 Non-Authoritative Information\r\n":utf8>> - 204 -> <<"HTTP/1.1 204 No Content\r\n":utf8>> - 205 -> <<"HTTP/1.1 205 Reset Content\r\n":utf8>> - 206 -> <<"HTTP/1.1 206 Partial Content\r\n":utf8>> - 207 -> <<"HTTP/1.1 207 Multi-Status\r\n":utf8>> - 208 -> <<"HTTP/1.1 208 Already Reported\r\n":utf8>> - 226 -> <<"HTTP/1.1 226 IM Used\r\n":utf8>> - 300 -> <<"HTTP/1.1 300 Multiple Choices\r\n":utf8>> - 301 -> <<"HTTP/1.1 301 Moved Permanently\r\n":utf8>> - 302 -> <<"HTTP/1.1 302 Found\r\n":utf8>> - 303 -> <<"HTTP/1.1 303 See Other\r\n":utf8>> - 304 -> <<"HTTP/1.1 304 Not Modified\r\n":utf8>> - 305 -> <<"HTTP/1.1 305 Use Proxy\r\n":utf8>> - 307 -> <<"HTTP/1.1 307 Temporary Redirect\r\n":utf8>> - 308 -> <<"HTTP/1.1 308 Permanent Redirect\r\n":utf8>> - 400 -> <<"HTTP/1.1 400 Bad Request\r\n":utf8>> - 401 -> <<"HTTP/1.1 401 Unauthorized\r\n":utf8>> - 402 -> <<"HTTP/1.1 402 Payment Required\r\n":utf8>> - 403 -> <<"HTTP/1.1 403 Forbidden\r\n":utf8>> - 404 -> <<"HTTP/1.1 404 Not Found\r\n":utf8>> - 405 -> <<"HTTP/1.1 405 Method Not Allowed\r\n":utf8>> - 406 -> <<"HTTP/1.1 406 Not Acceptable\r\n":utf8>> - 407 -> <<"HTTP/1.1 407 Proxy Authentication Required\r\n":utf8>> - 408 -> <<"HTTP/1.1 408 Request Timeout\r\n":utf8>> - 409 -> <<"HTTP/1.1 409 Conflict\r\n":utf8>> - 410 -> <<"HTTP/1.1 410 Gone\r\n":utf8>> - 411 -> <<"HTTP/1.1 411 Length Required\r\n":utf8>> - 412 -> <<"HTTP/1.1 412 Precondition Failed\r\n":utf8>> - 413 -> <<"HTTP/1.1 413 Content Too Large\r\n":utf8>> - 414 -> <<"HTTP/1.1 414 URI Too Long\r\n":utf8>> - 415 -> <<"HTTP/1.1 415 Unsupported Media Type\r\n":utf8>> - 416 -> <<"HTTP/1.1 416 Range Not Satisfiable\r\n":utf8>> - 417 -> <<"HTTP/1.1 417 Expectation Failed\r\n":utf8>> - 418 -> <<"HTTP/1.1 418 I'm a Teapot\r\n":utf8>> - 421 -> <<"HTTP/1.1 421 Misdirected Request\r\n":utf8>> - 422 -> <<"HTTP/1.1 422 Unprocessable Content\r\n":utf8>> - 423 -> <<"HTTP/1.1 423 Locked\r\n":utf8>> - 424 -> <<"HTTP/1.1 424 Failed Dependency\r\n":utf8>> - 425 -> <<"HTTP/1.1 425 Too Early\r\n":utf8>> - 426 -> <<"HTTP/1.1 426 Upgrade Required\r\n":utf8>> - 428 -> <<"HTTP/1.1 428 Precondition Required\r\n":utf8>> - 429 -> <<"HTTP/1.1 429 Too Many Requests\r\n":utf8>> - 431 -> <<"HTTP/1.1 431 Request Header Fields Too Large\r\n":utf8>> - 451 -> <<"HTTP/1.1 451 Unavailable For Legal Reasons\r\n":utf8>> - 500 -> <<"HTTP/1.1 500 Internal Server Error\r\n":utf8>> - 501 -> <<"HTTP/1.1 501 Not Implemented\r\n":utf8>> - 502 -> <<"HTTP/1.1 502 Bad Gateway\r\n":utf8>> - 503 -> <<"HTTP/1.1 503 Service Unavailable\r\n":utf8>> - 504 -> <<"HTTP/1.1 504 Gateway Timeout\r\n":utf8>> - 505 -> <<"HTTP/1.1 505 HTTP Version Not Supported\r\n":utf8>> - 506 -> <<"HTTP/1.1 506 Variant Also Negotiates\r\n":utf8>> - 507 -> <<"HTTP/1.1 507 Insufficient Storage\r\n":utf8>> - 508 -> <<"HTTP/1.1 508 Loop Detected\r\n":utf8>> - 510 -> <<"HTTP/1.1 510 Not Extended\r\n":utf8>> - 511 -> <<"HTTP/1.1 511 Network Authentication Required\r\n":utf8>> - _other -> <<"HTTP/1.1 ":utf8, int.to_string(status):utf8, " \r\n":utf8>> - } -} - -fn body_length(body: connection.Body) -> Int { - case body { - connection.Bytes(tree) -> bytes_tree.byte_size(tree) - connection.Text(text) -> string.byte_size(text) - connection.Empty -> 0 - connection.File(data) -> data.length - connection.Streaming(..) -> panic as "the Streaming body is handled above" - connection.Sse(..) -> panic as "the Sse body is handled above" - } -} - -fn internal_server_error() -> bytes_tree.BytesTree { - bytes_tree.new() - |> bytes_tree.append(<< - "HTTP/1.1 500 Internal Server Error\r\ndate: ":utf8, - >>) - |> bytes_tree.append(clock.get()) - |> bytes_tree.append(<< - "\r\nconnection: close\r\ncontent-length: 0\r\n\r\n":utf8, - >>) -} - -pub type Version { - Http10 - Http11 -} - -pub type Head { - Head( - method: http.Method, - host: String, - port: option.Option(Int), - path: String, - query: option.Option(String), - version: Version, - headers: List(#(String, String)), - ) -} - -pub type Metadata { - Metadata( - framing: connection.Framing, - keep_alive: Bool, - upgrade: option.Option(String), - ) -} - -pub type ParseError { - RequestLineTooLong - BadRequestLine - BadMethod - BadTarget - BadVersion - HeaderLineTooLong - BadHeader - TooManyHeaders - DuplicateContentLength - BadContentLength - DuplicateHost - BadHost - MissingHost - AmbiguousFraming - ChunkSizeLineTooLong - BadChunkSize - BadChunkFraming - ChunkTooLarge - BodyReadFailed -} - -pub fn error_to_string(error: ParseError) -> String { - case error { - RequestLineTooLong -> - "request line exceeds " <> int.to_string(max_request_line) <> " bytes" - BadRequestLine -> "malformed request line" - BadMethod -> "invalid request method" - BadTarget -> "invalid request target" - BadVersion -> "unsupported or malformed HTTP version" - HeaderLineTooLong -> - "header line exceeds " <> int.to_string(max_header_line) <> " bytes" - BadHeader -> "malformed header line" - TooManyHeaders -> - "too many headers (max " <> int.to_string(max_headers) <> ")" - DuplicateContentLength -> "duplicate Content-Length header" - BadContentLength -> "invalid Content-Length value" - DuplicateHost -> "duplicate Host header" - BadHost -> "invalid Host header" - MissingHost -> "missing required Host header" - AmbiguousFraming -> - "conflicting Content-Length and Transfer-Encoding headers" - ChunkSizeLineTooLong -> - "chunk size line exceeds " - <> int.to_string(max_chunk_size_line) - <> " bytes" - BadChunkSize -> "malformed chunk size" - BadChunkFraming -> "malformed chunk data framing" - ChunkTooLarge -> "chunked body exceeds size limit" - BodyReadFailed -> "failed to read request body from the socket" - } -} - -pub type Parsed { - Complete(head: Head, metadata: Metadata, remaining: BitArray) - Incomplete -} - -const max_request_line = 8192 - -const max_header_line = 8192 - -const max_headers = 100 - -const max_chunk_size_line = 128 - -pub fn parse(buffer: BitArray) -> Result(Parsed, ParseError) { - let step = { - use #(method, target, version, remaining) <- try_step(parse_request_line( - buffer, - )) - - use #(headers, state, remaining) <- try_step(parse_headers( - remaining, - [], - 0, - initial_header_state(), - )) - - use #(host, port, path, query) <- try_step(resolve_target( - method, - target, - version, - state.host, - )) - - use metadata <- try_step(resolve_metadata(state, version)) - - StepDone(Complete( - Head(method:, host:, port:, path:, query:, version:, headers:), - metadata, - remaining, - )) - } - - case step { - StepDone(parsed) -> Ok(parsed) - More -> Ok(Incomplete) - ParseError(error) -> Error(error) - } -} - -type Step(a) { - StepDone(a) - More - ParseError(ParseError) -} - -fn try_step(step: Step(a), next: fn(a) -> Step(b)) -> Step(b) { - case step { - StepDone(value) -> next(value) - More -> More - ParseError(error) -> ParseError(error) - } -} - -fn parse_request_line( - buffer: BitArray, -) -> Step(#(http.Method, BitArray, Version, BitArray)) { - use #(line, remaining) <- try_step(extract_line( - buffer, - max_request_line, - RequestLineTooLong, - BadRequestLine, - )) - - use #(method, target_and_version) <- try_step(parse_method(line)) - - use #(target, version) <- try_step(parse_target_version(target_and_version)) - - StepDone(#(method, target, version, remaining)) -} - -fn parse_method(line: BitArray) -> Step(#(http.Method, BitArray)) { - case line { - <<"GET ":utf8, remaining:bits>> -> StepDone(#(http.Get, remaining)) - <<"POST ":utf8, remaining:bits>> -> StepDone(#(http.Post, remaining)) - <<"PUT ":utf8, remaining:bits>> -> StepDone(#(http.Put, remaining)) - <<"DELETE ":utf8, remaining:bits>> -> StepDone(#(http.Delete, remaining)) - <<"HEAD ":utf8, remaining:bits>> -> StepDone(#(http.Head, remaining)) - <<"OPTIONS ":utf8, remaining:bits>> -> StepDone(#(http.Options, remaining)) - <<"PATCH ":utf8, remaining:bits>> -> StepDone(#(http.Patch, remaining)) - <<"TRACE ":utf8, remaining:bits>> -> StepDone(#(http.Trace, remaining)) - <<"CONNECT ":utf8, remaining:bits>> -> StepDone(#(http.Connect, remaining)) - _other -> parse_other_method(line) - } -} - -fn parse_other_method(line: BitArray) -> Step(#(http.Method, BitArray)) { - case find_space(line) { - Error(Nil) -> ParseError(BadRequestLine) - Ok(position) -> - case line { - <> -> - case bit_array_to_string(name) { - Error(Nil) -> ParseError(BadMethod) - Ok(name) -> - case http.parse_method(name) { - Ok(method) -> StepDone(#(method, remaining)) - Error(Nil) -> ParseError(BadMethod) - } - } - _line -> ParseError(BadRequestLine) - } - } -} - -fn parse_target_version(bits: BitArray) -> Step(#(BitArray, Version)) { - let size = bit_array.byte_size(bits) - - case size < 10 { - True -> ParseError(BadRequestLine) - False -> { - let target_size = size - 9 - case bits { - <> -> - StepDone(#(target, Http11)) - <> -> - StepDone(#(target, Http10)) - _bits -> ParseError(BadVersion) - } - } - } -} - -fn split_target(target: BitArray) -> Step(#(String, option.Option(String))) { - case find_question(target) { - Error(Nil) -> { - use path <- try_step(decode_component(target, BadTarget)) - StepDone(#(path, option.None)) - } - Ok(position) -> { - let size = bit_array.byte_size(target) + encoder.RemainderStream(handler: stream_handler, framing:) -> { + use Nil <- result.try(transport.send(transport, socket, head)) + + connection.Http1Writer(http1.ResponseWriter( + transport:, + socket:, + self:, + framing:, + keep_alive:, + )) + |> stream_handler - case target { - << - path:bytes-size(position), - "?":utf8, - query:bytes-size(size - position - 1), - >> -> { - use path <- try_step(decode_component(path, BadTarget)) - use query <- try_step(decode_component(query, BadTarget)) - StepDone(#(path, option.Some(query))) + let drained = drain_messages(self) + case drained.stream { + option.Some(http1.StreamFinished(keep_alive:)) -> + Ok(to_sent(keep_alive)) + // A handler that returns without finishing left the body unterminated, + // so close it out here and drop a connection we can no longer reuse. + option.None -> { + let _ = encoder.end_stream(transport, socket, framing) + Ok(SentClose) } - _target -> ParseError(BadTarget) } } - } -} - -fn decode_component(bits: BitArray, on_error: ParseError) -> Step(String) { - case bit_array_to_string(bits) { - Ok(value) -> StepDone(value) - Error(Nil) -> ParseError(on_error) - } -} - -fn validate_path(method: http.Method, path: String) -> Step(String) { - case path, method { - "*", http.Options -> StepDone(path) - "/" <> _remaining, _method -> StepDone(path) - _path, _method -> ParseError(BadTarget) - } -} + encoder.RemainderSse(handler: sse_handler) -> { + use Nil <- result.try(transport.send(transport, socket, head)) -fn resolve_target( - method: http.Method, - target: BitArray, - version: Version, - header_host: option.Option(#(String, option.Option(Int))), -) -> Step(#(String, option.Option(Int), String, option.Option(String))) { - case method { - http.Connect -> - case split_host_port(target) { - Ok(#(host, option.Some(_port) as port)) -> - case bit_array_to_string(host) { - Ok(host) -> StepDone(#(host, port, "", option.None)) - Error(Nil) -> ParseError(BadTarget) - } - Ok(#(_host, option.None)) -> ParseError(BadTarget) - Error(Nil) -> ParseError(BadTarget) - } - _method -> { - use #(path, query) <- try_step(split_target(target)) - use path <- try_step(validate_path(method, path)) - use #(host, port) <- try_step(resolve_host(version, header_host)) - StepDone(#(host, port, path, query)) + connection.Http1Sse(http1.SseConnection(transport:, socket:)) + |> sse_handler + |> to_sse_sent + |> Ok } } } -fn resolve_host( - version: Version, - header_host: option.Option(#(String, option.Option(Int))), -) -> Step(#(String, option.Option(Int))) { - case header_host, version { - option.Some(host_port), _version -> StepDone(host_port) - option.None, Http10 -> StepDone(#("", option.None)) - option.None, Http11 -> ParseError(MissingHost) +fn to_sent(keep_alive: http1.KeepAlive) -> Sent { + case keep_alive { + http1.KeepAlive -> SentKeepAlive + http1.CloseAfterResponse -> SentClose } } -fn split_host_port( - value: BitArray, -) -> Result(#(BitArray, option.Option(Int)), Nil) { - case value { - <<"[":utf8, _remaining:bits>> -> split_bracketed_host(value) - _value -> - case find_colon(value) { - Error(Nil) -> Ok(#(value, option.None)) - Ok(position) -> { - let size = bit_array.byte_size(value) - case value { - << - host:bytes-size(position), - ":":utf8, - port:bytes-size(size - position - 1), - >> -> - case parse_port(port) { - Ok(port) -> Ok(#(host, option.Some(port))) - Error(Nil) -> Error(Nil) - } - _value -> Error(Nil) - } - } - } +fn to_sse_sent(outcome: connection.Outcome) -> Sent { + case outcome { + connection.Stopped -> SentClose + connection.StoppedAbnormal(reason:) -> SentAbnormal(reason) } } -fn split_bracketed_host( - value: BitArray, -) -> Result(#(BitArray, option.Option(Int)), Nil) { - case find_close_bracket(value) { - Error(Nil) -> Error(Nil) - Ok(position) -> { - let size = bit_array.byte_size(value) +const auto_drain_limit = 1_048_576 - case value { - << - host:bytes-size(position + 1), - remaining:bytes-size(size - position - 1), - >> -> - case remaining { - <<>> -> Ok(#(host, option.None)) - <<":":utf8, port:bits>> -> - case parse_port(port) { - Ok(port) -> Ok(#(host, option.Some(port))) - Error(Nil) -> Error(Nil) - } - _remaining -> Error(Nil) - } - _value -> Error(Nil) - } - } - } -} +const auto_drain_chunk_bytes = 65_536 -fn parse_port(bits: BitArray) -> Result(Int, Nil) { - case bits { - <<>> -> Error(Nil) - _bits -> parse_port_digits(bits, 0) - } +/// What the request body left behind: the bytes after it, which begin the next +/// pipelined request, and whether it was consumed cleanly enough to reuse the +/// connection at all. +type ResolvedBody { + ResolvedBody(leftover: BitArray, keep_alive: http1.KeepAlive) } -fn parse_port_digits(bits: BitArray, acc: Int) -> Result(Int, Nil) { - case bits { - <<>> -> - case acc <= 65_535 { - True -> Ok(acc) - False -> Error(Nil) - } - <> if byte >= 48 && byte <= 57 -> - parse_port_digits(remaining, acc * 10 + { byte - 48 }) - _bits -> Error(Nil) +fn resolve_body( + conn: http1.Connection, + drained: option.Option(http1.BodySignal), +) -> ResolvedBody { + case drained { + option.Some(http1.BodyDrained(leftover)) -> + ResolvedBody(leftover, http1.KeepAlive) + option.Some(http1.BodyAbandoned) -> + ResolvedBody(<<>>, http1.CloseAfterResponse) + option.Some(http1.BodyProgress(buffer:, read:, chunk_remaining:)) -> + http1.Connection(..conn, buffer:, read:, chunk_remaining:) + |> drain_remaining + option.None -> drain_remaining(conn) } } -type HeaderState { - HeaderState( - content_length: option.Option(Int), - chunked: Bool, - connection: option.Option(Bool), - connection_upgrade: Bool, - upgrade: option.Option(String), - host: option.Option(#(String, option.Option(Int))), - ) -} - -fn initial_header_state() -> HeaderState { - HeaderState( - content_length: option.None, - chunked: False, - connection: option.None, - connection_upgrade: False, - upgrade: option.None, - host: option.None, +type Drained { + Drained( + body: option.Option(http1.BodySignal), + stream: option.Option(http1.StreamSignal), ) } -fn resolve_metadata(state: HeaderState, version: Version) -> Step(Metadata) { - case state.content_length, state.chunked { - option.Some(_length), True -> ParseError(AmbiguousFraming) - content_length, chunked -> { - let framing = case content_length, chunked { - option.Some(length), False -> connection.Fixed(length) - option.None, True -> connection.Chunked - option.None, False -> connection.NoBody - option.Some(_length), True -> - panic as "AmbiguousFraming already rejected above" - } - let keep_alive = case state.connection, version { - option.Some(keep_alive), _version -> keep_alive - option.None, Http11 -> True - option.None, Http10 -> False - } - let upgrade = case state.connection_upgrade { - True -> state.upgrade - False -> option.None - } - StepDone(Metadata(framing:, keep_alive:, upgrade:)) - } - } -} - -fn parse_headers( - buffer: BitArray, - acc: List(#(String, String)), - count: Int, - state: HeaderState, -) -> Step(#(List(#(String, String)), HeaderState, BitArray)) { - use #(line, remaining) <- try_step(extract_line( - buffer, - max_header_line, - HeaderLineTooLong, - BadHeader, - )) - - case line { - <<>> -> StepDone(#(list.reverse(acc), state, remaining)) - _line if count >= max_headers -> ParseError(TooManyHeaders) - _line -> { - use #(header, state) <- try_step(parse_header_line(line, state)) - parse_headers(remaining, [header, ..acc], count + 1, state) - } - } -} - -fn parse_header_line( - line: BitArray, - state: HeaderState, -) -> Step(#(#(String, String), HeaderState)) { - case find_colon(line) { - Error(Nil) -> ParseError(BadHeader) - Ok(0) -> ParseError(BadHeader) - Ok(position) -> - case line { - <> -> { - let name = lowercase_ascii(name) - let value = trim_ows(value) - case bit_array_to_string(name), bit_array_to_string(value) { - Ok(name), Ok(value) -> { - use state <- try_step(classify(name, value, state)) - StepDone(#(#(name, value), state)) - } - _other, _other -> ParseError(BadHeader) - } - } - _bad -> ParseError(BadHeader) - } - } -} - -fn classify( - name: String, - value: String, - state: HeaderState, -) -> Step(HeaderState) { - case name { - "content-length" -> - case state.content_length { - option.Some(_length) -> ParseError(DuplicateContentLength) - option.None -> - case int.parse(value) { - Ok(length) if length >= 0 -> - StepDone( - HeaderState(..state, content_length: option.Some(length)), - ) - _bad -> ParseError(BadContentLength) - } - } - "transfer-encoding" -> { - let lowered = value |> bit_array.from_string |> lowercase_ascii - let chunked = state.chunked || has_token(lowered, <<"chunked":utf8>>) - StepDone(HeaderState(..state, chunked:)) - } - "connection" -> { - let lowered = value |> bit_array.from_string |> lowercase_ascii - let connection = case has_token(lowered, <<"close":utf8>>) { - True -> option.Some(False) - False -> - case has_token(lowered, <<"keep-alive":utf8>>) { - True -> option.Some(True) - False -> state.connection - } - } - let connection_upgrade = - state.connection_upgrade || has_token(lowered, <<"upgrade":utf8>>) - StepDone(HeaderState(..state, connection:, connection_upgrade:)) - } - "upgrade" -> { - let lowered = - value - |> bit_array.from_string - |> lowercase_ascii - |> unsafe_to_string - StepDone(HeaderState(..state, upgrade: option.Some(lowered))) - } - "host" -> - case state.host { - option.Some(_host) -> ParseError(DuplicateHost) - option.None -> - case split_host_port(bit_array.from_string(value)) { - Ok(#(host, port)) -> { - let host = option.Some(#(unsafe_to_string(host), port)) - StepDone(HeaderState(..state, host:)) - } - Error(Nil) -> ParseError(BadHost) - } - } - _other -> StepDone(state) - } -} - -fn extract_line( - buffer: BitArray, - max_len: Int, - too_long: ParseError, - malformed: ParseError, -) -> Step(#(BitArray, BitArray)) { - case find_lf(buffer) { - Error(Nil) -> - case bit_array.byte_size(buffer) > max_len { - True -> ParseError(too_long) - False -> More - } - Ok(0) -> ParseError(malformed) - Ok(position) -> - case buffer { - <> -> - StepDone(#(line, remaining)) - _bad -> ParseError(malformed) - } - } -} - -fn lowercase_ascii(bits: BitArray) -> BitArray { - case has_uppercase(bits) { - False -> bits - True -> lowercase_walk(bits) |> list_to_bit_array - } -} - -fn has_uppercase(bits: BitArray) -> Bool { - case bits { - <<>> -> False - <> if byte >= 65 && byte <= 90 -> True - <<_byte, remaining:bits>> -> has_uppercase(remaining) - _other -> False - } -} - -fn lowercase_walk(bits: BitArray) -> List(Int) { - case bits { - <<>> -> [] - <<"A", remaining:bits>> -> [0x61, ..lowercase_walk(remaining)] - <<"B", remaining:bits>> -> [0x62, ..lowercase_walk(remaining)] - <<"C", remaining:bits>> -> [0x63, ..lowercase_walk(remaining)] - <<"D", remaining:bits>> -> [0x64, ..lowercase_walk(remaining)] - <<"E", remaining:bits>> -> [0x65, ..lowercase_walk(remaining)] - <<"F", remaining:bits>> -> [0x66, ..lowercase_walk(remaining)] - <<"G", remaining:bits>> -> [0x67, ..lowercase_walk(remaining)] - <<"H", remaining:bits>> -> [0x68, ..lowercase_walk(remaining)] - <<"I", remaining:bits>> -> [0x69, ..lowercase_walk(remaining)] - <<"J", remaining:bits>> -> [0x6A, ..lowercase_walk(remaining)] - <<"K", remaining:bits>> -> [0x6B, ..lowercase_walk(remaining)] - <<"L", remaining:bits>> -> [0x6C, ..lowercase_walk(remaining)] - <<"M", remaining:bits>> -> [0x6D, ..lowercase_walk(remaining)] - <<"N", remaining:bits>> -> [0x6E, ..lowercase_walk(remaining)] - <<"O", remaining:bits>> -> [0x6F, ..lowercase_walk(remaining)] - <<"P", remaining:bits>> -> [0x70, ..lowercase_walk(remaining)] - <<"Q", remaining:bits>> -> [0x71, ..lowercase_walk(remaining)] - <<"R", remaining:bits>> -> [0x72, ..lowercase_walk(remaining)] - <<"S", remaining:bits>> -> [0x73, ..lowercase_walk(remaining)] - <<"T", remaining:bits>> -> [0x74, ..lowercase_walk(remaining)] - <<"U", remaining:bits>> -> [0x75, ..lowercase_walk(remaining)] - <<"V", remaining:bits>> -> [0x76, ..lowercase_walk(remaining)] - <<"W", remaining:bits>> -> [0x77, ..lowercase_walk(remaining)] - <<"X", remaining:bits>> -> [0x78, ..lowercase_walk(remaining)] - <<"Y", remaining:bits>> -> [0x79, ..lowercase_walk(remaining)] - <<"Z", remaining:bits>> -> [0x7A, ..lowercase_walk(remaining)] - <> -> [byte, ..lowercase_walk(remaining)] - _other -> [] - } -} - -fn trim_ows(bits: BitArray) -> BitArray { - bits - |> trim_leading_ows - |> trim_trailing_ows +fn drain_messages(self: process.Subject(http1.Signal)) -> Drained { + do_drain_messages(self, Drained(body: option.None, stream: option.None)) } -fn trim_leading_ows(bits: BitArray) -> BitArray { - case bits { - <<" ", remaining:bits>> -> trim_leading_ows(remaining) - <<"\t", remaining:bits>> -> trim_leading_ows(remaining) - _bits -> bits +fn do_drain_messages( + self: process.Subject(http1.Signal), + acc: Drained, +) -> Drained { + case process.receive(self, 0) { + Ok(http1.BodySignal(signal)) -> + do_drain_messages(self, Drained(..acc, body: option.Some(signal))) + Ok(http1.StreamSignal(signal)) -> + do_drain_messages(self, Drained(..acc, stream: option.Some(signal))) + Error(Nil) -> acc } } -fn trim_trailing_ows(bits: BitArray) -> BitArray { - case bit_array.byte_size(bits) { - 0 -> bits - size -> - case bits { - <> -> trim_trailing_ows(init) - <> -> trim_trailing_ows(init) - _bits -> bits - } - } +fn drain_remaining(conn: http1.Connection) -> ResolvedBody { + let http1.Connection(read:, ..) = conn + do_drain_remaining(conn, read + auto_drain_limit) } -fn has_token(value: BitArray, token: BitArray) -> Bool { - case value == token { - True -> True - False -> - split_comma(value) - |> list.any(fn(part) { trim_ows(part) == token }) +fn do_drain_remaining(conn: http1.Connection, limit: Int) -> ResolvedBody { + case body.pull_chunk(conn, auto_drain_chunk_bytes, limit) { + Ok(body.PulledChunk(_data, next)) -> do_drain_remaining(next, limit) + Ok(body.PulledDone(_trailers, leftover)) -> + ResolvedBody(leftover, http1.KeepAlive) + Error(_reason) -> ResolvedBody(<<>>, http1.CloseAfterResponse) } } - -@external(erlang, "http1_ffi", "find_lf") -fn find_lf(bits: BitArray) -> Result(Int, Nil) - -@external(erlang, "http1_ffi", "find_colon") -fn find_colon(bits: BitArray) -> Result(Int, Nil) - -@external(erlang, "http1_ffi", "find_space") -fn find_space(bits: BitArray) -> Result(Int, Nil) - -@external(erlang, "http1_ffi", "find_question") -fn find_question(bits: BitArray) -> Result(Int, Nil) - -@external(erlang, "http1_ffi", "find_close_bracket") -fn find_close_bracket(bits: BitArray) -> Result(Int, Nil) - -@external(erlang, "http1_ffi", "find_unsafe_header_byte") -fn find_unsafe_header_byte(bits: String) -> Result(Int, Nil) - -@external(erlang, "http1_ffi", "split_comma") -fn split_comma(bits: BitArray) -> List(BitArray) - -@external(erlang, "http1_ffi", "list_to_bit_array") -fn list_to_bit_array(bytes: List(Int)) -> BitArray - -@external(erlang, "http1_ffi", "bit_array_to_string") -fn bit_array_to_string(bits: BitArray) -> Result(String, Nil) - -@external(erlang, "ewe_ffi", "identity") -fn unsafe_to_string(bits: BitArray) -> String diff --git a/src/ewe/internal/http1/body.gleam b/src/ewe/internal/http1/body.gleam new file mode 100644 index 0000000..3aad03d --- /dev/null +++ b/src/ewe/internal/http1/body.gleam @@ -0,0 +1,319 @@ +import ewe/internal/http1/connection as http1 +import ewe/internal/http1/parser +import gleam/bit_array +import gleam/bytes_tree +import gleam/erlang/process +import gleam/int +import gleam/result +import glisten/socket +import glisten/transport + +pub type BodyError { + BodyTooLarge + InvalidBody +} + +const body_read_timeout = 10_000 + +pub fn read_body( + conn: http1.Connection, + limit: Int, +) -> Result(#(BitArray, List(#(String, String))), BodyError) { + let http1.Connection(self:, framing:, ..) = conn + + case framing, consume_body(conn, limit, bytes_tree.new()) { + _framing, Ok(#(body, trailers, leftover)) -> { + send_body_signal(self, http1.BodyDrained(leftover:)) + Ok(#(body, trailers)) + } + // An oversized fixed body was rejected before reading anything, so the + // outer loop can still drain it and reuse the connection. + http1.Fixed(_length), Error(BodyTooLarge) -> Error(BodyTooLarge) + _framing, Error(error) -> { + send_body_signal(self, http1.BodyAbandoned) + Error(error) + } + } +} + +fn send_body_signal( + self: process.Subject(http1.Signal), + signal: http1.BodySignal, +) -> Nil { + process.send(self, http1.BodySignal(signal)) +} + +pub type ChunkRead { + Chunk(data: BitArray, connection: http1.Connection) + Done(trailers: List(#(String, String))) +} + +pub fn read_body_chunk( + conn: http1.Connection, + max_chunk_bytes max_chunk_bytes: Int, + limit limit: Int, +) -> Result(ChunkRead, BodyError) { + let http1.Connection(self:, buffer:, read:, chunk_remaining:, ..) = conn + + case pull_chunk(conn, max_chunk_bytes, limit) { + Ok(PulledChunk(data, next)) -> { + send_body_signal( + self, + http1.BodyProgress(buffer:, read:, chunk_remaining:), + ) + + Ok(Chunk(data, next)) + } + Ok(PulledDone(trailers, leftover)) -> { + send_body_signal(self, http1.BodyDrained(leftover:)) + + Ok(Done(trailers)) + } + Error(error) -> { + send_body_signal(self, http1.BodyAbandoned) + + Error(error) + } + } +} + +fn consume_body( + conn: http1.Connection, + limit: Int, + acc: bytes_tree.BytesTree, +) -> Result(#(BitArray, List(#(String, String)), BitArray), BodyError) { + case pull_chunk(conn, limit, limit) { + Ok(PulledChunk(data, next)) -> + consume_body(next, limit, bytes_tree.append(acc, data)) + Ok(PulledDone(trailers, leftover)) -> + Ok(#(bytes_tree.to_bit_array(acc), trailers, leftover)) + Error(error) -> Error(error) + } +} + +fn to_body_result( + result: Result(a, parser.ParseError), +) -> Result(a, BodyError) { + case result { + Ok(value) -> Ok(value) + Error(parser.ChunkTooLarge) -> Error(BodyTooLarge) + Error(_other) -> Error(InvalidBody) + } +} + +pub type Pulled { + PulledChunk(data: BitArray, connection: http1.Connection) + PulledDone(trailers: List(#(String, String)), leftover: BitArray) +} + +pub fn pull_chunk( + conn: http1.Connection, + max_chunk_bytes: Int, + limit: Int, +) -> Result(Pulled, BodyError) { + let http1.Connection(buffer:, framing:, read:, chunk_remaining:, ..) = conn + + case framing { + http1.NoBody -> Ok(PulledDone([], buffer)) + http1.Fixed(length) if length > limit -> Error(BodyTooLarge) + http1.Fixed(length) -> + pull_fixed_chunk(conn, length, read, max_chunk_bytes) |> to_body_result + http1.Chunked -> + pull_chunked_chunk(conn, limit, read, chunk_remaining, max_chunk_bytes) + |> to_body_result + } +} + +fn pull_fixed_chunk( + conn: http1.Connection, + length: Int, + read: Int, + max_chunk_bytes: Int, +) -> Result(Pulled, parser.ParseError) { + let http1.Connection(transport:, socket:, buffer:, ..) = conn + + case length - read { + 0 -> Ok(PulledDone([], buffer)) + remaining -> { + let want = int.min(remaining, max_chunk_bytes) + use #(data, leftover) <- result.try(read_exact( + transport, + socket, + buffer, + want, + )) + let conn = http1.Connection(..conn, buffer: leftover, read: read + want) + Ok(PulledChunk(data, conn)) + } + } +} + +fn read_exact( + transport: transport.Transport, + socket: socket.Socket, + buffer: BitArray, + length: Int, +) -> Result(#(BitArray, BitArray), parser.ParseError) { + case buffer { + <> -> Ok(#(data, leftover)) + _buffer -> + case + transport.receive_timeout( + transport, + socket, + length - bit_array.byte_size(buffer), + body_read_timeout, + ) + { + Ok(more) -> Ok(#(<>, <<>>)) + Error(_reason) -> Error(parser.BodyReadFailed) + } + } +} + +fn pull_chunked_chunk( + conn: http1.Connection, + limit: Int, + read: Int, + chunk_remaining: Int, + max_chunk_bytes: Int, +) -> Result(Pulled, parser.ParseError) { + let http1.Connection(transport:, socket:, buffer:, ..) = conn + + case chunk_remaining { + 0 -> { + use #(size, buffer) <- result.try(pull_until( + transport, + socket, + buffer, + parse_chunk_line, + )) + + case size { + 0 -> { + use #(trailers, _state, buffer) <- result.try({ + use buffer <- pull_until(transport, socket, buffer) + parser.parse_headers(buffer, [], 0, parser.initial_header_state()) + }) + + Ok(PulledDone(trailers, buffer)) + } + size if read + size > limit -> Error(parser.ChunkTooLarge) + size -> + http1.Connection(..conn, buffer:) + |> take_chunk_slice(read, size, max_chunk_bytes) + } + } + remaining -> take_chunk_slice(conn, read, remaining, max_chunk_bytes) + } +} + +fn take_chunk_slice( + conn: http1.Connection, + read: Int, + chunk_remaining: Int, + max_chunk_bytes: Int, +) -> Result(Pulled, parser.ParseError) { + let http1.Connection(transport:, socket:, buffer:, ..) = conn + + let want = int.min(chunk_remaining, max_chunk_bytes) + let slice = case want == chunk_remaining { + True -> LastSlice + False -> PartialSlice + } + + use #(data, buffer) <- result.try({ + use buffer <- pull_until(transport, socket, buffer) + take_chunk_prefix(buffer, want, slice) + }) + + let conn = + http1.Connection( + ..conn, + buffer:, + read: read + want, + chunk_remaining: chunk_remaining - want, + ) + Ok(PulledChunk(data, conn)) +} + +fn pull_until( + transport: transport.Transport, + socket: socket.Socket, + buffer: BitArray, + step: fn(BitArray) -> parser.Step(a), +) -> Result(a, parser.ParseError) { + case step(buffer) { + parser.StepDone(value) -> Ok(value) + parser.ParseError(error) -> Error(error) + parser.More -> + case transport.receive_timeout(transport, socket, 0, body_read_timeout) { + Ok(more) -> + pull_until(transport, socket, <>, step) + Error(_reason) -> Error(parser.BodyReadFailed) + } + } +} + +fn parse_chunk_line(buffer: BitArray) -> parser.Step(#(Int, BitArray)) { + use #(line, remaining) <- parser.try_step(parser.extract_line( + buffer, + parser.max_chunk_size_line, + parser.ChunkSizeLineTooLong, + parser.BadChunkSize, + )) + use size <- parser.try_step(parse_chunk_size(line)) + parser.StepDone(#(size, remaining)) +} + +fn parse_chunk_size(line: BitArray) -> parser.Step(Int) { + case parse_hex_digits(line, 0, False) { + Ok(size) -> parser.StepDone(size) + Error(Nil) -> parser.ParseError(parser.BadChunkSize) + } +} + +fn parse_hex_digits(bits: BitArray, acc: Int, any: Bool) -> Result(Int, Nil) { + case bits { + <> if byte >= 48 && byte <= 57 -> + parse_hex_digits(remaining, acc * 16 + { byte - 48 }, True) + <> if byte >= 97 && byte <= 102 -> + parse_hex_digits(remaining, acc * 16 + { byte - 87 }, True) + <> if byte >= 65 && byte <= 70 -> + parse_hex_digits(remaining, acc * 16 + { byte - 55 }, True) + _bits if any -> Ok(acc) + _bits -> Error(Nil) + } +} + +/// Whether a slice reaches the end of the current chunk, and so must be +/// followed by the chunk's trailing CRLF. +type Slice { + LastSlice + PartialSlice +} + +fn take_chunk_prefix( + buffer: BitArray, + want: Int, + slice: Slice, +) -> parser.Step(#(BitArray, BitArray)) { + case slice { + LastSlice -> + case buffer { + <> -> + parser.StepDone(#(data, remaining)) + _buffer -> + case bit_array.byte_size(buffer) < want + 2 { + True -> parser.More + False -> parser.ParseError(parser.BadChunkFraming) + } + } + PartialSlice -> + case buffer { + <> -> + parser.StepDone(#(data, remaining)) + _buffer -> parser.More + } + } +} diff --git a/src/ewe/internal/http1/connection.gleam b/src/ewe/internal/http1/connection.gleam new file mode 100644 index 0000000..5dde5f3 --- /dev/null +++ b/src/ewe/internal/http1/connection.gleam @@ -0,0 +1,76 @@ +import gleam/erlang/process +import glisten/socket +import glisten/transport + +/// A handler's HTTP/1 connection. Where to write, where to report back to, +/// and how much of the request body is still unread. +pub type Connection { + Connection( + transport: transport.Transport, + socket: socket.Socket, + self: process.Subject(Signal), + buffer: BitArray, + framing: Framing, + read: Int, + chunk_remaining: Int, + ) +} + +/// How the request body declares its length. +pub type Framing { + Fixed(length: Int) + Chunked + NoBody +} + +/// Handlers run inside the connection process, so they report what they did to +/// the request body and the response stream by messaging it. +pub type Signal { + BodySignal(BodySignal) + StreamSignal(StreamSignal) +} + +pub type BodySignal { + BodyDrained(leftover: BitArray) + BodyAbandoned + BodyProgress(buffer: BitArray, read: Int, chunk_remaining: Int) +} + +pub type StreamSignal { + StreamFinished(keep_alive: KeepAlive) +} + +pub type ResponseWriter { + ResponseWriter( + transport: transport.Transport, + socket: socket.Socket, + self: process.Subject(Signal), + framing: StreamFraming, + keep_alive: KeepAlive, + ) +} + +/// Whether the connection survives the response, or is closed once it is done. +pub type KeepAlive { + KeepAlive + CloseAfterResponse +} + +/// The connection is only reusable when every party to the exchange agrees. +pub fn and_keep_alive(left: KeepAlive, right: KeepAlive) -> KeepAlive { + case left { + KeepAlive -> right + CloseAfterResponse -> CloseAfterResponse + } +} + +/// How a streamed response body delimits itself: `chunked` transfer encoding on +/// HTTP/1.1, or by closing the connection on HTTP/1.0. +pub type StreamFraming { + ChunkedStream + CloseDelimitedStream +} + +pub type SseConnection { + SseConnection(transport: transport.Transport, socket: socket.Socket) +} diff --git a/src/ewe/internal/http1/encoder.gleam b/src/ewe/internal/http1/encoder.gleam new file mode 100644 index 0000000..bd79e4a --- /dev/null +++ b/src/ewe/internal/http1/encoder.gleam @@ -0,0 +1,341 @@ +import ewe/internal/clock +import ewe/internal/connection +import ewe/internal/http1/connection as http1 +import ewe/internal/http1/parser +import gleam/bit_array +import gleam/bytes_tree +import gleam/erlang/process +import gleam/http +import gleam/http/response +import gleam/int +import gleam/list +import gleam/result +import gleam/string +import glisten/socket +import glisten/transport + +pub type EncodeError { + UnsafeHeader(name: String) +} + +type EncodeState { + EncodeState(tree: bytes_tree.BytesTree, keep_alive: http1.KeepAlive) +} + +/// Whatever still has to reach the socket once the head has been written. +pub type Remainder { + NoRemainder + RemainderInline(bytes_tree.BytesTree) + RemainderFile(connection.File) + RemainderStream( + handler: fn(connection.ResponseWriter) -> Nil, + framing: http1.StreamFraming, + ) + RemainderSse(handler: fn(connection.SseConnection) -> connection.Outcome) +} + +pub type Encoded { + Encoded( + head: bytes_tree.BytesTree, + keep_alive: http1.KeepAlive, + remainder: Remainder, + ) +} + +pub fn encode_response( + response: response.Response(connection.Body), + method: http.Method, + version: parser.Version, + keep_alive: http1.KeepAlive, +) -> Result(Encoded, EncodeError) { + use state <- result.try(encode_headers(response.headers)) + let keep_alive = http1.and_keep_alive(keep_alive, state.keep_alive) + let status = response.status + + let encoded = case response.body { + connection.Bytes(tree) -> + sized( + state, + status, + keep_alive, + bytes_tree.byte_size(tree), + RemainderInline(tree), + ) + connection.Text(text) -> + sized( + state, + status, + keep_alive, + string.byte_size(text), + RemainderInline(bytes_tree.from_string(text)), + ) + connection.Empty -> sized(state, status, keep_alive, 0, NoRemainder) + connection.File(data) -> + sized(state, status, keep_alive, data.length, RemainderFile(data)) + connection.Streaming(connection.StreamingMetadata(handler)) -> + encode_stream(state, status, keep_alive, version, handler) + connection.Sse(connection.SseMetadata(handler)) -> + close_delimited(state, status, RemainderSse(handler)) + } + + // A HEAD response keeps the framing headers it would have had, minus the body. + Ok(case method { + http.Head -> Encoded(..encoded, remainder: NoRemainder) + _method -> encoded + }) +} + +fn sized( + state: EncodeState, + status: Int, + keep_alive: http1.KeepAlive, + length: Int, + remainder: Remainder, +) -> Encoded { + let framing = << + "content-length: ":utf8, + int.to_string(length):utf8, + "\r\n":utf8, + >> + + Encoded(build_head(state, status, keep_alive, framing), keep_alive, remainder) +} + +fn encode_stream( + state: EncodeState, + status: Int, + keep_alive: http1.KeepAlive, + version: parser.Version, + handler: fn(connection.ResponseWriter) -> Nil, +) -> Encoded { + case version { + parser.Http11 -> + Encoded( + build_head(state, status, keep_alive, << + "transfer-encoding: chunked\r\n":utf8, + >>), + keep_alive, + RemainderStream(handler:, framing: http1.ChunkedStream), + ) + // HTTP/1.0 has no chunked encoding, so the close delimits the body instead. + parser.Http10 -> + close_delimited( + state, + status, + RemainderStream(handler:, framing: http1.CloseDelimitedStream), + ) + } +} + +fn close_delimited( + state: EncodeState, + status: Int, + remainder: Remainder, +) -> Encoded { + Encoded( + build_head(state, status, http1.CloseAfterResponse, <<>>), + http1.CloseAfterResponse, + remainder, + ) +} + +fn build_head( + state: EncodeState, + status: Int, + keep_alive: http1.KeepAlive, + framing: BitArray, +) -> bytes_tree.BytesTree { + append_date(state.tree) + |> append_connection(keep_alive) + |> bytes_tree.append(framing) + |> bytes_tree.append(<<"\r\n":utf8>>) + |> bytes_tree.prepend(status_line(status)) +} + +pub type ResponseWriter = + http1.ResponseWriter + +const last_chunk = <<"0\r\n\r\n":utf8>> + +fn chunk_frame(chunk: BitArray) -> bytes_tree.BytesTree { + bytes_tree.new() + |> bytes_tree.append_string(int.to_base16(bit_array.byte_size(chunk))) + |> bytes_tree.append(<<"\r\n":utf8>>) + |> bytes_tree.append(chunk) + |> bytes_tree.append(<<"\r\n":utf8>>) +} + +pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { + let bytes = case writer.framing { + http1.ChunkedStream -> chunk_frame(chunk) + http1.CloseDelimitedStream -> bytes_tree.from_bit_array(chunk) + } + let _ = transport.send(writer.transport, writer.socket, bytes) + writer +} + +pub fn finish_chunk(writer: ResponseWriter, chunk: BitArray) -> Nil { + // The terminator rides along with the last chunk to save a write. + let bytes = case writer.framing { + http1.ChunkedStream -> bytes_tree.append(chunk_frame(chunk), last_chunk) + http1.CloseDelimitedStream -> bytes_tree.from_bit_array(chunk) + } + let _ = transport.send(writer.transport, writer.socket, bytes) + finish(writer) +} + +pub fn finish_response(writer: ResponseWriter) -> Nil { + let _ = end_stream(writer.transport, writer.socket, writer.framing) + finish(writer) +} + +pub fn end_stream( + transport: transport.Transport, + socket: socket.Socket, + framing: http1.StreamFraming, +) -> Result(Nil, socket.SocketReason) { + case framing { + http1.ChunkedStream -> + transport.send(transport, socket, bytes_tree.from_bit_array(last_chunk)) + http1.CloseDelimitedStream -> Ok(Nil) + } +} + +fn finish(writer: ResponseWriter) -> Nil { + http1.StreamFinished(keep_alive: writer.keep_alive) + |> http1.StreamSignal + |> process.send(writer.self, _) +} + +fn encode_headers( + headers: List(#(String, String)), +) -> Result(EncodeState, EncodeError) { + let initial = EncodeState(bytes_tree.new(), http1.KeepAlive) + use state, #(name, value) <- list.try_fold(headers, initial) + case name { + "content-length" | "transfer-encoding" | "date" -> Ok(state) + "connection" -> + case parser.find_unsafe_header_byte(value) { + Error(Nil) -> { + let lowered = value |> bit_array.from_string |> parser.lowercase_ascii + let keep_alive = case parser.has_token(lowered, <<"close":utf8>>) { + True -> http1.CloseAfterResponse + False -> state.keep_alive + } + Ok(EncodeState(..state, keep_alive:)) + } + Ok(_position) -> Error(UnsafeHeader(name)) + } + _name -> + case + parser.find_unsafe_header_byte(name), + parser.find_unsafe_header_byte(value) + { + Error(Nil), Error(Nil) -> { + let tree = + bytes_tree.append_string(state.tree, name) + |> bytes_tree.append(<<": ":utf8>>) + |> bytes_tree.append_string(value) + |> bytes_tree.append(<<"\r\n":utf8>>) + + Ok(EncodeState(..state, tree:)) + } + _other, _other -> Error(UnsafeHeader(name)) + } + } +} + +fn append_date(tree: bytes_tree.BytesTree) -> bytes_tree.BytesTree { + bytes_tree.append_string(tree, "date: ") + |> bytes_tree.append(clock.get()) + |> bytes_tree.append(<<"\r\n":utf8>>) +} + +fn append_connection( + tree: bytes_tree.BytesTree, + keep_alive: http1.KeepAlive, +) -> bytes_tree.BytesTree { + let value = case keep_alive { + http1.KeepAlive -> <<"keep-alive":utf8>> + http1.CloseAfterResponse -> <<"close":utf8>> + } + + bytes_tree.append(tree, <<"connection: ":utf8>>) + |> bytes_tree.append(value) + |> bytes_tree.append(<<"\r\n":utf8>>) +} + +fn status_line(status: Int) -> BitArray { + case status { + 100 -> <<"HTTP/1.1 100 Continue\r\n":utf8>> + 101 -> <<"HTTP/1.1 101 Switching Protocols\r\n":utf8>> + 102 -> <<"HTTP/1.1 102 Processing\r\n":utf8>> + 103 -> <<"HTTP/1.1 103 Early Hints\r\n":utf8>> + 200 -> <<"HTTP/1.1 200 OK\r\n":utf8>> + 201 -> <<"HTTP/1.1 201 Created\r\n":utf8>> + 202 -> <<"HTTP/1.1 202 Accepted\r\n":utf8>> + 203 -> <<"HTTP/1.1 203 Non-Authoritative Information\r\n":utf8>> + 204 -> <<"HTTP/1.1 204 No Content\r\n":utf8>> + 205 -> <<"HTTP/1.1 205 Reset Content\r\n":utf8>> + 206 -> <<"HTTP/1.1 206 Partial Content\r\n":utf8>> + 207 -> <<"HTTP/1.1 207 Multi-Status\r\n":utf8>> + 208 -> <<"HTTP/1.1 208 Already Reported\r\n":utf8>> + 226 -> <<"HTTP/1.1 226 IM Used\r\n":utf8>> + 300 -> <<"HTTP/1.1 300 Multiple Choices\r\n":utf8>> + 301 -> <<"HTTP/1.1 301 Moved Permanently\r\n":utf8>> + 302 -> <<"HTTP/1.1 302 Found\r\n":utf8>> + 303 -> <<"HTTP/1.1 303 See Other\r\n":utf8>> + 304 -> <<"HTTP/1.1 304 Not Modified\r\n":utf8>> + 305 -> <<"HTTP/1.1 305 Use Proxy\r\n":utf8>> + 307 -> <<"HTTP/1.1 307 Temporary Redirect\r\n":utf8>> + 308 -> <<"HTTP/1.1 308 Permanent Redirect\r\n":utf8>> + 400 -> <<"HTTP/1.1 400 Bad Request\r\n":utf8>> + 401 -> <<"HTTP/1.1 401 Unauthorized\r\n":utf8>> + 402 -> <<"HTTP/1.1 402 Payment Required\r\n":utf8>> + 403 -> <<"HTTP/1.1 403 Forbidden\r\n":utf8>> + 404 -> <<"HTTP/1.1 404 Not Found\r\n":utf8>> + 405 -> <<"HTTP/1.1 405 Method Not Allowed\r\n":utf8>> + 406 -> <<"HTTP/1.1 406 Not Acceptable\r\n":utf8>> + 407 -> <<"HTTP/1.1 407 Proxy Authentication Required\r\n":utf8>> + 408 -> <<"HTTP/1.1 408 Request Timeout\r\n":utf8>> + 409 -> <<"HTTP/1.1 409 Conflict\r\n":utf8>> + 410 -> <<"HTTP/1.1 410 Gone\r\n":utf8>> + 411 -> <<"HTTP/1.1 411 Length Required\r\n":utf8>> + 412 -> <<"HTTP/1.1 412 Precondition Failed\r\n":utf8>> + 413 -> <<"HTTP/1.1 413 Content Too Large\r\n":utf8>> + 414 -> <<"HTTP/1.1 414 URI Too Long\r\n":utf8>> + 415 -> <<"HTTP/1.1 415 Unsupported Media Type\r\n":utf8>> + 416 -> <<"HTTP/1.1 416 Range Not Satisfiable\r\n":utf8>> + 417 -> <<"HTTP/1.1 417 Expectation Failed\r\n":utf8>> + 418 -> <<"HTTP/1.1 418 I'm a Teapot\r\n":utf8>> + 421 -> <<"HTTP/1.1 421 Misdirected Request\r\n":utf8>> + 422 -> <<"HTTP/1.1 422 Unprocessable Content\r\n":utf8>> + 423 -> <<"HTTP/1.1 423 Locked\r\n":utf8>> + 424 -> <<"HTTP/1.1 424 Failed Dependency\r\n":utf8>> + 425 -> <<"HTTP/1.1 425 Too Early\r\n":utf8>> + 426 -> <<"HTTP/1.1 426 Upgrade Required\r\n":utf8>> + 428 -> <<"HTTP/1.1 428 Precondition Required\r\n":utf8>> + 429 -> <<"HTTP/1.1 429 Too Many Requests\r\n":utf8>> + 431 -> <<"HTTP/1.1 431 Request Header Fields Too Large\r\n":utf8>> + 451 -> <<"HTTP/1.1 451 Unavailable For Legal Reasons\r\n":utf8>> + 500 -> <<"HTTP/1.1 500 Internal Server Error\r\n":utf8>> + 501 -> <<"HTTP/1.1 501 Not Implemented\r\n":utf8>> + 502 -> <<"HTTP/1.1 502 Bad Gateway\r\n":utf8>> + 503 -> <<"HTTP/1.1 503 Service Unavailable\r\n":utf8>> + 504 -> <<"HTTP/1.1 504 Gateway Timeout\r\n":utf8>> + 505 -> <<"HTTP/1.1 505 HTTP parser.Version Not Supported\r\n":utf8>> + 506 -> <<"HTTP/1.1 506 Variant Also Negotiates\r\n":utf8>> + 507 -> <<"HTTP/1.1 507 Insufficient Storage\r\n":utf8>> + 508 -> <<"HTTP/1.1 508 Loop Detected\r\n":utf8>> + 510 -> <<"HTTP/1.1 510 Not Extended\r\n":utf8>> + 511 -> <<"HTTP/1.1 511 Network Authentication Required\r\n":utf8>> + _other -> <<"HTTP/1.1 ":utf8, int.to_string(status):utf8, " \r\n":utf8>> + } +} + +pub fn internal_server_error() -> bytes_tree.BytesTree { + EncodeState(bytes_tree.new(), http1.CloseAfterResponse) + |> build_head(500, http1.CloseAfterResponse, << + "content-length: 0\r\n":utf8, + >>) +} diff --git a/src/ewe/internal/http1/parser.gleam b/src/ewe/internal/http1/parser.gleam new file mode 100644 index 0000000..3fcab54 --- /dev/null +++ b/src/ewe/internal/http1/parser.gleam @@ -0,0 +1,675 @@ +import ewe/internal/http1/connection as http1 +import gleam/bit_array +import gleam/http +import gleam/int +import gleam/list +import gleam/option + +pub type Version { + Http10 + Http11 +} + +pub type Head { + Head( + method: http.Method, + host: String, + port: option.Option(Int), + path: String, + query: option.Option(String), + version: Version, + headers: List(#(String, String)), + ) +} + +pub type Metadata { + Metadata( + framing: http1.Framing, + keep_alive: http1.KeepAlive, + upgrade: option.Option(String), + ) +} + +pub type ParseError { + RequestLineTooLong + BadRequestLine + BadMethod + BadTarget + BadVersion + HeaderLineTooLong + BadHeader + TooManyHeaders + DuplicateContentLength + BadContentLength + DuplicateHost + BadHost + MissingHost + AmbiguousFraming + ChunkSizeLineTooLong + BadChunkSize + BadChunkFraming + ChunkTooLarge + BodyReadFailed +} + +pub fn error_to_string(error: ParseError) -> String { + case error { + RequestLineTooLong -> + "request line exceeds " <> int.to_string(max_request_line) <> " bytes" + BadRequestLine -> "malformed request line" + BadMethod -> "invalid request method" + BadTarget -> "invalid request target" + BadVersion -> "unsupported or malformed HTTP version" + HeaderLineTooLong -> + "header line exceeds " <> int.to_string(max_header_line) <> " bytes" + BadHeader -> "malformed header line" + TooManyHeaders -> + "too many headers (max " <> int.to_string(max_headers) <> ")" + DuplicateContentLength -> "duplicate Content-Length header" + BadContentLength -> "invalid Content-Length value" + DuplicateHost -> "duplicate Host header" + BadHost -> "invalid Host header" + MissingHost -> "missing required Host header" + AmbiguousFraming -> + "conflicting Content-Length and Transfer-Encoding headers" + ChunkSizeLineTooLong -> + "chunk size line exceeds " + <> int.to_string(max_chunk_size_line) + <> " bytes" + BadChunkSize -> "malformed chunk size" + BadChunkFraming -> "malformed chunk data framing" + ChunkTooLarge -> "chunked body exceeds size limit" + BodyReadFailed -> "failed to read request body from the socket" + } +} + +pub type Parsed { + Complete(head: Head, metadata: Metadata, remaining: BitArray) + Incomplete +} + +const max_request_line = 8192 + +const max_header_line = 8192 + +const max_headers = 100 + +pub const max_chunk_size_line = 128 + +pub fn parse(buffer: BitArray) -> Result(Parsed, ParseError) { + let step = { + use #(method, target, version, remaining) <- try_step(parse_request_line( + buffer, + )) + + use #(headers, state, remaining) <- try_step(parse_headers( + remaining, + [], + 0, + initial_header_state(), + )) + + use #(host, port, path, query) <- try_step(resolve_target( + method, + target, + version, + state.host, + )) + + use metadata <- try_step(resolve_metadata(state, version)) + + StepDone(Complete( + Head(method:, host:, port:, path:, query:, version:, headers:), + metadata, + remaining, + )) + } + + case step { + StepDone(parsed) -> Ok(parsed) + More -> Ok(Incomplete) + ParseError(error) -> Error(error) + } +} + +pub type Step(a) { + StepDone(a) + More + ParseError(ParseError) +} + +pub fn try_step(step: Step(a), next: fn(a) -> Step(b)) -> Step(b) { + case step { + StepDone(value) -> next(value) + More -> More + ParseError(error) -> ParseError(error) + } +} + +fn parse_request_line( + buffer: BitArray, +) -> Step(#(http.Method, BitArray, Version, BitArray)) { + use #(line, remaining) <- try_step(extract_line( + buffer, + max_request_line, + RequestLineTooLong, + BadRequestLine, + )) + + use #(method, target_and_version) <- try_step(parse_method(line)) + + use #(target, version) <- try_step(parse_target_version(target_and_version)) + + StepDone(#(method, target, version, remaining)) +} + +fn parse_method(line: BitArray) -> Step(#(http.Method, BitArray)) { + case line { + <<"GET ":utf8, remaining:bits>> -> StepDone(#(http.Get, remaining)) + <<"POST ":utf8, remaining:bits>> -> StepDone(#(http.Post, remaining)) + <<"PUT ":utf8, remaining:bits>> -> StepDone(#(http.Put, remaining)) + <<"DELETE ":utf8, remaining:bits>> -> StepDone(#(http.Delete, remaining)) + <<"HEAD ":utf8, remaining:bits>> -> StepDone(#(http.Head, remaining)) + <<"OPTIONS ":utf8, remaining:bits>> -> StepDone(#(http.Options, remaining)) + <<"PATCH ":utf8, remaining:bits>> -> StepDone(#(http.Patch, remaining)) + <<"TRACE ":utf8, remaining:bits>> -> StepDone(#(http.Trace, remaining)) + <<"CONNECT ":utf8, remaining:bits>> -> StepDone(#(http.Connect, remaining)) + _other -> parse_other_method(line) + } +} + +fn parse_other_method(line: BitArray) -> Step(#(http.Method, BitArray)) { + case find_space(line) { + Error(Nil) -> ParseError(BadRequestLine) + Ok(position) -> + case line { + <> -> + case bit_array_to_string(name) { + Error(Nil) -> ParseError(BadMethod) + Ok(name) -> + case http.parse_method(name) { + Ok(method) -> StepDone(#(method, remaining)) + Error(Nil) -> ParseError(BadMethod) + } + } + _line -> ParseError(BadRequestLine) + } + } +} + +fn parse_target_version(bits: BitArray) -> Step(#(BitArray, Version)) { + let size = bit_array.byte_size(bits) + + case size < 10 { + True -> ParseError(BadRequestLine) + False -> { + let target_size = size - 9 + case bits { + <> -> + StepDone(#(target, Http11)) + <> -> + StepDone(#(target, Http10)) + _bits -> ParseError(BadVersion) + } + } + } +} + +fn split_target(target: BitArray) -> Step(#(String, option.Option(String))) { + case find_question(target) { + Error(Nil) -> { + use path <- try_step(decode_component(target, BadTarget)) + StepDone(#(path, option.None)) + } + Ok(position) -> { + let size = bit_array.byte_size(target) + + case target { + << + path:bytes-size(position), + "?":utf8, + query:bytes-size(size - position - 1), + >> -> { + use path <- try_step(decode_component(path, BadTarget)) + use query <- try_step(decode_component(query, BadTarget)) + StepDone(#(path, option.Some(query))) + } + _target -> ParseError(BadTarget) + } + } + } +} + +fn decode_component(bits: BitArray, on_error: ParseError) -> Step(String) { + case bit_array_to_string(bits) { + Ok(value) -> StepDone(value) + Error(Nil) -> ParseError(on_error) + } +} + +fn validate_path(method: http.Method, path: String) -> Step(String) { + case path, method { + "*", http.Options -> StepDone(path) + "/" <> _remaining, _method -> StepDone(path) + _path, _method -> ParseError(BadTarget) + } +} + +fn resolve_target( + method: http.Method, + target: BitArray, + version: Version, + header_host: option.Option(#(String, option.Option(Int))), +) -> Step(#(String, option.Option(Int), String, option.Option(String))) { + case method { + http.Connect -> + case split_host_port(target) { + Ok(#(host, option.Some(_port) as port)) -> + case bit_array_to_string(host) { + Ok(host) -> StepDone(#(host, port, "", option.None)) + Error(Nil) -> ParseError(BadTarget) + } + Ok(#(_host, option.None)) -> ParseError(BadTarget) + Error(Nil) -> ParseError(BadTarget) + } + _method -> { + use #(path, query) <- try_step(split_target(target)) + use path <- try_step(validate_path(method, path)) + use #(host, port) <- try_step(resolve_host(version, header_host)) + StepDone(#(host, port, path, query)) + } + } +} + +fn resolve_host( + version: Version, + header_host: option.Option(#(String, option.Option(Int))), +) -> Step(#(String, option.Option(Int))) { + case header_host, version { + option.Some(host_port), _version -> StepDone(host_port) + option.None, Http10 -> StepDone(#("", option.None)) + option.None, Http11 -> ParseError(MissingHost) + } +} + +fn split_host_port( + value: BitArray, +) -> Result(#(BitArray, option.Option(Int)), Nil) { + case value { + <<"[":utf8, _remaining:bits>> -> split_bracketed_host(value) + _value -> + case find_colon(value) { + Error(Nil) -> Ok(#(value, option.None)) + Ok(position) -> { + let size = bit_array.byte_size(value) + case value { + << + host:bytes-size(position), + ":":utf8, + port:bytes-size(size - position - 1), + >> -> + case parse_port(port) { + Ok(port) -> Ok(#(host, option.Some(port))) + Error(Nil) -> Error(Nil) + } + _value -> Error(Nil) + } + } + } + } +} + +fn split_bracketed_host( + value: BitArray, +) -> Result(#(BitArray, option.Option(Int)), Nil) { + case find_close_bracket(value) { + Error(Nil) -> Error(Nil) + Ok(position) -> { + let size = bit_array.byte_size(value) + + case value { + << + host:bytes-size(position + 1), + remaining:bytes-size(size - position - 1), + >> -> + case remaining { + <<>> -> Ok(#(host, option.None)) + <<":":utf8, port:bits>> -> + case parse_port(port) { + Ok(port) -> Ok(#(host, option.Some(port))) + Error(Nil) -> Error(Nil) + } + _remaining -> Error(Nil) + } + _value -> Error(Nil) + } + } + } +} + +fn parse_port(bits: BitArray) -> Result(Int, Nil) { + case bits { + <<>> -> Error(Nil) + _bits -> parse_port_digits(bits, 0) + } +} + +fn parse_port_digits(bits: BitArray, acc: Int) -> Result(Int, Nil) { + case bits { + <<>> -> + case acc <= 65_535 { + True -> Ok(acc) + False -> Error(Nil) + } + <> if byte >= 48 && byte <= 57 -> + parse_port_digits(remaining, acc * 10 + { byte - 48 }) + _bits -> Error(Nil) + } +} + +/// What the request's `Connection` header asked for, before the version's +/// default is applied. +pub type ConnectionIntent { + RequestedKeepAlive + RequestedClose + NothingRequested +} + +pub type HeaderState { + HeaderState( + content_length: option.Option(Int), + chunked: Bool, + connection: ConnectionIntent, + connection_upgrade: Bool, + upgrade: option.Option(String), + host: option.Option(#(String, option.Option(Int))), + ) +} + +pub fn initial_header_state() -> HeaderState { + HeaderState( + content_length: option.None, + chunked: False, + connection: NothingRequested, + connection_upgrade: False, + upgrade: option.None, + host: option.None, + ) +} + +fn resolve_metadata(state: HeaderState, version: Version) -> Step(Metadata) { + case state.content_length, state.chunked { + option.Some(_length), True -> ParseError(AmbiguousFraming) + option.Some(length), False -> + StepDone(complete_metadata(state, version, http1.Fixed(length))) + option.None, True -> + StepDone(complete_metadata(state, version, http1.Chunked)) + option.None, False -> + StepDone(complete_metadata(state, version, http1.NoBody)) + } +} + +fn complete_metadata( + state: HeaderState, + version: Version, + framing: http1.Framing, +) -> Metadata { + let keep_alive = case state.connection, version { + RequestedKeepAlive, _version -> http1.KeepAlive + RequestedClose, _version -> http1.CloseAfterResponse + NothingRequested, Http11 -> http1.KeepAlive + NothingRequested, Http10 -> http1.CloseAfterResponse + } + let upgrade = case state.connection_upgrade { + True -> state.upgrade + False -> option.None + } + + Metadata(framing:, keep_alive:, upgrade:) +} + +pub fn parse_headers( + buffer: BitArray, + acc: List(#(String, String)), + count: Int, + state: HeaderState, +) -> Step(#(List(#(String, String)), HeaderState, BitArray)) { + use #(line, remaining) <- try_step(extract_line( + buffer, + max_header_line, + HeaderLineTooLong, + BadHeader, + )) + + case line { + <<>> -> StepDone(#(list.reverse(acc), state, remaining)) + _line if count >= max_headers -> ParseError(TooManyHeaders) + _line -> { + use #(header, state) <- try_step(parse_header_line(line, state)) + parse_headers(remaining, [header, ..acc], count + 1, state) + } + } +} + +fn parse_header_line( + line: BitArray, + state: HeaderState, +) -> Step(#(#(String, String), HeaderState)) { + case find_colon(line) { + Error(Nil) -> ParseError(BadHeader) + Ok(0) -> ParseError(BadHeader) + Ok(position) -> + case line { + <> -> { + let name = lowercase_ascii(name) + let value = trim_ows(value) + case bit_array_to_string(name), bit_array_to_string(value) { + Ok(name), Ok(value) -> { + use state <- try_step(classify(name, value, state)) + StepDone(#(#(name, value), state)) + } + _other, _other -> ParseError(BadHeader) + } + } + _bad -> ParseError(BadHeader) + } + } +} + +fn classify( + name: String, + value: String, + state: HeaderState, +) -> Step(HeaderState) { + case name { + "content-length" -> + case state.content_length { + option.Some(_length) -> ParseError(DuplicateContentLength) + option.None -> + case int.parse(value) { + Ok(length) if length >= 0 -> + StepDone( + HeaderState(..state, content_length: option.Some(length)), + ) + _bad -> ParseError(BadContentLength) + } + } + "transfer-encoding" -> { + let lowered = value |> bit_array.from_string |> lowercase_ascii + let chunked = state.chunked || has_token(lowered, <<"chunked":utf8>>) + StepDone(HeaderState(..state, chunked:)) + } + "connection" -> { + let lowered = value |> bit_array.from_string |> lowercase_ascii + let connection = case + has_token(lowered, <<"close":utf8>>), + has_token(lowered, <<"keep-alive":utf8>>) + { + True, _keep_alive -> RequestedClose + False, True -> RequestedKeepAlive + False, False -> state.connection + } + let connection_upgrade = + state.connection_upgrade || has_token(lowered, <<"upgrade":utf8>>) + StepDone(HeaderState(..state, connection:, connection_upgrade:)) + } + "upgrade" -> { + let lowered = + value + |> bit_array.from_string + |> lowercase_ascii + |> unsafe_to_string + StepDone(HeaderState(..state, upgrade: option.Some(lowered))) + } + "host" -> + case state.host { + option.Some(_host) -> ParseError(DuplicateHost) + option.None -> + case split_host_port(bit_array.from_string(value)) { + Ok(#(host, port)) -> { + let host = option.Some(#(unsafe_to_string(host), port)) + StepDone(HeaderState(..state, host:)) + } + Error(Nil) -> ParseError(BadHost) + } + } + _other -> StepDone(state) + } +} + +pub fn extract_line( + buffer: BitArray, + max_len: Int, + too_long: ParseError, + malformed: ParseError, +) -> Step(#(BitArray, BitArray)) { + case find_lf(buffer) { + Error(Nil) -> + case bit_array.byte_size(buffer) > max_len { + True -> ParseError(too_long) + False -> More + } + Ok(0) -> ParseError(malformed) + Ok(position) -> + case buffer { + <> -> + StepDone(#(line, remaining)) + _bad -> ParseError(malformed) + } + } +} + +pub fn lowercase_ascii(bits: BitArray) -> BitArray { + case has_uppercase(bits) { + False -> bits + True -> lowercase_walk(bits) |> list_to_bit_array + } +} + +fn has_uppercase(bits: BitArray) -> Bool { + case bits { + <<>> -> False + <> if byte >= 65 && byte <= 90 -> True + <<_byte, remaining:bits>> -> has_uppercase(remaining) + _other -> False + } +} + +fn lowercase_walk(bits: BitArray) -> List(Int) { + case bits { + <<>> -> [] + <<"A", remaining:bits>> -> [0x61, ..lowercase_walk(remaining)] + <<"B", remaining:bits>> -> [0x62, ..lowercase_walk(remaining)] + <<"C", remaining:bits>> -> [0x63, ..lowercase_walk(remaining)] + <<"D", remaining:bits>> -> [0x64, ..lowercase_walk(remaining)] + <<"E", remaining:bits>> -> [0x65, ..lowercase_walk(remaining)] + <<"F", remaining:bits>> -> [0x66, ..lowercase_walk(remaining)] + <<"G", remaining:bits>> -> [0x67, ..lowercase_walk(remaining)] + <<"H", remaining:bits>> -> [0x68, ..lowercase_walk(remaining)] + <<"I", remaining:bits>> -> [0x69, ..lowercase_walk(remaining)] + <<"J", remaining:bits>> -> [0x6A, ..lowercase_walk(remaining)] + <<"K", remaining:bits>> -> [0x6B, ..lowercase_walk(remaining)] + <<"L", remaining:bits>> -> [0x6C, ..lowercase_walk(remaining)] + <<"M", remaining:bits>> -> [0x6D, ..lowercase_walk(remaining)] + <<"N", remaining:bits>> -> [0x6E, ..lowercase_walk(remaining)] + <<"O", remaining:bits>> -> [0x6F, ..lowercase_walk(remaining)] + <<"P", remaining:bits>> -> [0x70, ..lowercase_walk(remaining)] + <<"Q", remaining:bits>> -> [0x71, ..lowercase_walk(remaining)] + <<"R", remaining:bits>> -> [0x72, ..lowercase_walk(remaining)] + <<"S", remaining:bits>> -> [0x73, ..lowercase_walk(remaining)] + <<"T", remaining:bits>> -> [0x74, ..lowercase_walk(remaining)] + <<"U", remaining:bits>> -> [0x75, ..lowercase_walk(remaining)] + <<"V", remaining:bits>> -> [0x76, ..lowercase_walk(remaining)] + <<"W", remaining:bits>> -> [0x77, ..lowercase_walk(remaining)] + <<"X", remaining:bits>> -> [0x78, ..lowercase_walk(remaining)] + <<"Y", remaining:bits>> -> [0x79, ..lowercase_walk(remaining)] + <<"Z", remaining:bits>> -> [0x7A, ..lowercase_walk(remaining)] + <> -> [byte, ..lowercase_walk(remaining)] + _other -> [] + } +} + +fn trim_ows(bits: BitArray) -> BitArray { + bits + |> trim_leading_ows + |> trim_trailing_ows +} + +fn trim_leading_ows(bits: BitArray) -> BitArray { + case bits { + <<" ", remaining:bits>> -> trim_leading_ows(remaining) + <<"\t", remaining:bits>> -> trim_leading_ows(remaining) + _bits -> bits + } +} + +fn trim_trailing_ows(bits: BitArray) -> BitArray { + case bit_array.byte_size(bits) { + 0 -> bits + size -> + case bits { + <> -> trim_trailing_ows(init) + <> -> trim_trailing_ows(init) + _bits -> bits + } + } +} + +pub fn has_token(value: BitArray, token: BitArray) -> Bool { + case value == token { + True -> True + False -> + split_comma(value) + |> list.any(fn(part) { trim_ows(part) == token }) + } +} + +@external(erlang, "http1_ffi", "find_lf") +fn find_lf(bits: BitArray) -> Result(Int, Nil) + +@external(erlang, "http1_ffi", "find_colon") +fn find_colon(bits: BitArray) -> Result(Int, Nil) + +@external(erlang, "http1_ffi", "find_space") +fn find_space(bits: BitArray) -> Result(Int, Nil) + +@external(erlang, "http1_ffi", "find_question") +fn find_question(bits: BitArray) -> Result(Int, Nil) + +@external(erlang, "http1_ffi", "find_close_bracket") +fn find_close_bracket(bits: BitArray) -> Result(Int, Nil) + +@external(erlang, "http1_ffi", "find_unsafe_header_byte") +pub fn find_unsafe_header_byte(value: String) -> Result(Int, Nil) + +@external(erlang, "http1_ffi", "split_comma") +fn split_comma(bits: BitArray) -> List(BitArray) + +@external(erlang, "http1_ffi", "list_to_bit_array") +fn list_to_bit_array(bytes: List(Int)) -> BitArray + +@external(erlang, "http1_ffi", "bit_array_to_string") +fn bit_array_to_string(bits: BitArray) -> Result(String, Nil) + +@external(erlang, "ewe_ffi", "identity") +fn unsafe_to_string(bits: BitArray) -> String diff --git a/test/ewe/internal/http1_test.gleam b/test/ewe/internal/http1/parser_test.gleam similarity index 63% rename from test/ewe/internal/http1_test.gleam rename to test/ewe/internal/http1/parser_test.gleam index 8bd3e09..367f9c0 100644 --- a/test/ewe/internal/http1_test.gleam +++ b/test/ewe/internal/http1/parser_test.gleam @@ -1,5 +1,5 @@ -import ewe/internal/connection -import ewe/internal/http1 +import ewe/internal/http1/connection as http1 +import ewe/internal/http1/parser import gleam/http import gleam/option.{None, Some} @@ -8,25 +8,26 @@ pub fn simple_get_test() { "GET /foo?a=1 HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(head, metadata, remaining)) = http1.parse(buffer) + let assert Ok(parser.Complete(head, metadata, remaining)) = + parser.parse(buffer) assert head.method == http.Get assert head.host == "example.com" assert head.port == None assert head.path == "/foo" assert head.query == Some("a=1") - assert head.version == http1.Http11 + assert head.version == parser.Http11 assert head.headers == [#("host", "example.com"), #("connection", "keep-alive")] - assert metadata.keep_alive - assert metadata.framing == connection.NoBody + assert metadata.keep_alive == http1.KeepAlive + assert metadata.framing == http1.NoBody assert remaining == <<>> } pub fn host_with_port_test() { let buffer = <<"GET / HTTP/1.1\r\nHost: example.com:8080\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.host == "example.com" assert head.port == Some(8080) @@ -34,8 +35,8 @@ pub fn host_with_port_test() { pub fn host_ipv6_with_port_test() { let buffer = <<"GET / HTTP/1.1\r\nHost: [::1]:8080\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.host == "[::1]" assert head.port == Some(8080) @@ -43,13 +44,13 @@ pub fn host_ipv6_with_port_test() { pub fn missing_host_on_http11_rejected_test() { let buffer = <<"GET / HTTP/1.1\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.MissingHost) + assert parser.parse(buffer) == Error(parser.MissingHost) } pub fn missing_host_on_http10_allowed_test() { let buffer = <<"GET / HTTP/1.0\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.host == "" assert head.port == None @@ -57,31 +58,31 @@ pub fn missing_host_on_http10_allowed_test() { pub fn duplicate_host_rejected_test() { let buffer = <<"GET / HTTP/1.1\r\nHost: a.com\r\nHost: b.com\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.DuplicateHost) + assert parser.parse(buffer) == Error(parser.DuplicateHost) } pub fn relative_path_rejected_test() { let buffer = <<"GET foo HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.BadTarget) + assert parser.parse(buffer) == Error(parser.BadTarget) } pub fn asterisk_form_allowed_for_options_test() { let buffer = <<"OPTIONS * HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.path == "*" } pub fn asterisk_form_rejected_for_get_test() { let buffer = <<"GET * HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.BadTarget) + assert parser.parse(buffer) == Error(parser.BadTarget) } pub fn connect_authority_form_test() { let buffer = <<"CONNECT example.com:443 HTTP/1.1\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.host == "example.com" assert head.port == Some(443) @@ -91,7 +92,7 @@ pub fn connect_authority_form_test() { pub fn connect_without_port_rejected_test() { let buffer = <<"CONNECT example.com HTTP/1.1\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.BadTarget) + assert parser.parse(buffer) == Error(parser.BadTarget) } pub fn mixed_case_and_ows_headers_test() { @@ -99,7 +100,8 @@ pub fn mixed_case_and_ows_headers_test() { "POST /submit HTTP/1.1\r\nHost: example.com\r\nContent-Length: 13 \r\nConnection: close\r\n\r\nHELLO WORLD!!":utf8, >> - let assert Ok(http1.Complete(head, metadata, remaining)) = http1.parse(buffer) + let assert Ok(parser.Complete(head, metadata, remaining)) = + parser.parse(buffer) assert head.headers == [ @@ -107,8 +109,8 @@ pub fn mixed_case_and_ows_headers_test() { #("content-length", "13"), #("connection", "close"), ] - assert metadata.framing == connection.Fixed(13) - assert !metadata.keep_alive + assert metadata.framing == http1.Fixed(13) + assert metadata.keep_alive == http1.CloseAfterResponse assert remaining == <<"HELLO WORLD!!":utf8>> } @@ -117,8 +119,8 @@ pub fn tab_ows_trimmed_test() { "GET / HTTP/1.1\r\nHost: example.com\r\nX-Name:\t\tvalue\t\t\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.headers == [#("host", "example.com"), #("x-name", "value")] } @@ -128,10 +130,10 @@ pub fn chunked_transfer_encoding_test() { "PUT /x HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert metadata.framing == connection.Chunked + assert metadata.framing == http1.Chunked } pub fn transfer_encoding_chunked_among_multiple_tokens_test() { @@ -139,10 +141,10 @@ pub fn transfer_encoding_chunked_among_multiple_tokens_test() { "PUT /x HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: gzip, chunked\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert metadata.framing == connection.Chunked + assert metadata.framing == http1.Chunked } pub fn conflicting_content_length_and_chunked_rejected_test() { @@ -150,7 +152,7 @@ pub fn conflicting_content_length_and_chunked_rejected_test() { "POST / HTTP/1.1\r\nHost: example.com\r\nContent-Length: 5\r\nTransfer-Encoding: chunked\r\n\r\nhello":utf8, >> - assert http1.parse(buffer) == Error(http1.AmbiguousFraming) + assert parser.parse(buffer) == Error(parser.AmbiguousFraming) } pub fn conflicting_transfer_encoding_and_content_length_reversed_order_rejected_test() { @@ -158,7 +160,7 @@ pub fn conflicting_transfer_encoding_and_content_length_reversed_order_rejected_ "POST / HTTP/1.1\r\nHost: example.com\r\nTransfer-Encoding: chunked\r\nContent-Length: 5\r\n\r\nhello":utf8, >> - assert http1.parse(buffer) == Error(http1.AmbiguousFraming) + assert parser.parse(buffer) == Error(parser.AmbiguousFraming) } pub fn connection_close_lookalike_is_not_close_test() { @@ -166,10 +168,11 @@ pub fn connection_close_lookalike_is_not_close_test() { "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close-enough\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert metadata.keep_alive as "\"close-enough\" is not the \"close\" token" + assert metadata.keep_alive == http1.KeepAlive + as "\"close-enough\" is not the \"close\" token" } pub fn connection_close_among_multiple_tokens_test() { @@ -177,84 +180,85 @@ pub fn connection_close_among_multiple_tokens_test() { "GET / HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade, Close\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert !metadata.keep_alive + assert metadata.keep_alive == http1.CloseAfterResponse } pub fn http11_defaults_to_keep_alive_test() { let buffer = <<"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert metadata.keep_alive + assert metadata.keep_alive == http1.KeepAlive as "HTTP/1.1 without Connection defaults to keep-alive" } pub fn http10_defaults_to_close_test() { let buffer = <<"GET / HTTP/1.0\r\n\r\n":utf8>> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert !metadata.keep_alive as "HTTP/1.0 without Connection defaults to close" + assert metadata.keep_alive == http1.CloseAfterResponse + as "HTTP/1.0 without Connection defaults to close" } pub fn http10_explicit_keep_alive_test() { let buffer = <<"GET / HTTP/1.0\r\nConnection: keep-alive\r\n\r\n":utf8>> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert metadata.keep_alive + assert metadata.keep_alive == http1.KeepAlive } pub fn http10_explicit_close_stays_close_test() { let buffer = <<"GET / HTTP/1.0\r\nConnection: close\r\n\r\n":utf8>> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) - assert !metadata.keep_alive + assert metadata.keep_alive == http1.CloseAfterResponse } pub fn custom_method_test() { let buffer = <<"PROPFIND /dav HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.method == http.Other("PROPFIND") } pub fn incomplete_request_line_test() { let buffer = <<"GET /foo HTTP/1.1\r\n":utf8>> - assert http1.parse(buffer) == Ok(http1.Incomplete) + assert parser.parse(buffer) == Ok(parser.Incomplete) } pub fn incomplete_headers_test() { let buffer = <<"GET /foo HTTP/1.1\r\nHost: example.com\r\n":utf8>> - assert http1.parse(buffer) == Ok(http1.Incomplete) + assert parser.parse(buffer) == Ok(parser.Incomplete) } pub fn split_across_reads_test() { let first = <<"GET / HTTP/1.1\r\nHo":utf8>> - assert http1.parse(first) == Ok(http1.Incomplete) + assert parser.parse(first) == Ok(parser.Incomplete) let second = <<"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(second) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(second) assert head.path == "/" } pub fn bad_request_line_test() { let buffer = <<"GET /foo HTTP/9.9\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.BadVersion) + assert parser.parse(buffer) == Error(parser.BadVersion) } pub fn duplicate_content_length_test() { let buffer = << "GET / HTTP/1.1\r\nContent-Length: 1\r\nContent-Length: 2\r\n\r\n":utf8, >> - assert http1.parse(buffer) == Error(http1.DuplicateContentLength) + assert parser.parse(buffer) == Error(parser.DuplicateContentLength) } pub fn multibyte_utf8_header_value_test() { @@ -262,8 +266,8 @@ pub fn multibyte_utf8_header_value_test() { "GET / HTTP/1.1\r\nHost: example.com\r\nX-Name: caf\u{00E9}\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(head, _metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(head, _metadata, _remaining)) = + parser.parse(buffer) assert head.headers == [#("host", "example.com"), #("x-name", "café")] } @@ -275,32 +279,32 @@ pub fn invalid_utf8_header_value_rejected_test() { "\r\n\r\n":utf8, >> - assert http1.parse(buffer) == Error(http1.BadHeader) + assert parser.parse(buffer) == Error(parser.BadHeader) } pub fn bare_lf_rejected_test() { let buffer = <<"GET / HTTP/1.1\nHost: example.com\r\n\r\n":utf8>> - assert http1.parse(buffer) == Error(http1.BadRequestLine) + assert parser.parse(buffer) == Error(parser.BadRequestLine) } pub fn no_query_string_test() { let buffer = <<"GET /plain HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - assert http1.parse(buffer) + assert parser.parse(buffer) == Ok( - http1.Complete( - http1.Head( + parser.Complete( + parser.Head( method: http.Get, host: "example.com", port: None, path: "/plain", query: None, - version: http1.Http11, + version: parser.Http11, headers: [#("host", "example.com")], ), - http1.Metadata( - framing: connection.NoBody, - keep_alive: True, + parser.Metadata( + framing: http1.NoBody, + keep_alive: http1.KeepAlive, upgrade: None, ), <<>>, @@ -313,8 +317,8 @@ pub fn websocket_upgrade_requested_test() { "GET /ws HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) assert metadata.upgrade == Some("websocket") } @@ -324,8 +328,8 @@ pub fn upgrade_token_case_insensitive_test() { "GET /ws HTTP/1.1\r\nHost: example.com\r\nConnection: Upgrade\r\nUpgrade: WebSocket\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) assert metadata.upgrade == Some("websocket") } @@ -335,8 +339,8 @@ pub fn upgrade_among_multiple_connection_tokens_test() { "GET /h2c HTTP/1.1\r\nHost: example.com\r\nConnection: keep-alive, Upgrade\r\nUpgrade: h2c\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) assert metadata.upgrade == Some("h2c") } @@ -346,8 +350,8 @@ pub fn upgrade_header_without_connection_token_ignored_test() { "GET /ws HTTP/1.1\r\nHost: example.com\r\nUpgrade: websocket\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) assert metadata.upgrade == None as "Upgrade requires Connection: upgrade to be honored (RFC 9110 §7.8)" @@ -358,8 +362,8 @@ pub fn upgrade_header_before_connection_header_test() { "GET /ws HTTP/1.1\r\nHost: example.com\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n":utf8, >> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) assert metadata.upgrade == Some("websocket") as "order of Upgrade vs. Connection headers shouldn't matter" @@ -367,8 +371,8 @@ pub fn upgrade_header_before_connection_header_test() { pub fn no_upgrade_requested_test() { let buffer = <<"GET / HTTP/1.1\r\nHost: example.com\r\n\r\n":utf8>> - let assert Ok(http1.Complete(_head, metadata, _remaining)) = - http1.parse(buffer) + let assert Ok(parser.Complete(_head, metadata, _remaining)) = + parser.parse(buffer) assert metadata.upgrade == None }