diff --git a/lib/pure/httpclient.nim b/lib/pure/httpclient.nim index 42735ad4ecb29..23d73e981cc70 100644 --- a/lib/pure/httpclient.nim +++ b/lib/pure/httpclient.nim @@ -474,8 +474,15 @@ proc getNewLocation(lastURL: string, headers: HttpHeaders): string = parsed.anchor = r.anchor result = $parsed +proc toTitleCase(s: string): string = + result = newString(len(s)) + var upper = true + for i in 0..len(s) - 1: + result[i] = if upper: toUpperAscii(s[i]) else: toLowerAscii(s[i]) + upper = s[i] == '-' + proc generateHeaders(requestUrl: Uri, httpMethod: string, headers: HttpHeaders, - proxy: Proxy): string = + proxy: Proxy, titleCaseHeaders: bool): string = # GET let upperMethod = httpMethod.toUpperAscii() result = upperMethod @@ -513,7 +520,10 @@ proc generateHeaders(requestUrl: Uri, httpMethod: string, headers: HttpHeaders, add(result, "Proxy-Authorization: basic " & auth & httpNewLine) for key, val in headers: - add(result, key & ": " & val & httpNewLine) + if titleCaseHeaders: + add(result, toTitleCase(key) & ": " & val & httpNewLine) + else: + add(result, key & ": " & val & httpNewLine) add(result, httpNewLine) @@ -548,13 +558,14 @@ type else: bodyStream: Stream getBody: bool ## When `false`, the body is never read in requestAux. - + titleCaseHeaders: bool type HttpClient* = HttpClientBase[Socket] proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5, sslContext = getDefaultSSL(), proxy: Proxy = nil, - timeout = -1, headers = newHttpHeaders()): HttpClient = + timeout = -1, headers = newHttpHeaders(), + titleCaseHeaders = false): HttpClient = ## Creates a new HttpClient instance. ## ## ``userAgent`` specifies the user agent that will be used when making @@ -573,6 +584,9 @@ proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5, ## ``TimeoutError`` is raised. ## ## ``headers`` specifies the HTTP Headers. + ## + ## ``titleCaseHeaders`` specifies if HTTP headers should be sent in title + ## case e.g. ``Content-Length`` runnableExamples: import asyncdispatch, httpclient, strutils @@ -593,6 +607,7 @@ proc newHttpClient*(userAgent = defUserAgent, maxRedirects = 5, result.onProgressChanged = nil result.bodyStream = newStringStream() result.getBody = true + result.titleCaseHeaders = titleCaseHeaders when defined(ssl): result.sslContext = sslContext @@ -601,7 +616,8 @@ type proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5, sslContext = getDefaultSSL(), proxy: Proxy = nil, - headers = newHttpHeaders()): AsyncHttpClient = + headers = newHttpHeaders(), + titleCaseHeaders = false): AsyncHttpClient = ## Creates a new AsyncHttpClient instance. ## ## ``userAgent`` specifies the user agent that will be used when making @@ -616,6 +632,9 @@ proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5, ## connections. ## ## ``headers`` specifies the HTTP Headers. + ## + ## ``titleCaseHeaders`` specifies if HTTP headers should be sent in title + ## case e.g. ``Content-Length`` new result result.headers = headers result.userAgent = userAgent @@ -625,6 +644,7 @@ proc newAsyncHttpClient*(userAgent = defUserAgent, maxRedirects = 5, result.onProgressChanged = nil result.bodyStream = newFutureStream[string]("newAsyncHttpClient") result.getBody = true + result.titleCaseHeaders = titleCaseHeaders when defined(ssl): result.sslContext = sslContext @@ -898,7 +918,7 @@ proc newConnection(client: HttpClient | AsyncHttpClient, connectUrl.port = if url.port != "": url.port else: "443" let proxyHeaderString = generateHeaders(connectUrl, $HttpConnect, - newHttpHeaders(), client.proxy) + newHttpHeaders(), client.proxy, client.titleCaseHeaders) await client.socket.send(proxyHeaderString) let proxyResp = await parseResponse(client, false) @@ -995,7 +1015,7 @@ proc requestAux(client: HttpClient | AsyncHttpClient, url, httpMethod: string, newHeaders["User-Agent"] = client.userAgent let headerString = generateHeaders(requestUrl, httpMethod, newHeaders, - client.proxy) + client.proxy, client.titleCaseHeaders) await client.socket.send(headerString) if data.len > 0: @@ -1188,3 +1208,7 @@ proc downloadFile*(client: AsyncHttpClient, url: string, result.addCallback( proc () = client.getBody = true ) + +when isMainModule: + block: # test title case + doAssert toTitleCase("content-length") == "Content-Length" diff --git a/lib/pure/httpcore.nim b/lib/pure/httpcore.nim index 78cb66ded9d09..8916aa17df086 100644 --- a/lib/pure/httpcore.nim +++ b/lib/pure/httpcore.nim @@ -17,7 +17,6 @@ import tables, strutils, parseutils type HttpHeaders* = ref object table*: TableRef[string, seq[string]] - isTitleCase: bool HttpHeaderValues* = distinct seq[string] @@ -101,32 +100,16 @@ const const httpNewLine* = "\c\L" const headerLimit* = 10_000 -proc toTitleCase(s: string): string = - result = newString(len(s)) - var upper = true - for i in 0..len(s) - 1: - result[i] = if upper: toUpperAscii(s[i]) else: toLowerAscii(s[i]) - upper = s[i] == '-' - -proc toCaseInsensitive(headers: HttpHeaders, s: string): string {.inline.} = - return if headers.isTitleCase: toTitleCase(s) else: toLowerAscii(s) - -proc newHttpHeaders*(titleCase=false): HttpHeaders = - ## Returns a new ``HttpHeaders`` object. if ``titleCase`` is set to true, - ## headers are passed to the server in title case (e.g. "Content-Length") +proc newHttpHeaders*(): HttpHeaders = new result result.table = newTable[string, seq[string]]() - result.isTitleCase = titleCase proc newHttpHeaders*(keyValuePairs: - openArray[tuple[key: string, val: string]], titleCase=false): HttpHeaders = - ## Returns a new ``HttpHeaders`` object from an array. if ``titleCase`` is set to true, - ## headers are passed to the server in title case (e.g. "Content-Length") + openArray[tuple[key: string, val: string]]): HttpHeaders = new result result.table = newTable[string, seq[string]]() - result.isTitleCase = titleCase for pair in keyValuePairs: - let key = result.toCaseInsensitive(pair.key) + let key = pair.key.toLowerAscii() if key in result.table: result.table[key].add(pair.val) else: @@ -147,7 +130,7 @@ proc `[]`*(headers: HttpHeaders, key: string): HttpHeaderValues = ## ## To access multiple values of a key, use the overloaded ``[]`` below or ## to get all of them access the ``table`` field directly. - return headers.table[headers.toCaseInsensitive(key)].HttpHeaderValues + return headers.table[key.toLowerAscii].HttpHeaderValues converter toString*(values: HttpHeaderValues): string = return seq[string](values)[0] @@ -156,30 +139,30 @@ proc `[]`*(headers: HttpHeaders, key: string, i: int): string = ## Returns the ``i``'th value associated with the given key. If there are ## no values associated with the key or the ``i``'th value doesn't exist, ## an exception is raised. - return headers.table[headers.toCaseInsensitive(key)][i] + return headers.table[key.toLowerAscii][i] proc `[]=`*(headers: HttpHeaders, key, value: string) = ## Sets the header entries associated with ``key`` to the specified value. ## Replaces any existing values. - headers.table[headers.toCaseInsensitive(key)] = @[value] + headers.table[key.toLowerAscii] = @[value] proc `[]=`*(headers: HttpHeaders, key: string, value: seq[string]) = ## Sets the header entries associated with ``key`` to the specified list of ## values. ## Replaces any existing values. - headers.table[headers.toCaseInsensitive(key)] = value + headers.table[key.toLowerAscii] = value proc add*(headers: HttpHeaders, key, value: string) = ## Adds the specified value to the specified key. Appends to any existing ## values associated with the key. - if not headers.table.hasKey(headers.toCaseInsensitive(key)): - headers.table[headers.toCaseInsensitive(key)] = @[value] + if not headers.table.hasKey(key.toLowerAscii): + headers.table[key.toLowerAscii] = @[value] else: - headers.table[headers.toCaseInsensitive(key)].add(value) + headers.table[key.toLowerAscii].add(value) proc del*(headers: HttpHeaders, key: string) = ## Delete the header entries associated with ``key`` - headers.table.del(headers.toCaseInsensitive(key)) + headers.table.del(key.toLowerAscii) iterator pairs*(headers: HttpHeaders): tuple[key, value: string] = ## Yields each key, value pair. @@ -194,7 +177,7 @@ proc contains*(values: HttpHeaderValues, value: string): bool = if val.toLowerAscii == value.toLowerAscii: return true proc hasKey*(headers: HttpHeaders, key: string): bool = - return headers.table.hasKey(headers.toCaseInsensitive(key)) + return headers.table.hasKey(key.toLowerAscii()) proc getOrDefault*(headers: HttpHeaders, key: string, default = @[""].HttpHeaderValues): HttpHeaderValues = @@ -353,12 +336,3 @@ when isMainModule: doAssert test["foobar"] == "" doAssert parseHeader("foobar:") == ("foobar", @[""]) - - block: # test title case - var testTitleCase = newHttpHeaders(titleCase=true) - testTitleCase.add("content-length", "1") - doAssert testTitleCase.hasKey("Content-Length") - for key, val in testTitleCase: - doAssert key == "Content-Length" - -