diff options
author | Dominik Picheta <dominikpicheta@gmail.com> | 2016-09-18 22:59:12 +0200 |
---|---|---|
committer | Dominik Picheta <dominikpicheta@gmail.com> | 2016-09-18 22:59:12 +0200 |
commit | 8386476592a1fafea7f62d18604ac8534f9b1834 (patch) | |
tree | 146ac4a257c2a48f3c66b30325c01fa6253fc122 | |
parent | 0c99523ad314433538df44c734cdf987c6c4337e (diff) | |
download | Nim-8386476592a1fafea7f62d18604ac8534f9b1834.tar.gz |
Implements proxy support for (Async)HttpClient. Ref #4423.
Fixes #2160.
-rw-r--r-- | lib/pure/httpclient.nim | 93 | ||||
-rw-r--r-- | lib/pure/httpcore.nim | 3 | ||||
-rw-r--r-- | tests/stdlib/thttpclient.nim | 6 |
3 files changed, 84 insertions, 18 deletions
diff --git a/lib/pure/httpclient.nim b/lib/pure/httpclient.nim index ae9378331..117709f95 100644 --- a/lib/pure/httpclient.nim +++ b/lib/pure/httpclient.nim @@ -606,26 +606,46 @@ proc downloadFile*(url: string, outputFilename: string, else: fileError("Unable to open file") -proc generateHeaders(r: Uri, httpMethod: string, - headers: HttpHeaders, body: string): string = - # TODO: Use this in the blocking HttpClient once it supports proxies. +proc generateHeaders(requestUrl: Uri, httpMethod: string, + headers: HttpHeaders, body: string, proxy: Proxy): string = + # GET result = substr(httpMethod, len("http")).toUpper() - # TODO: Proxies result.add ' ' - if r.path[0] != '/': result.add '/' - result.add(r.path) - if r.query.len > 0: - result.add("?" & r.query) + + if proxy.isNil: + # /path?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") - if r.port == "": - add(result, "Host: " & r.hostname & "\c\L") + # Host header. + if requestUrl.port == "": + add(result, "Host: " & requestUrl.hostname & "\c\L") 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"): 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: add(result, key & ": " & val & "\c\L") @@ -640,6 +660,7 @@ type maxRedirects: int userAgent: string timeout: int ## Only used for blocking HttpClient for now. + proxy: Proxy when defined(ssl): sslContext: net.SslContext @@ -647,7 +668,7 @@ type HttpClient* = HttpClientBase[Socket] proc newHttpClient*(userAgent = defUserAgent, - maxRedirects = 5, sslContext = defaultSslContext, + maxRedirects = 5, sslContext = defaultSslContext, proxy: Proxy = nil, timeout = -1): HttpClient = ## Creates a new HttpClient instance. ## @@ -659,12 +680,16 @@ proc newHttpClient*(userAgent = defUserAgent, ## ## ``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 ## ``TimeoutError`` is raised. new result result.headers = newHttpHeaders() result.userAgent = userAgent result.maxRedirects = maxRedirects + result.proxy = proxy result.timeout = timeout when defined(ssl): result.sslContext = sslContext @@ -675,7 +700,8 @@ type {.deprecated: [PAsyncHttpClient: AsyncHttpClient].} proc newAsyncHttpClient*(userAgent = defUserAgent, - maxRedirects = 5, sslContext = defaultSslContext): AsyncHttpClient = + maxRedirects = 5, sslContext = defaultSslContext, + proxy: Proxy = nil): AsyncHttpClient = ## Creates a new AsyncHttpClient instance. ## ## ``userAgent`` specifies the user agent that will be used when making @@ -685,10 +711,14 @@ proc newAsyncHttpClient*(userAgent = defUserAgent, ## default is 5. ## ## ``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 result.headers = newHttpHeaders() result.userAgent = userAgent result.maxRedirects = maxRedirects + result.proxy = proxy result.timeout = -1 # TODO when defined(ssl): result.sslContext = sslContext @@ -873,19 +903,46 @@ proc request*(client: HttpClient | AsyncHttpClient, url: string, ## connection can be closed by using the ``close`` procedure. ## ## The returned future will complete once the request is completed. - let r = parseUri(url) - await newConnection(client, r) + let connectionUrl = + 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 != "": 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) if 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, httpMethod = HttpGET, body = ""): Future[Response] {.multisync.} = diff --git a/lib/pure/httpcore.nim b/lib/pure/httpcore.nim index 7c631400f..2900d0ce4 100644 --- a/lib/pure/httpcore.nim +++ b/lib/pure/httpcore.nim @@ -210,6 +210,9 @@ proc `==`*(protocol: tuple[orig: string, major, minor: int], of HttpVer10: 0 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 = return rawCode.toLower() == ($code).toLower() diff --git a/tests/stdlib/thttpclient.nim b/tests/stdlib/thttpclient.nim index b5daa963a..d0cf25b45 100644 --- a/tests/stdlib/thttpclient.nim +++ b/tests/stdlib/thttpclient.nim @@ -16,6 +16,12 @@ proc asyncTest() {.async.} = resp = await client.request("https://google.com/") 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() = var client = newHttpClient() |