Skip to content

Commit

Permalink
Enable comma as array delimiter and adding tests. (#191)
Browse files Browse the repository at this point in the history
* Enable comma as array delimiter and adding tests.

* Bump version to 3.0.4.
  • Loading branch information
cheatfate committed May 17, 2021
1 parent 67f0f12 commit 7ccb170
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 8 deletions.
2 changes: 1 addition & 1 deletion chronos.nimble
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
packageName = "chronos"
version = "3.0.3"
version = "3.0.4"
author = "Status Research & Development GmbH"
description = "Chronos"
license = "Apache License 2.0 or MIT"
Expand Down
18 changes: 16 additions & 2 deletions chronos/apps/http/httpcommon.nim
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,20 @@ type
HttpRedirectError* = object of HttpError
HttpAddressError* = object of HttpError

KeyValueTuple* = tuple
key: string
value: string

TransferEncodingFlags* {.pure.} = enum
Identity, Chunked, Compress, Deflate, Gzip

ContentEncodingFlags* {.pure.} = enum
Identity, Br, Compress, Deflate, Gzip

QueryParamsFlag* {.pure.} = enum
CommaSeparatedArray ## Enable usage of comma symbol as separator of array
## items

proc raiseHttpCriticalError*(msg: string,
code = Http400) {.noinline, noreturn.} =
raise (ref HttpCriticalError)(code: code, msg: msg)
Expand Down Expand Up @@ -99,15 +107,21 @@ template newHttpReadError*(message: string): ref HttpReadError =
template newHttpWriteError*(message: string): ref HttpWriteError =
newException(HttpWriteError, message)

iterator queryParams*(query: string): tuple[key: string, value: string] {.
iterator queryParams*(query: string,
flags: set[QueryParamsFlag] = {}): KeyValueTuple {.
raises: [Defect].} =
## Iterate over url-encoded query string.
for pair in query.split('&'):
let items = pair.split('=', maxsplit = 1)
let k = items[0]
if len(k) > 0:
let v = if len(items) > 1: items[1] else: ""
yield (decodeUrl(k), decodeUrl(v))
if CommaSeparatedArray in flags:
let key = decodeUrl(k)
for av in decodeUrl(v).split(','):
yield (k, av)
else:
yield (decodeUrl(k), decodeUrl(v))

func getTransferEncoding*(ch: openarray[string]): HttpResult[
set[TransferEncodingFlags]] {.
Expand Down
27 changes: 23 additions & 4 deletions chronos/apps/http/httpserver.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,15 @@ export httptable, httpcommon, httputils, multipart, asyncstream,

type
HttpServerFlags* {.pure.} = enum
Secure, NoExpectHandler, NotifyDisconnect
Secure,
## Internal flag which indicates that server working in secure TLS mode
NoExpectHandler,
## Do not handle `Expect` header automatically
NotifyDisconnect,
## Notify user-callback when remote client disconnects.
QueryCommaSeparatedArray
## Enable usage of comma as an array item delimiter in url-encoded
## entities (e.g. query string or POST body).

HttpServerError* {.pure.} = enum
TimeoutError, CatchableError, RecoverableError, CriticalError,
Expand Down Expand Up @@ -49,7 +57,8 @@ type

HttpConnectionCallback* =
proc(server: HttpServerRef,
transp: StreamTransport): Future[HttpConnectionRef] {.gcsafe, raises: [Defect].}
transp: StreamTransport): Future[HttpConnectionRef] {.
gcsafe, raises: [Defect].}

HttpServer* = object of RootObj
instance*: StreamServer
Expand Down Expand Up @@ -275,8 +284,13 @@ proc prepareRequest(conn: HttpConnectionRef,

request.query =
block:
let queryFlags =
if QueryCommaSeparatedArray in conn.server.flags:
{QueryParamsFlag.CommaSeparatedArray}
else:
{}
var table = HttpTable.init()
for key, value in queryParams(request.uri.query):
for key, value in queryParams(request.uri.query, queryFlags):
table.add(key, value)
table

Expand Down Expand Up @@ -775,6 +789,11 @@ proc post*(req: HttpRequestRef): Future[HttpTable] {.async.} =
return HttpTable.init()

if UrlencodedForm in req.requestFlags:
let queryFlags =
if QueryCommaSeparatedArray in req.connection.server.flags:
{QueryParamsFlag.CommaSeparatedArray}
else:
{}
var table = HttpTable.init()
# getBody() will handle `Expect`.
var body = await req.getBody()
Expand All @@ -783,7 +802,7 @@ proc post*(req: HttpRequestRef): Future[HttpTable] {.async.} =
var strbody = newString(len(body))
if len(body) > 0:
copyMem(addr strbody[0], addr body[0], len(body))
for key, value in queryParams(strbody):
for key, value in queryParams(strbody, queryFlags):
table.add(key, value)
req.postTable = some(table)
return table
Expand Down
38 changes: 37 additions & 1 deletion tests/testhttpserver.nim
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
# MIT license (LICENSE-MIT)
import std/[strutils, algorithm, strutils]
import unittest2
import ../chronos, ../chronos/apps/http/httpserver
import ../chronos, ../chronos/apps/http/httpserver,
../chronos/apps/http/httpcommon
import stew/base10

when defined(nimHasUsed): {.used.}
Expand Down Expand Up @@ -819,6 +820,41 @@ suite "HTTP server testing suite":
getContentEncoding([]).tryGet() == { ContentEncodingFlags.Identity }
getContentEncoding(["", ""]).tryGet() == { ContentEncodingFlags.Identity }

test "queryParams() test":
const Vectors = [
("id=1&id=2&id=3&id=4", {}, "id:1,id:2,id:3,id:4"),
("id=1,2,3,4", {}, "id:1,2,3,4"),
("id=1%2C2%2C3%2C4", {}, "id:1,2,3,4"),
("id=", {}, "id:"),
("id=&id=", {}, "id:,id:"),
("id=1&id=2&id=3&id=4", {QueryParamsFlag.CommaSeparatedArray},
"id:1,id:2,id:3,id:4"),
("id=1,2,3,4", {QueryParamsFlag.CommaSeparatedArray},
"id:1,id:2,id:3,id:4"),
("id=1%2C2%2C3%2C4", {QueryParamsFlag.CommaSeparatedArray},
"id:1,id:2,id:3,id:4"),
("id=", {QueryParamsFlag.CommaSeparatedArray}, "id:"),
("id=&id=", {QueryParamsFlag.CommaSeparatedArray}, "id:,id:"),
("id=,", {QueryParamsFlag.CommaSeparatedArray}, "id:,id:"),
("id=,,", {QueryParamsFlag.CommaSeparatedArray}, "id:,id:,id:"),
("id=1&id=2&id=3,4,5,6&id=7%2C8%2C9%2C10",
{QueryParamsFlag.CommaSeparatedArray},
"id:1,id:2,id:3,id:4,id:5,id:6,id:7,id:8,id:9,id:10")
]

proc toString(ht: HttpTable): string =
var res: seq[string]
for key, value in ht.items():
for item in value:
res.add(key & ":" & item)
res.join(",")

for vector in Vectors:
var table = HttpTable.init()
for key, value in queryParams(vector[0], vector[1]):
table.add(key, value)
check toString(table) == vector[2]

test "Leaks test":
check:
getTracker("async.stream.reader").isLeaked() == false
Expand Down

0 comments on commit 7ccb170

Please sign in to comment.