@@ -115,6 +115,16 @@ type
115115
116116 HttpClientConnectionRef * = ref HttpClientConnection
117117
118+ HttpProxy * = object
119+ # # HTTP proxy configuration
120+ hostname* : string
121+ port* : uint16
122+ username* : string
123+ password* : string
124+ addresses* : seq [TransportAddress ]
125+
126+ HttpProxyRef * = ref HttpProxy
127+
118128 HttpSessionRef * = ref object
119129 connections* : Table [string , seq [HttpClientConnectionRef ]]
120130 counter* : uint64
130140 socketFlags* : set [SocketFlags ]
131141 flags* : HttpClientFlags
132142 dualstack* : DualStackType
143+ proxy* : HttpProxyRef
133144
134145 HttpAddress * = object
135146 id* : string
@@ -259,6 +270,25 @@ template isIdle(conn: HttpClientConnectionRef, timestamp: Moment,
259270
260271proc sessionWatcher (session: HttpSessionRef ) {.async : (raises: []).}
261272
273+ proc new * (t: typedesc [HttpProxy ],
274+ hostname: string ,
275+ port: uint16 = 8080 ,
276+ username: string = " " ,
277+ password: string = " " ): HttpProxyRef =
278+ # # Create new HTTP proxy configuration object.
279+ # #
280+ # # ``hostname`` - proxy server hostname
281+ # # ``port`` - proxy server port (default 8080)
282+ # # ``username`` - proxy authentication username (optional)
283+ # # ``password`` - proxy authentication password (optional)
284+ let proxy = new (HttpProxy )
285+ proxy.hostname = hostname
286+ proxy.port = port
287+ proxy.username = username
288+ proxy.password = password
289+ proxy.addresses = @ []
290+ proxy
291+
262292proc new * (t: typedesc [HttpSessionRef ],
263293 flags: HttpClientFlags = {},
264294 maxRedirections = HttpMaxRedirections ,
@@ -269,14 +299,16 @@ proc new*(t: typedesc[HttpSessionRef],
269299 idleTimeout = HttpConnectionIdleTimeout ,
270300 idlePeriod = HttpConnectionCheckPeriod ,
271301 socketFlags: set [SocketFlags ] = {},
272- dualstack = DualStackType .Auto ): HttpSessionRef =
302+ dualstack = DualStackType .Auto ,
303+ proxy: HttpProxyRef = nil ): HttpSessionRef =
273304 # # Create new HTTP session object.
274305 # #
275306 # # ``maxRedirections`` - maximum number of HTTP 3xx redirections
276307 # # ``connectTimeout`` - timeout for ongoing HTTP connection
277308 # # ``headersTimeout`` - timeout for receiving HTTP response headers
278309 # # ``idleTimeout`` - timeout to consider HTTP connection as idle
279310 # # ``idlePeriod`` - period of time to check HTTP connections for inactivity
311+ # # ``proxy`` - optional HTTP proxy configuration
280312 doAssert (maxRedirections >= 0 , " maxRedirections should not be negative" )
281313 var res = HttpSessionRef (
282314 flags: flags,
@@ -289,7 +321,8 @@ proc new*(t: typedesc[HttpSessionRef],
289321 idlePeriod: idlePeriod,
290322 connections: initTable [string , seq [HttpClientConnectionRef ]](),
291323 socketFlags: socketFlags,
292- dualstack: dualstack
324+ dualstack: dualstack,
325+ proxy: proxy
293326 )
294327 res.watcherFut =
295328 if HttpClientFlag .Http11Pipeline in flags:
@@ -423,28 +456,34 @@ proc getAddress*(session: HttpSessionRef, url: Uri): HttpResult[HttpAddress] =
423456
424457 let id = hostname & " :" & Base10 .toString (port)
425458
459+ # When using a proxy, don't resolve target address since proxy will do it
426460 let addresses =
427- try :
428- if ( HttpClientFlag . NoInet4Resolution in session.flags) and
429- ( HttpClientFlag . NoInet6Resolution in session.flags):
430- # DNS resolution is disabled.
431- @ [ initTAddress (hostname, Port (port))]
432- else :
433- if (HttpClientFlag .NoInet4Resolution notin session.flags) and
434- (HttpClientFlag .NoInet6Resolution notin session.flags):
435- # DNS resolution for both IPv4 and IPv6 addresses .
436- resolveTAddress (hostname, Port (port))
461+ if not ( isNil (session.proxy)) :
462+ # Proxy is configured: return empty addresses, proxy handles target resolution
463+ @ []
464+ else :
465+ # No proxy: resolve target address normally
466+ try :
467+ if (HttpClientFlag .NoInet4Resolution in session.flags) and
468+ (HttpClientFlag .NoInet6Resolution in session.flags):
469+ # DNS resolution is disabled .
470+ @ [ initTAddress (hostname, Port (port))]
437471 else :
438- if HttpClientFlag .NoInet6Resolution in session.flags:
439- # DNS resolution only for IPv4 addresses.
440- resolveTAddress (hostname, Port (port), AddressFamily .IPv4 )
472+ if (HttpClientFlag .NoInet4Resolution notin session.flags) and
473+ (HttpClientFlag .NoInet6Resolution notin session.flags):
474+ # DNS resolution for both IPv4 and IPv6 addresses.
475+ resolveTAddress (hostname, Port (port))
441476 else :
442- # DNS resolution only for IPv6 addresses
443- resolveTAddress (hostname, Port (port), AddressFamily .IPv6 )
444- except TransportAddressError :
445- return err (" Could not resolve address of remote server" )
477+ if HttpClientFlag .NoInet6Resolution in session.flags:
478+ # DNS resolution only for IPv4 addresses.
479+ resolveTAddress (hostname, Port (port), AddressFamily .IPv4 )
480+ else :
481+ # DNS resolution only for IPv6 addresses
482+ resolveTAddress (hostname, Port (port), AddressFamily .IPv6 )
483+ except TransportAddressError :
484+ return err (" Could not resolve address of remote server" )
446485
447- if len (addresses) == 0 :
486+ if isNil (session.proxy) and len (addresses) == 0 :
448487 return err (" Could not resolve address of remote server" )
449488
450489 ok (HttpAddress (id: id, scheme: scheme, hostname: hostname, port: port,
@@ -623,55 +662,169 @@ proc closeWait(conn: HttpClientConnectionRef) {.async: (raises: []).} =
623662 conn.state = HttpClientConnectionState .Closed
624663 untrackCounter (HttpClientConnectionTrackerName )
625664
665+ proc resolveProxyAddress (
666+ proxy: HttpProxyRef , dualstack: DualStackType
667+ ): Future [seq [TransportAddress ]] {.async : (raises: []).} =
668+ # # Resolve proxy hostname to transport addresses.
669+ if len (proxy.addresses) > 0 :
670+ return proxy.addresses
671+ try :
672+ proxy.addresses = resolveTAddress (proxy.hostname, Port (proxy.port))
673+ except TransportAddressError :
674+ proxy.addresses = @ []
675+ proxy.addresses
676+
677+ proc setupProxyAuth (proxy: HttpProxyRef , headers: var HttpTable ) =
678+ # # Add proxy authentication headers if credentials are provided.
679+ if len (proxy.username) > 0 or len (proxy.password) > 0 :
680+ let auth = proxy.username & " :" & proxy.password
681+ let header = " Basic " & Base64Pad .encode (auth.toOpenArrayByte (0 , len (auth) - 1 ))
682+ headers.add (" Proxy-Authorization" , header)
683+
684+ proc setupConnectTunnel (session: HttpSessionRef ,
685+ transp: StreamTransport , ha: HttpAddress ) {.
686+ async : (raises: [CancelledError , HttpConnectionError ]).} =
687+ # # Establish HTTPS tunnel through HTTP proxy using CONNECT method.
688+ var headers = HttpTable .init ()
689+ headers.add (HostHeader , ha.hostname & " :" & Base10 .toString (ha.port))
690+ headers.add (ConnectionHeader , " close" )
691+
692+ if not (isNil (session.proxy)):
693+ session.proxy.setupProxyAuth (headers)
694+
695+ var connectRequest =
696+ " CONNECT " & ha.hostname & " :" & Base10 .toString (ha.port) & " HTTP/1.1\r\n "
697+ for k, v in headers.stringItems ():
698+ if len (v) > 0 :
699+ connectRequest.add (normalizeHeaderName (k))
700+ connectRequest.add (" : " )
701+ connectRequest.add (v)
702+ connectRequest.add (" \r\n " )
703+ connectRequest.add (" \r\n " )
704+
705+ try :
706+ discard await transp.write (connectRequest)
707+ except TransportError as exc:
708+ raiseHttpConnectionError (" Could not send CONNECT request, reason: " & $ exc.msg)
709+
710+ var headersBuffer = newSeq [byte ](HttpMaxHeadersSize )
711+ let bytesRead =
712+ try :
713+ await transp
714+ .readUntil (addr headersBuffer[0 ], len (headersBuffer), HeadersMark )
715+ .wait (session.headersTimeout)
716+ except AsyncTimeoutError :
717+ raiseHttpConnectionError (" CONNECT response headers timed out" )
718+ except TransportError as exc:
719+ raiseHttpConnectionError (" Could not read CONNECT response, reason: " & $ exc.msg)
720+
721+ let resp = parseResponse (headersBuffer.toOpenArray (0 , bytesRead - 1 ), false )
722+ if resp.failed ():
723+ raiseHttpConnectionError (" Invalid CONNECT response headers" )
724+
725+ if resp.code != 200 :
726+ raiseHttpConnectionError (" CONNECT tunnel failed with status " & $ resp.code)
727+
626728proc connect (session: HttpSessionRef ,
627729 ha: HttpAddress ): Future [HttpClientConnectionRef ] {.
628730 async : (raises: [CancelledError , HttpConnectionError ]).} =
629- # # Establish new connection with remote server using ``url`` and ``flags``.
731+ # # Establish new connection with remote server using ``ha`` address.
732+ # # If proxy is configured, use proxy for connection.
630733 # # On success returns ``HttpClientConnectionRef`` object.
734+
735+ var transp: StreamTransport
631736 var lastError = " "
632- # Here we trying to connect to every possible remote host address we got after
633- # DNS resolution.
634- for address in ha.addresses:
635- let transp =
636- try :
637- await connect (address, bufferSize = session.connectionBufferSize,
638- flags = session.socketFlags,
639- dualstack = session.dualstack)
640- except TransportError :
641- nil
642- if not (isNil (transp)):
643- let conn =
644- block :
645- let res = HttpClientConnectionRef .new (session, ha, transp).valueOr:
646- raiseHttpConnectionError (
647- " Could not connect to remote host, reason: " & error)
648- if res.kind == HttpClientScheme .Secure :
649- try :
650- await res.tls.handshake ()
651- res.state = HttpClientConnectionState .Ready
652- except CancelledError as exc:
653- await res.closeWait ()
654- raise exc
655- except TLSStreamProtocolError as exc:
656- await res.closeWait ()
657- res.state = HttpClientConnectionState .Error
658- lastError = $ exc.msg
659- except AsyncStreamError as exc:
660- await res.closeWait ()
661- res.state = HttpClientConnectionState .Error
662- lastError = $ exc.msg
663- else :
664- res.state = HttpClientConnectionState .Ready
665- res
666- if conn.state == HttpClientConnectionState .Ready :
667- return conn
668-
669- # If all attempts to connect to the remote host have failed.
670- if len (lastError) > 0 :
671- raiseHttpConnectionError (" Could not connect to remote host, reason: " &
672- lastError)
737+ if not (isNil (session.proxy)):
738+ # Connect through proxy
739+ let proxyAddresses = await session.proxy.resolveProxyAddress (session.dualstack)
740+ if len (proxyAddresses) == 0 :
741+ raiseHttpConnectionError (" Could not resolve proxy address" )
742+
743+ for proxyAddr in proxyAddresses:
744+ transp =
745+ try :
746+ await connect (
747+ proxyAddr,
748+ bufferSize = session.connectionBufferSize,
749+ flags = session.socketFlags,
750+ dualstack = session.dualstack,
751+ )
752+ except TransportError :
753+ nil
754+
755+ if not (isNil (transp)):
756+ if ha.scheme == HttpClientScheme .Secure :
757+ try :
758+ await session.setupConnectTunnel (transp, ha)
759+ break
760+ except CancelledError as exc:
761+ await transp.closeWait ()
762+ transp = nil
763+ raise exc
764+ except HttpConnectionError as exc:
765+ await transp.closeWait ()
766+ transp = nil
767+ lastError = exc.msg
768+ else :
769+ break
770+
771+ if isNil (transp):
772+ # All proxy connection attempts failed
773+ if len (lastError) > 0 :
774+ raiseHttpConnectionError (" Could not connect to proxy, reason: " & lastError)
775+ else :
776+ raiseHttpConnectionError (" Could not connect to proxy" )
673777 else :
674- raiseHttpConnectionError (" Could not connect to remote host" )
778+ # Direct connection without proxy
779+ for address in ha.addresses:
780+ transp =
781+ try :
782+ await connect (address, bufferSize = session.connectionBufferSize,
783+ flags = session.socketFlags,
784+ dualstack = session.dualstack)
785+ except TransportError :
786+ nil
787+
788+ if not (isNil (transp)):
789+ break
790+ if isNil (transp):
791+ # If all attempts to connect to the remote host have failed.
792+ if len (lastError) > 0 :
793+ raiseHttpConnectionError (
794+ " Could not connect to remote host, reason: " & lastError)
795+ else :
796+ raiseHttpConnectionError (" Could not connect to remote host" )
797+
798+ let conn = block :
799+ let res = HttpClientConnectionRef .new (session, ha, transp).valueOr:
800+ raiseHttpConnectionError (" Could not connect to remote host, reason: " & error)
801+ if res.kind == HttpClientScheme .Secure :
802+ try :
803+ await res.tls.handshake ()
804+ res.state = HttpClientConnectionState .Ready
805+ except CancelledError as exc:
806+ await res.closeWait ()
807+ raise exc
808+ except TLSStreamProtocolError as exc:
809+ await res.closeWait ()
810+ res.state = HttpClientConnectionState .Error
811+ lastError = $ exc.msg
812+ except AsyncStreamError as exc:
813+ await res.closeWait ()
814+ res.state = HttpClientConnectionState .Error
815+ lastError = $ exc.msg
816+ else :
817+ res.state = HttpClientConnectionState .Ready
818+ res
819+
820+ if conn.state != HttpClientConnectionState .Ready :
821+ # If all attempts to connect to the remote host have failed.
822+ if len (lastError) > 0 :
823+ raiseHttpConnectionError (" Could not connect to remote host, reason: " & lastError)
824+ else :
825+ raiseHttpConnectionError (" Could not connect to remote host" )
826+
827+ return conn
675828
676829proc removeConnection (session: HttpSessionRef ,
677830 conn: HttpClientConnectionRef ) {.async : (raises: []).} =
@@ -1131,6 +1284,12 @@ proc prepareRequest(request: HttpClientRequestRef): string =
11311284 Base64Pad .encode (auth.toOpenArrayByte (0 , len (auth) - 1 ))
11321285 request.headers.add (AuthorizationHeader , header)
11331286
1287+ # We will send `Proxy-Authorization` information if proxy is configured
1288+ # with credentials and header is not already present.
1289+ if not (isNil (request.session.proxy)) and
1290+ request.address.scheme != HttpClientScheme .Secure :
1291+ request.session.proxy.setupProxyAuth (request.headers)
1292+
11341293 # Here we perform automatic detection: if request was created with non-zero
11351294 # body and `Content-Length` header is missing we will create one with size
11361295 # of body stored in request.
@@ -1150,14 +1309,33 @@ proc prepareRequest(request: HttpClientRequestRef): string =
11501309
11511310 let entity =
11521311 block :
1153- var res =
1312+ # For HTTP proxy connections (non-HTTPS), send absolute URI
1313+ # For direct connections or HTTPS through CONNECT tunnel, send relative path
1314+ var res: string
1315+ if not (isNil (request.session.proxy)) and
1316+ request.address.scheme != HttpClientScheme .Secure :
1317+ # HTTP proxy: use absolute URI
1318+ res = " http://" & request.address.hostname
1319+ if request.address.port != 80 :
1320+ res.add (" :" )
1321+ res.add (Base10 .toString (request.address.port))
11541322 if len (request.address.path) > 0 :
1155- request.address.path
1323+ res. add ( request.address.path)
11561324 else :
1157- " /"
1158- if len (request.address.query) > 0 :
1159- res.add (" ?" )
1160- res.add (request.address.query)
1325+ res.add (" /" )
1326+ if len (request.address.query) > 0 :
1327+ res.add (" ?" )
1328+ res.add (request.address.query)
1329+ else :
1330+ # Direct connection or HTTPS through CONNECT: use relative path
1331+ res =
1332+ if len (request.address.path) > 0 :
1333+ request.address.path
1334+ else :
1335+ " /"
1336+ if len (request.address.query) > 0 :
1337+ res.add (" ?" )
1338+ res.add (request.address.query)
11611339 if len (request.address.anchor) > 0 :
11621340 res.add (" #" )
11631341 res.add (request.address.anchor)
0 commit comments