diff --git a/.gitignore b/.gitignore index e0ba6f6..8df2bb4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ *.ez /build erl_crash.dump +.claude +CLAUDE.md /src/preview.gleam autobahn/server # examples diff --git a/CHANGELOG.md b/CHANGELOG.md index aff3089..6e09f9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +# Unreleased + +- Gracefully handle HTTP/2 connections: h2c upgrade requests are served as HTTP/1.1, and direct HTTP/2 connections receive a GOAWAY with HTTP_1_1_REQUIRED instead of being silently dropped. +- Fixed HTTP/2 prior-knowledge detection in ffi + # v3.0.1 - 25.01.2026 - Fix README file diff --git a/examples/manifest.toml b/examples/manifest.toml index eb7c6c5..22f0329 100644 --- a/examples/manifest.toml +++ b/examples/manifest.toml @@ -3,13 +3,13 @@ packages = [ { name = "compresso", version = "0.1.0", build_tools = ["gleam"], requirements = ["exception", "gleam_erlang", "gleam_stdlib", "gleam_yielder", "logging"], otp_app = "compresso", source = "hex", outer_checksum = "8BE29A1EDA42F70826ED148EAE40C46BB3FC18E78FE472663DB01DD4A38172D4" }, - { name = "ewe", version = "2.1.2", build_tools = ["gleam"], requirements = ["compresso", "exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "logging", "websocks"], source = "local", path = ".." }, + { name = "ewe", version = "3.0.1", build_tools = ["gleam"], requirements = ["compresso", "exception", "gleam_erlang", "gleam_http", "gleam_otp", "gleam_stdlib", "glisten", "logging", "websocks"], source = "local", path = ".." }, { name = "exception", version = "2.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "exception", source = "hex", outer_checksum = "329D269D5C2A314F7364BD2711372B6F2C58FA6F39981572E5CA68624D291F8C" }, { name = "gleam_crypto", version = "1.5.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_crypto", source = "hex", outer_checksum = "50774BAFFF1144E7872814C566C5D653D83A3EBF23ACC3156B757A1B6819086E" }, { name = "gleam_erlang", version = "1.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_erlang", source = "hex", outer_checksum = "1124AD3AA21143E5AF0FC5CF3D9529F6DB8CA03E43A55711B60B6B7B3874375C" }, { name = "gleam_http", version = "4.3.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_http", source = "hex", outer_checksum = "82EA6A717C842456188C190AFB372665EA56CE13D8559BF3B1DD9E40F619EE0C" }, { name = "gleam_otp", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_stdlib"], otp_app = "gleam_otp", source = "hex", outer_checksum = "BA6A294E295E428EC1562DC1C11EA7530DCB981E8359134BEABC8493B7B2258E" }, - { name = "gleam_stdlib", version = "0.68.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "F7FAEBD8EF260664E86A46C8DBA23508D1D11BB3BCC6EE1B89B3BC3E5C83FF1E" }, + { name = "gleam_stdlib", version = "0.69.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "AAB0962BEBFAA67A2FBEE9EEE218B057756808DC9AF77430F5182C6115B3A315" }, { name = "gleam_yielder", version = "1.1.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleam_yielder", source = "hex", outer_checksum = "8E4E4ECFA7982859F430C57F549200C7749823C106759F4A19A78AEA6687717A" }, { name = "gleeunit", version = "1.9.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "DA9553CE58B67924B3C631F96FE3370C49EB6D6DC6B384EC4862CC4AAA718F3C" }, { name = "glisten", version = "8.0.3", build_tools = ["gleam"], requirements = ["gleam_erlang", "gleam_otp", "gleam_stdlib", "logging", "telemetry"], otp_app = "glisten", source = "hex", outer_checksum = "86B838196592D9EBDE7A1D2369AE3A51E568F7DD2D168706C463C42D17B95312" }, diff --git a/examples/src/https.gleam b/examples/src/https.gleam index 269490d..35f8e29 100644 --- a/examples/src/https.gleam +++ b/examples/src/https.gleam @@ -5,7 +5,7 @@ import logging pub fn main() { logging.configure() - logging.set_level(logging.Info) + logging.set_level(logging.Debug) // Start the server that has TLS enabled. let assert Ok(_) = diff --git a/src/ewe.gleam b/src/ewe.gleam index 7b8882c..0ec6d2f 100644 --- a/src/ewe.gleam +++ b/src/ewe.gleam @@ -503,6 +503,8 @@ pub fn start( |> fn(glisten_builder) { case builder.tls { Some(#(cert, key)) -> glisten.with_tls(glisten_builder, cert, key) + // Uncomment once http2 will be implemented! + // |> glisten.with_http2 None -> glisten_builder } } diff --git a/src/ewe/internal/handler.gleam b/src/ewe/internal/handler.gleam index b8b255c..e1c1ace 100644 --- a/src/ewe/internal/handler.gleam +++ b/src/ewe/internal/handler.gleam @@ -1,5 +1,6 @@ import ewe/internal/http1.{type Connection, type ResponseBody} as ewe_http import ewe/internal/http1/handler as http1_handler +import gleam/bytes_tree import gleam/erlang/process import gleam/http/request.{type Request} import gleam/http/response.{type Response} @@ -7,16 +8,17 @@ import gleam/option.{type Option, Some} import gleam/otp/actor import gleam/otp/factory_supervisor as factory import glisten +import glisten/transport import logging /// State of the request handler. -/// +/// pub type Handler { Http1(state: http1_handler.Http1Handler, self: process.Subject(Nil)) } /// Initializes the request handler state. -/// +/// pub fn init(_) -> #(Handler, Option(process.Selector(Nil))) { let subject = process.new_subject() let selector = @@ -27,7 +29,7 @@ pub fn init(_) -> #(Handler, Option(process.Selector(Nil))) { } /// Main loop that processes incoming messages. -/// +/// pub fn loop( handler: fn(Request(Connection)) -> Response(ResponseBody), on_crash: Response(ResponseBody), @@ -59,21 +61,25 @@ pub fn loop( case result { http1_handler.Continue(state) -> glisten.continue(Http1(state, self)) - http1_handler.Http2Upgrade(http1_handler.OverCleartext( - _req, - _settings, - )) -> { - logging.log( - logging.Debug, - "Received HTTP/2 cleartext upgrade request", - ) + http1_handler.Http2Upgrade(http1_handler.Direct(_data)) -> { + logging.log(logging.Notice, "HTTP/2 upgrade; sending goaway") + let goaway = << + 0:size(24), 4, 0, 0:size(32), 8:size(24), 7, 0, 0:size(32), + 0:size(32), 13:size(32), + >> + + let _ = + transport.send( + conn.transport, + conn.socket, + bytes_tree.from_bit_array(goaway), + ) + glisten.stop() } - http1_handler.Http2Upgrade(_data) -> { - logging.log(logging.Debug, "Received HTTP/2 upgrade request") + http1_handler.Stop + | http1_handler.Http2Upgrade(http1_handler.Upgrade(..)) -> glisten.stop() - } - http1_handler.Stop -> glisten.stop() } } _, _ -> glisten.stop() diff --git a/src/ewe/internal/http1.gleam b/src/ewe/internal/http1.gleam index bf11fd0..d522e41 100644 --- a/src/ewe/internal/http1.gleam +++ b/src/ewe/internal/http1.gleam @@ -120,10 +120,10 @@ pub type ParsedRequest { } /// HTTP/2 upgrade options. -/// +/// pub type Http2Upgrade { - OverCleartext(req: Request(Connection), settings: String) - OverTLS(data: BitArray) + Upgrade(req: Request(Connection), settings: String) + Direct(data: BitArray) } /// Parses an HTTP request from the given buffer. @@ -207,7 +207,7 @@ pub fn parse_request( string.contains(string.lowercase(connection), "upgrade") case is_upgrade { - True -> Ok(Http2Upgrade(OverCleartext(req:, settings:))) + True -> Ok(Http2Upgrade(Upgrade(req:, settings:))) False -> Ok(Http1Request(req:, version: Http11)) } } @@ -218,7 +218,7 @@ pub fn parse_request( } } Ok(Packet(decoder.Http2Upgrade, <<"\r\nSM\r\n\r\n":utf8, data:bits>>)) -> - Ok(Http2Upgrade(OverTLS(data:))) + Ok(Http2Upgrade(Direct(data:))) Ok(More(size)) -> { use new_buffer <- try(read_from_socket( transport, diff --git a/src/ewe/internal/http1/handler.gleam b/src/ewe/internal/http1/handler.gleam index 4ff4493..f95971c 100644 --- a/src/ewe/internal/http1/handler.gleam +++ b/src/ewe/internal/http1/handler.gleam @@ -24,19 +24,19 @@ import glisten/transport import logging /// HTTP/1.1 handler state. -/// +/// pub type Http1Handler { Http1Handler(idle_timer: Option(process.Timer)) } /// Initializes the HTTP/1.1 handler state. -/// +/// pub fn init() -> Http1Handler { Http1Handler(idle_timer: None) } /// Action to take after handling a packet. -/// +/// pub type Next { Continue(state: Http1Handler) Stop @@ -44,14 +44,14 @@ pub type Next { } /// HTTP/2 upgrade options. -/// +/// pub type Http2Upgrade { - OverCleartext(request: Request(Connection), settings: String) - OverTLS(data: BitArray) + Upgrade(request: Request(Connection), settings: String) + Direct(data: BitArray) } /// Handles received glisten packet. -/// +/// pub fn handle_packet( state: Http1Handler, connection: Connection, @@ -76,10 +76,25 @@ pub fn handle_packet( Error(Nil) -> Stop } } - Ok(ewe_http.Http2Upgrade(ewe_http.OverTLS(data))) -> - Http2Upgrade(OverTLS(data:)) - Ok(ewe_http.Http2Upgrade(ewe_http.OverCleartext(request, settings))) -> - Http2Upgrade(OverCleartext(request:, settings:)) + Ok(ewe_http.Http2Upgrade(ewe_http.Direct(data))) -> + Http2Upgrade(Direct(data:)) + Ok(ewe_http.Http2Upgrade(ewe_http.Upgrade(request, _settings))) -> { + logging.log(logging.Notice, "HTTP/2 upgrade; using HTTP/1.1") + let call_result = + call( + request, + ewe_http.Http11, + glisten_subject, + handler, + on_crash, + idle_timeout, + ) + + case call_result { + Ok(state) -> Continue(state) + Error(Nil) -> Stop + } + } Error(reason) -> { let status = case reason { ewe_http.InvalidVersion -> 505 @@ -99,7 +114,7 @@ pub fn handle_packet( } /// Takes parsed HTTP request and calls the handler. -/// +/// fn call( request: Request(Connection), version: HttpVersion, @@ -130,7 +145,7 @@ fn call( } /// Actions to take after response is sent. -/// +/// fn on_sent( sent: Result(Nil, glisten.SocketReason), response: Response(ResponseBody), @@ -149,7 +164,7 @@ fn on_sent( } /// Sends a file to the client. -/// +/// fn send_file( request: Request(Connection), version: HttpVersion, @@ -185,7 +200,7 @@ fn send_file( } /// Sends a body to the client. -/// +/// fn send_body( request: Request(Connection), version: HttpVersion, @@ -236,7 +251,7 @@ fn send_body( } /// Can the body be encoded to gzip? -/// +/// fn can_encode_gzip(request: Request(Connection), response: Response(_)) -> Bool { let accept_encoding = request.get_header(request, "accept-encoding") @@ -251,7 +266,7 @@ fn can_encode_gzip(request: Request(Connection), response: Response(_)) -> Bool } /// Removes the charset from the content-type header. -/// +/// fn remove_charset(response: Response(_)) -> Response(_) { response.get_header(response, "content-type") |> result.try(string.split_once(_, ";")) @@ -262,7 +277,7 @@ fn remove_charset(response: Response(_)) -> Response(_) { } /// Is the connection set to close? -/// +/// fn is_connection_close(response: Response(_)) -> Bool { case response.get_header(response, "connection") { Ok("close") -> True diff --git a/src/ewe_ffi.erl b/src/ewe_ffi.erl index 212b6aa..cc1c653 100644 --- a/src/ewe_ffi.erl +++ b/src/ewe_ffi.erl @@ -14,8 +14,8 @@ coerce_tcp_message({ssl, _Socket, Data}) -> Data. decode_packet(Type, Packet, Options) -> case erlang:decode_packet(Type, Packet, Options) of - {ok, {http_request, <<"PRI">>, "*", {2, 0}}, Rest} -> - {ok, {packet, {http2_upgrade, Rest}}}; + {ok, {http_request, <<"PRI">>, '*', {2, 0}}, Rest} -> + {ok, {packet, http2_upgrade, Rest}}; {ok, {http_request, Method, Uri, Version}, Rest} -> {ok, {packet, {http_request, atom_to_binary(Method), Uri, Version}, Rest}}; {ok, {http_header, Idx, _, Field, Value}, Rest} -> diff --git a/test/https.gleam b/test/https.gleam new file mode 100644 index 0000000..fdbd72d --- /dev/null +++ b/test/https.gleam @@ -0,0 +1,28 @@ +import ewe.{type Request, type Response} +import gleam/erlang/process +import gleam/http/response +import logging + +pub fn main() { + logging.configure() + logging.set_level(logging.Debug) + + // Start the server that has TLS enabled. + let assert Ok(_) = + ewe.new(handler) + |> ewe.bind("0.0.0.0") + |> ewe.listening(port: 8080) + |> ewe.enable_tls( + certificate_file: "examples/priv/localhost.crt", + key_file: "examples/priv/localhost.key", + ) + |> ewe.start + + process.sleep_forever() +} + +fn handler(_req: Request) -> Response { + response.new(200) + |> response.set_header("content-type", "text/plain; charset=utf-8") + |> response.set_body(ewe.TextData("Hello, World!")) +} diff --git a/test/preview.gleam b/test/preview.gleam index 33a0f68..4908da4 100644 --- a/test/preview.gleam +++ b/test/preview.gleam @@ -19,7 +19,7 @@ import ewe.{type Request, type Response} pub fn main() { logging.configure() - logging.set_level(logging.Info) + logging.set_level(logging.Debug) // Create a named subject for the pubsub worker let pubsub_name = process.new_name("pubsub")