summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorDominik Picheta <dominikpicheta@gmail.com>2016-09-18 22:59:12 +0200
committerDominik Picheta <dominikpicheta@gmail.com>2016-09-18 22:59:12 +0200
commit8386476592a1fafea7f62d18604ac8534f9b1834 (patch)
tree146ac4a257c2a48f3c66b30325c01fa6253fc122
parent0c99523ad314433538df44c734cdf987c6c4337e (diff)
downloadNim-8386476592a1fafea7f62d18604ac8534f9b1834.tar.gz
Implements proxy support for (Async)HttpClient. Ref #4423.
Fixes #2160.
-rw-r--r--lib/pure/httpclient.nim93
-rw-r--r--lib/pure/httpcore.nim3
-rw-r--r--tests/stdlib/thttpclient.nim6
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()