Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Network server #13080

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
344 changes: 2 additions & 342 deletions src/http/server.cr
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require "socket"
require "uri"
require "../network_server"
require "./server/context"
require "./server/handler"
require "./server/response"
Expand All @@ -15,8 +16,6 @@ require "log"
# A server is initialized with a handler chain responsible for processing each
# incoming request.
#
# NOTE: To use `Server`, you must explicitly import it with `require "http/server"`
#
# ```
# require "http/server"
#
Expand Down Expand Up @@ -130,17 +129,9 @@ require "log"
#
# 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
class HTTP::Server < NetworkServer
Log = ::Log.for("http.server")

@sockets = [] of Socket::Server

# Returns `true` if this server is closed.
getter? closed : Bool = false

# 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)
Expand Down Expand Up @@ -197,339 +188,8 @@ class HTTP::Server
@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.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind_tcp("127.0.0.100", 8080) # => Socket::IPAddress.new("127.0.0.100", 8080)
# ```
#
# If *reuse_port* is `true`, it enables the `SO_REUSEPORT` socket option,
# which allows multiple processes to bind to the same port.
def bind_tcp(host : String, port : Int32, reuse_port : Bool = false) : Socket::IPAddress
tcp_server = TCPServer.new(host, port, reuse_port: reuse_port)

begin
bind(tcp_server)
rescue exc
tcp_server.close
raise exc
end

tcp_server.local_address
end

# Creates a `TCPServer` listening on `127.0.0.1:port` and adds it as a socket,
# returning the local address and port the server listens on.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind_tcp(8080) # => Socket::IPAddress.new("127.0.0.1", 8080)
# ```
#
# If *reuse_port* is `true`, it enables the `SO_REUSEPORT` socket option,
# which allows multiple processes to bind to the same port.
def bind_tcp(port : Int32, reuse_port : Bool = false) : Socket::IPAddress
bind_tcp Socket::IPAddress::LOOPBACK, port, reuse_port
end

# Creates a `TCPServer` listening on *address* and adds it as a socket, returning the local address
# and port the server listens on.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind_tcp(Socket::IPAddress.new("127.0.0.100", 8080)) # => Socket::IPAddress.new("127.0.0.100", 8080)
# server.bind_tcp(Socket::IPAddress.new("127.0.0.100", 0)) # => Socket::IPAddress.new("127.0.0.100", 35487)
# ```
#
# If *reuse_port* is `true`, it enables the `SO_REUSEPORT` socket option,
# which allows multiple processes to bind to the same port.
def bind_tcp(address : Socket::IPAddress, reuse_port : Bool = false) : Socket::IPAddress
bind_tcp(address.address, address.port, reuse_port: reuse_port)
end

# Creates a `TCPServer` listening on an unused port and adds it as a socket.
#
# Returns the `Socket::IPAddress` with the determined port number.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind_unused_port # => Socket::IPAddress.new("127.0.0.1", 12345)
# ```
def bind_unused_port(host : String = Socket::IPAddress::LOOPBACK, reuse_port : Bool = false) : Socket::IPAddress
bind_tcp host, 0, reuse_port
end

# Creates a `UNIXServer` bound to *path* and adds it as a socket.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind_unix "/tmp/my-socket.sock"
# ```
def bind_unix(path : String) : Socket::UNIXAddress
server = UNIXServer.new(path)

begin
bind(server)
rescue exc
server.close
raise exc
end

server.local_address
end

# Creates a `UNIXServer` bound to *address* and adds it as a socket.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind_unix(Socket::UNIXAddress.new("/tmp/my-socket.sock"))
# ```
def bind_unix(address : Socket::UNIXAddress) : Socket::UNIXAddress
bind_unix(address.path)
end

{% unless flag?(:without_openssl) %}
# Creates an `OpenSSL::SSL::Server` and adds it as a socket.
#
# The SSL server wraps a `TCPServer` listening on `host:port`.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# context = OpenSSL::SSL::Context::Server.new
# context.certificate_chain = "openssl.crt"
# context.private_key = "openssl.key"
# server.bind_tls "127.0.0.1", 8080, context
# ```
def bind_tls(host : String, port : Int32, context : OpenSSL::SSL::Context::Server, reuse_port : Bool = false) : Socket::IPAddress
tcp_server = TCPServer.new(host, port, reuse_port: reuse_port)
server = OpenSSL::SSL::Server.new(tcp_server, context)
server.start_immediately = false

begin
bind(server)
rescue exc
server.close
raise exc
end

tcp_server.local_address
end

# Creates an `OpenSSL::SSL::Server` and adds it as a socket.
#
# The SSL server wraps a `TCPServer` listening on an unused port on *host*.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# context = OpenSSL::SSL::Context::Server.new
# context.certificate_chain = "openssl.crt"
# context.private_key = "openssl.key"
# address = server.bind_tls "127.0.0.1", context
# ```
def bind_tls(host : String, context : OpenSSL::SSL::Context::Server) : Socket::IPAddress
bind_tls(host, 0, context)
end

# Creates an `OpenSSL::SSL::Server` and adds it as a socket.
#
# The SSL server wraps a `TCPServer` listening on an unused port on *host*.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# context = OpenSSL::SSL::Context::Server.new
# context.certificate_chain = "openssl.crt"
# context.private_key = "openssl.key"
# address = server.bind_tls Socket::IPAddress.new("127.0.0.1", 8000), context
# ```
def bind_tls(address : Socket::IPAddress, context : OpenSSL::SSL::Context::Server) : Socket::IPAddress
bind_tls(address.address, address.port, context)
end
{% end %}

# Parses a socket configuration from *uri* and adds it to this server.
# Returns the effective address it is bound to.
#
# ```
# require "http/server"
#
# server = HTTP::Server.new { }
# server.bind("tcp://localhost:80") # => Socket::IPAddress.new("127.0.0.1", 8080)
# server.bind("unix:///tmp/server.sock") # => Socket::UNIXAddress.new("/tmp/server.sock")
# server.bind("tls://127.0.0.1:443?key=private.key&cert=certificate.cert&ca=ca.crt") # => Socket::IPAddress.new("127.0.0.1", 443)
# ```
def bind(uri : String) : Socket::Address
bind(URI.parse(uri))
end

# :ditto:
def bind(uri : URI) : Socket::Address
case uri.scheme
when "tcp"
bind_tcp(Socket::IPAddress.parse(uri))
when "unix"
bind_unix(Socket::UNIXAddress.parse(uri))
when "tls", "ssl"
address = Socket::IPAddress.parse(uri)
{% unless flag?(:without_openssl) %}
context = OpenSSL::SSL::Context::Server.from_hash(uri.query_params)

bind_tls(address, context)
{% else %}
raise ArgumentError.new "Unsupported socket type: #{uri.scheme} (program was compiled without openssl support)"
{% end %}
else
raise ArgumentError.new "Unsupported socket type: #{uri.scheme}"
end
end

# Adds a `Socket::Server` *socket* to this server.
def bind(socket : Socket::Server) : Nil
raise "Can't add socket to running server" if listening?
raise "Can't add socket to closed server" if closed?

@sockets << socket
end

# Enumerates all addresses this server is bound to.
def each_address(&block : Socket::Address ->)
@sockets.each do |socket|
yield socket.local_address
end
end

def addresses : Array(Socket::Address)
array = [] of Socket::Address
each_address do |address|
array << address
end
array
end

# Creates a `TCPServer` listening on `127.0.0.1:port`, adds it as a socket
# and starts the server. Blocks until the server is closed.
#
# See `#bind(port : Int32)` for details.
def listen(port : Int32, reuse_port : Bool = false)
bind_tcp(port, reuse_port)

listen
end

# Creates a `TCPServer` listening on `host:port`, adds it as a socket
# and starts the server. Blocks until the server is closed.
#
# See `#bind(host : String, port : Int32)` for details.
def listen(host : String, port : Int32, reuse_port : Bool = false)
bind_tcp(host, port, reuse_port)

listen
end

# 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 running server" if listening?

@listening = true
done = Channel(Nil).new

@sockets.each do |socket|
spawn do
loop do
io = begin
socket.accept?
rescue e
handle_exception(e)
next
end

if io
# a non nillable version of the closured io
_io = io
spawn handle_client(_io)
else
break
end
end
ensure
done.send nil
end
end

@sockets.size.times { done.receive }
end

# Gracefully terminates the server. It will process currently accepted
# requests, but it won't accept new connections.
def close : Nil
raise "Can't close server, it's already closed" if closed?

@closed = true
@processor.close

@sockets.each do |socket|
socket.close
rescue
# ignore exception on close
end

@listening = false
@sockets.clear
end

private def handle_client(io : IO)
if io.is_a?(IO::Buffered)
io.sync = false
end

{% unless flag?(:without_openssl) %}
if io.is_a?(OpenSSL::SSL::Socket::Server)
begin
io.accept
rescue ex
Log.debug(exception: ex) { "Error during SSL handshake" }
return
end
end
{% end %}

@processor.process(io, io)
ensure
{% begin %}
begin
io.close
rescue IO::Error{% unless flag?(:without_openssl) %} | OpenSSL::SSL::Error{% end %}
end
{% end %}
end

# 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
# error and should be logged as such. Client malfunction should only be informational.
# 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`.
Expand Down
3 changes: 2 additions & 1 deletion src/http/server/context.cr
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
3 changes: 2 additions & 1 deletion src/http/server/response.cr
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading