On this page:
«make-socks5-proxy-body»
«λ-proxy-connect»
«proxy-connect-body»1
«proxy-connect-body»2
«proxy-connect-body»3
«ssl-and-tunnel-properties»
«establish-ssl-ports-for-https»

2.2 Proxy for http-easy🔗ℹ

The http-easy library has a proxies feature. If a proxy is added to a session, then when http-easy needs to establish an HTTP connection, it will run the proxy’s connect procedure instead of the usual http-conn-open! code. After the connection is established, HTTP requests and responses are sent like normal through the ports without explicitly calling proxy code.

Writing this procedure was very difficult for me to get my head around, which is why I’m documenting it so verbosely. The http-easy library includes some sample proxies which my code is based on.

First, I’ll set up the scaffolding. make-proxy is called with the desired matches? function and the connect procedure. matches? is called to determine whether a URL should be handled by this proxy.

Now for the connect procedure. Normally with no proxy, these specific steps would be followed to establish a new HTTP connection:

  1. A blank HTTP connection object http-conn? is created. It starts with no state.

  2. http-conn-open! is called on the HTTP connection object to connect to a host. This basically just calls tcp-connect to the destination host and port. If SSL should be used, it instead calls ssl-connect. The HTTP connection object is mutated to store the newly established connection.

  3. HTTP requests and responses may now be sent through the ports with functions like http-conn-send!.

For a proxy connection, step 2 is replaced. The proxy’s connect procedure receives the blank HTTP connection object (as well as the destination URL and SSL context), opens the ports, does SSL negotiation on them if needed, then finally mutates the HTTP connection object’s state. Once the procedure is done, the HTTP connection object is ready to rock.

I’ll first define the signature and the parameter types of the proxy connect function.

(λ ([conn : HTTP-Connection] [u : URL] [ssl-ctx : SSL-Client-Context])
  «proxy-connect-body»)

The first step in the procedure is to determine where to connect to. This information is stored in u. I also determine that SSL should be used iif the protocol is https://.

(define main-stream-encrypted? (equal? (url-scheme u) "https"))
(define target-host (cast (url-host u) String))
(define target-port (cast (or (url-port u) (if main-stream-encrypted? 443 80)) Positive-Integer))

Now I know where to connect to, I can establish the SOCKS connection and get my pair of ports.

(define-values (in out)
  (socks5-connect socks-host socks-port target-host target-port #:username-password username-password))

Finally, I open the HTTP connection on the provided HTTP connection object atop that pair of ports.

(http-conn-open! conn
                 target-host #:port target-port
                 #:ssl? «ssl-and-tunnel-properties»)

"But wait! Why are you calling http-conn-open! when that procedure sets up a new, normal, not proxied connection?", I hear you ask. That’s a very good and accurate question, but in this case, the #:ssl? parameter is actually a misnomer. While it would normally accept an ssl-client-context? and establish its own connection, it can also accept (list/c ssl-client-context? input-port? output-port? connection-abandon-procedure). In this case, it makes the connection atop the already-established input and output port. This is exactly what I want when I’m tunnelling the connection through those ports!

When SSL is not used, it really is as simple as making that list, with #f for the context to indicate SSL is not used:

(cond [(not main-stream-encrypted?)
       (list #f
             in
             out
             (λ (_) (close-input-port in) (close-output-port out)))]
      [else «establish-ssl-ports-for-https»])

When SSL is used, http-conn-open! won’t do SSL for me, so I need to do it myself by calling ports->ssl-ports.

I need to set up ssl-ctx* first, just to normalise whatever value I got in to be a real ssl-client-context?.

Specifying #:close-original #t means when the SSL ports are closed, the underlying tunnel ports are also closed. Therefore I can just use ssl-abandon-port for the connection-abandon-procedure.

(define ssl-ctx*
  (cond
    [(ssl-client-context? ssl-ctx) ssl-ctx]
    [(symbol? ssl-ctx) (ssl-make-client-context ssl-ctx)]
    [else (error 'make-socks5-proxy-connect! "don't know how to normalise ssl-ctx: ~v" ssl-ctx)]))
(define-values (in* out*)
  (ports->ssl-ports in out
                    #:mode 'connect
                    #:context ssl-ctx*
                    #:close-original? #t
                    #:hostname target-host))
(list ssl-ctx* in* out* ssl-abandon-port)

To see how make-socks5-proxy would be used in another application, see examples.