HttpServer now supports TLS.

Some TLSStream fixes to properly support EOF.
Some HttpServer to properly support TLS handshake problems.
HttpServer test suite for HTTPS.
This commit is contained in:
cheatfate 2021-02-03 12:47:03 +02:00 committed by zah
parent 1a3e9162a4
commit d43a9cb92d
4 changed files with 387 additions and 81 deletions

View File

@ -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.

View File

@ -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

View File

@ -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()

View File

@ -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