From c3635e11acd47983a60745268a998ea4e328574c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 6 Mar 2018 15:34:36 +0000 Subject: [PATCH] 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 | 151 +++++++++++------- 7 files changed, 154 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 e43f3b159f53..95b465defe54 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 + + interface1 = server.bind(0) + interface2 = server.bind(0) + + port1 = interface1.local_address.port + port2 = interface2.local_address.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 62fdad175e91..50573e67bfdb 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..a29b36726c0b 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,102 @@ class HTTP::Server {% end %} @wants_close = false + @sockets = [] of Socket::Server - def self.new(port, &handler : Context ->) - new("127.0.0.1", port, &handler) - end - - def self.new(port, handlers : Array(HTTP::Handler), &handler : Context ->) - new("127.0.0.1", port, 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) + # Creates a new HTTP server with the given block as handler. + def self.new(&handler : HTTP::Handler::Proc) : self + new(handler) end - def initialize(@host : String, @port : Int32, &handler : Context ->) - @processor = RequestProcessor.new(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 initialize(@host : String, @port : Int32, handlers : Array(HTTP::Handler), &handler : Context ->) - handler = HTTP::Server.build_middleware handlers, handler - @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) + # 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, handler : HTTP::Handler | HTTP::Handler::Proc) + # Creates a new HTTP server with the given *handler*. + def initialize(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. + # + # 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) : TCPServer + bind TCPServer.new(host, port, reuse_port: reuse_port) + end + + # Creates a `TCPServer` listenting on `127.0.0.1` and adds it as a socket. + # + # 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) : TCPServer + bind "127.0.0.1", port, reuse_port end - # Starts the server. Blocks until the server is closed. + # Creates a `TCPServer` listening on an unused port and adds it as a socket. # - # 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?) + # Returns the determined port number. + # + # ``` + # server = HTTP::Server.new { } + # server.bind_unused_port # => 12345 + # ``` + def bind_unused_port(host : String = "127.0.0.1", reuse_port : Bool = false) : Int32 + tcp_server = bind(host, 0, reuse_port) + tcp_server.local_address.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. + 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 +203,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) %}