2019-10-08 15:46:27 +00:00
|
|
|
#
|
|
|
|
# Chronos Asynchronous TLS Stream
|
|
|
|
# (c) Copyright 2019-Present
|
|
|
|
# Status Research & Development GmbH
|
|
|
|
#
|
|
|
|
# Licensed under either of
|
|
|
|
# Apache License, version 2.0, (LICENSE-APACHEv2)
|
|
|
|
# MIT license (LICENSE-MIT)
|
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
## This module implements Transport Layer Security (TLS) stream. This module
|
|
|
|
## uses sources of BearSSL <https://www.bearssl.org> by Thomas Pornin.
|
2019-10-08 15:46:27 +00:00
|
|
|
import bearssl, bearssl/cacert
|
|
|
|
import ../asyncloop, ../timer, ../asyncsync
|
|
|
|
import asyncstream, ../transports/stream, ../transports/common
|
|
|
|
|
|
|
|
type
|
|
|
|
TLSStreamKind {.pure.} = enum
|
|
|
|
Client, Server
|
|
|
|
|
|
|
|
TLSVersion* {.pure.} = enum
|
|
|
|
TLS10 = 0x0301, TLS11 = 0x0302, TLS12 = 0x0303
|
|
|
|
|
|
|
|
TLSFlags* {.pure.} = enum
|
2019-10-16 06:01:52 +00:00
|
|
|
NoVerifyHost, # Client: Skip remote certificate check
|
|
|
|
NoVerifyServerName, # Client: Skip Server Name Indication (SNI) check
|
|
|
|
EnforceServerPref, # Server: Enforce server preferences
|
|
|
|
NoRenegotiation, # Server: Reject renegotiations requests
|
|
|
|
TolerateNoClientAuth, # Server: Disable strict client authentication
|
|
|
|
FailOnAlpnMismatch # Server: Fail on application protocol mismatch
|
|
|
|
|
|
|
|
TLSKeyType {.pure.} = enum
|
|
|
|
RSA, EC
|
|
|
|
|
2021-02-03 10:47:03 +00:00
|
|
|
TLSResult {.pure.} = enum
|
|
|
|
Success, Error, EOF
|
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
TLSPrivateKey* = ref object
|
|
|
|
case kind: TLSKeyType
|
|
|
|
of RSA:
|
|
|
|
rsakey: RsaPrivateKey
|
|
|
|
of EC:
|
|
|
|
eckey: EcPrivateKey
|
|
|
|
storage: seq[byte]
|
|
|
|
|
|
|
|
TLSCertificate* = ref object
|
|
|
|
certs: seq[X509Certificate]
|
|
|
|
storage: seq[byte]
|
|
|
|
|
|
|
|
TLSSessionCache* = ref object
|
|
|
|
storage: seq[byte]
|
|
|
|
context: SslSessionCacheLru
|
|
|
|
|
|
|
|
PEMElement* = object
|
|
|
|
name*: string
|
|
|
|
data*: seq[byte]
|
|
|
|
|
|
|
|
PEMContext = ref object
|
|
|
|
data: seq[byte]
|
|
|
|
|
|
|
|
TLSStreamWriter* = ref object of AsyncStreamWriter
|
|
|
|
case kind: TLSStreamKind
|
|
|
|
of TLSStreamKind.Client:
|
2019-10-09 06:12:54 +00:00
|
|
|
ccontext: ptr SslClientContext
|
2019-10-16 06:01:52 +00:00
|
|
|
of TLSStreamKind.Server:
|
2019-10-09 06:12:54 +00:00
|
|
|
scontext: ptr SslServerContext
|
2019-10-16 06:01:52 +00:00
|
|
|
stream*: TLSAsyncStream
|
|
|
|
handshaked*: bool
|
|
|
|
handshakeFut*: Future[void]
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
TLSStreamReader* = ref object of AsyncStreamReader
|
|
|
|
case kind: TLSStreamKind
|
|
|
|
of TLSStreamKind.Client:
|
2019-10-08 15:46:27 +00:00
|
|
|
ccontext: ptr SslClientContext
|
2019-10-16 06:01:52 +00:00
|
|
|
of TLSStreamKind.Server:
|
2019-10-08 15:46:27 +00:00
|
|
|
scontext: ptr SslServerContext
|
2019-10-16 06:01:52 +00:00
|
|
|
stream*: TLSAsyncStream
|
|
|
|
handshaked*: bool
|
|
|
|
handshakeFut*: Future[void]
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
TLSAsyncStream* = ref object of RootRef
|
2019-10-08 15:46:27 +00:00
|
|
|
xwc*: X509NoAnchorContext
|
2019-10-16 06:01:52 +00:00
|
|
|
ccontext*: SslClientContext
|
|
|
|
scontext*: SslServerContext
|
2019-10-08 15:46:27 +00:00
|
|
|
sbuffer*: seq[byte]
|
|
|
|
x509*: X509MinimalContext
|
2019-10-16 06:01:52 +00:00
|
|
|
reader*: TLSStreamReader
|
|
|
|
writer*: TLSStreamWriter
|
2021-01-22 08:36:37 +00:00
|
|
|
mainLoop*: Future[void]
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
SomeTLSStreamType* = TLSStreamReader|TLSStreamWriter|TLSAsyncStream
|
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
TLSStreamError* = object of AsyncStreamError
|
2021-01-22 08:36:37 +00:00
|
|
|
TLSStreamHandshakeError* = object of TLSStreamError
|
|
|
|
TLSStreamReadError* = object of TLSStreamError
|
|
|
|
par*: ref AsyncStreamError
|
|
|
|
TLSStreamWriteError* = object of TLSStreamError
|
|
|
|
par*: ref AsyncStreamError
|
2019-10-16 06:01:52 +00:00
|
|
|
TLSStreamProtocolError* = object of TLSStreamError
|
2019-10-08 15:46:27 +00:00
|
|
|
errCode*: int
|
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
proc newTLSStreamReadError(p: ref AsyncStreamError): ref TLSStreamReadError {.
|
|
|
|
inline.} =
|
|
|
|
var w = newException(TLSStreamReadError, "Read stream failed")
|
|
|
|
w.msg = w.msg & ", originated from [" & $p.name & "] " & p.msg
|
|
|
|
w.par = p
|
|
|
|
w
|
|
|
|
|
|
|
|
proc newTLSStreamWriteError(p: ref AsyncStreamError): ref TLSStreamWriteError {.
|
|
|
|
inline.} =
|
|
|
|
var w = newException(TLSStreamWriteError, "Write stream failed")
|
|
|
|
w.msg = w.msg & ", originated from [" & $p.name & "] " & p.msg
|
|
|
|
w.par = p
|
|
|
|
w
|
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
template newTLSStreamProtocolError[T](message: T): ref TLSStreamProtocolError =
|
2019-10-08 15:46:27 +00:00
|
|
|
var msg = ""
|
|
|
|
var code = 0
|
|
|
|
when T is string:
|
|
|
|
msg.add(message)
|
|
|
|
elif T is cint:
|
|
|
|
msg.add(sslErrorMsg(message) & " (code: " & $int(message) & ")")
|
|
|
|
code = int(message)
|
|
|
|
elif T is int:
|
|
|
|
msg.add(sslErrorMsg(message) & " (code: " & $message & ")")
|
|
|
|
code = message
|
|
|
|
else:
|
|
|
|
msg.add("Internal Error")
|
2019-10-16 06:01:52 +00:00
|
|
|
var err = newException(TLSStreamProtocolError, msg)
|
2019-10-08 15:46:27 +00:00
|
|
|
err.errCode = code
|
|
|
|
err
|
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
proc tlsWriteRec(engine: ptr SslEngineContext,
|
2021-02-03 10:47:03 +00:00
|
|
|
writer: TLSStreamWriter): Future[TLSResult] {.async.} =
|
2021-01-22 08:36:37 +00:00
|
|
|
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)
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Success
|
2021-01-22 08:36:37 +00:00
|
|
|
except AsyncStreamError as exc:
|
|
|
|
writer.state = AsyncStreamState.Error
|
|
|
|
writer.error = exc
|
|
|
|
except CancelledError:
|
|
|
|
writer.state = AsyncStreamState.Stopped
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Error
|
2021-01-22 08:36:37 +00:00
|
|
|
|
|
|
|
proc tlsWriteApp(engine: ptr SslEngineContext,
|
2021-02-03 10:47:03 +00:00
|
|
|
writer: TLSStreamWriter): Future[TLSResult] {.async.} =
|
2021-01-22 08:36:37 +00:00
|
|
|
try:
|
|
|
|
var item = await writer.queue.get()
|
|
|
|
if item.size > 0:
|
|
|
|
var length = 0'u
|
|
|
|
var buf = sslEngineSendappBuf(engine, length)
|
|
|
|
let toWrite = min(int(length), item.size)
|
|
|
|
copyOut(buf, item, toWrite)
|
|
|
|
if int(length) >= item.size:
|
|
|
|
# BearSSL is ready to accept whole item size.
|
|
|
|
sslEngineSendappAck(engine, uint(item.size))
|
|
|
|
sslEngineFlush(engine, 0)
|
|
|
|
item.future.complete()
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Success
|
2021-01-22 08:36:37 +00:00
|
|
|
else:
|
|
|
|
# BearSSL is not ready to accept whole item, so we will send
|
|
|
|
# only part of item and adjust offset.
|
|
|
|
item.offset = item.offset + int(length)
|
|
|
|
item.size = item.size - int(length)
|
|
|
|
writer.queue.addFirstNoWait(item)
|
|
|
|
sslEngineSendappAck(engine, length)
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Success
|
2021-01-22 08:36:37 +00:00
|
|
|
else:
|
|
|
|
sslEngineClose(engine)
|
|
|
|
item.future.complete()
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Success
|
2021-01-22 08:36:37 +00:00
|
|
|
except CancelledError:
|
|
|
|
writer.state = AsyncStreamState.Stopped
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Error
|
2021-01-22 08:36:37 +00:00
|
|
|
|
|
|
|
proc tlsReadRec(engine: ptr SslEngineContext,
|
2021-02-03 10:47:03 +00:00
|
|
|
reader: TLSStreamReader): Future[TLSResult] {.async.} =
|
2021-01-22 08:36:37 +00:00
|
|
|
try:
|
|
|
|
var length = 0'u
|
|
|
|
var buf = sslEngineRecvrecBuf(engine, length)
|
|
|
|
let res = await reader.rsource.readOnce(buf, int(length))
|
|
|
|
sslEngineRecvrecAck(engine, uint(res))
|
2021-02-03 10:47:03 +00:00
|
|
|
if res == 0:
|
|
|
|
sslEngineClose(engine)
|
|
|
|
|
|
|
|
return TLSResult.EOF
|
|
|
|
else:
|
|
|
|
return TLSResult.Success
|
2021-01-22 08:36:37 +00:00
|
|
|
except CancelledError:
|
|
|
|
reader.state = AsyncStreamState.Stopped
|
|
|
|
except AsyncStreamError as exc:
|
|
|
|
reader.state = AsyncStreamState.Error
|
|
|
|
reader.error = exc
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Error
|
2021-01-22 08:36:37 +00:00
|
|
|
|
|
|
|
proc tlsReadApp(engine: ptr SslEngineContext,
|
2021-02-03 10:47:03 +00:00
|
|
|
reader: TLSStreamReader): Future[TLSResult] {.async.} =
|
2021-01-22 08:36:37 +00:00
|
|
|
try:
|
|
|
|
var length = 0'u
|
|
|
|
var buf = sslEngineRecvappBuf(engine, length)
|
|
|
|
await upload(addr reader.buffer, buf, int(length))
|
|
|
|
sslEngineRecvappAck(engine, length)
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Success
|
2021-01-22 08:36:37 +00:00
|
|
|
except CancelledError:
|
|
|
|
reader.state = AsyncStreamState.Stopped
|
2021-02-03 10:47:03 +00:00
|
|
|
return TLSResult.Error
|
2021-01-21 03:42:44 +00:00
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
template raiseTLSStreamProtoError*[T](message: T) =
|
2019-10-16 06:01:52 +00:00
|
|
|
raise newTLSStreamProtocolError(message)
|
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
template readAndReset(fut: untyped) =
|
|
|
|
if fut.finished():
|
2021-02-03 10:47:03 +00:00
|
|
|
let res = fut.read()
|
|
|
|
case res
|
|
|
|
of TLSREsult.Success:
|
2021-01-22 08:36:37 +00:00
|
|
|
fut = nil
|
|
|
|
continue
|
2021-02-03 10:47:03 +00:00
|
|
|
of TLSResult.Error:
|
2021-01-22 08:36:37 +00:00
|
|
|
fut = nil
|
|
|
|
loopState = AsyncStreamState.Error
|
|
|
|
break
|
2021-02-03 10:47:03 +00:00
|
|
|
of TLSResult.EOF:
|
|
|
|
fut = nil
|
|
|
|
loopState = AsyncStreamState.Finished
|
|
|
|
break
|
2019-10-09 06:12:54 +00:00
|
|
|
|
2021-02-03 10:47:03 +00:00
|
|
|
proc cancelAndWait*(a, b, c, d: Future[TLSResult]): Future[void] =
|
|
|
|
var waiting: seq[Future[TLSResult]]
|
2021-01-22 08:36:37 +00:00
|
|
|
if not(isNil(a)) and not(a.finished()):
|
|
|
|
a.cancel()
|
|
|
|
waiting.add(a)
|
|
|
|
if not(isNil(b)) and not(b.finished()):
|
|
|
|
b.cancel()
|
|
|
|
waiting.add(b)
|
|
|
|
if not(isNil(c)) and not(c.finished()):
|
|
|
|
c.cancel()
|
|
|
|
waiting.add(c)
|
|
|
|
if not(isNil(d)) and not(d.finished()):
|
|
|
|
d.cancel()
|
|
|
|
waiting.add(d)
|
|
|
|
allFutures(waiting)
|
|
|
|
|
2021-02-03 10:47:03 +00:00
|
|
|
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 & "}"
|
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
proc tlsLoop*(stream: TLSAsyncStream) {.async.} =
|
|
|
|
var
|
2021-02-03 10:47:03 +00:00
|
|
|
sendRecFut, sendAppFut: Future[TLSResult]
|
|
|
|
recvRecFut, recvAppFut: Future[TLSResult]
|
2021-01-22 08:36:37 +00:00
|
|
|
|
|
|
|
let engine =
|
|
|
|
case stream.reader.kind
|
|
|
|
of TLSStreamKind.Server:
|
|
|
|
addr stream.scontext.eng
|
|
|
|
of TLSStreamKind.Client:
|
|
|
|
addr stream.ccontext.eng
|
2019-10-09 06:12:54 +00:00
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
var loopState = AsyncStreamState.Running
|
2019-10-09 06:12:54 +00:00
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
while true:
|
2021-02-03 10:47:03 +00:00
|
|
|
var waiting: seq[Future[TLSResult]]
|
2021-01-22 08:36:37 +00:00
|
|
|
var state = sslEngineCurrentState(engine)
|
2021-01-21 18:11:43 +00:00
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
if (state and SSL_CLOSED) == SSL_CLOSED:
|
|
|
|
loopState = AsyncStreamState.Finished
|
|
|
|
break
|
2021-01-21 18:11:43 +00:00
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
if isNil(sendRecFut):
|
|
|
|
if (state and SSL_SENDREC) == SSL_SENDREC:
|
|
|
|
sendRecFut = tlsWriteRec(engine, stream.writer)
|
|
|
|
else:
|
|
|
|
sendRecFut.readAndReset()
|
|
|
|
|
|
|
|
if isNil(sendAppFut):
|
|
|
|
if (state and SSL_SENDAPP) == SSL_SENDAPP:
|
|
|
|
# Application data can be sent over stream.
|
|
|
|
if not(stream.writer.handshaked):
|
|
|
|
stream.reader.handshaked = true
|
|
|
|
stream.writer.handshaked = true
|
|
|
|
if not(isNil(stream.writer.handshakeFut)):
|
|
|
|
stream.writer.handshakeFut.complete()
|
|
|
|
|
|
|
|
sendAppFut = tlsWriteApp(engine, stream.writer)
|
|
|
|
else:
|
|
|
|
sendAppFut.readAndReset()
|
|
|
|
|
|
|
|
if isNil(recvRecFut):
|
|
|
|
if (state and SSL_RECVREC) == SSL_RECVREC:
|
|
|
|
recvRecFut = tlsReadRec(engine, stream.reader)
|
|
|
|
else:
|
|
|
|
recvRecFut.readAndReset()
|
|
|
|
|
|
|
|
if isNil(recvAppFut):
|
|
|
|
if (state and SSL_RECVAPP) == SSL_RECVAPP:
|
|
|
|
recvAppFut = tlsReadApp(engine, stream.reader)
|
|
|
|
else:
|
|
|
|
recvAppFut.readAndReset()
|
|
|
|
|
|
|
|
if not(isNil(sendRecFut)):
|
|
|
|
waiting.add(sendRecFut)
|
|
|
|
if not(isNil(sendAppFut)):
|
|
|
|
waiting.add(sendAppFut)
|
|
|
|
if not(isNil(recvRecFut)):
|
|
|
|
waiting.add(recvRecFut)
|
|
|
|
if not(isNil(recvAppFut)):
|
|
|
|
waiting.add(recvAppFut)
|
|
|
|
|
|
|
|
if len(waiting) > 0:
|
|
|
|
try:
|
|
|
|
discard await one(waiting)
|
|
|
|
except CancelledError:
|
|
|
|
loopState = AsyncStreamState.Stopped
|
|
|
|
|
|
|
|
if loopState != AsyncStreamState.Running:
|
2021-01-20 13:40:15 +00:00
|
|
|
break
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
# Cancelling and waiting all the pending operations
|
|
|
|
await cancelAndWait(sendRecFut, sendAppFut, recvRecFut, recvAppFut)
|
|
|
|
# Calculating error
|
|
|
|
let error =
|
|
|
|
case loopState
|
|
|
|
of AsyncStreamState.Stopped:
|
|
|
|
newAsyncStreamUseClosedError()
|
|
|
|
of AsyncStreamState.Error:
|
|
|
|
if not(isNil(stream.writer.error)):
|
|
|
|
stream.writer.error
|
|
|
|
else:
|
|
|
|
newTLSStreamWriteError(stream.reader.error)
|
|
|
|
of AsyncStreamState.Finished:
|
|
|
|
let err = engine.sslEngineLastError()
|
|
|
|
if err != 0:
|
|
|
|
newTLSStreamProtocolError(err)
|
|
|
|
else:
|
|
|
|
nil
|
|
|
|
of AsyncStreamState.Running:
|
|
|
|
nil
|
|
|
|
else:
|
|
|
|
nil
|
|
|
|
|
|
|
|
# Syncing state for reader and writer
|
|
|
|
stream.writer.state = loopState
|
|
|
|
if loopState == AsyncStreamState.Error:
|
|
|
|
if isNil(stream.reader.error):
|
|
|
|
stream.reader.error = newTLSStreamReadError(error)
|
|
|
|
stream.reader.state = loopState
|
|
|
|
|
|
|
|
if not(isNil(error)):
|
|
|
|
# Completing all pending writes
|
|
|
|
while(not(stream.writer.queue.empty())):
|
|
|
|
let item = stream.writer.queue.popFirstNoWait()
|
|
|
|
if not(item.future.finished()):
|
|
|
|
item.future.fail(error)
|
|
|
|
# Completing handshake
|
|
|
|
if not(stream.writer.handshaked):
|
|
|
|
if not(isNil(stream.writer.handshakeFut)):
|
|
|
|
if not(stream.writer.handshakeFut.finished()):
|
|
|
|
stream.writer.handshakeFut.fail(error)
|
2021-02-03 10:47:03 +00:00
|
|
|
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")
|
|
|
|
)
|
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
# Completing readers
|
|
|
|
stream.reader.buffer.forget()
|
2019-10-09 06:12:54 +00:00
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
proc tlsWriteLoop(stream: AsyncStreamWriter) {.async.} =
|
|
|
|
var wstream = cast[TLSStreamWriter](stream)
|
|
|
|
wstream.state = AsyncStreamState.Running
|
|
|
|
await stepsAsync(1)
|
|
|
|
if isNil(wstream.stream.mainLoop):
|
|
|
|
wstream.stream.mainLoop = tlsLoop(wstream.stream)
|
|
|
|
await wstream.stream.mainLoop
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2021-01-22 08:36:37 +00:00
|
|
|
proc tlsReadLoop(stream: AsyncStreamReader) {.async.} =
|
|
|
|
var rstream = cast[TLSStreamReader](stream)
|
2019-10-09 06:12:54 +00:00
|
|
|
rstream.state = AsyncStreamState.Running
|
2021-01-22 08:36:37 +00:00
|
|
|
await stepsAsync(1)
|
|
|
|
if isNil(rstream.stream.mainLoop):
|
|
|
|
rstream.stream.mainLoop = tlsLoop(rstream.stream)
|
|
|
|
await rstream.stream.mainLoop
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
proc getSignerAlgo(xc: X509Certificate): int =
|
|
|
|
## Get certificate's signing algorithm.
|
|
|
|
var dc: X509DecoderContext
|
|
|
|
x509DecoderInit(addr dc, nil, nil)
|
|
|
|
x509DecoderPush(addr dc, xc.data, xc.dataLen)
|
|
|
|
let err = x509DecoderLastError(addr dc)
|
|
|
|
if err != 0:
|
2021-01-20 13:40:15 +00:00
|
|
|
-1
|
2019-10-16 06:01:52 +00:00
|
|
|
else:
|
2021-01-20 13:40:15 +00:00
|
|
|
int(x509DecoderGetSignerKeyType(addr dc))
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc newTLSClientAsyncStream*(rsource: AsyncStreamReader,
|
2019-10-08 15:46:27 +00:00
|
|
|
wsource: AsyncStreamWriter,
|
2019-10-16 06:07:46 +00:00
|
|
|
serverName: string,
|
2019-10-08 15:46:27 +00:00
|
|
|
bufferSize = SSL_BUFSIZE_BIDI,
|
2021-02-10 16:15:59 +00:00
|
|
|
minVersion = TLSVersion.TLS12,
|
2019-10-08 15:46:27 +00:00
|
|
|
maxVersion = TLSVersion.TLS12,
|
2019-10-16 06:01:52 +00:00
|
|
|
flags: set[TLSFlags] = {}): TLSAsyncStream =
|
|
|
|
## Create new TLS asynchronous stream for outbound (client) connections
|
|
|
|
## using reading stream ``rsource`` and writing stream ``wsource``.
|
2019-10-08 15:46:27 +00:00
|
|
|
##
|
|
|
|
## You can specify remote server name using ``serverName``, if while
|
|
|
|
## handshake server reports different name you will get an error. If
|
|
|
|
## ``serverName`` is empty string, remote server name checking will be
|
|
|
|
## disabled.
|
|
|
|
##
|
|
|
|
## ``bufferSize`` - is SSL/TLS buffer which is used for encoding/decoding
|
|
|
|
## incoming data.
|
|
|
|
##
|
|
|
|
## ``minVersion`` and ``maxVersion`` are TLS versions which will be used
|
|
|
|
## for handshake with remote server. If server's version will be lower then
|
|
|
|
## ``minVersion`` of bigger then ``maxVersion`` you will get an error.
|
|
|
|
##
|
|
|
|
## ``flags`` - custom TLS connection flags.
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = TLSAsyncStream()
|
|
|
|
var reader = TLSStreamReader(
|
|
|
|
kind: TLSStreamKind.Client,
|
|
|
|
stream: res,
|
|
|
|
ccontext: addr res.ccontext
|
|
|
|
)
|
|
|
|
var writer = TLSStreamWriter(
|
|
|
|
kind: TLSStreamKind.Client,
|
|
|
|
stream: res,
|
|
|
|
ccontext: addr res.ccontext
|
|
|
|
)
|
|
|
|
res.reader = reader
|
|
|
|
res.writer = writer
|
2019-10-08 15:46:27 +00:00
|
|
|
|
|
|
|
if TLSFlags.NoVerifyHost in flags:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslClientInitFull(addr res.ccontext, addr res.x509, nil, 0)
|
|
|
|
initNoAnchor(addr res.xwc, addr res.x509.vtable)
|
|
|
|
sslEngineSetX509(addr res.ccontext.eng, addr res.xwc.vtable)
|
2019-10-08 15:46:27 +00:00
|
|
|
else:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslClientInitFull(addr res.ccontext, addr res.x509,
|
2019-10-08 15:46:27 +00:00
|
|
|
unsafeAddr MozillaTrustAnchors[0],
|
|
|
|
len(MozillaTrustAnchors))
|
|
|
|
|
|
|
|
let size = max(SSL_BUFSIZE_BIDI, bufferSize)
|
2021-01-20 13:40:15 +00:00
|
|
|
res.sbuffer = newSeq[byte](size)
|
|
|
|
sslEngineSetBuffer(addr res.ccontext.eng, addr res.sbuffer[0],
|
|
|
|
uint(len(res.sbuffer)), 1)
|
|
|
|
sslEngineSetVersions(addr res.ccontext.eng, uint16(minVersion),
|
2019-10-08 15:46:27 +00:00
|
|
|
uint16(maxVersion))
|
|
|
|
|
2019-10-16 06:01:52 +00:00
|
|
|
if TLSFlags.NoVerifyServerName in flags:
|
2021-01-20 13:40:15 +00:00
|
|
|
let err = sslClientReset(addr res.ccontext, "", 0)
|
2019-10-08 15:46:27 +00:00
|
|
|
if err == 0:
|
2019-10-16 06:01:52 +00:00
|
|
|
raise newException(TLSStreamError, "Could not initialize TLS layer")
|
2019-10-08 15:46:27 +00:00
|
|
|
else:
|
2019-10-16 06:07:46 +00:00
|
|
|
if len(serverName) == 0:
|
|
|
|
raise newException(TLSStreamError, "serverName must not be empty string")
|
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
let err = sslClientReset(addr res.ccontext, serverName, 0)
|
2019-10-08 15:46:27 +00:00
|
|
|
if err == 0:
|
2019-10-16 06:01:52 +00:00
|
|
|
raise newException(TLSStreamError, "Could not initialize TLS layer")
|
2019-10-08 15:46:27 +00:00
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
init(cast[AsyncStreamWriter](res.writer), wsource, tlsWriteLoop,
|
2019-10-08 15:46:27 +00:00
|
|
|
bufferSize)
|
2021-01-20 13:40:15 +00:00
|
|
|
init(cast[AsyncStreamReader](res.reader), rsource, tlsReadLoop,
|
2019-10-08 15:46:27 +00:00
|
|
|
bufferSize)
|
2021-01-20 13:40:15 +00:00
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc newTLSServerAsyncStream*(rsource: AsyncStreamReader,
|
|
|
|
wsource: AsyncStreamWriter,
|
|
|
|
privateKey: TLSPrivateKey,
|
|
|
|
certificate: TLSCertificate,
|
|
|
|
bufferSize = SSL_BUFSIZE_BIDI,
|
|
|
|
minVersion = TLSVersion.TLS11,
|
|
|
|
maxVersion = TLSVersion.TLS12,
|
|
|
|
cache: TLSSessionCache = nil,
|
|
|
|
flags: set[TLSFlags] = {}): TLSAsyncStream =
|
|
|
|
## Create new TLS asynchronous stream for inbound (server) connections
|
|
|
|
## using reading stream ``rsource`` and writing stream ``wsource``.
|
|
|
|
##
|
|
|
|
## You need to specify local private key ``privateKey`` and certificate
|
|
|
|
## ``certificate``.
|
|
|
|
##
|
|
|
|
## ``bufferSize`` - is SSL/TLS buffer which is used for encoding/decoding
|
|
|
|
## incoming data.
|
|
|
|
##
|
|
|
|
## ``minVersion`` and ``maxVersion`` are TLS versions which will be used
|
|
|
|
## for handshake with remote server. If server's version will be lower then
|
|
|
|
## ``minVersion`` of bigger then ``maxVersion`` you will get an error.
|
|
|
|
##
|
|
|
|
## ``flags`` - custom TLS connection flags.
|
|
|
|
if isNil(privateKey) or privateKey.kind notin {TLSKeyType.RSA, TLSKeyType.EC}:
|
|
|
|
raiseTLSStreamProtoError("Incorrect private key")
|
|
|
|
if isNil(certificate) or len(certificate.certs) == 0:
|
|
|
|
raiseTLSStreamProtoError("Incorrect certificate")
|
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = TLSAsyncStream()
|
|
|
|
var reader = TLSStreamReader(
|
|
|
|
kind: TLSStreamKind.Server,
|
|
|
|
stream: res,
|
|
|
|
scontext: addr res.scontext
|
|
|
|
)
|
|
|
|
var writer = TLSStreamWriter(
|
|
|
|
kind: TLSStreamKind.Server,
|
|
|
|
stream: res,
|
|
|
|
scontext: addr res.scontext
|
|
|
|
)
|
|
|
|
res.reader = reader
|
|
|
|
res.writer = writer
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
if privateKey.kind == TLSKeyType.EC:
|
|
|
|
let algo = getSignerAlgo(certificate.certs[0])
|
|
|
|
if algo == -1:
|
|
|
|
raiseTLSStreamProtoError("Could not decode certificate")
|
2021-01-20 13:40:15 +00:00
|
|
|
sslServerInitFullEc(addr res.scontext, addr certificate.certs[0],
|
2019-10-16 06:01:52 +00:00
|
|
|
len(certificate.certs), cuint(algo),
|
|
|
|
addr privateKey.eckey)
|
|
|
|
elif privateKey.kind == TLSKeyType.RSA:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslServerInitFullRsa(addr res.scontext, addr certificate.certs[0],
|
2019-10-16 06:01:52 +00:00
|
|
|
len(certificate.certs), addr privateKey.rsakey)
|
|
|
|
|
|
|
|
let size = max(SSL_BUFSIZE_BIDI, bufferSize)
|
2021-01-20 13:40:15 +00:00
|
|
|
res.sbuffer = newSeq[byte](size)
|
|
|
|
sslEngineSetBuffer(addr res.scontext.eng, addr res.sbuffer[0],
|
|
|
|
uint(len(res.sbuffer)), 1)
|
|
|
|
sslEngineSetVersions(addr res.scontext.eng, uint16(minVersion),
|
2019-10-16 06:01:52 +00:00
|
|
|
uint16(maxVersion))
|
|
|
|
|
|
|
|
if not isNil(cache):
|
2021-01-20 13:40:15 +00:00
|
|
|
sslServerSetCache(addr res.scontext, addr cache.context.vtable)
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
if TLSFlags.EnforceServerPref in flags:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslEngineAddFlags(addr res.scontext.eng, OPT_ENFORCE_SERVER_PREFERENCES)
|
2019-10-16 06:01:52 +00:00
|
|
|
if TLSFlags.NoRenegotiation in flags:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslEngineAddFlags(addr res.scontext.eng, OPT_NO_RENEGOTIATION)
|
2019-10-16 06:01:52 +00:00
|
|
|
if TLSFlags.TolerateNoClientAuth in flags:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslEngineAddFlags(addr res.scontext.eng, OPT_TOLERATE_NO_CLIENT_AUTH)
|
2019-10-16 06:01:52 +00:00
|
|
|
if TLSFlags.FailOnAlpnMismatch in flags:
|
2021-01-20 13:40:15 +00:00
|
|
|
sslEngineAddFlags(addr res.scontext.eng, OPT_FAIL_ON_ALPN_MISMATCH)
|
2019-10-16 06:01:52 +00:00
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
let err = sslServerReset(addr res.scontext)
|
2019-10-16 06:01:52 +00:00
|
|
|
if err == 0:
|
|
|
|
raise newException(TLSStreamError, "Could not initialize TLS layer")
|
|
|
|
|
2021-01-20 13:40:15 +00:00
|
|
|
init(cast[AsyncStreamWriter](res.writer), wsource, tlsWriteLoop,
|
2019-10-16 06:01:52 +00:00
|
|
|
bufferSize)
|
2021-01-20 13:40:15 +00:00
|
|
|
init(cast[AsyncStreamReader](res.reader), rsource, tlsReadLoop,
|
2019-10-16 06:01:52 +00:00
|
|
|
bufferSize)
|
2021-01-20 13:40:15 +00:00
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc copyKey(src: RsaPrivateKey): TLSPrivateKey =
|
|
|
|
## Creates copy of RsaPrivateKey ``src``.
|
|
|
|
var offset = 0
|
|
|
|
let keySize = src.plen + src.qlen + src.dplen + src.dqlen + src.iqlen
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = TLSPrivateKey(kind: TLSKeyType.RSA, storage: newSeq[byte](keySize))
|
|
|
|
copyMem(addr res.storage[offset], src.p, src.plen)
|
|
|
|
res.rsakey.p = cast[ptr cuchar](addr res.storage[offset])
|
|
|
|
res.rsakey.plen = src.plen
|
2019-10-16 06:01:52 +00:00
|
|
|
offset = offset + src.plen
|
2021-01-20 13:40:15 +00:00
|
|
|
copyMem(addr res.storage[offset], src.q, src.qlen)
|
|
|
|
res.rsakey.q = cast[ptr cuchar](addr res.storage[offset])
|
|
|
|
res.rsakey.qlen = src.qlen
|
2019-10-16 06:01:52 +00:00
|
|
|
offset = offset + src.qlen
|
2021-01-20 13:40:15 +00:00
|
|
|
copyMem(addr res.storage[offset], src.dp, src.dplen)
|
|
|
|
res.rsakey.dp = cast[ptr cuchar](addr res.storage[offset])
|
|
|
|
res.rsakey.dplen = src.dplen
|
2019-10-16 06:01:52 +00:00
|
|
|
offset = offset + src.dplen
|
2021-01-20 13:40:15 +00:00
|
|
|
copyMem(addr res.storage[offset], src.dq, src.dqlen)
|
|
|
|
res.rsakey.dq = cast[ptr cuchar](addr res.storage[offset])
|
|
|
|
res.rsakey.dqlen = src.dqlen
|
2019-10-16 06:01:52 +00:00
|
|
|
offset = offset + src.dqlen
|
2021-01-20 13:40:15 +00:00
|
|
|
copyMem(addr res.storage[offset], src.iq, src.iqlen)
|
|
|
|
res.rsakey.iq = cast[ptr cuchar](addr res.storage[offset])
|
|
|
|
res.rsakey.iqlen = src.iqlen
|
|
|
|
res.rsakey.nBitlen = src.nBitlen
|
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc copyKey(src: EcPrivateKey): TLSPrivateKey =
|
|
|
|
## Creates copy of EcPrivateKey ``src``.
|
|
|
|
var offset = 0
|
|
|
|
let keySize = src.xlen
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = TLSPrivateKey(kind: TLSKeyType.EC, storage: newSeq[byte](keySize))
|
|
|
|
copyMem(addr res.storage[offset], src.x, src.xlen)
|
|
|
|
res.eckey.x = cast[ptr cuchar](addr res.storage[offset])
|
|
|
|
res.eckey.xlen = src.xlen
|
|
|
|
res.eckey.curve = src.curve
|
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc init*(tt: typedesc[TLSPrivateKey], data: openarray[byte]): TLSPrivateKey =
|
|
|
|
## Initialize TLS private key from array of bytes ``data``.
|
|
|
|
##
|
|
|
|
## This procedure initializes private key using raw, DER-encoded format,
|
|
|
|
## or wrapped in an unencrypted PKCS#8 archive (again DER-encoded).
|
|
|
|
var ctx: SkeyDecoderContext
|
|
|
|
if len(data) == 0:
|
|
|
|
raiseTLSStreamProtoError("Incorrect private key")
|
|
|
|
skeyDecoderInit(addr ctx)
|
|
|
|
skeyDecoderPush(addr ctx, cast[pointer](unsafeAddr data[0]), len(data))
|
|
|
|
let err = skeyDecoderLastError(addr ctx)
|
|
|
|
if err != 0:
|
|
|
|
raiseTLSStreamProtoError(err)
|
|
|
|
let keyType = skeyDecoderKeyType(addr ctx)
|
2021-01-20 13:40:15 +00:00
|
|
|
let res =
|
|
|
|
if keyType == KEYTYPE_RSA:
|
|
|
|
copyKey(ctx.key.rsa)
|
|
|
|
elif keyType == KEYTYPE_EC:
|
|
|
|
copyKey(ctx.key.ec)
|
|
|
|
else:
|
|
|
|
raiseTLSStreamProtoError("Unknown key type (" & $keyType & ")")
|
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc pemDecode*(data: openarray[char]): seq[PEMElement] =
|
|
|
|
## Decode PEM encoded string and get array of binary blobs.
|
|
|
|
if len(data) == 0:
|
|
|
|
raiseTLSStreamProtoError("Empty PEM message")
|
|
|
|
var ctx: PemDecoderContext
|
|
|
|
var pctx = new PEMContext
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = newSeq[PEMElement]()
|
2019-10-16 06:01:52 +00:00
|
|
|
pemDecoderInit(addr ctx)
|
|
|
|
|
|
|
|
proc itemAppend(ctx: pointer, pbytes: pointer, nbytes: int) {.cdecl.} =
|
|
|
|
var p = cast[PEMContext](ctx)
|
|
|
|
var o = len(p.data)
|
|
|
|
p.data.setLen(o + nbytes)
|
|
|
|
copyMem(addr p.data[o], pbytes, nbytes)
|
|
|
|
|
|
|
|
var length = len(data)
|
|
|
|
var offset = 0
|
|
|
|
var inobj = false
|
|
|
|
var elem: PEMElement
|
|
|
|
|
|
|
|
while length > 0:
|
|
|
|
var tlen = pemDecoderPush(addr ctx,
|
|
|
|
cast[pointer](unsafeAddr data[offset]), length)
|
|
|
|
offset = offset + tlen
|
|
|
|
length = length - tlen
|
|
|
|
|
|
|
|
let event = pemDecoderEvent(addr ctx)
|
|
|
|
if event == PEM_BEGIN_OBJ:
|
|
|
|
inobj = true
|
|
|
|
elem.name = $pemDecoderName(addr ctx)
|
|
|
|
pctx.data = newSeq[byte]()
|
|
|
|
pemDecoderSetdest(addr ctx, itemAppend, cast[pointer](pctx))
|
|
|
|
elif event == PEM_END_OBJ:
|
|
|
|
if inobj:
|
|
|
|
elem.data = pctx.data
|
2021-01-20 13:40:15 +00:00
|
|
|
res.add(elem)
|
2019-10-16 06:01:52 +00:00
|
|
|
inobj = false
|
|
|
|
else:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raiseTLSStreamProtoError("Invalid PEM encoding")
|
2021-01-20 13:40:15 +00:00
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc init*(tt: typedesc[TLSPrivateKey], data: openarray[char]): TLSPrivateKey =
|
|
|
|
## Initialize TLS private key from string ``data``.
|
|
|
|
##
|
|
|
|
## This procedure initializes private key using unencrypted PKCS#8 PEM
|
|
|
|
## encoded string.
|
|
|
|
##
|
|
|
|
## Note that PKCS#1 PEM encoded objects are not supported.
|
2021-01-20 13:40:15 +00:00
|
|
|
var res: TLSPrivateKey
|
2019-10-16 06:01:52 +00:00
|
|
|
var items = pemDecode(data)
|
|
|
|
for item in items:
|
|
|
|
if item.name == "PRIVATE KEY":
|
2021-01-20 13:40:15 +00:00
|
|
|
res = TLSPrivateKey.init(item.data)
|
2019-10-16 06:01:52 +00:00
|
|
|
break
|
2021-01-20 13:40:15 +00:00
|
|
|
if isNil(res):
|
2019-10-16 06:01:52 +00:00
|
|
|
raiseTLSStreamProtoError("Could not find private key")
|
2021-01-20 13:40:15 +00:00
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc init*(tt: typedesc[TLSCertificate],
|
|
|
|
data: openarray[char]): TLSCertificate =
|
|
|
|
## Initialize TLS certificates from string ``data``.
|
|
|
|
##
|
|
|
|
## This procedure initializes array of certificates from PEM encoded string.
|
|
|
|
var items = pemDecode(data)
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = TLSCertificate()
|
2019-10-16 06:01:52 +00:00
|
|
|
for item in items:
|
|
|
|
if item.name == "CERTIFICATE" and len(item.data) > 0:
|
2021-01-20 13:40:15 +00:00
|
|
|
let offset = len(res.storage)
|
|
|
|
res.storage.add(item.data)
|
2019-10-16 06:01:52 +00:00
|
|
|
let cert = X509Certificate(
|
2021-01-20 13:40:15 +00:00
|
|
|
data: cast[ptr cuchar](addr res.storage[offset]),
|
2019-10-16 06:01:52 +00:00
|
|
|
dataLen: len(item.data)
|
|
|
|
)
|
2021-01-20 13:40:15 +00:00
|
|
|
let ares = getSignerAlgo(cert)
|
|
|
|
if ares == -1:
|
2019-10-16 06:01:52 +00:00
|
|
|
raiseTLSStreamProtoError("Could not decode certificate")
|
2021-01-20 13:40:15 +00:00
|
|
|
elif ares != KEYTYPE_RSA and ares != KEYTYPE_EC:
|
2019-10-16 06:01:52 +00:00
|
|
|
raiseTLSStreamProtoError("Unsupported signing key type in certificate")
|
2021-01-20 13:40:15 +00:00
|
|
|
res.certs.add(cert)
|
|
|
|
if len(res.storage) == 0:
|
2019-10-16 06:01:52 +00:00
|
|
|
raiseTLSStreamProtoError("Could not find any certificates")
|
2021-01-20 13:40:15 +00:00
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc init*(tt: typedesc[TLSSessionCache], size: int = 4096): TLSSessionCache =
|
|
|
|
## Create new TLS session cache with size ``size``.
|
|
|
|
##
|
|
|
|
## One cached item is near 100 bytes size.
|
|
|
|
var rsize = min(size, 4096)
|
2021-01-20 13:40:15 +00:00
|
|
|
var res = TLSSessionCache(storage: newSeq[byte](rsize))
|
|
|
|
sslSessionCacheLruInit(addr res.context, addr res.storage[0], rsize)
|
|
|
|
res
|
2019-10-16 06:01:52 +00:00
|
|
|
|
|
|
|
proc handshake*(rws: SomeTLSStreamType): Future[void] =
|
|
|
|
## Wait until initial TLS handshake will be successfully performed.
|
|
|
|
var retFuture = newFuture[void]("tlsstream.handshake")
|
|
|
|
when rws is TLSStreamReader:
|
|
|
|
if rws.handshaked:
|
|
|
|
retFuture.complete()
|
|
|
|
else:
|
|
|
|
rws.handshakeFut = retFuture
|
|
|
|
rws.stream.writer.handshakeFut = retFuture
|
|
|
|
elif rws is TLSStreamWriter:
|
|
|
|
if rws.handshaked:
|
|
|
|
retFuture.complete()
|
|
|
|
else:
|
|
|
|
rws.handshakeFut = retFuture
|
|
|
|
rws.stream.reader.handshakeFut = retFuture
|
|
|
|
elif rws is TLSAsyncStream:
|
|
|
|
if rws.reader.handshaked:
|
|
|
|
retFuture.complete()
|
|
|
|
else:
|
|
|
|
rws.reader.handshakeFut = retFuture
|
|
|
|
rws.writer.handshakeFut = retFuture
|
2021-01-20 13:40:15 +00:00
|
|
|
retFuture
|