diff --git a/chronos.nimble b/chronos.nimble index 5bb69075..583c5ba7 100644 --- a/chronos.nimble +++ b/chronos.nimble @@ -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" diff --git a/chronos/apps/http/httpcommon.nim b/chronos/apps/http/httpcommon.nim index 14c2e956..fb8fb732 100644 --- a/chronos/apps/http/httpcommon.nim +++ b/chronos/apps/http/httpcommon.nim @@ -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) @@ -99,7 +107,8 @@ 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('&'): @@ -107,7 +116,12 @@ iterator queryParams*(query: string): tuple[key: string, value: string] {. 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]] {. diff --git a/chronos/apps/http/httpserver.nim b/chronos/apps/http/httpserver.nim index 4d1ded03..47ac2805 100644 --- a/chronos/apps/http/httpserver.nim +++ b/chronos/apps/http/httpserver.nim @@ -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, @@ -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 @@ -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 @@ -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() @@ -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 diff --git a/tests/testhttpserver.nim b/tests/testhttpserver.nim index 86fd4f12..de7ea4e7 100644 --- a/tests/testhttpserver.nim +++ b/tests/testhttpserver.nim @@ -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.} @@ -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