Implements proxy support for (Async)HttpClient. Ref #4423.
Fixes #2160.
This commit is contained in:
parent
0c99523ad3
commit
8386476592
3 changed files with 84 additions and 18 deletions
|
|
@ -606,26 +606,46 @@ proc downloadFile*(url: string, outputFilename: string,
|
||||||
else:
|
else:
|
||||||
fileError("Unable to open file")
|
fileError("Unable to open file")
|
||||||
|
|
||||||
proc generateHeaders(r: Uri, httpMethod: string,
|
proc generateHeaders(requestUrl: Uri, httpMethod: string,
|
||||||
headers: HttpHeaders, body: string): string =
|
headers: HttpHeaders, body: string, proxy: Proxy): string =
|
||||||
# TODO: Use this in the blocking HttpClient once it supports proxies.
|
# GET
|
||||||
result = substr(httpMethod, len("http")).toUpper()
|
result = substr(httpMethod, len("http")).toUpper()
|
||||||
# TODO: Proxies
|
|
||||||
result.add ' '
|
result.add ' '
|
||||||
if r.path[0] != '/': result.add '/'
|
|
||||||
result.add(r.path)
|
if proxy.isNil:
|
||||||
if r.query.len > 0:
|
# /path?query
|
||||||
result.add("?" & r.query)
|
if requestUrl.path[0] != '/': result.add '/'
|
||||||
|
result.add(requestUrl.path)
|
||||||
|
if requestUrl.query.len > 0:
|
||||||
|
result.add("?" & requestUrl.query)
|
||||||
|
else:
|
||||||
|
# Remove the 'http://' from the URL for CONNECT requests.
|
||||||
|
var modifiedUrl = requestUrl
|
||||||
|
modifiedUrl.scheme = ""
|
||||||
|
result.add($modifiedUrl)
|
||||||
|
|
||||||
|
# HTTP/1.1\c\l
|
||||||
result.add(" HTTP/1.1\c\L")
|
result.add(" HTTP/1.1\c\L")
|
||||||
|
|
||||||
if r.port == "":
|
# Host header.
|
||||||
add(result, "Host: " & r.hostname & "\c\L")
|
if requestUrl.port == "":
|
||||||
|
add(result, "Host: " & requestUrl.hostname & "\c\L")
|
||||||
else:
|
else:
|
||||||
add(result, "Host: " & r.hostname & ":" & r.port & "\c\L")
|
add(result, "Host: " & requestUrl.hostname & ":" & requestUrl.port & "\c\L")
|
||||||
|
|
||||||
add(result, "Connection: Keep-Alive\c\L")
|
# Connection header.
|
||||||
|
if not headers.hasKey("Connection"):
|
||||||
|
add(result, "Connection: Keep-Alive\c\L")
|
||||||
|
|
||||||
|
# Content length header.
|
||||||
if body.len > 0 and not headers.hasKey("Content-Length"):
|
if body.len > 0 and not headers.hasKey("Content-Length"):
|
||||||
add(result, "Content-Length: " & $body.len & "\c\L")
|
add(result, "Content-Length: " & $body.len & "\c\L")
|
||||||
|
|
||||||
|
# Proxy auth header.
|
||||||
|
if not proxy.isNil and proxy.auth != "":
|
||||||
|
let auth = base64.encode(proxy.auth, newline = "")
|
||||||
|
add(result, "Proxy-Authorization: basic " & auth & "\c\L")
|
||||||
|
|
||||||
for key, val in headers:
|
for key, val in headers:
|
||||||
add(result, key & ": " & val & "\c\L")
|
add(result, key & ": " & val & "\c\L")
|
||||||
|
|
||||||
|
|
@ -640,6 +660,7 @@ type
|
||||||
maxRedirects: int
|
maxRedirects: int
|
||||||
userAgent: string
|
userAgent: string
|
||||||
timeout: int ## Only used for blocking HttpClient for now.
|
timeout: int ## Only used for blocking HttpClient for now.
|
||||||
|
proxy: Proxy
|
||||||
when defined(ssl):
|
when defined(ssl):
|
||||||
sslContext: net.SslContext
|
sslContext: net.SslContext
|
||||||
|
|
||||||
|
|
@ -647,7 +668,7 @@ type
|
||||||
HttpClient* = HttpClientBase[Socket]
|
HttpClient* = HttpClientBase[Socket]
|
||||||
|
|
||||||
proc newHttpClient*(userAgent = defUserAgent,
|
proc newHttpClient*(userAgent = defUserAgent,
|
||||||
maxRedirects = 5, sslContext = defaultSslContext,
|
maxRedirects = 5, sslContext = defaultSslContext, proxy: Proxy = nil,
|
||||||
timeout = -1): HttpClient =
|
timeout = -1): HttpClient =
|
||||||
## Creates a new HttpClient instance.
|
## Creates a new HttpClient instance.
|
||||||
##
|
##
|
||||||
|
|
@ -659,12 +680,16 @@ proc newHttpClient*(userAgent = defUserAgent,
|
||||||
##
|
##
|
||||||
## ``sslContext`` specifies the SSL context to use for HTTPS requests.
|
## ``sslContext`` specifies the SSL context to use for HTTPS requests.
|
||||||
##
|
##
|
||||||
|
## ``proxy`` specifies an HTTP proxy to use for this HTTP client's
|
||||||
|
## connections.
|
||||||
|
##
|
||||||
## ``timeout`` specifies the number of miliseconds to allow before a
|
## ``timeout`` specifies the number of miliseconds to allow before a
|
||||||
## ``TimeoutError`` is raised.
|
## ``TimeoutError`` is raised.
|
||||||
new result
|
new result
|
||||||
result.headers = newHttpHeaders()
|
result.headers = newHttpHeaders()
|
||||||
result.userAgent = userAgent
|
result.userAgent = userAgent
|
||||||
result.maxRedirects = maxRedirects
|
result.maxRedirects = maxRedirects
|
||||||
|
result.proxy = proxy
|
||||||
result.timeout = timeout
|
result.timeout = timeout
|
||||||
when defined(ssl):
|
when defined(ssl):
|
||||||
result.sslContext = sslContext
|
result.sslContext = sslContext
|
||||||
|
|
@ -675,7 +700,8 @@ type
|
||||||
{.deprecated: [PAsyncHttpClient: AsyncHttpClient].}
|
{.deprecated: [PAsyncHttpClient: AsyncHttpClient].}
|
||||||
|
|
||||||
proc newAsyncHttpClient*(userAgent = defUserAgent,
|
proc newAsyncHttpClient*(userAgent = defUserAgent,
|
||||||
maxRedirects = 5, sslContext = defaultSslContext): AsyncHttpClient =
|
maxRedirects = 5, sslContext = defaultSslContext,
|
||||||
|
proxy: Proxy = nil): AsyncHttpClient =
|
||||||
## Creates a new AsyncHttpClient instance.
|
## Creates a new AsyncHttpClient instance.
|
||||||
##
|
##
|
||||||
## ``userAgent`` specifies the user agent that will be used when making
|
## ``userAgent`` specifies the user agent that will be used when making
|
||||||
|
|
@ -685,10 +711,14 @@ proc newAsyncHttpClient*(userAgent = defUserAgent,
|
||||||
## default is 5.
|
## default is 5.
|
||||||
##
|
##
|
||||||
## ``sslContext`` specifies the SSL context to use for HTTPS requests.
|
## ``sslContext`` specifies the SSL context to use for HTTPS requests.
|
||||||
|
##
|
||||||
|
## ``proxy`` specifies an HTTP proxy to use for this HTTP client's
|
||||||
|
## connections.
|
||||||
new result
|
new result
|
||||||
result.headers = newHttpHeaders()
|
result.headers = newHttpHeaders()
|
||||||
result.userAgent = userAgent
|
result.userAgent = userAgent
|
||||||
result.maxRedirects = maxRedirects
|
result.maxRedirects = maxRedirects
|
||||||
|
result.proxy = proxy
|
||||||
result.timeout = -1 # TODO
|
result.timeout = -1 # TODO
|
||||||
when defined(ssl):
|
when defined(ssl):
|
||||||
result.sslContext = sslContext
|
result.sslContext = sslContext
|
||||||
|
|
@ -873,19 +903,46 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string,
|
||||||
## connection can be closed by using the ``close`` procedure.
|
## connection can be closed by using the ``close`` procedure.
|
||||||
##
|
##
|
||||||
## The returned future will complete once the request is completed.
|
## The returned future will complete once the request is completed.
|
||||||
let r = parseUri(url)
|
let connectionUrl =
|
||||||
await newConnection(client, r)
|
if client.proxy.isNil: parseUri(url) else: client.proxy.url
|
||||||
|
let requestUrl = parseUri(url)
|
||||||
|
|
||||||
|
let savedProxy = client.proxy # client's proxy may be overwritten.
|
||||||
|
|
||||||
|
if requestUrl.scheme == "https" and not client.proxy.isNil:
|
||||||
|
when defined(ssl):
|
||||||
|
client.proxy.url = connectionUrl
|
||||||
|
var connectUrl = requestUrl
|
||||||
|
connectUrl.scheme = "http"
|
||||||
|
connectUrl.port = "443"
|
||||||
|
let proxyResp = await request(client, $connectUrl, $HttpConnect)
|
||||||
|
|
||||||
|
if not proxyResp.status.startsWith("200"):
|
||||||
|
raise newException(HttpRequestError,
|
||||||
|
"The proxy server rejected a CONNECT request, " &
|
||||||
|
"so a secure connection could not be established.")
|
||||||
|
client.sslContext.wrapConnectedSocket(client.socket, handshakeAsClient)
|
||||||
|
client.proxy = nil
|
||||||
|
else:
|
||||||
|
raise newException(HttpRequestError,
|
||||||
|
"SSL support not available. Cannot connect to https site over proxy.")
|
||||||
|
else:
|
||||||
|
await newConnection(client, connectionUrl)
|
||||||
|
|
||||||
if not client.headers.hasKey("user-agent") and client.userAgent != "":
|
if not client.headers.hasKey("user-agent") and client.userAgent != "":
|
||||||
client.headers["User-Agent"] = client.userAgent
|
client.headers["User-Agent"] = client.userAgent
|
||||||
|
|
||||||
var headers = generateHeaders(r, $httpMethod, client.headers, body)
|
var headers = generateHeaders(requestUrl, $httpMethod,
|
||||||
|
client.headers, body, client.proxy)
|
||||||
|
|
||||||
await client.socket.send(headers)
|
await client.socket.send(headers)
|
||||||
if body != "":
|
if body != "":
|
||||||
await client.socket.send(body)
|
await client.socket.send(body)
|
||||||
|
|
||||||
result = await parseResponse(client, httpMethod != "httpHEAD")
|
result = await parseResponse(client, httpMethod notin {HttpHead, HttpConnect})
|
||||||
|
|
||||||
|
# Restore the clients proxy in case it was overwritten.
|
||||||
|
client.proxy = savedProxy
|
||||||
|
|
||||||
proc request*(client: HttpClient | AsyncHttpClient, url: string,
|
proc request*(client: HttpClient | AsyncHttpClient, url: string,
|
||||||
httpMethod = HttpGET, body = ""): Future[Response] {.multisync.} =
|
httpMethod = HttpGET, body = ""): Future[Response] {.multisync.} =
|
||||||
|
|
|
||||||
|
|
@ -210,6 +210,9 @@ proc `==`*(protocol: tuple[orig: string, major, minor: int],
|
||||||
of HttpVer10: 0
|
of HttpVer10: 0
|
||||||
result = protocol.major == major and protocol.minor == minor
|
result = protocol.major == major and protocol.minor == minor
|
||||||
|
|
||||||
|
proc contains*(methods: set[HttpMethod], x: string): bool =
|
||||||
|
return parseEnum[HttpMethod](x) in methods
|
||||||
|
|
||||||
proc `==`*(rawCode: string, code: HttpCode): bool =
|
proc `==`*(rawCode: string, code: HttpCode): bool =
|
||||||
return rawCode.toLower() == ($code).toLower()
|
return rawCode.toLower() == ($code).toLower()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,12 @@ proc asyncTest() {.async.} =
|
||||||
|
|
||||||
resp = await client.request("https://google.com/")
|
resp = await client.request("https://google.com/")
|
||||||
doAssert(resp.code.is2xx or resp.code.is3xx)
|
doAssert(resp.code.is2xx or resp.code.is3xx)
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
# Proxy test
|
||||||
|
#client = newAsyncHttpClient(proxy = newProxy("http://51.254.106.76:80/"))
|
||||||
|
#var resp = await client.request("https://github.com")
|
||||||
|
#echo resp
|
||||||
|
|
||||||
proc syncTest() =
|
proc syncTest() =
|
||||||
var client = newHttpClient()
|
var client = newHttpClient()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue