diff options
Diffstat (limited to 'lib/pure/httpcore.nim')
-rw-r--r-- | lib/pure/httpcore.nim | 270 |
1 files changed, 157 insertions, 113 deletions
diff --git a/lib/pure/httpcore.nim b/lib/pure/httpcore.nim index f85375111..5ccab379c 100644 --- a/lib/pure/httpcore.nim +++ b/lib/pure/httpcore.nim @@ -7,14 +7,17 @@ # distribution, for details about the copyright. # -## Contains functionality shared between the ``httpclient`` and -## ``asynchttpserver`` modules. - -import tables, strutils, parseutils +## Contains functionality shared between the `httpclient` and +## `asynchttpserver` modules. +## +## Unstable API. +import std/private/since +import std/[tables, strutils, parseutils] type HttpHeaders* = ref object table*: TableRef[string, seq[string]] + isTitleCase: bool HttpHeaderValues* = distinct seq[string] @@ -26,29 +29,33 @@ type HttpVer11, HttpVer10 - HttpMethod* = enum ## the requested HttpMethod - HttpHead, ## Asks for the response identical to the one that would - ## correspond to a GET request, but without the response - ## body. - HttpGet, ## Retrieves the specified resource. - HttpPost, ## Submits data to be processed to the identified - ## resource. The data is included in the body of the - ## request. - HttpPut, ## Uploads a representation of the specified resource. - HttpDelete, ## Deletes the specified resource. - HttpTrace, ## Echoes back the received request, so that a client - ## can see what intermediate servers are adding or - ## changing in the request. - HttpOptions, ## Returns the HTTP methods that the server supports - ## for specified address. - HttpConnect, ## Converts the request connection to a transparent - ## TCP/IP tunnel, usually used for proxies. - HttpPatch ## Applies partial modifications to a resource. + HttpMethod* = enum ## the requested HttpMethod + HttpHead = "HEAD" ## Asks for the response identical to the one that + ## would correspond to a GET request, but without + ## the response body. + HttpGet = "GET" ## Retrieves the specified resource. + HttpPost = "POST" ## Submits data to be processed to the identified + ## resource. The data is included in the body of + ## the request. + HttpPut = "PUT" ## Uploads a representation of the specified + ## resource. + HttpDelete = "DELETE" ## Deletes the specified resource. + HttpTrace = "TRACE" ## Echoes back the received request, so that a + ## client + ## can see what intermediate servers are adding or + ## changing in the request. + HttpOptions = "OPTIONS" ## Returns the HTTP methods that the server + ## supports for specified address. + HttpConnect = "CONNECT" ## Converts the request connection to a transparent + ## TCP/IP tunnel, usually used for proxies. + HttpPatch = "PATCH" ## Applies partial modifications to a resource. const Http100* = HttpCode(100) Http101* = HttpCode(101) + Http102* = HttpCode(102) ## https://tools.ietf.org/html/rfc2518.html WebDAV + Http103* = HttpCode(103) ## https://tools.ietf.org/html/rfc8297.html Early hints Http200* = HttpCode(200) Http201* = HttpCode(201) Http202* = HttpCode(202) @@ -56,6 +63,9 @@ const Http204* = HttpCode(204) Http205* = HttpCode(205) Http206* = HttpCode(206) + Http207* = HttpCode(207) ## https://tools.ietf.org/html/rfc4918.html WebDAV + Http208* = HttpCode(208) ## https://tools.ietf.org/html/rfc5842.html WebDAV, Section 7.1 + Http226* = HttpCode(226) ## https://tools.ietf.org/html/rfc3229.html Delta encoding, Section 10.4.1 Http300* = HttpCode(300) Http301* = HttpCode(301) Http302* = HttpCode(302) @@ -63,8 +73,10 @@ const Http304* = HttpCode(304) Http305* = HttpCode(305) Http307* = HttpCode(307) + Http308* = HttpCode(308) Http400* = HttpCode(400) Http401* = HttpCode(401) + Http402* = HttpCode(402) ## https://tools.ietf.org/html/rfc7231.html Payment required, Section 6.5.2 Http403* = HttpCode(403) Http404* = HttpCode(404) Http405* = HttpCode(405) @@ -83,6 +95,9 @@ const Http418* = HttpCode(418) Http421* = HttpCode(421) Http422* = HttpCode(422) + Http423* = HttpCode(423) ## https://tools.ietf.org/html/rfc4918.html WebDAV, Section 11.3 + Http424* = HttpCode(424) ## https://tools.ietf.org/html/rfc4918.html WebDAV, Section 11.3 + Http425* = HttpCode(425) ## https://tools.ietf.org/html/rfc8470.html Early data Http426* = HttpCode(426) Http428* = HttpCode(428) Http429* = HttpCode(429) @@ -94,68 +109,99 @@ const Http503* = HttpCode(503) Http504* = HttpCode(504) Http505* = HttpCode(505) + Http506* = HttpCode(506) ## https://tools.ietf.org/html/rfc2295.html Content negotiation, Section 8.1 + Http507* = HttpCode(507) ## https://tools.ietf.org/html/rfc4918.html WebDAV, Section 11.5 + Http508* = HttpCode(508) ## https://tools.ietf.org/html/rfc5842.html WebDAV, Section 7.2 + Http510* = HttpCode(510) ## https://tools.ietf.org/html/rfc2774.html Extension framework, Section 7 + Http511* = HttpCode(511) ## https://tools.ietf.org/html/rfc6585.html Additional status code, Section 6 + +const httpNewLine* = "\c\L" const headerLimit* = 10_000 -proc newHttpHeaders*(): HttpHeaders = - new result - result.table = newTable[string, seq[string]]() +func 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] == '-' + +func toCaseInsensitive*(headers: HttpHeaders, s: string): string {.inline.} = + ## For internal usage only. Do not use. + return if headers.isTitleCase: toTitleCase(s) else: toLowerAscii(s) + +func 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") + result = HttpHeaders(table: newTable[string, seq[string]](), isTitleCase: titleCase) + +func 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") + result = HttpHeaders(table: newTable[string, seq[string]](), isTitleCase: titleCase) -proc newHttpHeaders*(keyValuePairs: - openarray[tuple[key: string, val: string]]): HttpHeaders = - var pairs: seq[tuple[key: string, val: seq[string]]] = @[] for pair in keyValuePairs: - pairs.add((pair.key.toLowerAscii(), @[pair.val])) - new result - result.table = newTable[string, seq[string]](pairs) + let key = result.toCaseInsensitive(pair.key) + {.cast(noSideEffect).}: + if key in result.table: + result.table[key].add(pair.val) + else: + result.table[key] = @[pair.val] -proc `$`*(headers: HttpHeaders): string = - return $headers.table +func `$`*(headers: HttpHeaders): string {.inline.} = + $headers.table -proc clear*(headers: HttpHeaders) = +proc clear*(headers: HttpHeaders) {.inline.} = headers.table.clear() -proc `[]`*(headers: HttpHeaders, key: string): HttpHeaderValues = - ## Returns the values associated with the given ``key``. If the returned - ## values are passed to a procedure expecting a ``string``, the first +func `[]`*(headers: HttpHeaders, key: string): HttpHeaderValues = + ## Returns the values associated with the given `key`. If the returned + ## values are passed to a procedure expecting a `string`, the first ## value is automatically picked. If there are ## no values associated with the key, an exception is raised. ## - ## 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[key.toLowerAscii].HttpHeaderValues + ## To access multiple values of a key, use the overloaded `[]` below or + ## to get all of them access the `table` field directly. + {.cast(noSideEffect).}: + let tmp = headers.table[headers.toCaseInsensitive(key)] + return HttpHeaderValues(tmp) converter toString*(values: HttpHeaderValues): string = return seq[string](values)[0] -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, +func `[]`*(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[key.toLowerAscii][i] + {.cast(noSideEffect).}: + return headers.table[headers.toCaseInsensitive(key)][i] proc `[]=`*(headers: HttpHeaders, key, value: string) = - ## Sets the header entries associated with ``key`` to the specified value. + ## Sets the header entries associated with `key` to the specified value. ## Replaces any existing values. - headers.table[key.toLowerAscii] = @[value] + headers.table[headers.toCaseInsensitive(key)] = @[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[key.toLowerAscii] = value + ## Sets the header entries associated with `key` to the specified list of + ## values. Replaces any existing values. If `value` is empty, + ## deletes the header entries associated with `key`. + if value.len > 0: + headers.table[headers.toCaseInsensitive(key)] = value + else: + headers.table.del(headers.toCaseInsensitive(key)) 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(key.toLowerAscii): - headers.table[key.toLowerAscii] = @[value] + if not headers.table.hasKey(headers.toCaseInsensitive(key)): + headers.table[headers.toCaseInsensitive(key)] = @[value] else: - headers.table[key.toLowerAscii].add(value) + headers.table[headers.toCaseInsensitive(key)].add(value) proc del*(headers: HttpHeaders, key: string) = - ## Delete the header entries associated with ``key`` - headers.table.del(key.toLowerAscii) + ## Deletes the header entries associated with `key` + headers.table.del(headers.toCaseInsensitive(key)) iterator pairs*(headers: HttpHeaders): tuple[key, value: string] = ## Yields each key, value pair. @@ -163,54 +209,57 @@ iterator pairs*(headers: HttpHeaders): tuple[key, value: string] = for value in v: yield (k, value) -proc contains*(values: HttpHeaderValues, value: string): bool = - ## Determines if ``value`` is one of the values inside ``values``. Comparison +func contains*(values: HttpHeaderValues, value: string): bool = + ## Determines if `value` is one of the values inside `values`. Comparison ## is performed without case sensitivity. for val in seq[string](values): if val.toLowerAscii == value.toLowerAscii: return true -proc hasKey*(headers: HttpHeaders, key: string): bool = - return headers.table.hasKey(key.toLowerAscii()) +func hasKey*(headers: HttpHeaders, key: string): bool = + return headers.table.hasKey(headers.toCaseInsensitive(key)) -proc getOrDefault*(headers: HttpHeaders, key: string, +func getOrDefault*(headers: HttpHeaders, key: string, default = @[""].HttpHeaderValues): HttpHeaderValues = - ## Returns the values associated with the given ``key``. If there are no - ## values associated with the key, then ``default`` is returned. + ## Returns the values associated with the given `key`. If there are no + ## values associated with the key, then `default` is returned. if headers.hasKey(key): return headers[key] else: return default -proc len*(headers: HttpHeaders): int = return headers.table.len +func len*(headers: HttpHeaders): int {.inline.} = headers.table.len -proc parseList(line: string, list: var seq[string], start: int): int = +func parseList(line: string, list: var seq[string], start: int): int = var i = 0 var current = "" while start+i < line.len and line[start + i] notin {'\c', '\l'}: i += line.skipWhitespace(start + i) i += line.parseUntil(current, {'\c', '\l', ','}, start + i) - list.add(current) + list.add(move current) # implicit current.setLen(0) if start+i < line.len and line[start + i] == ',': i.inc # Skip , - current.setLen(0) -proc parseHeader*(line: string): tuple[key: string, value: seq[string]] = +func parseHeader*(line: string): tuple[key: string, value: seq[string]] = ## Parses a single raw header HTTP line into key value pairs. ## - ## Used by ``asynchttpserver`` and ``httpclient`` internally and should not + ## Used by `asynchttpserver` and `httpclient` internally and should not ## be used by you. result.value = @[] var i = 0 i = line.parseUntil(result.key, ':') inc(i) # skip : if i < len(line): - i += parseList(line, result.value, i) + if cmpIgnoreCase(result.key, "cookie") == 0: + i += line.skipWhitespace(i) + result.value.add line.substr(i) + else: + i += parseList(line, result.value, i) elif result.key.len > 0: result.value = @[""] else: result.value = @[] -proc `==`*(protocol: tuple[orig: string, major, minor: int], +func `==`*(protocol: tuple[orig: string, major, minor: int], ver: HttpVersion): bool = let major = case ver @@ -221,19 +270,18 @@ 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 = +func contains*(methods: set[HttpMethod], x: string): bool = return parseEnum[HttpMethod](x) in methods -proc `$`*(code: HttpCode): string = - ## Converts the specified ``HttpCode`` into a HTTP status. - ## - ## For example: - ## - ## .. code-block:: nim - ## doAssert($Http404 == "404 Not Found") +func `$`*(code: HttpCode): string = + ## Converts the specified `HttpCode` into a HTTP status. + runnableExamples: + doAssert($Http404 == "404 Not Found") case code.int of 100: "100 Continue" of 101: "101 Switching Protocols" + of 102: "102 Processing" + of 103: "103 Early Hints" of 200: "200 OK" of 201: "201 Created" of 202: "202 Accepted" @@ -241,6 +289,9 @@ proc `$`*(code: HttpCode): string = of 204: "204 No Content" of 205: "205 Reset Content" of 206: "206 Partial Content" + of 207: "207 Multi-Status" + of 208: "208 Already Reported" + of 226: "226 IM Used" of 300: "300 Multiple Choices" of 301: "301 Moved Permanently" of 302: "302 Found" @@ -248,8 +299,10 @@ proc `$`*(code: HttpCode): string = of 304: "304 Not Modified" of 305: "305 Use Proxy" of 307: "307 Temporary Redirect" + of 308: "308 Permanent Redirect" of 400: "400 Bad Request" of 401: "401 Unauthorized" + of 402: "402 Payment Required" of 403: "403 Forbidden" of 404: "404 Not Found" of 405: "405 Method Not Allowed" @@ -268,6 +321,9 @@ proc `$`*(code: HttpCode): string = of 418: "418 I'm a teapot" of 421: "421 Misdirected Request" of 422: "422 Unprocessable Entity" + of 423: "423 Locked" + of 424: "424 Failed Dependency" + of 425: "425 Too Early" of 426: "426 Upgrade Required" of 428: "428 Precondition Required" of 429: "429 Too Many Requests" @@ -279,46 +335,34 @@ proc `$`*(code: HttpCode): string = of 503: "503 Service Unavailable" of 504: "504 Gateway Timeout" of 505: "505 HTTP Version Not Supported" + of 506: "506 Variant Also Negotiates" + of 507: "507 Insufficient Storage" + of 508: "508 Loop Detected" + of 510: "510 Not Extended" + of 511: "511 Network Authentication Required" else: $(int(code)) -proc `==`*(a, b: HttpCode): bool {.borrow.} - -proc `==`*(rawCode: string, code: HttpCode): bool = - return cmpIgnoreCase(rawCode, $code) == 0 - -proc is2xx*(code: HttpCode): bool = - ## Determines whether ``code`` is a 2xx HTTP status code. - return code.int in {200 .. 299} - -proc is3xx*(code: HttpCode): bool = - ## Determines whether ``code`` is a 3xx HTTP status code. - return code.int in {300 .. 399} +func `==`*(a, b: HttpCode): bool {.borrow.} -proc is4xx*(code: HttpCode): bool = - ## Determines whether ``code`` is a 4xx HTTP status code. - return code.int in {400 .. 499} +func is1xx*(code: HttpCode): bool {.inline, since: (1, 5).} = + ## Determines whether `code` is a 1xx HTTP status code. + runnableExamples: + doAssert is1xx(HttpCode(103)) -proc is5xx*(code: HttpCode): bool = - ## Determines whether ``code`` is a 5xx HTTP status code. - return code.int in {500 .. 599} + code.int in 100 .. 199 -proc `$`*(httpMethod: HttpMethod): string = - return (system.`$`(httpMethod))[4 .. ^1].toUpperAscii() +func is2xx*(code: HttpCode): bool {.inline.} = + ## Determines whether `code` is a 2xx HTTP status code. + code.int in 200 .. 299 -when isMainModule: - var test = newHttpHeaders() - test["Connection"] = @["Upgrade", "Close"] - doAssert test["Connection", 0] == "Upgrade" - doAssert test["Connection", 1] == "Close" - test.add("Connection", "Test") - doAssert test["Connection", 2] == "Test" - doAssert "upgrade" in test["Connection"] +func is3xx*(code: HttpCode): bool {.inline.} = + ## Determines whether `code` is a 3xx HTTP status code. + code.int in 300 .. 399 - # Bug #5344. - doAssert parseHeader("foobar: ") == ("foobar", @[""]) - let (key, value) = parseHeader("foobar: ") - test = newHttpHeaders() - test[key] = value - doAssert test["foobar"] == "" +func is4xx*(code: HttpCode): bool {.inline.} = + ## Determines whether `code` is a 4xx HTTP status code. + code.int in 400 .. 499 - doAssert parseHeader("foobar:") == ("foobar", @[""]) \ No newline at end of file +func is5xx*(code: HttpCode): bool {.inline.} = + ## Determines whether `code` is a 5xx HTTP status code. + code.int in 500 .. 599 |