summary refs log tree commit diff stats
path: root/lib/pure/httpcore.nim
diff options
context:
space:
mode:
Diffstat (limited to 'lib/pure/httpcore.nim')
-rw-r--r--lib/pure/httpcore.nim270
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