Skip to content

Fix HTTP client proxy support #742

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

Merged
merged 5 commits into from
Feb 25, 2025
Merged
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
18 changes: 15 additions & 3 deletions src/aleph/http.clj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
(aleph.utils
ConnectionTimeoutException
PoolTimeoutException
ProxyConnectionTimeoutException
ReadTimeoutException
RequestCancellationException
RequestTimeoutException)
Expand Down Expand Up @@ -414,12 +415,23 @@
;; connection establishment failed
(d/catch'
(fn [e]
(if (or (instance? TimeoutException e)
;; Unintuitively, this type doesn't inherit from TimeoutException
(instance? ConnectTimeoutException e))
(cond
;; Handled separately because it inherits from
;; TimeoutException but we don't want to wrap it in
;; ConnectionTimeoutException.
(instance? ProxyConnectionTimeoutException e)
(do
(log/trace "Timed out waiting for proxy connection to be established")
(d/error-deferred e))

(or (instance? TimeoutException e)
;; Unintuitively, this type doesn't inherit from TimeoutException
(instance? ConnectTimeoutException e))
(do
(log/trace "Timed out waiting for connection to be established")
(d/error-deferred (ConnectionTimeoutException. ^Throwable e)))

:else
(do
(log/trace "Connection establishment failed")
(d/error-deferred e)))))
Expand Down
245 changes: 126 additions & 119 deletions src/aleph/http/client.clj
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@
(.setConnectTimeoutMillis ^ProxyHandler handler connection-timeout))
handler))

(defn pending-proxy-connection-handler [response-stream]
(defn pending-proxy-connection-handler [proxy-connected]
(netty/channel-inbound-handler
:exception-caught
([_ ctx cause]
Expand All @@ -388,30 +388,31 @@
headers (when (instance? HttpProxyHandler$HttpProxyConnectException cause)
(.headers ^HttpProxyHandler$HttpProxyConnectException cause))
response (cond
(= "timeout" message)
(.contains message "timeout")
(ProxyConnectionTimeoutException. ^Throwable cause)

(some? headers)
(ex-info message {:headers (http1/headers->map headers)})
(ex-info message {:headers (http1/headers->map headers)} cause)

:else
cause)]
(s/put! response-stream response)
(d/error! proxy-connected response)
;; client handler should take care of the rest
(netty/close ctx))))

:user-event-triggered
([this ctx evt]
(when (instance? ProxyConnectionEvent evt)
(.remove (.pipeline ctx) this))
(.remove (.pipeline ctx) this)
(d/success! proxy-connected true))
(.fireUserEventTriggered ^ChannelHandlerContext ctx evt))))

(defn- add-proxy-handlers
"Inserts handlers for proxying through a server"
[^ChannelPipeline p response-stream proxy-options ssl?]
(when (some? proxy-options)
[^ChannelPipeline p proxy-connected proxy-options ssl?]
(if (some? proxy-options)
(let [proxy (proxy-handler (assoc proxy-options :ssl? ssl?))]
(.addFirst p "proxy" ^ChannelHandler proxy)
(.addLast p "proxy" ^ChannelHandler proxy)
;; well, we need to wait before the proxy responded with
;; HTTP/1.1 200 Connection established
;; before sending any requests
Expand All @@ -420,7 +421,8 @@
"proxy"
"pending-proxy-connection"
^ChannelHandler
(pending-proxy-connection-handler response-stream)))))
(pending-proxy-connection-handler proxy-connected))))
(d/success! proxy-connected false))
p)

(defn- setup-http1-pipeline
Expand Down Expand Up @@ -458,7 +460,6 @@
false))
(.addLast "streamer" ^ChannelHandler (ChunkedWriteHandler.))
(.addLast "handler" ^ChannelHandler handler)
(add-proxy-handlers responses proxy-options ssl?)
(common/add-non-http-handlers logger pipeline-transform))

(log/debug "http1 client pipeline" pipeline)
Expand All @@ -473,9 +474,10 @@

Can't use an ApnHandler/ApplicationProtocolNegotiationHandler here,
because it's tricky to run Manifold code on Netty threads."
[{:keys [ssl? remote-address ssl-context ssl-endpoint-id-alg]}]
[{:keys [ssl? remote-address ssl-context ssl-endpoint-id-alg proxy-options proxy-connected]}]
(fn pipeline-builder*
[^ChannelPipeline pipeline]
(add-proxy-handlers pipeline proxy-connected proxy-options ssl?)
(when ssl?
(do
(.addLast pipeline
Expand Down Expand Up @@ -803,16 +805,18 @@
(some? log-activity) (netty/activity-logger "aleph-client" log-activity)
:else nil)

proxy-connected (d/deferred)
pipeline-builder (make-pipeline-builder
(assoc opts
:ssl? ssl?
:ssl-context ssl-context
:ssl-endpoint-id-alg ssl-endpoint-id-alg
:remote-address remote-address
:raw-stream? raw-stream?
:response-buffer-size response-buffer-size
:logger logger
:pipeline-transform pipeline-transform))
(assoc opts
:proxy-connected proxy-connected
:ssl? ssl?
:ssl-context ssl-context
:ssl-endpoint-id-alg ssl-endpoint-id-alg
:remote-address remote-address
:raw-stream? raw-stream?
:response-buffer-size response-buffer-size
:logger logger
:pipeline-transform pipeline-transform))

ch-d (netty/create-client-chan
{:pipeline-builder pipeline-builder
Expand All @@ -825,108 +829,111 @@

(attach-on-close-handler ch-d on-closed)

(d/chain' ch-d
(fn setup-client
[^Channel ch]
(log/debug "Channel:" ch)

;; We know the SSL handshake must be complete because create-client wraps the
;; future with maybe-ssl-handshake-future, so we can get the negotiated
;; protocol, falling back to HTTP/1.1 by default.
(let [pipeline (.pipeline ch)
protocol (cond
ssl?
(or (-> pipeline
^SslHandler (.get ^Class SslHandler)
(.applicationProtocol))
ApplicationProtocolNames/HTTP_1_1) ; Not using ALPN, HTTP/2 isn't allowed

force-h2c?
(d/chain'
proxy-connected
(fn [_]
(d/chain' ch-d
(fn setup-client
[^Channel ch]
(log/debug "Channel:" ch)

;; We know the SSL handshake must be complete because create-client wraps the
;; future with maybe-ssl-handshake-future, so we can get the negotiated
;; protocol, falling back to HTTP/1.1 by default.
(let [pipeline (.pipeline ch)
protocol (cond
ssl?
(or (-> pipeline
^SslHandler (.get ^Class SslHandler)
(.applicationProtocol))
ApplicationProtocolNames/HTTP_1_1) ; Not using ALPN, HTTP/2 isn't allowed

force-h2c?
(do
(log/info "Forcing HTTP/2 over cleartext. Be sure to do this only with servers you control.")
ApplicationProtocolNames/HTTP_2)

:else
ApplicationProtocolNames/HTTP_1_1) ; Not using SSL, HTTP/2 isn't allowed unless h2c requested
setup-opts (assoc opts
:authority authority
:ch ch
:server? false
:keep-alive? keep-alive?
:keep-alive?' keep-alive?'
:logger logger
:non-tun-proxy? non-tun-proxy?
:pipeline pipeline
:pipeline-transform pipeline-transform
:raw-stream? raw-stream?
:remote-address remote-address
:response-buffer-size response-buffer-size
:ssl-context ssl-context
:ssl? ssl?)]

(log/debug (str "Using HTTP protocol: " protocol)
{:authority authority
:ssl? ssl?
:force-h2c? force-h2c?})

;; can't use ApnHandler, because we need to coordinate with Manifold code
(let [http-req-handler
(cond (.equals ApplicationProtocolNames/HTTP_1_1 protocol)
(setup-http1-client setup-opts)

(.equals ApplicationProtocolNames/HTTP_2 protocol)
(do
(log/info "Forcing HTTP/2 over cleartext. Be sure to do this only with servers you control.")
ApplicationProtocolNames/HTTP_2)
(http2/setup-conn-pipeline setup-opts)
(http2-req-handler setup-opts))

:else
ApplicationProtocolNames/HTTP_1_1) ; Not using SSL, HTTP/2 isn't allowed unless h2c requested
setup-opts (assoc opts
:authority authority
:ch ch
:server? false
:keep-alive? keep-alive?
:keep-alive?' keep-alive?'
:logger logger
:non-tun-proxy? non-tun-proxy?
:pipeline pipeline
:pipeline-transform pipeline-transform
:raw-stream? raw-stream?
:remote-address remote-address
:response-buffer-size response-buffer-size
:ssl-context ssl-context
:ssl? ssl?)]

(log/debug (str "Using HTTP protocol: " protocol)
{:authority authority
:ssl? ssl?
:force-h2c? force-h2c?})

;; can't use ApnHandler, because we need to coordinate with Manifold code
(let [http-req-handler
(cond (.equals ApplicationProtocolNames/HTTP_1_1 protocol)
(setup-http1-client setup-opts)

(.equals ApplicationProtocolNames/HTTP_2 protocol)
(do
(http2/setup-conn-pipeline setup-opts)
(http2-req-handler setup-opts))

:else
(do
(let [msg (str "Unknown protocol: " protocol)
e (IllegalStateException. msg)]
(log/error e msg)
(netty/close ch)
(throw e))))]

;; Both Netty and Aleph are set up, unpause the pipeline
(when (.get pipeline "pause-handler")
(log/debug "Unpausing pipeline")
(.remove pipeline "pause-handler"))

(fn http-req-fn
[req]
(log/trace "http-req-fn fired")
(log/debug "client request:" (pr-str req))

;; If :aleph/close is set in the req, closes the channel and
;; returns a deferred containing the result.
(if (or (contains? req :aleph/close)
(contains? req ::close))
(-> ch (netty/close) (netty/wrap-future))

(let [t0 (System/nanoTime)
;; I suspect the below is an error for http1
;; since the shared handler might not match.
;; Should work for HTTP2, though
raw-stream? (get req :raw-stream? raw-stream?)]

(if (or (not (.isActive ch))
(not (.isOpen ch)))

(d/error-deferred
(ex-info "Channel is inactive/closed."
{:req req
:ch ch
:open? (.isOpen ch)
:active? (.isActive ch)}))

(-> (http-req-handler req)
(d/chain' (rsp-handler
{:ch ch
:keep-alive? keep-alive? ; why not keep-alive?'
:raw-stream? raw-stream?
:req req
:response-buffer-size response-buffer-size
:t0 t0})))))))))))))
(do
(let [msg (str "Unknown protocol: " protocol)
e (IllegalStateException. msg)]
(log/error e msg)
(netty/close ch)
(throw e))))]

;; Both Netty and Aleph are set up, unpause the pipeline
(when (.get pipeline "pause-handler")
(log/debug "Unpausing pipeline")
(.remove pipeline "pause-handler"))

(fn http-req-fn
[req]
(log/trace "http-req-fn fired")
(log/debug "client request:" (pr-str req))

;; If :aleph/close is set in the req, closes the channel and
;; returns a deferred containing the result.
(if (or (contains? req :aleph/close)
(contains? req ::close))
(-> ch (netty/close) (netty/wrap-future))

(let [t0 (System/nanoTime)
;; I suspect the below is an error for http1
;; since the shared handler might not match.
;; Should work for HTTP2, though
raw-stream? (get req :raw-stream? raw-stream?)]

(if (or (not (.isActive ch))
(not (.isOpen ch)))

(d/error-deferred
(ex-info "Channel is inactive/closed."
{:req req
:ch ch
:open? (.isOpen ch)
:active? (.isActive ch)}))

(-> (http-req-handler req)
(d/chain' (rsp-handler
{:ch ch
:keep-alive? keep-alive? ; why not keep-alive?'
:raw-stream? raw-stream?
:req req
:response-buffer-size response-buffer-size
:t0 t0})))))))))))))))



Expand Down
4 changes: 0 additions & 4 deletions src/aleph/http/http2.clj
Original file line number Diff line number Diff line change
Expand Up @@ -1251,17 +1251,13 @@
[{:keys [^ChannelPipeline pipeline
handler
server?
proxy-options
logger
http2-stream-pipeline-transform
max-request-body-size
compression?
compression-options]}]
(log/trace "setup-stream-pipeline called")

(when (some? proxy-options)
(throw (IllegalArgumentException. "Proxying HTTP/2 messages not supported yet")))

(cond-> pipeline

;; necessary for multipart support in HTTP/2?
Expand Down
Loading