From 5d7d1994bfbe7281fbbf2b7befe88a63ca7e92be Mon Sep 17 00:00:00 2001 From: John W Higgins Date: Tue, 14 Feb 2023 15:36:06 -0800 Subject: [PATCH 1/2] Rename http/server to network_server for refactoring --- src/{http/server.cr => network_server.cr} | 149 ++-------------------- 1 file changed, 11 insertions(+), 138 deletions(-) rename src/{http/server.cr => network_server.cr} (69%) diff --git a/src/http/server.cr b/src/network_server.cr similarity index 69% rename from src/http/server.cr rename to src/network_server.cr index 6e44f4150582..8d69d5f6a082 100644 --- a/src/http/server.cr +++ b/src/network_server.cr @@ -1,29 +1,18 @@ require "socket" require "uri" -require "./server/context" -require "./server/handler" -require "./server/response" -require "./server/request_processor" -require "./common" require "log" {% unless flag?(:without_openssl) %} require "openssl" {% end %} -# A concurrent HTTP server implementation. -# -# A server is initialized with a handler chain responsible for processing each -# incoming request. +# A concurrent network server base class. # # NOTE: To use `Server`, you must explicitly import it with `require "http/server"` # # ``` # require "http/server" # -# server = HTTP::Server.new do |context| -# context.response.content_type = "text/plain" -# context.response.print "Hello world!" -# end +# server = HTTP::Server.new { } # # address = server.bind_tcp 8080 # puts "Listening on http://#{address}" @@ -32,47 +21,9 @@ require "log" # # ## Request processing # -# The handler chain receives an instance of `HTTP::Server::Context` that holds -# the `HTTP::Request` to process and a `HTTP::Server::Response` which it can -# configure and write to. -# # Each connection is processed concurrently in a separate `Fiber` and can handle # multiple subsequent requests-response cycles with connection keep-alive. # -# ### Handler chain -# -# The handler given to a server can simply be a block that receives an `HTTP::Server::Context`, -# or it can be an instance of `HTTP::Handler`. An `HTTP::Handler` has a `#next` -# method to forward processing to the next handler in the chain. -# -# For example, an initial handler might handle exceptions raised from subsequent -# handlers and return a `500 Server Error` status (see `HTTP::ErrorHandler`). -# The next handler might log all incoming requests (see `HTTP::LogHandler`). -# And the final handler deals with routing and application logic. -# -# ``` -# require "http/server" -# -# server = HTTP::Server.new([ -# HTTP::ErrorHandler.new, -# HTTP::LogHandler.new, -# HTTP::CompressHandler.new, -# HTTP::StaticFileHandler.new("."), -# ]) -# -# server.bind_tcp "127.0.0.1", 8080 -# server.listen -# ``` -# -# ### Response object -# -# The `HTTP::Server::Response` object has `status` and `headers` properties that can be -# configured before writing the response body. Once any response output has been -# written, changing the `status` and `headers` properties has no effect. -# -# The `HTTP::Server::Response` is a write-only `IO`, so all `IO` methods are available -# on it for sending the response body. -# # ## Binding to sockets # # The server can be bound to one or more server sockets (see `#bind`) @@ -93,10 +44,7 @@ require "log" # ``` # require "http/server" # -# server = HTTP::Server.new do |context| -# context.response.content_type = "text/plain" -# context.response.print "Hello world!" -# end +# server = HTTP::Server.new { } # # address = server.bind_tcp "0.0.0.0", 8080 # puts "Listening on http://#{address}" @@ -117,21 +65,8 @@ require "log" # Currently processing requests are not interrupted but also not waited for. # In order to give them some grace period for finishing, the calling context # can add a timeout like `sleep 10.seconds` after `#listen` returns. -# -# ### Reusing connections -# -# The request processor supports reusing a connection for subsequent -# requests. This is used by default for HTTP/1.1 or when requested by -# the `Connection: keep-alive` header. This is signalled by this header being -# set on the `HTTP::Server::Response` when it's passed into the handler chain. -# -# If in the handler chain this header is overridden to `Connection: close`, then -# the connection will not be reused after the request has been processed. -# -# Reusing the connection also requires that the request body (if present) is -# entirely consumed in the handler chain. Otherwise the connection will be closed. -class HTTP::Server - Log = ::Log.for("http.server") +abstract class NetworkServer + Log = ::Log.for("network.server") @sockets = [] of Socket::Server @@ -141,62 +76,6 @@ class HTTP::Server # Returns `true` if this server is listening on its sockets. getter? listening : Bool = false - # Creates a new HTTP server with the given block as handler. - def self.new(&handler : HTTP::Handler::HandlerProc) : self - new(handler) - end - - # Creates a new HTTP server with a handler chain constructed from the *handlers* - # array and the given block. - def self.new(handlers : Array(HTTP::Handler), &handler : HTTP::Handler::HandlerProc) : self - new(HTTP::Server.build_middleware(handlers, handler)) - end - - # Creates a new HTTP server with the *handlers* array as handler chain. - def self.new(handlers : Array(HTTP::Handler)) : self - new(HTTP::Server.build_middleware(handlers)) - end - - # Creates a new HTTP server with the given *handler*. - def initialize(handler : HTTP::Handler | HTTP::Handler::HandlerProc) - @processor = RequestProcessor.new(handler) - end - - # Returns the maximum permitted size for the request line in an HTTP request. - # - # The request line is the first line of a request, consisting of method, - # resource and HTTP version and the delimiting line break. - # If the request line has a larger byte size than the permitted size, - # the server responds with the status code `414 URI Too Long` (see `HTTP::Status::URI_TOO_LONG`). - # - # Default: `HTTP::MAX_REQUEST_LINE_SIZE` - def max_request_line_size : Int32 - @processor.max_request_line_size - end - - # Sets the maximum permitted size for the request line in an HTTP request. - def max_request_line_size=(size : Int32) - @processor.max_request_line_size = size - end - - # Returns the maximum permitted combined size for the headers in an HTTP request. - # - # When parsing a request, the server keeps track of the amount of total bytes - # consumed for all headers (including line breaks). - # If combined byte size of all headers is larger than the permitted size, - # the server responds with the status code `432 Request Header Fields Too Large` - # (see `HTTP::Status::REQUEST_HEADER_FIELDS_TOO_LARGE`). - # - # Default: `HTTP::MAX_HEADERS_SIZE` - def max_headers_size : Int32 - @processor.max_headers_size - end - - # Sets the maximum permitted combined size for the headers in an HTTP request. - def max_headers_size=(size : Int32) - @processor.max_headers_size = size - end - # Creates a `TCPServer` listening on `host:port` and adds it as a socket, returning the local address # and port the server listens on. # @@ -448,7 +327,7 @@ class HTTP::Server # Starts the server. Blocks until the server is closed. def listen : Nil raise "Can't re-start closed server" if closed? - raise "Can't start server with no sockets to listen to, use HTTP::Server#bind first" if @sockets.empty? + raise "Can't start server with no sockets to listen to, use NetworkServer#bind first" if @sockets.empty? raise "Can't start running server" if listening? @listening = true @@ -467,7 +346,7 @@ class HTTP::Server if io # a non nillable version of the closured io _io = io - spawn handle_client(_io) + spawn new_client(_io) else break end @@ -498,7 +377,7 @@ class HTTP::Server @sockets.clear end - private def handle_client(io : IO) + private def new_client(io : IO) if io.is_a?(IO::Buffered) io.sync = false end @@ -514,7 +393,7 @@ class HTTP::Server end {% end %} - @processor.process(io, io) + handle_client(io) ensure {% begin %} begin @@ -524,6 +403,8 @@ class HTTP::Server {% end %} end + private abstract def handle_client(io : IO) + # This method handles exceptions raised at `Socket#accept?`. private def handle_exception(e : Exception) # TODO: This needs more refinement. Not every exception is an actual server @@ -531,12 +412,4 @@ class HTTP::Server # See https://github.com/crystal-lang/crystal/pull/9034#discussion_r407038999 Log.error(exception: e) { "Error while connecting a new socket" } end - - # Builds all handlers as the middleware for `HTTP::Server`. - def self.build_middleware(handlers, last_handler : (Context ->)? = nil) - raise ArgumentError.new "You must specify at least one HTTP Handler." if handlers.empty? - 0.upto(handlers.size - 2) { |i| handlers[i].next = handlers[i + 1] } - handlers.last.next = last_handler if last_handler - handlers.first - end end From f0f090a36162a9034a740e36a0ebc80a532178fc Mon Sep 17 00:00:00 2001 From: John W Higgins Date: Tue, 14 Feb 2023 15:36:51 -0800 Subject: [PATCH 2/2] Refactor http/server via network_server --- src/http/server.cr | 202 ++++++++++++++++++++++++++++++++++++ src/http/server/context.cr | 3 +- src/http/server/response.cr | 3 +- 3 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 src/http/server.cr diff --git a/src/http/server.cr b/src/http/server.cr new file mode 100644 index 000000000000..20bd1182c42d --- /dev/null +++ b/src/http/server.cr @@ -0,0 +1,202 @@ +require "socket" +require "uri" +require "../network_server" +require "./server/context" +require "./server/handler" +require "./server/response" +require "./server/request_processor" +require "./common" +require "log" +{% unless flag?(:without_openssl) %} + require "openssl" +{% end %} + +# A concurrent HTTP server implementation. +# +# A server is initialized with a handler chain responsible for processing each +# incoming request. +# +# ``` +# require "http/server" +# +# server = HTTP::Server.new do |context| +# context.response.content_type = "text/plain" +# context.response.print "Hello world!" +# end +# +# address = server.bind_tcp 8080 +# puts "Listening on http://#{address}" +# server.listen +# ``` +# +# ## Request processing +# +# The handler chain receives an instance of `HTTP::Server::Context` that holds +# the `HTTP::Request` to process and a `HTTP::Server::Response` which it can +# configure and write to. +# +# Each connection is processed concurrently in a separate `Fiber` and can handle +# multiple subsequent requests-response cycles with connection keep-alive. +# +# ### Handler chain +# +# The handler given to a server can simply be a block that receives an `HTTP::Server::Context`, +# or it can be an instance of `HTTP::Handler`. An `HTTP::Handler` has a `#next` +# method to forward processing to the next handler in the chain. +# +# For example, an initial handler might handle exceptions raised from subsequent +# handlers and return a `500 Server Error` status (see `HTTP::ErrorHandler`). +# The next handler might log all incoming requests (see `HTTP::LogHandler`). +# And the final handler deals with routing and application logic. +# +# ``` +# require "http/server" +# +# server = HTTP::Server.new([ +# HTTP::ErrorHandler.new, +# HTTP::LogHandler.new, +# HTTP::CompressHandler.new, +# HTTP::StaticFileHandler.new("."), +# ]) +# +# server.bind_tcp "127.0.0.1", 8080 +# server.listen +# ``` +# +# ### Response object +# +# The `HTTP::Server::Response` object has `status` and `headers` properties that can be +# configured before writing the response body. Once any response output has been +# written, changing the `status` and `headers` properties has no effect. +# +# The `HTTP::Server::Response` is a write-only `IO`, so all `IO` methods are available +# on it for sending the response body. +# +# ## Binding to sockets +# +# The server can be bound to one or more server sockets (see `#bind`) +# +# Supported types: +# +# * TCP socket: `#bind_tcp`, `#bind_unused_port` +# * TCP socket with TLS/SSL: `#bind_tls` +# * Unix socket `#bind_unix` +# +# `#bind(uri : URI)` and `#bind(uri : String)` parse socket configuration for +# one of these types from an `URI`. This can be useful for injecting plain text +# configuration values. +# +# Each of these methods returns the `Socket::Address` that was added to this +# server. +# +# ``` +# require "http/server" +# +# server = HTTP::Server.new do |context| +# context.response.content_type = "text/plain" +# context.response.print "Hello world!" +# end +# +# address = server.bind_tcp "0.0.0.0", 8080 +# puts "Listening on http://#{address}" +# server.listen +# ``` +# +# It is also possible to bind a generic `Socket::Server` using +# `#bind(socket : Socket::Server)` which can be used for custom network protocol +# configurations. +# +# ## Server loop +# +# After defining all server sockets to listen to, the server can be started by +# calling `#listen`. This call blocks until the server is closed. +# +# A server can be closed by calling `#close`. This closes the server sockets and +# stops processing any new requests, even on connections with keep-alive enabled. +# Currently processing requests are not interrupted but also not waited for. +# In order to give them some grace period for finishing, the calling context +# can add a timeout like `sleep 10.seconds` after `#listen` returns. +# +# ### Reusing connections +# +# The request processor supports reusing a connection for subsequent +# requests. This is used by default for HTTP/1.1 or when requested by +# the `Connection: keep-alive` header. This is signalled by this header being +# set on the `HTTP::Server::Response` when it's passed into the handler chain. +# +# If in the handler chain this header is overridden to `Connection: close`, then +# the connection will not be reused after the request has been processed. +# +# Reusing the connection also requires that the request body (if present) is +# entirely consumed in the handler chain. Otherwise the connection will be closed. +class HTTP::Server < NetworkServer + Log = ::Log.for("http.server") + + # Creates a new HTTP server with the given block as handler. + def self.new(&handler : HTTP::Handler::HandlerProc) : self + new(handler) + end + + # Creates a new HTTP server with a handler chain constructed from the *handlers* + # array and the given block. + def self.new(handlers : Array(HTTP::Handler), &handler : HTTP::Handler::HandlerProc) : self + new(HTTP::Server.build_middleware(handlers, handler)) + end + + # Creates a new HTTP server with the *handlers* array as handler chain. + def self.new(handlers : Array(HTTP::Handler)) : self + new(HTTP::Server.build_middleware(handlers)) + end + + # Creates a new HTTP server with the given *handler*. + def initialize(handler : HTTP::Handler | HTTP::Handler::HandlerProc) + @processor = RequestProcessor.new(handler) + end + + # Returns the maximum permitted size for the request line in an HTTP request. + # + # The request line is the first line of a request, consisting of method, + # resource and HTTP version and the delimiting line break. + # If the request line has a larger byte size than the permitted size, + # the server responds with the status code `414 URI Too Long` (see `HTTP::Status::URI_TOO_LONG`). + # + # Default: `HTTP::MAX_REQUEST_LINE_SIZE` + def max_request_line_size : Int32 + @processor.max_request_line_size + end + + # Sets the maximum permitted size for the request line in an HTTP request. + def max_request_line_size=(size : Int32) + @processor.max_request_line_size = size + end + + # Returns the maximum permitted combined size for the headers in an HTTP request. + # + # When parsing a request, the server keeps track of the amount of total bytes + # consumed for all headers (including line breaks). + # If combined byte size of all headers is larger than the permitted size, + # the server responds with the status code `432 Request Header Fields Too Large` + # (see `HTTP::Status::REQUEST_HEADER_FIELDS_TOO_LARGE`). + # + # Default: `HTTP::MAX_HEADERS_SIZE` + def max_headers_size : Int32 + @processor.max_headers_size + end + + # Sets the maximum permitted combined size for the headers in an HTTP request. + def max_headers_size=(size : Int32) + @processor.max_headers_size = size + end + + private def handle_client(io : IO) + @processor.process(io, io) + end + + # Builds all handlers as the middleware for `HTTP::Server`. + def self.build_middleware(handlers, last_handler : (Context ->)? = nil) + raise ArgumentError.new "You must specify at least one HTTP Handler." if handlers.empty? + 0.upto(handlers.size - 2) { |i| handlers[i].next = handlers[i + 1] } + handlers.last.next = last_handler if last_handler + handlers.first + end +end diff --git a/src/http/server/context.cr b/src/http/server/context.cr index f972fcd40b57..48725604d5bb 100644 --- a/src/http/server/context.cr +++ b/src/http/server/context.cr @@ -1,7 +1,8 @@ +require "network_server" require "../request" require "./response" -class HTTP::Server +class HTTP::Server < NetworkServer # Instances of this class are passed to an `HTTP::Server` handler. class Context # The `HTTP::Request` to process. diff --git a/src/http/server/response.cr b/src/http/server/response.cr index de3312a81e57..11e66628093d 100644 --- a/src/http/server/response.cr +++ b/src/http/server/response.cr @@ -1,8 +1,9 @@ +require "network_server" require "http/headers" require "http/status" require "http/cookie" -class HTTP::Server +class HTTP::Server < NetworkServer # The response to configure and write to in an `HTTP::Server` handler. # # The response `status` and `headers` must be configured before writing