From 8c4ec7f271d18572cc4c813bd8973091367ee4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 14 Mar 2018 15:07:02 +0000 Subject: [PATCH 1/7] Refactor HTTP::Server to use multiple interfaces --- samples/http_server.cr | 3 +- spec/std/http/server/server_spec.cr | 62 +++++--- spec/std/http/web_socket_spec.cr | 12 +- .../crystal/tools/playground/server.cr | 6 +- .../tools/playground/views/_about.html | 3 +- src/http/formdata.cr | 3 +- src/http/server.cr | 142 +++++++++++------- 7 files changed, 145 insertions(+), 86 deletions(-) diff --git a/samples/http_server.cr b/samples/http_server.cr index f885fb42d51a..43e74316a8ca 100644 --- a/samples/http_server.cr +++ b/samples/http_server.cr @@ -1,9 +1,10 @@ require "http/server" -server = HTTP::Server.new "0.0.0.0", 8080 do |context| +server = HTTP::Server.new do |context| context.response.headers["Content-Type"] = "text/plain" context.response.print("Hello world!") end +server.bind "0.0.0.0", 8080 puts "Listening on http://0.0.0.0:8080" server.listen diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index cd64fedcd376..aa1cdc8d54ab 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -195,20 +195,21 @@ module HTTP describe HTTP::Server do it "re-sets special port zero after bind" do - server = Server.new(0) { |ctx| } - server.bind + server = Server.new { |ctx| } + server.bind 0 server.port.should_not eq(0) end - it "re-sets port to zero after close" do - server = Server.new(0) { |ctx| } - server.bind + it "re-sets port to nil after close" do + server = Server.new { |ctx| } + server.bind 0 server.close - server.port.should eq(0) + server.port.should be_nil end it "doesn't raise on accept after close #2692" do - server = Server.new("0.0.0.0", 0) { } + server = Server.new { } + server.bind "0.0.0.0", 0 spawn do server.close @@ -219,15 +220,36 @@ module HTTP end it "reuses the TCP port (SO_REUSEPORT)" do - s1 = Server.new(0) { |ctx| } - s1.bind(reuse_port: true) + s1 = Server.new { |ctx| } + s1.bind(0, reuse_port: true) - s2 = Server.new(s1.port) { |ctx| } - s2.bind(reuse_port: true) + s2 = Server.new { |ctx| } + s2.bind(s1.port.not_nil!, reuse_port: true) s1.close s2.close end + + it "binds to different interfaces" do + server = Server.new do |context| + context.response.print "Test Server (#{context.request.headers["Host"]?})" + end + + address1 = server.bind(0) + address2 = server.bind(0) + + port1 = address1.port + port2 = address2.port + port1.should_not eq port2 + + spawn { server.listen } + + Fiber.yield + + HTTP::Client.get("http://127.0.0.1:#{port2}/").body.should eq "Test Server (127.0.0.1:#{port2})" + HTTP::Client.get("http://127.0.0.1:#{port1}/").body.should eq "Test Server (127.0.0.1:#{port1})" + HTTP::Client.get("http://127.0.0.1:#{port1}/").body.should eq "Test Server (127.0.0.1:#{port1})" + end end describe HTTP::Server::RequestProcessor do @@ -310,40 +332,46 @@ module HTTP typeof(begin # Initialize with custom host - server = Server.new("0.0.0.0", 0) { |ctx| } + server = Server.new { |ctx| } + server.bind "0.0.0.0", 0 server.listen server.close - server = Server.new("0.0.0.0", 0, [ + server = Server.new([ ErrorHandler.new, LogHandler.new, CompressHandler.new, StaticFileHandler.new("."), ] ) + server.bind "0.0.0.0", 0 server.listen server.close - server = Server.new("0.0.0.0", 0, [StaticFileHandler.new(".")]) { |ctx| } + server = Server.new([StaticFileHandler.new(".")]) { |ctx| } + server.bind "0.0.0.0", 0 server.listen server.close # Initialize with default host - server = Server.new(0) { |ctx| } + server = Server.new { |ctx| } + server.bind 0 server.listen server.close - server = Server.new(0, [ + server = Server.new([ ErrorHandler.new, LogHandler.new, CompressHandler.new, StaticFileHandler.new("."), ] ) + server.bind 0 server.listen server.close - server = Server.new(0, [StaticFileHandler.new(".")]) { |ctx| } + server = Server.new([StaticFileHandler.new(".")]) { |ctx| } + server.bind 0 server.listen server.close end) diff --git a/spec/std/http/web_socket_spec.cr b/spec/std/http/web_socket_spec.cr index 81cd6a460970..79d17fa56129 100644 --- a/spec/std/http/web_socket_spec.cr +++ b/spec/std/http/web_socket_spec.cr @@ -313,9 +313,9 @@ describe HTTP::WebSocket do end end - http_server = http_ref = HTTP::Server.new(0, [ws_handler]) - http_server.bind - port_chan.send(http_server.port) + http_server = http_ref = HTTP::Server.new([ws_handler]) + http_server.bind 0 + port_chan.send(http_server.port.not_nil!) http_server.listen end @@ -350,12 +350,12 @@ describe HTTP::WebSocket do end end - http_server = http_ref = HTTP::Server.new(0, [ws_handler]) + http_server = http_ref = HTTP::Server.new([ws_handler]) tls = http_server.tls = OpenSSL::SSL::Context::Server.new tls.certificate_chain = File.join(__DIR__, "../openssl/ssl/openssl.crt") tls.private_key = File.join(__DIR__, "../openssl/ssl/openssl.key") - http_server.bind - port_chan.send(http_server.port) + http_server.bind 0 + port_chan.send(http_server.port.not_nil!) http_server.listen end diff --git a/src/compiler/crystal/tools/playground/server.cr b/src/compiler/crystal/tools/playground/server.cr index e25292450783..20b105f833b9 100644 --- a/src/compiler/crystal/tools/playground/server.cr +++ b/src/compiler/crystal/tools/playground/server.cr @@ -503,11 +503,13 @@ module Crystal::Playground HTTP::StaticFileHandler.new(public_dir), ] + server = HTTP::Server.new handlers + host = @host if host - server = HTTP::Server.new host, @port, handlers + server.bind host, @port else - server = HTTP::Server.new @port, handlers + server.bind @port host = "localhost" end diff --git a/src/compiler/crystal/tools/playground/views/_about.html b/src/compiler/crystal/tools/playground/views/_about.html index b690355d320b..c4b576c69d0b 100644 --- a/src/compiler/crystal/tools/playground/views/_about.html +++ b/src/compiler/crystal/tools/playground/views/_about.html @@ -60,11 +60,12 @@

Usage

 require "http/server"
 
-server = HTTP::Server.new "0.0.0.0", 5678 do |context|
+server = HTTP::Server.new do |context|
   context.request.path
   context.response.headers["Content-Type"] = "text/plain"
   context.response.print("Hello world!")
 end
+server.bind "0.0.0.0", 5678
 
 puts "Listening on http://0.0.0.0:5678"
 server.listen
diff --git a/src/http/formdata.cr b/src/http/formdata.cr index 70e8a58f3aba..0aa455329aa4 100644 --- a/src/http/formdata.cr +++ b/src/http/formdata.cr @@ -12,7 +12,7 @@ require "./formdata/**" # require "http" # require "tempfile" # -# server = HTTP::Server.new(8085) do |context| +# server = HTTP::Server.new do |context| # name = nil # file = nil # HTTP::FormData.parse(context.request) do |part| @@ -34,6 +34,7 @@ require "./formdata/**" # context.response << file.path # end # +# server.bind 8085 # server.listen # ``` # diff --git a/src/http/server.cr b/src/http/server.cr index b117d8420c18..f9aca6c14c00 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -33,11 +33,12 @@ require "./common" # ``` # require "http/server" # -# server = HTTP::Server.new(8080) do |context| +# server = HTTP::Server.new do |context| # context.response.content_type = "text/plain" # context.response.print "Hello world!" # end # +# server.bind 8080 # puts "Listening on http://127.0.0.1:8080" # server.listen # ``` @@ -47,11 +48,12 @@ require "./common" # ``` # require "http/server" # -# server = HTTP::Server.new("0.0.0.0", 8080) do |context| +# server = HTTP::Server.new do |context| # context.response.content_type = "text/plain" # context.response.print "Hello world!" # end # +# server.bind "0.0.0.0", 8080 # puts "Listening on http://0.0.0.0:8080" # server.listen # ``` @@ -63,12 +65,15 @@ require "./common" # ``` # require "http/server" # -# HTTP::Server.new("127.0.0.1", 8080, [ +# server = HTTP::Server.new([ # HTTP::ErrorHandler.new, # HTTP::LogHandler.new, # HTTP::CompressHandler.new, # HTTP::StaticFileHandler.new("."), -# ]).listen +# ]) +# +# server.bind "127.0.0.1", 8080 +# server.listen # ``` # # ### Add handlers and block @@ -78,15 +83,15 @@ require "./common" # ``` # require "http/server" # -# server = HTTP::Server.new("0.0.0.0", 8080, -# [ -# HTTP::ErrorHandler.new, -# HTTP::LogHandler.new, -# ]) do |context| +# server = HTTP::Server.new([ +# HTTP::ErrorHandler.new, +# HTTP::LogHandler.new, +# ]) do |context| # context.response.content_type = "text/plain" # context.response.print "Hello world!" # end # +# server.bind "0.0.0.0", 8080 # server.listen # ``` class HTTP::Server @@ -95,73 +100,93 @@ class HTTP::Server {% end %} @wants_close = false + @sockets = [] of Socket::Server - def self.new(port, &handler : Context ->) - new("127.0.0.1", port, &handler) + # Creates a new HTTP server with the given block as handler. + def self.new(&handler : HTTP::Handler::Proc) : self + new(handler) end - def self.new(port, handlers : Array(HTTP::Handler), &handler : Context ->) - new("127.0.0.1", port, handlers, &handler) + # 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::Proc) : self + new(HTTP::Server.build_middleware(handlers, handler)) end - def self.new(port, handlers : Array(HTTP::Handler)) - new("127.0.0.1", port, handlers) - end - - def self.new(port, handler) - new("127.0.0.1", port, handler) - end - - def initialize(@host : String, @port : Int32, &handler : Context ->) - @processor = RequestProcessor.new(handler) + # 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 - def initialize(@host : String, @port : Int32, handlers : Array(HTTP::Handler), &handler : Context ->) - handler = HTTP::Server.build_middleware handlers, handler + # Creates a new HTTP server with the given *handler*. + def initialize(handler : HTTP::Handler | HTTP::Handler::Proc) @processor = RequestProcessor.new(handler) end - def initialize(@host : String, @port : Int32, handlers : Array(HTTP::Handler)) - handler = HTTP::Server.build_middleware handlers - @processor = RequestProcessor.new(handler) - end - - def initialize(@host : String, @port : Int32, handler : HTTP::Handler | HTTP::Handler::Proc) - @processor = RequestProcessor.new(handler) - end - - # Returns the TCP port the server is connected to. + # Returns the TCP port of the first socket the server is bound to. # # For example you may let the system choose a port, then report it: # ``` - # server = HTTP::Server.new(0) { } - # server.bind + # server = HTTP::Server.new { } + # server.bind 0 # server.port # => 12345 # ``` - def port - if server = @server - server.local_address.port.to_i - else - @port + def port : Int32? + @sockets.each do |socket| + if socket.is_a?(TCPServer) + return socket.local_address.port.to_i + end end end - # Creates the underlying `TCPServer` if the doesn't already exist. + # Creates a `TCPServer` and adds it as a socket, returning the local address + # and port the server listens on. + # + # If *port* is `0`, a random, free port will be chosen. + # + # You may set *reuse_port* to `true` to enable the `SO_REUSEPORT` socket option, + # which allows multiple processes to bind to the same port. + def bind(host : String, port : Int32, reuse_port : Bool = false) : Socket::IPAddress + tcp_server = TCPServer.new(host, port, reuse_port: reuse_port) + bind(tcp_server) + tcp_server.local_address + end + + # Creates a `TCPServer` listenting on `127.0.0.1` and adds it as a socket, + # returning the local address and port the server listens on. + # + # If *port* is `0`, a random, free port will be chosen. # # You may set *reuse_port* to true to enable the `SO_REUSEPORT` socket option, # which allows multiple processes to bind to the same port. - def bind(reuse_port = false) - @server ||= TCPServer.new(@host, @port, reuse_port: reuse_port) + def bind(port : Int32, reuse_port : Bool = false) : Socket::IPAddress + bind "127.0.0.1", port, reuse_port + end + + # Adds a `Socket::Server` *socket* to this server. + def bind(socket : Socket::Server) : Socket::Server + @sockets << socket + + socket end # Starts the server. Blocks until the server is closed. - # - # See `#bind` for details on the *reuse_port* argument. - def listen(reuse_port = false) - server = bind(reuse_port) - until @wants_close - spawn handle_client(server.accept?) + def listen + raise "Can't start server with not sockets to listen to" if @sockets.empty? + + done = Channel(Nil).new + + @sockets.each do |socket| + spawn do + until @wants_close + spawn handle_client(socket.accept? || break) + end + + done.send nil + end end + + @sockets.size.times { done.receive } end # Gracefully terminates the server. It will process currently accepted @@ -169,16 +194,17 @@ class HTTP::Server def close @wants_close = true @processor.close - if server = @server - server.close - @server = nil + + @sockets.each do |socket| + socket.close + rescue + # ignore exception on close end - end - private def handle_client(io) - # nil means the server was closed - return unless io + @sockets.clear + end + private def handle_client(io : IO) io.sync = false {% if !flag?(:without_openssl) %} From de2eb47effb8c56539a88a73d06cf8a80c3eac48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Thu, 15 Mar 2018 12:08:36 +0000 Subject: [PATCH 2/7] Remove HTTP::Server#port and add HTTP::Server#bind_unused_port The return type is now an Socket::IPAddress instead of only the port number. --- spec/std/http/server/server_spec.cr | 38 ++++++++++++++--------------- spec/std/http/web_socket_spec.cr | 20 +++++++-------- src/http/server.cr | 28 +++++++++------------ 3 files changed, 40 insertions(+), 46 deletions(-) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index aa1cdc8d54ab..a3de6dac4f4d 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -194,22 +194,19 @@ module HTTP end describe HTTP::Server do - it "re-sets special port zero after bind" do + it "binds to unused port" do server = Server.new { |ctx| } - server.bind 0 - server.port.should_not eq(0) - end + address = server.bind_unused_port + address.port.should_not eq(0) - it "re-sets port to nil after close" do server = Server.new { |ctx| } - server.bind 0 - server.close - server.port.should be_nil + port = server.bind(0).port + port.should_not eq(0) end it "doesn't raise on accept after close #2692" do server = Server.new { } - server.bind "0.0.0.0", 0 + server.bind_unused_port spawn do server.close @@ -221,34 +218,35 @@ module HTTP it "reuses the TCP port (SO_REUSEPORT)" do s1 = Server.new { |ctx| } - s1.bind(0, reuse_port: true) + address = s1.bind_unused_port(reuse_port: true) s2 = Server.new { |ctx| } - s2.bind(s1.port.not_nil!, reuse_port: true) + s2.bind(address.port, reuse_port: true) s1.close s2.close end - it "binds to different interfaces" do + it "binds to different ports" do server = Server.new do |context| context.response.print "Test Server (#{context.request.headers["Host"]?})" end - address1 = server.bind(0) - address2 = server.bind(0) + tcp_server = TCPServer.new("127.0.0.1", 0) + server.bind tcp_server + address1 = tcp_server.local_address + + address2 = server.bind_unused_port - port1 = address1.port - port2 = address2.port - port1.should_not eq port2 + address1.should_not eq address2 spawn { server.listen } Fiber.yield - HTTP::Client.get("http://127.0.0.1:#{port2}/").body.should eq "Test Server (127.0.0.1:#{port2})" - HTTP::Client.get("http://127.0.0.1:#{port1}/").body.should eq "Test Server (127.0.0.1:#{port1})" - HTTP::Client.get("http://127.0.0.1:#{port1}/").body.should eq "Test Server (127.0.0.1:#{port1})" + HTTP::Client.get("http://#{address2}/").body.should eq "Test Server (#{address2})" + HTTP::Client.get("http://#{address1}/").body.should eq "Test Server (#{address1})" + HTTP::Client.get("http://#{address1}/").body.should eq "Test Server (#{address1})" end end diff --git a/spec/std/http/web_socket_spec.cr b/spec/std/http/web_socket_spec.cr index 79d17fa56129..77a6119855de 100644 --- a/spec/std/http/web_socket_spec.cr +++ b/spec/std/http/web_socket_spec.cr @@ -295,7 +295,7 @@ describe HTTP::WebSocket do end it "negotiates over HTTP correctly" do - port_chan = Channel(Int32).new + address_chan = Channel(Socket::IPAddress).new spawn do http_ref = nil @@ -314,14 +314,14 @@ describe HTTP::WebSocket do end http_server = http_ref = HTTP::Server.new([ws_handler]) - http_server.bind 0 - port_chan.send(http_server.port.not_nil!) + address = http_server.bind_unused_port + address_chan.send(address) http_server.listen end - listen_port = port_chan.receive + listen_address = address_chan.receive - ws2 = HTTP::WebSocket.new("ws://127.0.0.1:#{listen_port}/foo/bar?query=arg&yes=please") + ws2 = HTTP::WebSocket.new("ws://#{listen_address}/foo/bar?query=arg&yes=please") random = Random::Secure.hex ws2.on_message do |str| @@ -334,7 +334,7 @@ describe HTTP::WebSocket do end it "negotiates over HTTPS correctly" do - port_chan = Channel(Int32).new + address_chan = Channel(Socket::IPAddress).new spawn do http_ref = nil @@ -354,15 +354,15 @@ describe HTTP::WebSocket do tls = http_server.tls = OpenSSL::SSL::Context::Server.new tls.certificate_chain = File.join(__DIR__, "../openssl/ssl/openssl.crt") tls.private_key = File.join(__DIR__, "../openssl/ssl/openssl.key") - http_server.bind 0 - port_chan.send(http_server.port.not_nil!) + address = http_server.bind_unused_port + address_chan.send(address) http_server.listen end - listen_port = port_chan.receive + listen_address = address_chan.receive client_context = OpenSSL::SSL::Context::Client.insecure - ws2 = HTTP::WebSocket.new("127.0.0.1", port: listen_port, path: "/", tls: client_context) + ws2 = HTTP::WebSocket.new(listen_address.address, port: listen_address.port, path: "/", tls: client_context) random = Random::Secure.hex ws2.on_message do |str| diff --git a/src/http/server.cr b/src/http/server.cr index f9aca6c14c00..0c1b6e90fc66 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -123,22 +123,6 @@ class HTTP::Server @processor = RequestProcessor.new(handler) end - # Returns the TCP port of the first socket the server is bound to. - # - # For example you may let the system choose a port, then report it: - # ``` - # server = HTTP::Server.new { } - # server.bind 0 - # server.port # => 12345 - # ``` - def port : Int32? - @sockets.each do |socket| - if socket.is_a?(TCPServer) - return socket.local_address.port.to_i - end - end - end - # Creates a `TCPServer` and adds it as a socket, returning the local address # and port the server listens on. # @@ -163,6 +147,18 @@ class HTTP::Server bind "127.0.0.1", 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. + # + # ``` + # server = HTTP::Server.new { } + # server.bind_unused_port # => Socket::IPAddress.new("127.0.0.1", 12345) + # ``` + def bind_unused_port(host : String = "127.0.0.1", reuse_port : Bool = false) : Socket::IPAddress + bind host, 0, reuse_port + end + # Adds a `Socket::Server` *socket* to this server. def bind(socket : Socket::Server) : Socket::Server @sockets << socket From 3d08dcd0aafa5ad6a8e9716d9938abf77adc1e18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 26 Mar 2018 13:38:11 +0000 Subject: [PATCH 3/7] Rename `HTTP::Server#bind` to `#bind_tcp`, add overloads to `#listen` --- samples/http_server.cr | 3 +- spec/std/http/server/server_spec.cr | 16 +++---- .../crystal/tools/playground/server.cr | 8 ++-- src/http/server.cr | 42 +++++++++++++------ 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/samples/http_server.cr b/samples/http_server.cr index 43e74316a8ca..9bcbd0b9c66d 100644 --- a/samples/http_server.cr +++ b/samples/http_server.cr @@ -5,6 +5,5 @@ server = HTTP::Server.new do |context| context.response.print("Hello world!") end -server.bind "0.0.0.0", 8080 puts "Listening on http://0.0.0.0:8080" -server.listen +server.listen "0.0.0.0", 8080 diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index a3de6dac4f4d..968c9a3d064f 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -200,7 +200,7 @@ module HTTP address.port.should_not eq(0) server = Server.new { |ctx| } - port = server.bind(0).port + port = server.bind_tcp(0).port port.should_not eq(0) end @@ -221,7 +221,7 @@ module HTTP address = s1.bind_unused_port(reuse_port: true) s2 = Server.new { |ctx| } - s2.bind(address.port, reuse_port: true) + s2.bind_tcp(address.port, reuse_port: true) s1.close s2.close @@ -331,7 +331,7 @@ module HTTP typeof(begin # Initialize with custom host server = Server.new { |ctx| } - server.bind "0.0.0.0", 0 + server.bind_tcp "0.0.0.0", 0 server.listen server.close @@ -342,18 +342,18 @@ module HTTP StaticFileHandler.new("."), ] ) - server.bind "0.0.0.0", 0 + server.bind_tcp "0.0.0.0", 0 server.listen server.close server = Server.new([StaticFileHandler.new(".")]) { |ctx| } - server.bind "0.0.0.0", 0 + server.bind_tcp "0.0.0.0", 0 server.listen server.close # Initialize with default host server = Server.new { |ctx| } - server.bind 0 + server.bind_tcp 0 server.listen server.close @@ -364,12 +364,12 @@ module HTTP StaticFileHandler.new("."), ] ) - server.bind 0 + server.bind_tcp 0 server.listen server.close server = Server.new([StaticFileHandler.new(".")]) { |ctx| } - server.bind 0 + server.bind_tcp 0 server.listen server.close end) diff --git a/src/compiler/crystal/tools/playground/server.cr b/src/compiler/crystal/tools/playground/server.cr index 20b105f833b9..ee26f93338b1 100644 --- a/src/compiler/crystal/tools/playground/server.cr +++ b/src/compiler/crystal/tools/playground/server.cr @@ -507,13 +507,13 @@ module Crystal::Playground host = @host if host - server.bind host, @port + address = server.bind_tcp host, @port else - server.bind @port - host = "localhost" + address = server.bind_tcp @port end + @port = address.port - puts "Listening on http://#{host}:#{@port}" + puts "Listening on http://#{address}" if host == "0.0.0.0" puts "WARNING running playground with 0.0.0.0 is unsecure." end diff --git a/src/http/server.cr b/src/http/server.cr index 0c1b6e90fc66..2924e443f77c 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -123,28 +123,26 @@ class HTTP::Server @processor = RequestProcessor.new(handler) end - # Creates a `TCPServer` and adds it as a socket, returning the local address + # Creates a `TCPServer` listenting on `host:port` and adds it as a socket, returning the local address # and port the server listens on. # - # If *port* is `0`, a random, free port will be chosen. - # - # You may set *reuse_port* to `true` to enable the `SO_REUSEPORT` socket option, + # If *reuse_port* is `true`, it enables the `SO_REUSEPORT` socket option, # which allows multiple processes to bind to the same port. - def bind(host : String, port : Int32, reuse_port : Bool = false) : Socket::IPAddress + def bind_tcp(host : String, port : Int32, reuse_port : Bool = false) : Socket::IPAddress tcp_server = TCPServer.new(host, port, reuse_port: reuse_port) + bind(tcp_server) + tcp_server.local_address end - # Creates a `TCPServer` listenting on `127.0.0.1` and adds it as a socket, + # Creates a `TCPServer` listenting on `127.0.0.1:port` and adds it as a socket, # returning the local address and port the server listens on. # - # If *port* is `0`, a random, free port will be chosen. - # - # You may set *reuse_port* to true to enable the `SO_REUSEPORT` socket option, + # If *reuse_port* is `true`, it enables the `SO_REUSEPORT` socket option, # which allows multiple processes to bind to the same port. - def bind(port : Int32, reuse_port : Bool = false) : Socket::IPAddress - bind "127.0.0.1", port, reuse_port + def bind_tcp(port : Int32, reuse_port : Bool = false) : Socket::IPAddress + bind_tcp "127.0.0.1", port, reuse_port end # Creates a `TCPServer` listening on an unused port and adds it as a socket. @@ -156,7 +154,7 @@ class HTTP::Server # server.bind_unused_port # => Socket::IPAddress.new("127.0.0.1", 12345) # ``` def bind_unused_port(host : String = "127.0.0.1", reuse_port : Bool = false) : Socket::IPAddress - bind host, 0, reuse_port + bind_tcp host, 0, reuse_port end # Adds a `Socket::Server` *socket* to this server. @@ -166,6 +164,26 @@ class HTTP::Server socket end + # Creates a `TCPServer` listenting 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` listenting 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 raise "Can't start server with not sockets to listen to" if @sockets.empty? From 97aa31fa8b13c5b02b24a71c4f3d5cfa012d5d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 26 Mar 2018 13:38:57 +0000 Subject: [PATCH 4/7] Add HTTP::Server#addresses --- spec/std/http/server/server_spec.cr | 10 ++++++++++ src/http/server.cr | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index 968c9a3d064f..b035841502aa 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -248,6 +248,16 @@ module HTTP HTTP::Client.get("http://#{address1}/").body.should eq "Test Server (#{address1})" HTTP::Client.get("http://#{address1}/").body.should eq "Test Server (#{address1})" end + + it "lists addresses" do + server = Server.new { } + + tcp_server = TCPServer.new("127.0.0.1", 0) + addresses = [server.bind_unused_port, server.bind_unused_port, tcp_server.local_address] + server.bind tcp_server + + server.addresses.should eq addresses + end end describe HTTP::Server::RequestProcessor do diff --git a/src/http/server.cr b/src/http/server.cr index 2924e443f77c..d73d2740c976 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -164,6 +164,21 @@ class HTTP::Server 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` listenting on `127.0.0.1:port`, adds it as a socket # and starts the server. Blocks until the server is closed. # From bdfd8ec070cd6e274c543569dd7938f27d2971e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sun, 15 Apr 2018 12:51:18 +0200 Subject: [PATCH 5/7] fixup! Refactor HTTP::Server to use multiple interfaces --- src/http/server.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/http/server.cr b/src/http/server.cr index d73d2740c976..154b98f691fc 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -210,7 +210,7 @@ class HTTP::Server until @wants_close spawn handle_client(socket.accept? || break) end - + ensure done.send nil end end From 8a91ee02778e882766ac9b70bf5ebed04870da8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Sun, 15 Apr 2018 12:52:19 +0200 Subject: [PATCH 6/7] fixup! Refactor HTTP::Server to use multiple interfaces --- src/http/server.cr | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/http/server.cr b/src/http/server.cr index 154b98f691fc..e5b7bcbb23df 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -158,10 +158,8 @@ class HTTP::Server end # Adds a `Socket::Server` *socket* to this server. - def bind(socket : Socket::Server) : Socket::Server + def bind(socket : Socket::Server) : Nil @sockets << socket - - socket end # Enumerates all addresses this server is bound to. From 8e17445f1df08e20eee001914132009a1b06b6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 17 Apr 2018 08:09:57 +0000 Subject: [PATCH 7/7] Add HTTP::Server.listening? and raise errors when running or closed --- spec/std/http/server/server_spec.cr | 52 +++++++++++++++++++++++++++++ src/http/server.cr | 22 +++++++++--- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index b035841502aa..c0c562433704 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -258,6 +258,58 @@ module HTTP server.addresses.should eq addresses end + + describe "#bind" do + it "fails after listen" do + server = Server.new { } + server.bind_unused_port + spawn { server.listen } + Fiber.yield + expect_raises(Exception, "Can't add socket to running server") do + server.bind_unused_port + end + server.close + end + + it "fails after close" do + server = Server.new { } + server.bind_unused_port + spawn { server.listen } + Fiber.yield + server.close + expect_raises(Exception, "Can't add socket to closed server") do + server.bind_unused_port + end + server.close unless server.closed? + end + end + + describe "#listen" do + it "fails after listen" do + server = Server.new { } + server.bind_unused_port + spawn { server.listen } + Fiber.yield + server.listening?.should be_true + expect_raises(Exception, "Can't start running server") do + server.listen + end + server.close + end + + it "fails after close" do + server = Server.new { } + server.bind_unused_port + spawn { server.listen } + Fiber.yield + server.listening?.should be_true + server.close + server.listening?.should be_false + expect_raises(Exception, "Can't re-start closed server") do + server.listen + end + end + end end describe HTTP::Server::RequestProcessor do diff --git a/src/http/server.cr b/src/http/server.cr index e5b7bcbb23df..b038a2fe9ab3 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -99,9 +99,14 @@ class HTTP::Server property tls : OpenSSL::SSL::Context::Server? {% end %} - @wants_close = false @sockets = [] of Socket::Server + # Returns `true` if this server is closed. + getter? closed : Bool = false + + # Returns `true` if this server is listening on it's sockets. + getter? listening : Bool = false + # Creates a new HTTP server with the given block as handler. def self.new(&handler : HTTP::Handler::Proc) : self new(handler) @@ -159,6 +164,9 @@ class HTTP::Server # 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 @@ -199,13 +207,16 @@ class HTTP::Server # Starts the server. Blocks until the server is closed. def listen - raise "Can't start server with not sockets to listen to" if @sockets.empty? + 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 - until @wants_close + until closed? spawn handle_client(socket.accept? || break) end ensure @@ -219,7 +230,9 @@ class HTTP::Server # Gracefully terminates the server. It will process currently accepted # requests, but it won't accept new connections. def close - @wants_close = true + raise "Can't close server, it's already closed" if closed? + + @closed = true @processor.close @sockets.each do |socket| @@ -228,6 +241,7 @@ class HTTP::Server # ignore exception on close end + @listening = false @sockets.clear end