diff --git a/chronos/apps/http/httpcommon.nim b/chronos/apps/http/httpcommon.nim index 42befd35..5d2a6fb0 100644 --- a/chronos/apps/http/httpcommon.nim +++ b/chronos/apps/http/httpcommon.nim @@ -10,8 +10,6 @@ import stew/results, httputils, strutils, uri export results, httputils, strutils const - useChroniclesLogging* {.booldefine.} = false - HeadersMark* = @[byte(0x0D), byte(0x0A), byte(0x0D), byte(0x0A)] PostMethods* = {MethodPost, MethodPatch, MethodPut, MethodDelete} @@ -19,9 +17,12 @@ type HttpResult*[T] = Result[T, string] HttpResultCode*[T] = Result[T, HttpCode] + HttpDefect* = object of Defect HttpError* = object of CatchableError HttpCriticalError* = object of HttpError + code*: HttpCode HttpRecoverableError* = object of HttpError + code*: HttpCode TransferEncodingFlags* {.pure.} = enum Identity, Chunked, Compress, Deflate, Gzip @@ -29,15 +30,19 @@ type ContentEncodingFlags* {.pure.} = enum Identity, Br, Compress, Deflate, Gzip -template log*(body: untyped) = - when defined(useChroniclesLogging): - body +proc newHttpDefect*(msg: string): ref HttpDefect = + newException(HttpDefect, msg) -proc newHttpCriticalError*(msg: string): ref HttpCriticalError = - newException(HttpCriticalError, msg) +proc newHttpCriticalError*(msg: string, code = Http400): ref HttpCriticalError = + var tre = newException(HttpCriticalError, msg) + tre.code = code + tre -proc newHttpRecoverableError*(msg: string): ref HttpRecoverableError = - newException(HttpRecoverableError, msg) +proc newHttpRecoverableError*(msg: string, + code = Http400): ref HttpRecoverableError = + var tre = newException(HttpRecoverableError, msg) + tre.code = code + tre iterator queryParams*(query: string): tuple[key: string, value: string] = ## Iterate over url-encoded query string. diff --git a/chronos/apps/http/httpserver.nim b/chronos/apps/http/httpserver.nim index b0677774..6cdb354c 100644 --- a/chronos/apps/http/httpserver.nim +++ b/chronos/apps/http/httpserver.nim @@ -9,12 +9,9 @@ import std/[tables, options, uri, strutils] import stew/results, httputils import ../../asyncloop, ../../asyncsync -import ../../streams/[asyncstream, boundstream, chunkstream] +import ../../streams/[asyncstream, boundstream, chunkstream, tlsstream] import httptable, httpcommon, multipart -export httptable, httpcommon, multipart - -when defined(useChroniclesLogging): - import chronicles +export httptable, httpcommon, multipart, tlsstream, asyncstream type HttpServerFlags* {.pure.} = enum @@ -28,6 +25,7 @@ type HttpProcessError* = object error*: HTTPServerError + code*: HttpCode exc*: ref CatchableError remote*: TransportAddress @@ -54,6 +52,7 @@ type baseUri*: Uri flags*: set[HttpServerFlags] socketFlags*: set[ServerFlags] + secureFlags*: set[TLSFlags] connections*: Table[string, Future[void]] acceptLoop*: Future[void] lifetime*: Future[void] @@ -62,6 +61,8 @@ type maxHeadersSize: int maxRequestBodySize: int processCallback: HttpProcessCallback + tlsPrivateKey: TLSPrivateKey + tlsCertificate: TLSCertificate HttpServerRef* = ref HttpServer @@ -99,16 +100,19 @@ type HttpConnection* = object of RootObj server*: HttpServerRef transp: StreamTransport - mainReader*: AsyncStreamReader - mainWriter*: AsyncStreamWriter + mainReader: AsyncStreamReader + mainWriter: AsyncStreamWriter + tlsStream: TLSAsyncStream + reader*: AsyncStreamReader + writer*: AsyncStreamWriter buffer: seq[byte] HttpConnectionRef* = ref HttpConnection proc init(htype: typedesc[HttpProcessError], error: HTTPServerError, - exc: ref CatchableError, - remote: TransportAddress): HttpProcessError = - HttpProcessError(error: error, exc: exc, remote: remote) + exc: ref CatchableError, remote: TransportAddress, + code: HttpCode): HttpProcessError = + HttpProcessError(error: error, exc: exc, remote: remote, code: code) proc new*(htype: typedesc[HttpServerRef], address: TransportAddress, @@ -116,6 +120,9 @@ proc new*(htype: typedesc[HttpServerRef], serverFlags: set[HttpServerFlags] = {}, socketFlags: set[ServerFlags] = {ReuseAddr}, serverUri = Uri(), + tlsPrivateKey: TLSPrivateKey = nil, + tlsCertificate: TLSCertificate = nil, + secureFlags: set[TLSFlags] = {}, maxConnections: int = -1, bufferSize: int = 4096, backlogSize: int = 100, @@ -124,6 +131,10 @@ proc new*(htype: typedesc[HttpServerRef], maxHeadersSize: int = 8192, maxRequestBodySize: int = 1_048_576): HttpResult[HttpServerRef] = + if HttpServerFlags.Secure in serverFlags: + if isNil(tlsPrivateKey) or isNil(tlsCertificate): + return err("PrivateKey or Certificate is missing") + var res = HttpServerRef( maxConnections: maxConnections, headersTimeout: httpHeadersTimeout, @@ -133,7 +144,9 @@ proc new*(htype: typedesc[HttpServerRef], processCallback: processCallback, backLogSize: backLogSize, flags: serverFlags, - socketFlags: socketFlags + socketFlags: socketFlags, + tlsPrivateKey: tlsPrivateKey, + tlsCertificate: tlsCertificate ) res.baseUri = @@ -311,10 +324,10 @@ proc getBodyStream*(request: HttpRequestRef): HttpResult[AsyncStreamReader] = ## Streams which was obtained using this procedure must be closed to avoid ## leaks. if HttpRequestFlags.BoundBody in request.requestFlags: - ok(newBoundedStreamReader(request.connection.mainReader, + ok(newBoundedStreamReader(request.connection.reader, request.contentLength)) elif HttpRequestFlags.UnboundBody in request.requestFlags: - ok(newChunkedStreamReader(request.connection.mainReader)) + ok(newChunkedStreamReader(request.connection.reader)) else: err("Request do not have body available") @@ -326,7 +339,7 @@ proc handleExpect*(request: HttpRequestRef) {.async.} = if request.version == HttpVersion11: try: let message = $request.version & " " & $Http100 & "\r\n\r\n" - await request.connection.mainWriter.write(message) + await request.connection.writer.write(message) except CancelledError as exc: raise exc except AsyncStreamWriteError, AsyncStreamIncompleteError: @@ -379,7 +392,7 @@ proc sendErrorResponse(conn: HttpConnectionRef, version: HttpVersion, if len(databody) > 0: answer.add(databody) try: - await conn.mainWriter.write(answer) + await conn.writer.write(answer) return true except CancelledError: return false @@ -388,44 +401,73 @@ proc sendErrorResponse(conn: HttpConnectionRef, version: HttpVersion, except AsyncStreamIncompleteError: return false -proc getRequest*(conn: HttpConnectionRef): Future[HttpRequestRef] {.async.} = +proc getRequest(conn: HttpConnectionRef): Future[HttpRequestRef] {.async.} = try: conn.buffer.setLen(conn.server.maxHeadersSize) - let res = await conn.transp.readUntil(addr conn.buffer[0], len(conn.buffer), + let res = await conn.reader.readUntil(addr conn.buffer[0], len(conn.buffer), HeadersMark) conn.buffer.setLen(res) let header = parseRequest(conn.buffer) if header.failed(): - discard await conn.sendErrorResponse(HttpVersion11, Http400, false) raise newHttpCriticalError("Malformed request recieved") else: let res = prepareRequest(conn, header) if res.isErr(): - discard await conn.sendErrorResponse(HttpVersion11, Http400, false) raise newHttpCriticalError("Invalid request received") else: return res.get() - except TransportOsError: - raise newHttpCriticalError("Unexpected OS error") - except TransportIncompleteError: + except AsyncStreamIncompleteError: raise newHttpCriticalError("Remote peer disconnected") - except TransportLimitError: - discard await conn.sendErrorResponse(HttpVersion11, Http413, false) - raise newHttpCriticalError("Maximum size of request headers reached") + except AsyncStreamReadError: + raise newHttpCriticalError("Connection with remote peer has been lost") + except AsyncStreamLimitError: + raise newHttpCriticalError("Maximum size of request headers reached", + Http413) proc new(ht: typedesc[HttpConnectionRef], server: HttpServerRef, transp: StreamTransport): HttpConnectionRef = + let mainReader = newAsyncStreamReader(transp) + let mainWriter = newAsyncStreamWriter(transp) + let tlsStream = + if HttpServerFlags.Secure in server.flags: + newTLSServerAsyncStream(mainReader, mainWriter, server.tlsPrivateKey, + server.tlsCertificate, + minVersion = TLSVersion.TLS12, + flags = server.secureFlags) + else: + nil + + let reader = + if isNil(tlsStream): + mainReader + else: + cast[AsyncStreamReader](tlsStream.reader) + + let writer = + if isNil(tlsStream): + mainWriter + else: + cast[AsyncStreamWriter](tlsStream.writer) + HttpConnectionRef( transp: transp, server: server, buffer: newSeq[byte](server.maxHeadersSize), - mainReader: newAsyncStreamReader(transp), - mainWriter: newAsyncStreamWriter(transp) + mainReader: mainReader, + mainWriter: mainWriter, + tlsStream: tlsStream, + reader: reader, + writer: writer ) -proc close(conn: HttpConnectionRef): Future[void] = - allFutures(conn.mainReader.closeWait(), conn.mainWriter.closeWait(), - conn.transp.closeWait()) +proc close(conn: HttpConnectionRef) {.async.} = + if HttpServerFlags.Secure in conn.server.flags: + # First we will close TLS streams. + await allFutures(conn.reader.closeWait(), conn.writer.closeWait()) + + # After we going to close everything else. + await allFutures(conn.mainReader.closeWait(), conn.mainWriter.closeWait(), + conn.transp.closeWait()) proc close(req: HttpRequestRef) {.async.} = if req.response.isSome(): @@ -434,10 +476,57 @@ proc close(req: HttpRequestRef) {.async.} = not(isNil(resp.chunkedWriter)): await resp.chunkedWriter.closeWait() -proc processLoop(server: HttpServerRef, transp: StreamTransport) {.async.} = +proc createConnection(server: HttpServerRef, + transp: StreamTransport): Future[HttpConnectionRef] {. + async.} = var conn = HttpConnectionRef.new(server, transp) + if HttpServerFlags.Secure notin server.flags: + # Non secure connection + return conn + + try: + await handshake(conn.tlsStream) + return conn + except CancelledError as exc: + await conn.close() + raise exc + except TLSStreamError: + await conn.close() + raise newHttpCriticalError("Unable to establish secure connection") + +proc processLoop(server: HttpServerRef, transp: StreamTransport) {.async.} = + var + conn: HttpConnectionRef + connArg: RequestFence[HttpRequestRef] + runLoop = false + + try: + conn = await createConnection(server, transp) + runLoop = true + except CancelledError: + # We could be cancelled only when we perform TLS handshake, connection + server.connections.del(transp.getId()) + return + except HttpCriticalError as exc: + let error = HttpProcessError.init(HTTPServerError.CriticalError, exc, + transp.remoteAddress(), exc.code) + connArg = RequestFence[HttpRequestRef].err(error) + runLoop = false + + if not(runLoop): + try: + # We still want to notify process callback about failure, but we ignore + # result and swallow all the exceptions. + discard await server.processCallback(connArg) + except CancelledError: + server.connections.del(transp.getId()) + return + except CatchableError as exc: + # There should be no exceptions, so we will raise `Defect`. + raise newHttpDefect("Unexpected exception catched [" & $exc.name & "]") + var breakLoop = false - while true: + while runLoop: var arg: RequestFence[HttpRequestRef] resp: HttpResponseRef @@ -449,19 +538,19 @@ proc processLoop(server: HttpServerRef, transp: StreamTransport) {.async.} = breakLoop = true except AsyncTimeoutError as exc: let error = HttpProcessError.init(HTTPServerError.TimeoutError, exc, - transp.remoteAddress()) + transp.remoteAddress(), Http408) arg = RequestFence[HttpRequestRef].err(error) except HttpRecoverableError as exc: let error = HttpProcessError.init(HTTPServerError.RecoverableError, exc, - transp.remoteAddress()) + transp.remoteAddress(), exc.code) arg = RequestFence[HttpRequestRef].err(error) except HttpCriticalError as exc: let error = HttpProcessError.init(HTTPServerError.CriticalError, exc, - transp.remoteAddress()) + transp.remoteAddress(), exc.code) arg = RequestFence[HttpRequestRef].err(error) except CatchableError as exc: let error = HttpProcessError.init(HTTPServerError.CatchableError, exc, - transp.remoteAddress()) + transp.remoteAddress(), Http500) arg = RequestFence[HttpRequestRef].err(error) if breakLoop: @@ -481,15 +570,16 @@ proc processLoop(server: HttpServerRef, transp: StreamTransport) {.async.} = break if arg.isErr(): + let code = arg.error().code case arg.error().error of HTTPServerError.TimeoutError: - discard await conn.sendErrorResponse(HttpVersion11, Http408, false) + discard await conn.sendErrorResponse(HttpVersion11, code, false) of HTTPServerError.RecoverableError: - discard await conn.sendErrorResponse(HttpVersion11, Http400, false) + discard await conn.sendErrorResponse(HttpVersion11, code, false) of HTTPServerError.CriticalError: - discard await conn.sendErrorResponse(HttpVersion11, Http400, false) + discard await conn.sendErrorResponse(HttpVersion11, code, false) of HTTPServerError.CatchableError: - discard await conn.sendErrorResponse(HttpVersion11, Http400, false) + discard await conn.sendErrorResponse(HttpVersion11, code, false) break else: let request = arg.get() @@ -521,7 +611,10 @@ proc processLoop(server: HttpServerRef, transp: StreamTransport) {.async.} = if not(keepConn): break - await conn.close() + # Connection could be `nil` only when secure handshake is failed. + if not(isNil(conn)): + await conn.close() + server.connections.del(transp.getId()) # if server.maxConnections > 0: # server.semaphore.release() @@ -784,9 +877,9 @@ proc sendBody*(resp: HttpResponseRef, pbytes: pointer, nbytes: int) {.async.} = resp.state = HttpResponseState.Prepared try: resp.state = HttpResponseState.Sending - await resp.connection.mainWriter.write(responseHeaders) + await resp.connection.writer.write(responseHeaders) if nbytes > 0: - await resp.connection.mainWriter.write(pbytes, nbytes) + await resp.connection.writer.write(pbytes, nbytes) resp.state = HttpResponseState.Finished except CancelledError as exc: resp.state = HttpResponseState.Cancelled @@ -802,9 +895,9 @@ proc sendBody*[T: string|seq[byte]](resp: HttpResponseRef, data: T) {.async.} = resp.state = HttpResponseState.Prepared try: resp.state = HttpResponseState.Sending - await resp.connection.mainWriter.write(responseHeaders) + await resp.connection.writer.write(responseHeaders) if len(data) > 0: - await resp.connection.mainWriter.write(data) + await resp.connection.writer.write(data) resp.state = HttpResponseState.Finished except CancelledError as exc: resp.state = HttpResponseState.Cancelled @@ -821,9 +914,9 @@ proc sendError*(resp: HttpResponseRef, code: HttpCode, body = "") {.async.} = resp.state = HttpResponseState.Prepared try: resp.state = HttpResponseState.Sending - await resp.connection.mainWriter.write(responseHeaders) + await resp.connection.writer.write(responseHeaders) if len(body) > 0: - await resp.connection.mainWriter.write(body) + await resp.connection.writer.write(body) resp.state = HttpResponseState.Finished except CancelledError as exc: resp.state = HttpResponseState.Cancelled @@ -841,8 +934,8 @@ proc prepare*(resp: HttpResponseRef) {.async.} = resp.state = HttpResponseState.Prepared try: resp.state = HttpResponseState.Sending - await resp.connection.mainWriter.write(responseHeaders) - resp.chunkedWriter = newChunkedStreamWriter(resp.connection.mainWriter) + await resp.connection.writer.write(responseHeaders) + resp.chunkedWriter = newChunkedStreamWriter(resp.connection.writer) resp.flags.incl(HttpResponseFlags.Chunked) except CancelledError as exc: resp.state = HttpResponseState.Cancelled diff --git a/chronos/streams/tlsstream.nim b/chronos/streams/tlsstream.nim index 1f1c83d7..c5c23818 100644 --- a/chronos/streams/tlsstream.nim +++ b/chronos/streams/tlsstream.nim @@ -31,6 +31,9 @@ type TLSKeyType {.pure.} = enum RSA, EC + TLSResult {.pure.} = enum + Success, Error, EOF + TLSPrivateKey* = ref object case kind: TLSKeyType of RSA: @@ -127,24 +130,23 @@ template newTLSStreamProtocolError[T](message: T): ref TLSStreamProtocolError = err proc tlsWriteRec(engine: ptr SslEngineContext, - writer: TLSStreamWriter): Future[bool] {.async.} = + writer: TLSStreamWriter): Future[TLSResult] {.async.} = try: var length = 0'u var buf = sslEngineSendrecBuf(engine, length) doAssert(length != 0 and not isNil(buf)) await writer.wsource.write(buf, int(length)) sslEngineSendrecAck(engine, length) - return true + return TLSResult.Success except AsyncStreamError as exc: writer.state = AsyncStreamState.Error writer.error = exc except CancelledError: writer.state = AsyncStreamState.Stopped - - return false + return TLSResult.Error proc tlsWriteApp(engine: ptr SslEngineContext, - writer: TLSStreamWriter): Future[bool] {.async.} = + writer: TLSStreamWriter): Future[TLSResult] {.async.} = try: var item = await writer.queue.get() if item.size > 0: @@ -157,7 +159,7 @@ proc tlsWriteApp(engine: ptr SslEngineContext, sslEngineSendappAck(engine, uint(item.size)) sslEngineFlush(engine, 0) item.future.complete() - return true + return TLSResult.Success else: # BearSSL is not ready to accept whole item, so we will send # only part of item and adjust offset. @@ -165,58 +167,68 @@ proc tlsWriteApp(engine: ptr SslEngineContext, item.size = item.size - int(length) writer.queue.addFirstNoWait(item) sslEngineSendappAck(engine, length) - return true + return TLSResult.Success else: sslEngineClose(engine) item.future.complete() - return true + return TLSResult.Success except CancelledError: writer.state = AsyncStreamState.Stopped - - return false + return TLSResult.Error proc tlsReadRec(engine: ptr SslEngineContext, - reader: TLSStreamReader): Future[bool] {.async.} = + reader: TLSStreamReader): Future[TLSResult] {.async.} = try: var length = 0'u var buf = sslEngineRecvrecBuf(engine, length) let res = await reader.rsource.readOnce(buf, int(length)) sslEngineRecvrecAck(engine, uint(res)) - return true + if res == 0: + sslEngineClose(engine) + + return TLSResult.EOF + else: + return TLSResult.Success except CancelledError: reader.state = AsyncStreamState.Stopped except AsyncStreamError as exc: reader.state = AsyncStreamState.Error reader.error = exc - return false + return TLSResult.Error proc tlsReadApp(engine: ptr SslEngineContext, - reader: TLSStreamReader): Future[bool] {.async.} = + reader: TLSStreamReader): Future[TLSResult] {.async.} = try: var length = 0'u var buf = sslEngineRecvappBuf(engine, length) await upload(addr reader.buffer, buf, int(length)) sslEngineRecvappAck(engine, length) - return true + return TLSResult.Success except CancelledError: reader.state = AsyncStreamState.Stopped - return false + return TLSResult.Error template raiseTLSStreamProtoError*[T](message: T) = raise newTLSStreamProtocolError(message) template readAndReset(fut: untyped) = if fut.finished(): - if fut.read(): + let res = fut.read() + case res + of TLSREsult.Success: fut = nil continue - else: + of TLSResult.Error: fut = nil loopState = AsyncStreamState.Error break + of TLSResult.EOF: + fut = nil + loopState = AsyncStreamState.Finished + break -proc cancelAndWait*(a, b, c, d: Future[bool]): Future[void] = - var waiting: seq[Future[bool]] +proc cancelAndWait*(a, b, c, d: Future[TLSResult]): Future[void] = + var waiting: seq[Future[TLSResult]] if not(isNil(a)) and not(a.finished()): a.cancel() waiting.add(a) @@ -231,10 +243,29 @@ proc cancelAndWait*(a, b, c, d: Future[bool]): Future[void] = waiting.add(d) allFutures(waiting) +proc dumpState*(state: cuint): string = + var res = "" + if (state and SSL_CLOSED) == SSL_CLOSED: + if len(res) > 0: res.add(", ") + res.add("SSL_CLOSED") + if (state and SSL_SENDREC) == SSL_SENDREC: + if len(res) > 0: res.add(", ") + res.add("SSL_SENDREC") + if (state and SSL_SENDAPP) == SSL_SENDAPP: + if len(res) > 0: res.add(", ") + res.add("SSL_SENDAPP") + if (state and SSL_RECVREC) == SSL_RECVREC: + if len(res) > 0: res.add(", ") + res.add("SSL_RECVREC") + if (state and SSL_RECVAPP) == SSL_RECVAPP: + if len(res) > 0: res.add(", ") + res.add("SSL_RECVAPP") + "{" & res & "}" + proc tlsLoop*(stream: TLSAsyncStream) {.async.} = var - sendRecFut, sendAppFut: Future[bool] - recvRecFut, recvAppFut: Future[bool] + sendRecFut, sendAppFut: Future[TLSResult] + recvRecFut, recvAppFut: Future[TLSResult] let engine = case stream.reader.kind @@ -246,7 +277,7 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} = var loopState = AsyncStreamState.Running while true: - var waiting: seq[Future[bool]] + var waiting: seq[Future[TLSResult]] var state = sslEngineCurrentState(engine) if (state and SSL_CLOSED) == SSL_CLOSED: @@ -343,6 +374,14 @@ proc tlsLoop*(stream: TLSAsyncStream) {.async.} = if not(isNil(stream.writer.handshakeFut)): if not(stream.writer.handshakeFut.finished()): stream.writer.handshakeFut.fail(error) + else: + if not(stream.writer.handshaked): + if not(isNil(stream.writer.handshakeFut)): + if not(stream.writer.handshakeFut.finished()): + stream.writer.handshakeFut.fail( + newTLSStreamProtocolError("Connection with remote peer lost") + ) + # Completing readers stream.reader.buffer.forget() diff --git a/tests/testhttpserver.nim b/tests/testhttpserver.nim index 1ee2d623..9fc324de 100644 --- a/tests/testhttpserver.nim +++ b/tests/testhttpserver.nim @@ -8,6 +8,68 @@ import std/[strutils, unittest, algorithm, strutils] import ../chronos, ../chronos/apps + +# To create self-signed certificate and key you can use openssl +# openssl req -new -x509 -sha256 -newkey rsa:2048 -nodes \ +# -keyout example-com.key.pem -days 3650 -out example-com.cert.pem +const HttpsSelfSignedRsaKey = """ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCn7tXGLKMIMzOG +tVzUixax1/ftlSLcpEAkZMORuiCCnYjtIJhGZdzRFZC8fBlfAJZpLIAOfX2L2f1J +ZuwpwDkOIvNqKMBrl5Mvkl5azPT0rtnjuwrcqN5NFtbmZPKFYvbjex2aXGqjl5MW +nQIs/ZA++DVEXmaN9oDxcZsvRMDKfrGQf9iLeoVL47Gx9KpqNqD/JLIn4LpieumV +yYidm6ukTOqHRvrWm36y6VvKW4TE97THacULmkeahtTf8zDJbbh4EO+gifgwgJ2W +BUS0+5hMcWu8111mXmanlOVlcoW8fH8RmPjL1eK1Z3j3SVHEf7oWZtIVW5gGA0jQ +nfA4K51RAgMBAAECggEANZ7/R13tWKrwouy6DWuz/WlWUtgx333atUQvZhKmWs5u +cDjeJmxUC7b1FhoSB9GqNT7uTLIpKkSaqZthgRtNnIPwcU890Zz+dEwqMJgNByvl +it+oYjjRco/+YmaNQaYN6yjelPE5Y678WlYb4b29Fz4t0/zIhj/VgEKkKH2tiXpS +TIicoM7pSOscEUfaW3yp5bS5QwNU6/AaF1wws0feBACd19ZkcdPvr52jopbhxlXw +h3XTV/vXIJd5zWGp0h/Jbd4xcD4MVo2GjfkeORKY6SjDaNzt8OGtePcKnnbUVu8b +2XlDxukhDQXqJ3g0sHz47mhvo4JeIM+FgymRm+3QmQKBgQDTawrEA3Zy9WvucaC7 +Zah02oE9nuvpF12lZ7WJh7+tZ/1ss+Fm7YspEKaUiEk7nn1CAVFtem4X4YCXTBiC +Oqq/o+ipv1yTur0ae6m4pwLm5wcMWBh3H5zjfQTfrClNN8yjWv8u3/sq8KesHPnT +R92/sMAptAChPgTzQphWbxFiYwKBgQDLWFaBqXfZYVnTyUvKX8GorS6jGWc6Eh4l +lAFA+2EBWDICrUxsDPoZjEXrWCixdqLhyehaI3KEFIx2bcPv6X2c7yx3IG5lA/Gx +TZiKlY74c6jOTstkdLW9RJbg1VUHUVZMf/Owt802YmEfUI5S5v7jFmKW6VG+io+K ++5KYeHD1uwKBgQDMf53KPA82422jFwYCPjLT1QduM2q97HwIomhWv5gIg63+l4BP +rzYMYq6+vZUYthUy41OAMgyLzPQ1ZMXQMi83b7R9fTxvKRIBq9xfYCzObGnE5vHD +SDDZWvR75muM5Yxr9nkfPkgVIPMO6Hg+hiVYZf96V0LEtNjU9HWmJYkLQQKBgQCQ +ULGUdGHKtXy7AjH3/t3CiKaAupa4cANVSCVbqQy/l4hmvfdu+AbH+vXkgTzgNgKD +nHh7AI1Vj//gTSayLlQn/Nbh9PJkXtg5rYiFUn+VdQBo6yMOuIYDPZqXFtCx0Nge +kvCwisHpxwiG4PUhgS+Em259DDonsM8PJFx2OYRx4QKBgEQpGhg71Oi9MhPJshN7 +dYTowaMS5eLTk2264ARaY+hAIV7fgvUa+5bgTVaWL+Cfs33hi4sMRqlEwsmfds2T +cnQiJ4cU20Euldfwa5FLnk6LaWdOyzYt/ICBJnKFRwfCUbS4Bu5rtMEM+3t0wxnJ +IgaD04WhoL9EX0Qo3DC1+0kG +-----END PRIVATE KEY----- +""" + +# This SSL certificate will expire 13 October 2030. +const HttpsSelfSignedRsaCert = """ +-----BEGIN CERTIFICATE----- +MIIDnzCCAoegAwIBAgIUUdcusjDd3XQi3FPM8urdFG3qI+8wDQYJKoZIhvcNAQEL +BQAwXzELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDEYMBYGA1UEAwwPMTI3LjAuMC4xOjQz +ODA4MB4XDTIwMTAxMjIxNDUwMVoXDTMwMTAxMDIxNDUwMVowXzELMAkGA1UEBhMC +QVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdp +dHMgUHR5IEx0ZDEYMBYGA1UEAwwPMTI3LjAuMC4xOjQzODA4MIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp+7VxiyjCDMzhrVc1IsWsdf37ZUi3KRAJGTD +kboggp2I7SCYRmXc0RWQvHwZXwCWaSyADn19i9n9SWbsKcA5DiLzaijAa5eTL5Je +Wsz09K7Z47sK3KjeTRbW5mTyhWL243sdmlxqo5eTFp0CLP2QPvg1RF5mjfaA8XGb +L0TAyn6xkH/Yi3qFS+OxsfSqajag/ySyJ+C6YnrplcmInZurpEzqh0b61pt+sulb +yluExPe0x2nFC5pHmobU3/MwyW24eBDvoIn4MICdlgVEtPuYTHFrvNddZl5mp5Tl +ZXKFvHx/EZj4y9XitWd490lRxH+6FmbSFVuYBgNI0J3wOCudUQIDAQABo1MwUTAd +BgNVHQ4EFgQUBKha84woY5WkFxKw7qx1cONg1H8wHwYDVR0jBBgwFoAUBKha84wo +Y5WkFxKw7qx1cONg1H8wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AQEAHZMYt9Ry+Xj3vTbzpGFQzYQVTJlfJWSN6eWNOivRFQE5io9kOBEe5noa8aLo +dLkw6ztxRP2QRJmlhGCO9/HwS17ckrkgZp3EC2LFnzxcBmoZu+owfxOT1KqpO52O +IKOl8eVohi1pEicE4dtTJVcpI7VCMovnXUhzx1Ci4Vibns4a6H+BQa19a1JSpifN +tO8U5jkjJ8Jprs/VPFhJj2O3di53oDHaYSE5eOrm2ZO14KFHSk9cGcOGmcYkUv8B +nV5vnGadH5Lvfxb/BCpuONabeRdOxMt9u9yQ89vNpxFtRdZDCpGKZBCfmUP+5m3m +N8r5CwGcIX/XPC3lKazzbZ8baA== +-----END CERTIFICATE----- +""" + + suite "HTTP server testing suite": proc httpClient(address: TransportAddress, data: string): Future[string] {.async.} = @@ -27,6 +89,37 @@ suite "HTTP server testing suite": if not(isNil(transp)): await closeWait(transp) + proc httpsClient(address: TransportAddress, + data: string, flags = {NoVerifyHost, NoVerifyServerName} + ): Future[string] {.async.} = + var + transp: StreamTransport + tlsstream: TlsAsyncStream + reader: AsyncStreamReader + writer: AsyncStreamWriter + + try: + transp = await connect(address) + reader = newAsyncStreamReader(transp) + writer = newAsyncStreamWriter(transp) + tlsstream = newTLSClientAsyncStream(reader, writer, "", flags = flags) + if len(data) > 0: + await tlsstream.writer.write(data) + var rres = await tlsstream.reader.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(tlsstream)): + await allFutures(tlsstream.reader.closeWait(), + tlsstream.writer.closeWait()) + if not(isNil(reader)): + await allFutures(reader.closeWait(), writer.closeWait(), + transp.closeWait()) + test "Request headers timeout test": proc testTimeout(address: TransportAddress): Future[bool] {.async.} = var serverRes = false @@ -359,6 +452,82 @@ suite "HTTP server testing suite": check waitFor(testPostMultipart2(initTAddress("127.0.0.1:30080"))) == true + test "HTTPS server (successful handshake) test": + proc testHTTPS(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + serverRes = true + return await request.respond(Http200, "TEST_OK:" & $request.meth, + HttpTable.init()) + else: + serverRes = false + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let serverFlags = {Secure} + let secureKey = TLSPrivateKey.init(HttpsSelfSignedRsaKey) + let secureCert = TLSCertificate.init(HttpsSelfSignedRsaCert) + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags, + serverFlags = serverFlags, + tlsPrivateKey = secureKey, + tlsCertificate = secureCert) + if res.isErr(): + return false + + let server = res.get() + server.start() + let message = "GET / HTTP/1.0\r\nHost: https://127.0.0.1:80\r\n\r\n" + let data = await httpsClient(address, message) + + await server.stop() + await server.close() + return serverRes and (data.find("TEST_OK:GET") >= 0) + + check waitFor(testHTTPS(initTAddress("127.0.0.1:30080"))) == true + + test "HTTPS server (failed handshake) test": + proc testHTTPS2(address: TransportAddress): Future[bool] {.async.} = + var serverRes = false + var testFut = newFuture[void]() + proc process(r: RequestFence[HttpRequestRef]): Future[HttpResponseRef] {. + async.} = + if r.isOk(): + let request = r.get() + serverRes = false + return await request.respond(Http200, "TEST_OK:" & $request.meth, + HttpTable.init()) + else: + serverRes = true + testFut.complete() + return dumbResponse() + + let socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr} + let serverFlags = {Secure} + let secureKey = TLSPrivateKey.init(HttpsSelfSignedRsaKey) + let secureCert = TLSCertificate.init(HttpsSelfSignedRsaCert) + let res = HttpServerRef.new(address, process, + socketFlags = socketFlags, + serverFlags = serverFlags, + tlsPrivateKey = secureKey, + tlsCertificate = secureCert) + if res.isErr(): + return false + + let server = res.get() + server.start() + let message = "GET / HTTP/1.0\r\nHost: https://127.0.0.1:80\r\n\r\n" + let data = await httpsClient(address, message, {NoVerifyServerName}) + await testFut + await server.stop() + await server.close() + return serverRes and data == "EXCEPTION" + + check waitFor(testHTTPS2(initTAddress("127.0.0.1:30080"))) == true + test "Leaks test": check: getTracker("async.stream.reader").isLeaked() == false