diff --git a/spec/client_spec.cr b/spec/client_spec.cr index ba992b1..bc0b6f5 100644 --- a/spec/client_spec.cr +++ b/spec/client_spec.cr @@ -137,25 +137,5 @@ describe HTTP::Proxy::Client do end end end - - describe "HTTP::Client#set_proxy" do - context HTTP::Client do - it "should make HTTP request with proxy" do - with_proxy_server do |host, port, _username, _password, wants_close| - proxy_client = HTTP::Proxy::Client.new(host, port) - - uri = URI.parse("http://httpbingo.org") - client = HTTP::Client.new(uri) - client.set_proxy(proxy_client) - response = client.get("/get") - - (client.proxy?).should eq(true) - (response.status_code).should eq(200) - ensure - wants_close.send(nil) - end - end - end - end end end diff --git a/src/ext/http/client.cr b/src/ext/http/client.cr index d4b6a4e..5265da4 100644 --- a/src/ext/http/client.cr +++ b/src/ext/http/client.cr @@ -26,11 +26,6 @@ module HTTP end end - @[Deprecated("Use `#proxy=` instead")] - def set_proxy(proxy_client : HTTP::Proxy::Client) - self.proxy = proxy_client - end - # True if requests for this connection will be proxied def proxy? : Bool !!@proxy diff --git a/src/http/proxy/server.cr b/src/http/proxy/server.cr index b05f7d9..b726174 100644 --- a/src/http/proxy/server.cr +++ b/src/http/proxy/server.cr @@ -13,27 +13,46 @@ require "./server/basic_auth" # ``` # class HTTP::Proxy::Server + Log = ::Log.for("http.proxy.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 Proxy server def initialize handler = build_middleware - @processor = RequestProcessor.new(handler) + + @processor = HTTP::Server::RequestProcessor.new(handler) end + # Creates a new HTTP Proxy server with the given block as handler. def initialize(&handler : Context ->) - @processor = RequestProcessor.new(handler) + @processor = HTTP::Server::RequestProcessor.new(handler) end - def initialize(handlers : Array(HTTP::Handler), &handler : Context ->) + # Creates a new HTTP Proxy server with a handler chain constructed from the *handlers* + # array and the given block. + def initialize(handlers : Indexable(HTTP::Handler), &handler : Context ->) handler = build_middleware(handlers, handler) - @processor = RequestProcessor.new(handler) + + @processor = HTTP::Server::RequestProcessor.new(handler) end - def initialize(handlers : Array(HTTP::Handler)) + # Creates a new HTTP Proxy server with the *handlers* array as handler chain. + def initialize(handlers : Indexable(HTTP::Handler)) handler = build_middleware(handlers) - @processor = RequestProcessor.new(handler) + + @processor = HTTP::Server::RequestProcessor.new(handler) end + # Creates a new HTTP server with the given *handler*. def initialize(handler : HTTP::Handler | HTTP::Handler::HandlerProc) - @processor = RequestProcessor.new(handler) + @processor = HTTP::Server::RequestProcessor.new(handler) end private def build_middleware(handler : (Context ->)? = nil) @@ -50,4 +69,118 @@ class HTTP::Proxy::Server handlers.last.next = proxy_handler if proxy_handler handlers.first end + + # Creates a `TCPServer` listening on `host:port` and adds it as a socket, returning the local address + # and port the server listens on. + # + # 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 + + # 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 + + # Overwrite this method to implement an alternative concurrency handler + # one example could be the use of a fiber pool + protected def dispatch(io) + spawn handle_client(io) + 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 + dispatch(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) + Log.error(exception: e) { "Error while connecting a new socket" } + end end diff --git a/src/http/proxy/server/context.cr b/src/http/proxy/server/context.cr index d75e7d3..0c35148 100644 --- a/src/http/proxy/server/context.cr +++ b/src/http/proxy/server/context.cr @@ -1,5 +1,15 @@ -class HTTP::Proxy::Server < HTTP::Server - class Context < HTTP::Server::Context +class HTTP::Proxy::Server + class Context + # The `HTTP::Request` to process. + getter request : HTTP::Request + + # The `HTTP::Server::Response` to configure and write to. + getter response : HTTP::Server::Response + + # :nodoc: + def initialize(@request : HTTP::Request, @response : HTTP::Server::Response) + end + def perform case @request.method when "OPTIONS" diff --git a/src/http/proxy/server/handler.cr b/src/http/proxy/server/handler.cr index de078cb..5178e6c 100644 --- a/src/http/proxy/server/handler.cr +++ b/src/http/proxy/server/handler.cr @@ -3,15 +3,11 @@ require "./context" class HTTP::Proxy::Server::Handler include HTTP::Handler - property next : HTTP::Handler | Proc | Nil - - alias Proc = Context -> + property next : HTTP::Handler | HandlerProc | Nil def call(context) - request = context.request - response = context.response - context = Context.new(request, response) - - context.perform + HTTP::Proxy::Server::Context.new(context.request, context.response).perform end + + alias HandlerProc = HTTP::Proxy::Server::Context -> end