From bee55998b82d8488927fe3f785f6199522acc2b3 Mon Sep 17 00:00:00 2001 From: vshakitskiy Date: Fri, 24 Jul 2026 13:10:19 +0300 Subject: [PATCH] remove unsafe type manipulations --- src/ewe.gleam | 115 ++++++++++++++++-------------- src/ewe/internal/connection.gleam | 45 +++++++----- src/ewe/internal/ewe_ffi.erl | 5 +- src/ewe/internal/file_ffi.erl | 2 - src/ewe/internal/http1.gleam | 94 ++++++++++-------------- 5 files changed, 128 insertions(+), 133 deletions(-) diff --git a/src/ewe.gleam b/src/ewe.gleam index da9cc72..e252237 100644 --- a/src/ewe.gleam +++ b/src/ewe.gleam @@ -40,23 +40,30 @@ pub type IpAddress { } pub fn ip_address_to_string(address: IpAddress) -> String { - unsafe_to_internal_ip_address(address) + to_internal_ip_address(address) |> glisten.ip_address_to_string } -// IpAddress and glisten.IpAddress are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_to_internal_ip_address(address: IpAddress) -> glisten.IpAddress +fn to_internal_ip_address(address: IpAddress) -> glisten.IpAddress { + case address { + IpV4(a, b, c, d) -> glisten.IpV4(a, b, c, d) + IpV6(a, b, c, d, e, f, g, h) -> glisten.IpV6(a, b, c, d, e, f, g, h) + } +} -// IpAddress and options.IpAddress are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_from_internal_options_ip_address( - address: options.IpAddress, -) -> IpAddress +fn from_internal_options_ip_address(address: options.IpAddress) -> IpAddress { + case address { + options.IpV4(a, b, c, d) -> IpV4(a, b, c, d) + options.IpV6(a, b, c, d, e, f, g, h) -> IpV6(a, b, c, d, e, f, g, h) + } +} -// glisten.IpAddress and IpAddress are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_from_internal_ip_address(address: glisten.IpAddress) -> IpAddress +fn from_internal_ip_address(address: glisten.IpAddress) -> IpAddress { + case address { + glisten.IpV4(a, b, c, d) -> IpV4(a, b, c, d) + glisten.IpV6(a, b, c, d, e, f, g, h) -> IpV6(a, b, c, d, e, f, g, h) + } +} /// The address a socket is bound to, or the address of a connected peer. pub type SocketAddress { @@ -68,10 +75,7 @@ pub type SocketAddress { fn convert_socket_address(address: glisten.SocketAddress) -> SocketAddress { case address { glisten.TcpSocketAddress(port:, ip_address:) -> - TcpSocketAddress( - ip_address: unsafe_from_internal_ip_address(ip_address), - port:, - ) + TcpSocketAddress(ip_address: from_internal_ip_address(ip_address), port:) glisten.UnixSocketAddress(path:) -> UnixSocketAddress(path:) } } @@ -80,13 +84,13 @@ fn convert_socket_address(address: glisten.SocketAddress) -> SocketAddress { /// the socket information is unavailable. pub fn get_client_info(connection: Connection) -> Result(SocketAddress, Nil) { case connection { - connection.Http1(transport:, socket:, ..) -> { - let peername = transport.peername(transport, socket) + connection.Http1(connection) -> { + let peername = transport.peername(connection.transport, connection.socket) use info <- result.map(over: peername) case info { socket.TcpSockName(ip_address:, port:) -> - unsafe_from_internal_options_ip_address(ip_address) + from_internal_options_ip_address(ip_address) |> TcpSocketAddress(port:) socket.UnixSockName(path:) -> UnixSocketAddress(path:) } @@ -128,9 +132,14 @@ pub type TlsKeyType { PrivateKeyInfo } -// TlsKeyType and options.TlsKeyType are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_to_internal_tls_key_type(key_type: TlsKeyType) -> options.TlsKeyType +fn to_internal_tls_key_type(key_type: TlsKeyType) -> options.TlsKeyType { + case key_type { + RsaPrivateKey -> options.RsaPrivateKey + EcPrivateKey -> options.EcPrivateKey + DsaPrivateKey -> options.DsaPrivateKey + PrivateKeyInfo -> options.PrivateKeyInfo + } +} /// Contains all server configurations, can be adjusted by different builder /// functions. @@ -283,7 +292,7 @@ pub fn quiet(builder: Builder) -> Builder { } // Body and connection.Body are structurally identical. -@external(erlang, "gleam_stdlib", "identity") +@external(erlang, "ewe_ffi", "identity") fn unsafe_to_internal_response( response: response.Response(Body), ) -> response.Response(connection.Body) @@ -313,7 +322,7 @@ pub fn start( glisten.with_tls_der( pool, cert:, - key_type: unsafe_to_internal_tls_key_type(key_type), + key_type: to_internal_tls_key_type(key_type), key:, ) None -> pool @@ -363,9 +372,16 @@ pub type FileError { InvalidLimit } -// FileError and file.FileError are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_from_internal_file_error(error: file.FileError) -> FileError +fn from_internal_file_error(error: file.FileError) -> FileError { + case error { + file.NotFound -> NotFound + file.IsDirectory -> IsDirectory + file.AccessDenied -> AccessDenied + file.UnknownError -> UnknownError + file.InvalidOffset -> InvalidOffset + file.InvalidLimit -> InvalidLimit + } +} /// Prepares a file to be streamed as a response body. `offset` and `limit` in /// bytes let you serve a byte range from the file. leave either as `None` to @@ -377,7 +393,7 @@ pub fn file( ) -> Result(Body, FileError) { case file.resolve(path, offset, limit) { Ok(file) -> Ok(File(file)) - Error(error) -> Error(unsafe_from_internal_file_error(error)) + Error(error) -> Error(from_internal_file_error(error)) } } @@ -389,9 +405,12 @@ pub type BodyError { InvalidBody } -// BodyError and http1.BodyError are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_from_internal_body_error(error: http1.BodyError) -> BodyError +fn from_internal_http1_body_error(error: http1.BodyError) -> BodyError { + case error { + http1.BodyTooLarge -> BodyTooLarge + http1.InvalidBody -> InvalidBody + } +} /// Reads the entire request body into memory, up to `limit` bytes. For a /// chunked request, any trailer fields are appended to the returned request's @@ -401,10 +420,10 @@ pub fn read_body( limit limit: Int, ) -> Result(request.Request(BitArray), BodyError) { case req.body { - connection.Http1(..) -> { + connection.Http1(connection) -> { use #(body, trailers) <- result.try( - http1.read_body(http1.unsafe_to_http1_connection(req.body), limit) - |> result.map_error(unsafe_from_internal_body_error), + http1.read_body(connection, limit) + |> result.map_error(from_internal_http1_body_error), ) request.Request(..req, headers: list.append(req.headers, trailers), body:) @@ -414,10 +433,6 @@ pub fn read_body( } } -// http1.Connection and connection.Connection are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_from_http1_connection(conn: http1.Connection) -> Connection - /// The result of one `read_body_chunk` call. pub type ReadEvent { /// Up to `max_chunk_bytes` of body data. Feed `request` into the next call. @@ -436,18 +451,17 @@ pub fn read_body_chunk( limit limit: Int, ) -> Result(ReadEvent, BodyError) { case req.body { - connection.Http1(..) -> { - let conn = http1.unsafe_to_http1_connection(req.body) - case http1.read_body_chunk(conn, max_chunk_bytes:, limit:) { + connection.Http1(connection) -> { + case http1.read_body_chunk(connection, max_chunk_bytes:, limit:) { Ok(http1.Chunk(data, connection)) -> { - let body = unsafe_from_http1_connection(connection) + let body = connection.Http1(connection) Ok(Chunk(data, request.set_body(req, body))) } Ok(http1.Done(trailers)) -> { let headers = list.append(req.headers, trailers) Ok(Done(request.Request(..req, headers:, body: Nil))) } - Error(error) -> Error(unsafe_from_internal_body_error(error)) + Error(error) -> Error(from_internal_http1_body_error(error)) } } connection.Http2 -> todo as "HTTP/2 is not implemented yet!" @@ -459,10 +473,6 @@ pub fn read_body_chunk( pub type ResponseWriter = connection.ResponseWriter -// http1.ResponseWriter and ResponseWriter are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -fn unsafe_from_http1_writer(writer: http1.ResponseWriter) -> ResponseWriter - /// Starts a streamed response. `handler` must end by calling `finish_chunk` or /// `finish_response` on it, since that's what closes the stream. pub fn stream_response( @@ -477,9 +487,8 @@ pub fn stream_response( /// stream in the same round trip. pub fn send_chunk(writer: ResponseWriter, chunk: BitArray) -> ResponseWriter { case writer { - connection.Http1Writer(..) -> - http1.send_chunk(http1.unsafe_to_http1_writer(writer), chunk) - |> unsafe_from_http1_writer + connection.Http1Writer(writer) -> + connection.Http1Writer(http1.send_chunk(writer, chunk)) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } @@ -487,8 +496,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(..) -> - http1.finish_chunk(http1.unsafe_to_http1_writer(writer), chunk) + connection.Http1Writer(writer) -> http1.finish_chunk(writer, chunk) connection.Http2Writer -> todo as "HTTP/2 is not implemented yet!" } } @@ -497,8 +505,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(..) -> - http1.finish_response(http1.unsafe_to_http1_writer(writer)) + connection.Http1Writer(writer) -> http1.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 0083823..996d06f 100644 --- a/src/ewe/internal/connection.gleam +++ b/src/ewe/internal/connection.gleam @@ -4,7 +4,12 @@ import glisten/socket import glisten/transport pub type Connection { - Http1( + Http1(Http1Connection) + Http2 +} + +pub type Http1Connection { + Http1Connection( transport: transport.Transport, socket: socket.Socket, self: process.Subject(Http1Signal), @@ -12,12 +17,11 @@ pub type Connection { framing: Framing, // Body bytes delivered to the caller so far, via `read_body_chunk`. read: Int, - // Bytes left in the chunked-encoding chunk currently being delivered; + // Bytes left in the chunked-encoding chunk currently being delivered. // 0 means the next pull starts at a chunk boundary. Unused for `Fixed` // and `NoBody`. chunk_remaining: Int, ) - Http2 } /// How to find the end of a request body on the wire, derived once from @@ -45,30 +49,34 @@ pub type Message { Timeout } -/// Sent by `http1.gleam` to a request's own private subject, drained -/// synchronously within the same `http1.handle_message` call +/// Sent to a http1 request's own subject, drained synchronously within the same +/// `http1.handle_message` call pub type Http1Signal { - /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves - /// once the request body has been fully consumed, carrying whatever bytes - /// came after it. + /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves once + /// the request body has been fully consumed, carrying whatever bytes came + /// after it. BodyDrained(leftover: BitArray) - /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves - /// when they gave up on the body part-way through, leaving the connection - /// in an unknown position. + /// Sent by `http1.read_body` and `http1.read_body_chunk` to themselves when + /// they gave up on the body part way through, leaving the connection in an + /// unknown position. BodyAbandoned - /// Sent by `http1.read_body_chunk` to itself after every chunk it - /// delivers, so that if the caller stops reading before `BodyDrained`, - /// the connection can still resume draining from here instead of from the - /// start of the body. + /// Sent by `http1.read_body_chunk` to itself after every chunk it delivers, + /// so that if the caller stops reading before `BodyDrained`, the connection + /// can still resume draining from here instead of from the start of the body. BodyProgress(buffer: BitArray, read: Int, chunk_remaining: Int) - /// Sent by `http1.finish_chunk` and `http1.finish_response` to themselves once - /// a streamed response's terminator has been written. + /// Sent by `http1.finish_chunk` and `http1.finish_response` to themselves + /// once a streamed response's terminator has been written. StreamFinished(keep_alive: Bool) } /// How a streamed response writes its body chunks to the wire. pub type ResponseWriter { - Http1Writer( + Http1Writer(Http1ResponseWriter) + Http2Writer +} + +pub type Http1ResponseWriter { + Http1ResponseWriter( transport: transport.Transport, socket: socket.Socket, self: process.Subject(Http1Signal), @@ -82,5 +90,4 @@ pub type ResponseWriter { // `False` when `chunked` is `False`. keep_alive: Bool, ) - Http2Writer } diff --git a/src/ewe/internal/ewe_ffi.erl b/src/ewe/internal/ewe_ffi.erl index b53dd36..e9f1455 100644 --- a/src/ewe/internal/ewe_ffi.erl +++ b/src/ewe/internal/ewe_ffi.erl @@ -1,6 +1,9 @@ -module(ewe_ffi). --export([now_datetime/0, set_http_date/1, get_http_date/0]). +-export([identity/1, now_datetime/0, set_http_date/1, get_http_date/0]). + +identity(X) -> + X. now_datetime() -> {Date, Time} = calendar:universal_time(), diff --git a/src/ewe/internal/file_ffi.erl b/src/ewe/internal/file_ffi.erl index 44446a1..0e510f6 100644 --- a/src/ewe/internal/file_ffi.erl +++ b/src/ewe/internal/file_ffi.erl @@ -13,8 +13,6 @@ stat(Path) -> {error, _Reason} -> {error, unknown_error} end. -%% the kernel streams the file straight to the socket without passing through -%% userspace memory. sendfile(Fd, Socket, Offset, Bytes) -> case file:sendfile(Fd, Socket, Offset, Bytes, []) of {ok, _Sent} -> {ok, nil}; diff --git a/src/ewe/internal/http1.gleam b/src/ewe/internal/http1.gleam index e015916..fd407b3 100644 --- a/src/ewe/internal/http1.gleam +++ b/src/ewe/internal/http1.gleam @@ -53,7 +53,7 @@ pub fn handle_message( let self = process.new_subject() let body_connection = - connection.Http1( + connection.Http1Connection( transport: connection.transport, socket: connection.socket, self:, @@ -67,7 +67,7 @@ pub fn handle_message( request.Request( method: head.method, headers: head.headers, - body: body_connection, + body: connection.Http1(body_connection), scheme:, host: head.host, port: head.port, @@ -79,8 +79,7 @@ pub fn handle_message( let drained = drain_messages(self) - let #(buffer, body_drained) = - resolve_body(unsafe_to_http1_connection(body_connection), drained.body) + let #(buffer, body_drained) = resolve_body(body_connection, drained.body) let metadata = Metadata(..metadata, keep_alive: metadata.keep_alive && body_drained) @@ -104,13 +103,13 @@ pub fn handle_message( } option.None, option.Some(Stream(handler: stream_handler, chunked:)) -> { - connection.Http1Writer( + connection.Http1Writer(connection.Http1ResponseWriter( transport: connection.transport, socket: connection.socket, self:, chunked:, keep_alive:, - ) + )) |> stream_handler let stream_drained = drain_messages(self) @@ -213,20 +212,8 @@ pub type BodyError { InvalidBody } -pub type Connection { - Http1( - transport: transport.Transport, - socket: socket.Socket, - self: process.Subject(connection.Http1Signal), - buffer: BitArray, - framing: connection.Framing, - read: Int, - chunk_remaining: Int, - ) -} - -@external(erlang, "gleam_stdlib", "identity") -pub fn unsafe_to_http1_connection(conn: connection.Connection) -> Connection +pub type Connection = + connection.Http1Connection const body_read_timeout = 10_000 @@ -242,7 +229,14 @@ pub fn read_body( conn: Connection, limit: Int, ) -> Result(#(BitArray, List(#(String, String))), BodyError) { - let Http1(transport:, socket:, self:, buffer:, framing:, ..) = conn + let connection.Http1Connection( + transport:, + socket:, + self:, + buffer:, + framing:, + .., + ) = conn case framing, consume_body(transport, socket, buffer, framing, limit) { _framing, Ok(#(body, trailers, leftover)) -> { @@ -275,7 +269,8 @@ pub fn read_body_chunk( max_chunk_bytes max_chunk_bytes: Int, limit limit: Int, ) -> Result(ChunkRead, BodyError) { - let Http1(self:, buffer:, read:, chunk_remaining:, ..) = conn + let connection.Http1Connection(self:, buffer:, read:, chunk_remaining:, ..) = + conn case pull_chunk(conn, max_chunk_bytes, limit) { Ok(PulledChunk(data, next)) -> { @@ -319,7 +314,8 @@ fn resolve_body( option.Some(connection.BodyDrained(leftover)) -> #(leftover, True) option.Some(connection.BodyAbandoned) -> #(<<>>, False) option.Some(connection.BodyProgress(buffer:, read:, chunk_remaining:)) -> - drain_remaining(Http1(..conn, 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 routes stream messages to the other slot" @@ -359,7 +355,7 @@ fn do_drain_messages( // Drains whatever's left of `conn`'s body, up to `auto_drain_limit` more bytes // past however much has already been read. fn drain_remaining(conn: Connection) -> #(BitArray, Bool) { - let Http1(read:, ..) = conn + let connection.Http1Connection(read:, ..) = conn do_drain_remaining(conn, read + auto_drain_limit) } @@ -492,7 +488,8 @@ fn pull_chunk( max_chunk_bytes: Int, limit: Int, ) -> Result(Pulled, BodyError) { - let Http1(buffer:, framing:, read:, chunk_remaining:, ..) = conn + let connection.Http1Connection(buffer:, framing:, read:, chunk_remaining:, ..) = + conn case framing { connection.NoBody -> Ok(PulledDone([], buffer)) @@ -514,7 +511,7 @@ fn pull_fixed_chunk( read: Int, max_chunk_bytes: Int, ) -> Result(Pulled, ParseError) { - let Http1(transport:, socket:, buffer:, ..) = conn + let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn case length - read { 0 -> Ok(PulledDone([], buffer)) @@ -526,7 +523,9 @@ fn pull_fixed_chunk( buffer, want, )) - Ok(PulledChunk(data, Http1(..conn, buffer: leftover, read: read + want))) + let conn = + connection.Http1Connection(..conn, buffer: leftover, read: read + want) + Ok(PulledChunk(data, conn)) } } } @@ -541,7 +540,7 @@ fn pull_chunked_chunk( chunk_remaining: Int, max_chunk_bytes: Int, ) -> Result(Pulled, ParseError) { - let Http1(transport:, socket:, buffer:, ..) = conn + let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn case chunk_remaining { 0 -> { @@ -563,7 +562,8 @@ fn pull_chunked_chunk( } size if read + size > limit -> Error(ChunkTooLarge) size -> - take_chunk_slice(Http1(..conn, buffer:), read, size, max_chunk_bytes) + connection.Http1Connection(..conn, buffer:) + |> take_chunk_slice(read, size, max_chunk_bytes) } } remaining -> take_chunk_slice(conn, read, remaining, max_chunk_bytes) @@ -576,7 +576,7 @@ fn take_chunk_slice( chunk_remaining: Int, max_chunk_bytes: Int, ) -> Result(Pulled, ParseError) { - let Http1(transport:, socket:, buffer:, ..) = conn + let connection.Http1Connection(transport:, socket:, buffer:, ..) = conn let want = int.min(chunk_remaining, max_chunk_bytes) let final_slice = want == chunk_remaining @@ -586,15 +586,14 @@ fn take_chunk_slice( take_chunk_prefix(buffer, want, final_slice) }) - Ok(PulledChunk( - data, - Http1( + let conn = + connection.Http1Connection( ..conn, buffer:, read: read + want, chunk_remaining: chunk_remaining - want, - ), - )) + ) + Ok(PulledChunk(data, conn)) } // Runs `step` against `buffer`, pulling more bytes from the socket only when @@ -823,27 +822,8 @@ fn encode_stream( } /// How a streamed response writes its body chunks to the wire. -pub type ResponseWriter { - ResponseWriter( - transport: transport.Transport, - socket: socket.Socket, - self: process.Subject(connection.Http1Signal), - // `True` on HTTP/1.1: frame each chunk with its hex size and a trailing - // `0\r\n\r\n` terminator. `False` on HTTP/1.0, which has no chunked - // encoding: write raw bytes and let the body end when the connection - // closes. - chunked: Bool, - // Whether the connection should stay alive once the stream finishes. - // Always `False` when `chunked` is `False`. - keep_alive: Bool, - ) -} - -// ResponseWriter and connection.ResponseWriter are structurally identical. -@external(erlang, "gleam_stdlib", "identity") -pub fn unsafe_to_http1_writer( - writer: connection.ResponseWriter, -) -> ResponseWriter +pub type ResponseWriter = + connection.Http1ResponseWriter fn chunk_frame(chunk: BitArray) -> bytes_tree.BytesTree { bytes_tree.new() @@ -1737,5 +1717,5 @@ fn list_to_bit_array(bytes: List(Int)) -> BitArray fn bit_array_to_string(bits: BitArray) -> Result(String, Nil) // Used only for bytes already proven valid UTF-8 elsewhere!! -@external(erlang, "gleam_stdlib", "identity") +@external(erlang, "ewe_ffi", "identity") fn unsafe_to_string(bits: BitArray) -> String -- 2.51.2