From 1a3e9162a406530f729797bcafc37319a62a537b Mon Sep 17 00:00:00 2001 From: cheatfate Date: Wed, 3 Feb 2021 00:33:14 +0200 Subject: [PATCH] Fix multipart end of message handling. Add apps.nim. Change copyrights dates. Add httpserver tests to test suite. --- chronos/apps.nim | 10 + chronos/apps/http/httpcommon.nim | 2 +- chronos/apps/http/httpserver.nim | 87 +++++--- chronos/apps/http/httptable.nim | 16 +- chronos/apps/http/multipart.nim | 28 ++- tests/testall.nim | 2 +- tests/testhttpserver.nim | 367 +++++++++++++++++++++++++++++++ 7 files changed, 471 insertions(+), 41 deletions(-) create mode 100644 chronos/apps.nim create mode 100644 tests/testhttpserver.nim diff --git a/chronos/apps.nim b/chronos/apps.nim new file mode 100644 index 00000000..53b3fac7 --- /dev/null +++ b/chronos/apps.nim @@ -0,0 +1,10 @@ +# +# Chronos HTTP/S common types +# (c) Copyright 2021-Present +# Status Research & Development GmbH +# +# Licensed under either of +# Apache License, version 2.0, (LICENSE-APACHEv2) +# MIT license (LICENSE-MIT) +import apps/http/httpserver +export httpserver diff --git a/chronos/apps/http/httpcommon.nim b/chronos/apps/http/httpcommon.nim index f6525b5f..42befd35 100644 --- a/chronos/apps/http/httpcommon.nim +++ b/chronos/apps/http/httpcommon.nim @@ -1,6 +1,6 @@ # # Chronos HTTP/S common types -# (c) Copyright 2019-Present +# (c) Copyright 2021-Present # Status Research & Development GmbH # # Licensed under either of diff --git a/chronos/apps/http/httpserver.nim b/chronos/apps/http/httpserver.nim index cb514722..b0677774 100644 --- a/chronos/apps/http/httpserver.nim +++ b/chronos/apps/http/httpserver.nim @@ -1,6 +1,6 @@ # # Chronos HTTP/S server implementation -# (c) Copyright 2019-Present +# (c) Copyright 2021-Present # Status Research & Development GmbH # # Licensed under either of @@ -14,7 +14,6 @@ import httptable, httpcommon, multipart export httptable, httpcommon, multipart when defined(useChroniclesLogging): - echo "Importing chronicles" import chronicles type @@ -67,8 +66,8 @@ type HttpServerRef* = ref HttpServer HttpRequest* = object of RootObj - headersTable: HttpTable - queryTable: HttpTable + headers*: HttpTable + query*: HttpTable postTable: Option[HttpTable] rawPath*: string rawQuery*: string @@ -222,14 +221,14 @@ proc prepareRequest(conn: HttpConnectionRef, uri.path = "*" uri - request.queryTable = + request.query = block: var table = HttpTable.init() for key, value in queryParams(request.uri.query): table.add(key, value) table - request.headersTable = + request.headers = block: var table = HttpTable.init() # Retrieve headers and values @@ -248,8 +247,7 @@ proc prepareRequest(conn: HttpConnectionRef, # Preprocessing "Content-Encoding" header. request.contentEncoding = block: - let res = getContentEncoding( - request.headersTable.getList("content-encoding")) + let res = getContentEncoding(request.headers.getList("content-encoding")) if res.isErr(): return err(Http400) else: @@ -259,7 +257,7 @@ proc prepareRequest(conn: HttpConnectionRef, request.transferEncoding = block: let res = getTransferEncoding( - request.headersTable.getList("transfer-encoding")) + request.headers.getList("transfer-encoding")) if res.isErr(): return err(Http400) else: @@ -267,8 +265,8 @@ proc prepareRequest(conn: HttpConnectionRef, # Almost all HTTP requests could have body (except TRACE), we perform some # steps to reveal information about body. - if "content-length" in request.headersTable: - let length = request.headersTable.getInt("content-length") + if "content-length" in request.headers: + let length = request.headers.getInt("content-length") if length > 0: if request.meth == MethodTrace: return err(Http400) @@ -290,16 +288,16 @@ proc prepareRequest(conn: HttpConnectionRef, UrlEncodedType = "application/x-www-form-urlencoded" MultipartType = "multipart/form-data" - if "content-type" in request.headersTable: - let contentType = request.headersTable.getString("content-type") + if "content-type" in request.headers: + let contentType = request.headers.getString("content-type") let tmp = strip(contentType).toLowerAscii() if tmp.startsWith(UrlEncodedType): request.requestFlags.incl(HttpRequestFlags.UrlencodedForm) elif tmp.startsWith(MultipartType): request.requestFlags.incl(HttpRequestFlags.MultipartForm) - if "expect" in request.headersTable: - let expectHeader = request.headersTable.getString("expect") + if "expect" in request.headers: + let expectHeader = request.headers.getString("expect") if strip(expectHeader).toLowerAscii() == "100-continue": request.requestFlags.incl(HttpRequestFlags.ClientExpect) @@ -369,9 +367,10 @@ proc sendErrorResponse(conn: HttpConnectionRef, version: HttpVersion, databody = ""): Future[bool] {.async.} = var answer = $version & " " & $code & "\r\n" answer.add("Date: " & httpDate() & "\r\n") - if len(databody) > 0: + if len(datatype) > 0: answer.add("Content-Type: " & datatype & "\r\n") - answer.add("Content-Length: " & $len(databody) & "\r\n") + if len(databody) > 0: + answer.add("Content-Length: " & $len(databody) & "\r\n") if keepAlive: answer.add("Connection: keep-alive\r\n") else: @@ -494,18 +493,25 @@ proc processLoop(server: HttpServerRef, transp: StreamTransport) {.async.} = break else: let request = arg.get() - let keepConn = if request.version == HttpVersion11: true else: false + var keepConn = if request.version == HttpVersion11: true else: false if isNil(lastError): - case resp.state - of HttpResponseState.Empty: - # Response was ignored - discard await conn.sendErrorResponse(HttpVersion11, Http404, keepConn) - of HttpResponseState.Prepared: - # Response was prepared but not sent. - discard await conn.sendErrorResponse(HttpVersion11, Http409, keepConn) + if isNil(resp): + # Response was `nil`. + discard await conn.sendErrorResponse(HttpVersion11, Http404, + false) else: - # some data was already sent to the client. - discard + case resp.state + of HttpResponseState.Empty: + # Response was ignored + discard await conn.sendErrorResponse(HttpVersion11, Http404, + keepConn) + of HttpResponseState.Prepared: + # Response was prepared but not sent. + discard await conn.sendErrorResponse(HttpVersion11, Http409, + keepConn) + else: + # some data was already sent to the client. + discard else: discard await conn.sendErrorResponse(HttpVersion11, Http503, true) @@ -607,12 +613,12 @@ proc getMultipartReader*(req: HttpRequestRef): HttpResult[MultiPartReaderRef] = ## Create new MultiPartReader interface for specific request. if req.meth in PostMethods: if MultipartForm in req.requestFlags: - let ctype = ? getContentType(req.headersTable.getList("content-type")) + let ctype = ? getContentType(req.headers.getList("content-type")) if ctype != "multipart/form-data": err("Content type is not supported") else: let boundary = ? getMultipartBoundary( - req.headersTable.getList("content-type") + req.headers.getList("content-type") ) var stream = ? req.getBodyStream() ok(MultiPartReaderRef.new(stream, boundary)) @@ -899,7 +905,22 @@ proc finish*(resp: HttpResponseRef) {.async.} = resp.state = HttpResponseState.Failed raise newHttpCriticalError("Unable to send response") +proc respond*(req: HttpRequestRef, code: HttpCode, content: string, + headers: HttpTable): Future[HttpResponseRef] {.async.} = + ## Responds to the request with the specified ``HttpCode``, headers and + ## content. + let response = req.getResponse() + response.status = code + for k, v in headers.stringItems(): + response.addHeader(k, v) + await response.sendBody(content) + return response + proc requestInfo*(req: HttpRequestRef, contentType = "text/text"): string = + ## Returns comprehensive information about request for specific content + ## type. + ## + ## Only two content-types are supported: "text/text" and "text/html". proc h(t: string): string = case contentType of "text/text": @@ -952,14 +973,14 @@ proc requestInfo*(req: HttpRequestRef, contentType = "text/text"): string = "not available" res.add(kv("request.body", body)) - if not(req.queryTable.isEmpty()): + if not(req.query.isEmpty()): res.add(h("Query arguments")) - for k, v in req.queryTable.stringItems(): + for k, v in req.query.stringItems(): res.add(kv(k, v)) - if not(req.headersTable.isEmpty()): + if not(req.headers.isEmpty()): res.add(h("HTTP headers")) - for k, v in req.headersTable.stringItems(true): + for k, v in req.headers.stringItems(true): res.add(kv(k, v)) if req.meth in PostMethods: diff --git a/chronos/apps/http/httptable.nim b/chronos/apps/http/httptable.nim index 73d9807c..a1b518a3 100644 --- a/chronos/apps/http/httptable.nim +++ b/chronos/apps/http/httptable.nim @@ -1,7 +1,7 @@ # # Chronos HTTP/S case-insensitive non-unique # key-value memory storage -# (c) Copyright 2019-Present +# (c) Copyright 2021-Present # Status Research & Development GmbH # # Licensed under either of @@ -105,6 +105,20 @@ proc init*(htt: typedesc[HttpTable]): HttpTable = proc new*(htt: typedesc[HttpTableRef]): HttpTableRef = HttpTableRef(table: initTable[string, seq[string]]()) +proc init*(htt: typedesc[HttpTable], + data: openArray[tuple[key: string, value: string]]): HttpTable = + var res = HttpTable.init() + for item in data: + res.add(item.key, item.value) + res + +proc new*(htt: typedesc[HttpTableRef], + data: openArray[tuple[key: string, value: string]]): HttpTableRef = + var res = HttpTableRef.new() + for item in data: + res.add(item.key, item.value) + res + proc isEmpty*(ht: HttpTables): bool = ## Returns ``true`` if HttpTable ``ht`` is empty (do not have any values). len(ht.table) == 0 diff --git a/chronos/apps/http/multipart.nim b/chronos/apps/http/multipart.nim index 363f80d3..33172b5b 100644 --- a/chronos/apps/http/multipart.nim +++ b/chronos/apps/http/multipart.nim @@ -1,7 +1,7 @@ # # Chronos HTTP/S multipart/form # encoding and decoding helper procedures -# (c) Copyright 2019-Present +# (c) Copyright 2021-Present # Status Research & Development GmbH # # Licensed under either of @@ -144,7 +144,7 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} = doAssert(mpr.kind == MultiPartSource.Stream) if mpr.firstTime: try: - # Read and verify initial <-><-> + # Read and verify initial <-><-> await mpr.stream.readExactly(addr mpr.buffer[0], len(mpr.boundary) - 2) mpr.firstTime = false if not(startsWith(mpr.buffer.toOpenArray(0, len(mpr.boundary) - 3), @@ -160,13 +160,23 @@ proc readPart*(mpr: MultiPartReaderRef): Future[MultiPart] {.async.} = # Reading part's headers try: + # Read 2 bytes more await mpr.stream.readExactly(addr mpr.buffer[0], 2) if mpr.buffer[0] == byte('-') and mpr.buffer[1] == byte('-'): - raise newException(MultiPartEoM, - "End of multipart message") + # If two bytes are "--" we are at the end + await mpr.stream.readExactly(addr mpr.buffer[0], 2) + if mpr.buffer[0] == 0x0D'u8 and mpr.buffer[1] == 0x0A'u8: + # If 3rd and 4th bytes are CRLF we are exactly at the end of message. + raise newException(MultiPartEoM, + "End of multipart message") + else: + raise newException(MultiPartIncorrectError, + "Incorrect part headers found") if mpr.buffer[0] != 0x0D'u8 or mpr.buffer[1] != 0x0A'u8: raise newException(MultiPartIncorrectError, "Unexpected boundary suffix") + # If two bytes are CRLF we are at the part beginning. + # Reading part's headers let res = await mpr.stream.readUntil(addr mpr.buffer[0], len(mpr.buffer), HeadersMark) var headersList = parseHeaders(mpr.buffer.toOpenArray(0, res - 1), false) @@ -314,7 +324,15 @@ proc getPart*(mpr: var MultiPartReader): Result[MultiPart, string] = # If we have <-><-><-><-> it means we have found last boundary # of multipart message. mpr.offset += 2 - return err("End of multipart form encountered") + if len(mpr.buffer) <= mpr.offset + 1: + if mpr.buffer[mpr.offset] == 0x0D'u8 and + mpr.buffer[mpr.offset + 1] == 0x0A'u8: + mpr.offset += 2 + return err("End of multipart form encountered") + else: + return err("Incorrect multipart last boundary") + else: + return err("Incomplete multipart form") if mpr.buffer[mpr.offset] == 0x0D'u8 and mpr.buffer[mpr.offset + 1] == 0x0A'u8: diff --git a/tests/testall.nim b/tests/testall.nim index f56cbc76..0e5d59a9 100644 --- a/tests/testall.nim +++ b/tests/testall.nim @@ -7,4 +7,4 @@ # MIT license (LICENSE-MIT) import testmacro, testsync, testsoon, testtime, testfut, testsignal, testaddress, testdatagram, teststream, testserver, testbugs, testnet, - testasyncstream, testutils + testasyncstream, testutils, testhttpserver diff --git a/tests/testhttpserver.nim b/tests/testhttpserver.nim new file mode 100644 index 00000000..1ee2d623 --- /dev/null +++ b/tests/testhttpserver.nim @@ -0,0 +1,367 @@ +# Chronos Test Suite +# (c) Copyright 2021-Present +# Status Research & Development GmbH +# +# Licensed under either of +# Apache License, version 2.0, (LICENSE-APACHEv2) +# MIT license (LICENSE-MIT) +import std/[strutils, unittest, algorithm, strutils] +import ../chronos, ../chronos/apps + +suite "HTTP server testing suite": + proc httpClient(address: TransportAddress, + data: string): Future[string] {.async.} = + var transp: StreamTransport + try: + transp = await connect(address) + if len(data) > 0: + let wres {.used.} = await transp.write(data) + var rres = await transp.read() + var sres = newString(len(rres)) + if len(rres) > 0: + copyMem(addr sres[0], addr rres[0], len(rres)) + return sres + except CatchableError: + return "EXCEPTION" + finally: + if not(isNil(transp)): + await closeWait(transp) + + test "Request headers timeout test": + proc testTimeout(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + return await request.respond(Http200, "TEST_OK", HttpTable.init()) + else: + if r.error().error == HTTPServerError.TimeoutError: + serverRes = true + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, socketFlags = socketFlags, + httpHeadersTimeout = 100.milliseconds) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let data = await httpClient(address, "") + await server.stop() + await server.close() + return serverRes and (data.startsWith("HTTP/1.1 408")) + + check waitFor(testTimeout(initTAddress("127.0.0.1:30080"))) == true + + test "Empty headers test": + proc testEmpty(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + return await request.respond(Http200, "TEST_OK", HttpTable.init()) + else: + if r.error().error == HTTPServerError.CriticalError: + serverRes = true + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let data = await httpClient(address, "\r\n\r\n") + await server.stop() + await server.close() + return serverRes and (data.startsWith("HTTP/1.1 400")) + + check waitFor(testEmpty(initTAddress("127.0.0.1:30080"))) == true + + test "Too big headers test": + proc testTooBig(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + return await request.respond(Http200, "TEST_OK", HttpTable.init()) + else: + if r.error().error == HTTPServerError.CriticalError: + serverRes = true + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, + maxHeadersSize = 10, + socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let data = await httpClient(address, "GET / HTTP/1.1\r\n\r\n") + await server.stop() + await server.close() + return serverRes and (data.startsWith("HTTP/1.1 413")) + + check waitFor(testTooBig(initTAddress("127.0.0.1:30080"))) == true + + test "Query arguments test": + proc testQuery(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + var kres = newSeq[string]() + for k, v in request.query.stringItems(): + kres.add(k & ":" & v) + sort(kres) + serverRes = true + return await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + else: + serverRes = false + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let data1 = await httpClient(address, + "GET /?a=1&a=2&b=3&c=4 HTTP/1.0\r\n\r\n") + let data2 = await httpClient(address, + "GET /?a=%D0%9F&%D0%A4=%D0%91&b=%D0%A6&c=%D0%AE HTTP/1.0\r\n\r\n") + await server.stop() + await server.close() + let r = serverRes and + (data1.find("TEST_OK:a:1:a:2:b:3:c:4") >= 0) and + (data2.find("TEST_OK:a:П:b:Ц:c:Ю:Ф:Б") >= 0) + return r + + check waitFor(testQuery(initTAddress("127.0.0.1:30080"))) == true + + test "Headers test": + proc testHeaders(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + var kres = newSeq[string]() + for k, v in request.headers.stringItems(): + kres.add(k & ":" & v) + sort(kres) + serverRes = true + return await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + else: + serverRes = false + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let message = + "GET / HTTP/1.0\r\n" & + "Host: www.google.com\r\n" & + "Content-Type: text/html\r\n" & + "Expect: 100-continue\r\n" & + "Cookie: 1\r\n" & + "Cookie: 2\r\n\r\n" + let expect = "TEST_OK:content-type:text/html:cookie:1:cookie:2" & + ":expect:100-continue:host:www.google.com" + let data = await httpClient(address, message) + await server.stop() + await server.close() + return serverRes and (data.find(expect) >= 0) + + check waitFor(testHeaders(initTAddress("127.0.0.1:30080"))) == true + + test "POST arguments (application/x-www-form-urlencoded) test": + proc testPostUrl(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + var kres = newSeq[string]() + let request = r.get() + if request.meth in PostMethods: + let post = await request.post() + for k, v in post.stringItems(): + kres.add(k & ":" & v) + sort(kres) + serverRes = true + return await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + else: + serverRes = false + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let message = + "POST / HTTP/1.0\r\n" & + "Content-Type: application/x-www-form-urlencoded\r\n" & + "Content-Length: 20" & + "Cookie: 2\r\n\r\n" & + "a=a&b=b&c=c&d=%D0%9F" + let data = await httpClient(address, message) + let expect = "TEST_OK:a:a:b:b:c:c:d:П" + await server.stop() + await server.close() + return serverRes and (data.find(expect) >= 0) + + check waitFor(testPostUrl(initTAddress("127.0.0.1:30080"))) == true + + test "POST arguments (multipart/form-data) test": + proc testPostMultipart(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + var kres = newSeq[string]() + let request = r.get() + if request.meth in PostMethods: + let post = await request.post() + for k, v in post.stringItems(): + kres.add(k & ":" & v) + sort(kres) + serverRes = true + return await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + else: + serverRes = false + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let message = + "POST / HTTP/1.0\r\n" & + "Host: 127.0.0.1:30080\r\n" & + "User-Agent: curl/7.55.1\r\n" & + "Accept: */*\r\n" & + "Content-Length: 343\r\n" & + "Content-Type: multipart/form-data; " & + "boundary=------------------------ab5706ba6f80b795\r\n\r\n" & + "--------------------------ab5706ba6f80b795\r\n" & + "Content-Disposition: form-data; name=\"key1\"\r\n\r\n" & + "value1\r\n" & + "--------------------------ab5706ba6f80b795\r\n" & + "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" & + "value2\r\n" & + "--------------------------ab5706ba6f80b795\r\n" & + "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" & + "value4\r\n" & + "--------------------------ab5706ba6f80b795--\r\n" + let data = await httpClient(address, message) + let expect = "TEST_OK:key1:value1:key2:value2:key2:value4" + await server.stop() + await server.close() + return serverRes and (data.find(expect) >= 0) + + check waitFor(testPostMultipart(initTAddress("127.0.0.1:30080"))) == true + + test "POST arguments (multipart/form-data + chunked encoding) test": + proc testPostMultipart2(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + var kres = newSeq[string]() + let request = r.get() + if request.meth in PostMethods: + let post = await request.post() + for k, v in post.stringItems(): + kres.add(k & ":" & v) + sort(kres) + serverRes = true + return await request.respond(Http200, "TEST_OK:" & kres.join(":"), + HttpTable.init()) + else: + serverRes = false + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags) + if res.isErr(): + return false + + let server = res.get() + server.start() + + let message = + "POST / HTTP/1.0\r\n" & + "Host: 127.0.0.1:30080\r\n" & + "Transfer-Encoding: chunked\r\n" & + "Content-Type: multipart/form-data; boundary=---" & + "---------------------f98f0e32c55fa2ae\r\n\r\n" & + "271\r\n" & + "--------------------------f98f0e32c55fa2ae\r\n" & + "Content-Disposition: form-data; name=\"key1\"\r\n\r\n" & + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" & + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\r\n" & + "--------------------------f98f0e32c55fa2ae\r\n" & + "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" & + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" & + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB\r\n" & + "--------------------------f98f0e32c55fa2ae\r\n" & + "Content-Disposition: form-data; name=\"key2\"\r\n\r\n" & + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" & + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC\r\n" & + "--------------------------f98f0e32c55fa2ae--\r\n" & + "\r\n0\r\n\r\n" + + let data = await httpClient(address, message) + let expect = "TEST_OK:key1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" & + "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" & + "AAAAA:key2:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" & + "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB" & + "BBB:key2:CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" & + "CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC" + await server.stop() + await server.close() + return serverRes and (data.find(expect) >= 0) + + check waitFor(testPostMultipart2(initTAddress("127.0.0.1:30080"))) == true + + test "Leaks test": + check: + getTracker("async.stream.reader").isLeaked() == false + getTracker("async.stream.writer").isLeaked() == false + getTracker("stream.server").isLeaked() == false + getTracker("stream.transport").isLeaked() == false