220
220
# # ```Nim
221
221
# # import std/httpclient
222
222
# #
223
- # # let myProxy = newProxy("http://myproxy.network", auth="user:password")
223
+ # # let myProxy = newProxy("http://user:[email protected] ")
224
+ # # let client = newHttpClient(proxy = myProxy)
225
+ # # ```
226
+ # #
227
+ # # SOCKS5 proxy with proxy-side DNS resolving:
228
+ # #
229
+ # # ```Nim
230
+ # # import std/httpclient
231
+ # #
232
+ # # let myProxy = newProxy("socks5h://user:[email protected] ")
224
233
# # let client = newHttpClient(proxy = myProxy)
225
234
# # ```
226
235
# #
@@ -338,7 +347,6 @@ proc body*(response: AsyncResponse): Future[string] {.async.} =
338
347
type
339
348
Proxy* = ref object
340
349
url* : Uri
341
- auth* : string
342
350
343
351
MultipartEntry = object
344
352
name, content: string
@@ -387,13 +395,30 @@ proc getDefaultSSL(): SslContext =
387
395
result = defaultSslContext
388
396
doAssert result != nil , " failure to initialize the SSL context"
389
397
390
- proc newProxy* (url: string ; auth = " " ): Proxy =
398
+ proc newProxy* (url: Uri ): Proxy =
391
399
# # Constructs a new `TProxy` object.
392
- result = Proxy(url: parseUri( url), auth: auth )
400
+ result = Proxy(url: url)
393
401
394
- proc newProxy* (url: Uri; auth = " " ): Proxy =
402
+ proc newProxy* (url: string ): Proxy =
395
403
# # Constructs a new `TProxy` object.
396
- result = Proxy(url: url, auth: auth)
404
+ result = Proxy(url: parseUri(url))
405
+
406
+ proc newProxy* (url: Uri; auth: string ): Proxy {.deprecated: " Provide auth in url instead" .} =
407
+ result = Proxy(url: url)
408
+ if auth != " " :
409
+ let parts = auth.split(':' )
410
+ if parts.len != 2 :
411
+ raise newException(ValueError, " Invalid auth string" )
412
+ result .url.username = parts[0 ]
413
+ result .url.password = parts[1 ]
414
+
415
+ proc newProxy* (url: string ; auth: string ): Proxy {.deprecated: " Provide auth in url instead" .} =
416
+ result = newProxy(parseUri(url), auth)
417
+
418
+ proc auth* (p: Proxy): string {.deprecated: " Get auth from p.url.username and p.url.password" .} =
419
+ result = " "
420
+ if p.url.username != " " or p.url.password != " " :
421
+ result = p.url.username & " :" & p.url.password
397
422
398
423
proc newMultipartData* : MultipartData {.inline.} =
399
424
# # Constructs a new `MultipartData` object.
@@ -548,7 +573,7 @@ proc generateHeaders(requestUrl: Uri, httpMethod: HttpMethod, headers: HttpHeade
548
573
result = $ httpMethod
549
574
result .add ' '
550
575
551
- if proxy.isNil or requestUrl.scheme == " https" :
576
+ if proxy.isNil or ( requestUrl.scheme == " https" and proxy.url.scheme == " socks5h " ) :
552
577
# /path?query
553
578
if not requestUrl.path.startsWith(" /" ): result .add '/'
554
579
result .add(requestUrl.path)
@@ -575,8 +600,8 @@ proc generateHeaders(requestUrl: Uri, httpMethod: HttpMethod, headers: HttpHeade
575
600
add(result , " Connection: Keep-Alive" & httpNewLine)
576
601
577
602
# Proxy auth header.
578
- if not proxy.isNil and proxy.auth != " " :
579
- let auth = base64.encode(proxy.auth )
603
+ if not proxy.isNil and proxy.url.username != " " :
604
+ let auth = base64.encode(proxy.url.username & " : " & proxy.url.password )
580
605
add(result , " Proxy-Authorization: Basic " & auth & httpNewLine)
581
606
582
607
for key, val in headers:
@@ -689,7 +714,7 @@ proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5,
689
714
let exampleHtml = waitFor asyncProc()
690
715
assert " Example Domain" in exampleHtml
691
716
assert " Pizza" notin exampleHtml
692
-
717
+
693
718
new result
694
719
result .headers = headers
695
720
result .userAgent = userAgent
@@ -941,17 +966,75 @@ proc parseResponse(client: HttpClient | AsyncHttpClient,
941
966
when client is AsyncHttpClient:
942
967
result .bodyStream.complete()
943
968
969
+ proc startSsl(client: HttpClient | AsyncHttpClient, hostname: string ) =
970
+ when defined(ssl):
971
+ try :
972
+ client.sslContext.wrapConnectedSocket(
973
+ client.socket, handshakeAsClient, hostname)
974
+ except :
975
+ client.socket.close()
976
+ raise getCurrentException()
977
+
978
+ proc socks5hHandshake(client: HttpClient | AsyncHttpClient,
979
+ url: Uri) {.multisync.} =
980
+ var hasAuth = client.proxy.url.username != " "
981
+ if hasAuth:
982
+ await client.socket.send(" \x05\x02\x00\x02 " ) # Propose auth
983
+ else :
984
+ await client.socket.send(" \x05\x01\x00 " ) # Connect with no auth
985
+
986
+ when client.socket is Socket:
987
+ var resp = client.socket.recv(2 , client.timeout)
988
+ else :
989
+ var resp = await client.socket.recv(2 )
990
+
991
+ if resp == " \x05\x02 " and hasAuth:
992
+ # Perform auth
993
+ let authStr = " \x01 " &
994
+ char (client.proxy.url.username.len) & client.proxy.url.username &
995
+ char (client.proxy.url.password.len) & client.proxy.url.password
996
+ await client.socket.send(authStr)
997
+ when client.socket is Socket:
998
+ resp = client.socket.recv(2 , client.timeout)
999
+ else :
1000
+ resp = await client.socket.recv(2 )
1001
+ if resp != " \x01\x00 " :
1002
+ httpError(" Proxy authentication failed" )
1003
+ elif resp != " \x05\x00 " :
1004
+ httpError(" Unexpected proxy response: " & resp.toHex())
1005
+
1006
+ let port = if url.port != " " : parseInt(url.port)
1007
+ elif url.scheme == " http" : 80
1008
+ else : 443
1009
+ var p = " "
1010
+ p[0 ] = cast [char ](port.uint16 shr 8 )
1011
+ p[1 ] = cast [char ](port)
1012
+ await client.socket.send(" \x05\x01\x00\x03 " & url.hostname.len.char & url.hostname & p)
1013
+ when client.socket is Socket:
1014
+ resp = client.socket.recv(10 , client.timeout)
1015
+ else :
1016
+ resp = await client.socket.recv(10 )
1017
+ if resp.len != 10 or resp[0 ] != '\x05 ' or resp[1 ] != '\x00 ' :
1018
+ httpError(" Unexpected proxy response: " & resp.toHex())
1019
+
944
1020
proc newConnection(client: HttpClient | AsyncHttpClient,
945
1021
url: Uri) {.multisync.} =
946
1022
if client.currentURL.hostname != url.hostname or
947
1023
client.currentURL.scheme != url.scheme or
948
1024
client.currentURL.port != url.port or
949
1025
(not client.connected):
950
- # Connect to proxy if specified
951
- let connectionUrl =
952
- if client.proxy.isNil: url else : client.proxy.url
953
1026
954
- let isSsl = connectionUrl.scheme.toLowerAscii() == " https"
1027
+ var isSsl = false
1028
+ var connectionUrl = url
1029
+ if client.proxy.isNil:
1030
+ isSsl = url.scheme.toLowerAscii() == " https"
1031
+ else :
1032
+ connectionUrl = client.proxy.url
1033
+ let proxyScheme = connectionUrl.scheme.toLowerAscii()
1034
+ if proxyScheme == " https" :
1035
+ isSsl = true
1036
+ elif proxyScheme == " socks5h" :
1037
+ isSsl = url.scheme.toLowerAscii() == " https"
955
1038
956
1039
if isSsl and not defined(ssl):
957
1040
raise newException(HttpRequestError,
@@ -976,37 +1059,33 @@ proc newConnection(client: HttpClient | AsyncHttpClient,
976
1059
client.socket = await asyncnet.dial(connectionUrl.hostname, port)
977
1060
else : {.fatal: " Unsupported client type" .}
978
1061
979
- when defined(ssl):
980
- if isSsl:
981
- try :
1062
+ if not client.proxy.isNil and client.proxy.url.scheme.toLowerAscii() == " socks5h" :
1063
+ await socks5hHandshake(client, url)
1064
+ if isSsl: startSsl(client, url.hostname)
1065
+ else :
1066
+ if isSsl: startSsl(client, connectionUrl.hostname)
1067
+ # If need to CONNECT through http(s) proxy
1068
+ if url.scheme == " https" and not client.proxy.isNil:
1069
+ when defined(ssl):
1070
+ # Pass only host:port for CONNECT
1071
+ var connectUrl = initUri()
1072
+ connectUrl.hostname = url.hostname
1073
+ connectUrl.port = if url.port != " " : url.port else : " 443"
1074
+
1075
+ let proxyHeaderString = generateHeaders(connectUrl, HttpConnect,
1076
+ newHttpHeaders(), client.proxy)
1077
+ await client.socket.send(proxyHeaderString)
1078
+ let proxyResp = await parseResponse(client, false )
1079
+
1080
+ if not proxyResp.status.startsWith(" 200" ):
1081
+ raise newException(HttpRequestError,
1082
+ " The proxy server rejected a CONNECT request, " &
1083
+ " so a secure connection could not be established." )
982
1084
client.sslContext.wrapConnectedSocket(
983
- client.socket, handshakeAsClient, connectionUrl.hostname)
984
- except :
985
- client.socket.close()
986
- raise getCurrentException()
987
-
988
- # If need to CONNECT through proxy
989
- if url.scheme == " https" and not client.proxy.isNil:
990
- when defined(ssl):
991
- # Pass only host:port for CONNECT
992
- var connectUrl = initUri()
993
- connectUrl.hostname = url.hostname
994
- connectUrl.port = if url.port != " " : url.port else : " 443"
995
-
996
- let proxyHeaderString = generateHeaders(connectUrl, HttpConnect,
997
- newHttpHeaders(), client.proxy)
998
- await client.socket.send(proxyHeaderString)
999
- let proxyResp = await parseResponse(client, false )
1000
-
1001
- if not proxyResp.status.startsWith(" 200" ):
1085
+ client.socket, handshakeAsClient, url.hostname)
1086
+ else :
1002
1087
raise newException(HttpRequestError,
1003
- " The proxy server rejected a CONNECT request, " &
1004
- " so a secure connection could not be established." )
1005
- client.sslContext.wrapConnectedSocket(
1006
- client.socket, handshakeAsClient, url.hostname)
1007
- else :
1008
- raise newException(HttpRequestError,
1009
- " SSL support is not available. Cannot connect over SSL. Compile with -d:ssl to enable." )
1088
+ " SSL support is not available. Cannot connect over SSL. Compile with -d:ssl to enable." )
1010
1089
1011
1090
# May be connected through proxy but remember actual URL being accessed
1012
1091
client.currentURL = url
@@ -1086,7 +1165,7 @@ proc requestAux(client: HttpClient | AsyncHttpClient, url: Uri,
1086
1165
1087
1166
var data: seq [string ] = @ []
1088
1167
if multipart != nil and multipart.content.len > 0 :
1089
- # `format` modifies `client.headers`, see
1168
+ # `format` modifies `client.headers`, see
1090
1169
# https://github.com/nim-lang/Nim/pull/18208#discussion_r647036979
1091
1170
data = await client.format(multipart)
1092
1171
newHeaders = client.headers.override(headers)
@@ -1319,7 +1398,7 @@ proc downloadFile*(client: HttpClient, url: Uri | string, filename: string) =
1319
1398
defer :
1320
1399
client.getBody = true
1321
1400
let resp = client.get(url)
1322
-
1401
+
1323
1402
if resp.code.is4xx or resp.code.is5xx:
1324
1403
raise newException(HttpRequestError, resp.status)
1325
1404
@@ -1334,7 +1413,7 @@ proc downloadFileEx(client: AsyncHttpClient,
1334
1413
# # Downloads `url` and saves it to `filename`.
1335
1414
client.getBody = false
1336
1415
let resp = await client.get(url)
1337
-
1416
+
1338
1417
if resp.code.is4xx or resp.code.is5xx:
1339
1418
raise newException(HttpRequestError, resp.status)
1340
1419
0 commit comments