Skip to content

Commit

Permalink
Merge pull request #35 from mamantoha/develop
Browse files Browse the repository at this point in the history
make HTTP::Proxy::Server independent
  • Loading branch information
mamantoha authored Jul 26, 2024
2 parents ea17189 + 8b71f2d commit 3871d80
Show file tree
Hide file tree
Showing 5 changed files with 156 additions and 42 deletions.
20 changes: 0 additions & 20 deletions spec/client_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 0 additions & 5 deletions src/ext/http/client.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
147 changes: 140 additions & 7 deletions src/http/proxy/server.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
14 changes: 12 additions & 2 deletions src/http/proxy/server/context.cr
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
12 changes: 4 additions & 8 deletions src/http/proxy/server/handler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 3871d80

Please sign in to comment.