Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 31 additions & 7 deletions lib/pure/httpclient.nim
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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

Expand All @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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"
50 changes: 12 additions & 38 deletions lib/pure/httpcore.nim
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import tables, strutils, parseutils
type
HttpHeaders* = ref object
table*: TableRef[string, seq[string]]
isTitleCase: bool

HttpHeaderValues* = distinct seq[string]

Expand Down Expand Up @@ -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:
Expand All @@ -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]
Expand All @@ -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.
Expand All @@ -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 =
Expand Down Expand Up @@ -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"