Chronos HTTP(S) Server (#111)

This commit is contained in:
Bernardo A. Rodrigues 2021-11-22 10:09:13 -03:00 committed by Zahary Karadjov
parent 9db5407e81
commit 3599a73b58
No known key found for this signature in database
GPG Key ID: C8936F8A3073D609
8 changed files with 456 additions and 404 deletions

View File

@ -7,6 +7,7 @@ skipDirs = @["tests"]
### Dependencies
requires "nim >= 1.2.0",
"stew",
"nimcrypto",
"stint",
"chronos",

View File

@ -23,14 +23,21 @@ type
const
MaxHttpRequestSize = 128 * 1024 * 1024 # maximum size of HTTP body in octets
proc new(T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize): T =
proc new(T: type RpcHttpClient, maxBodySize = MaxHttpRequestSize, secure = false): T =
if secure:
T(
maxBodySize: maxBodySize,
httpSession: HttpSessionRef.new(flags={HttpClientFlag.NoVerifyHost,
HttpClientFlag.NoVerifyServerName}),
)
else:
T(
maxBodySize: maxBodySize,
httpSession: HttpSessionRef.new(),
)
proc newRpcHttpClient*(maxBodySize = MaxHttpRequestSize): RpcHttpClient =
RpcHttpClient.new(maxBodySize)
proc newRpcHttpClient*(maxBodySize = MaxHttpRequestSize, secure = false): RpcHttpClient =
RpcHttpClient.new(maxBodySize, secure)
method call*(client: RpcHttpClient, name: string,
params: JsonNode): Future[Response]
@ -45,16 +52,27 @@ method call*(client: RpcHttpClient, name: string,
req = HttpClientRequestRef.post(client.httpSession,
client.httpAddress.get,
body = reqBody.toOpenArrayByte(0, reqBody.len - 1))
res = await req.send()
res =
try:
await req.send()
except CatchableError as exc:
raise (ref RpcPostError)(msg: "Failed to send POST Request with JSON-RPC.", parent: exc)
if res.status < 200 or res.status >= 300: # res.status is not 2xx (success)
raise newException(ErrorResponse, "POST Response: " & $res.status)
debug "Message sent to RPC server",
address = client.httpAddress, msg_len = len(reqBody)
trace "Message", msg = reqBody
echo "req body ", reqBody
let resText = string.fromBytes(await res.getBodyBytes(client.maxBodySize))
let resBytes =
try:
await res.getBodyBytes(client.maxBodySize)
except CatchableError as exc:
raise (ref FailedHttpResponse)(msg: "Failed to read POST Response for JSON-RPC.", parent: exc)
let resText = string.fromBytes(resBytes)
trace "Response", text = resText
echo "response ", resText
# completed by processMessage - the flow is quite weird here to accomodate
# socket and ws clients, but could use a more thorough refactoring
@ -83,9 +101,18 @@ proc connect*(client: RpcHttpClient, url: string)
if client.httpAddress.isErr:
raise newException(RpcAddressUnresolvableError, client.httpAddress.error)
proc connect*(client: RpcHttpClient, address: string, port: Port) {.async.} =
let addresses = resolveTAddress(address, port)
if addresses.len == 0:
raise newException(RpcAddressUnresolvableError, "Failed to resolve address: " & address)
ok client.httpAddress, getAddress(addresses[0])
proc connect*(client: RpcHttpClient, address: string, port: Port, secure=false) {.async.} =
var uri = initUri()
if secure:
uri.scheme = "https"
else:
uri.scheme = "http"
uri.hostname = address
uri.port = $port
let res = getAddress(client.httpSession, uri)
if res.isOk:
client.httpAddress = res
else:
raise newException(RpcAddressUnresolvableError, res.error)

View File

@ -8,6 +8,12 @@ type
InvalidResponse* = object of JsonRpcError
## raised when the server response violates the JSON-RPC protocol
FailedHttpResponse* = object of JsonRpcError
## raised when fail to read the underlying HTTP server response
RpcPostError* = object of JsonRpcError
## raised when the client fails to send the POST request with JSON-RPC
RpcBindError* = object of JsonRpcError
RpcAddressUnresolvableError* = object of JsonRpcError

View File

@ -0,0 +1,3 @@
import server
import servers/[socketserver, shttpserver]
export server, socketserver, shttpserver

View File

@ -1,6 +1,8 @@
import
stew/byteutils,
std/[strutils],
chronicles, httputils, chronos,
chronos/apps/http/httpserver,
".."/[errors, server]
export server
@ -8,226 +10,57 @@ export server
logScope:
topics = "JSONRPC-HTTP-SERVER"
const
MaxHttpHeadersSize = 8192 # maximum size of HTTP headers in octets
MaxHttpRequestSize = 128 * 1024 # maximum size of HTTP body in octets
HttpHeadersTimeout = 120.seconds # timeout for receiving headers (120 sec)
HttpBodyTimeout = 12.seconds # timeout for receiving body (12 sec)
HeadersMark = @[byte(0x0D), byte(0x0A), byte(0x0D), byte(0x0A)]
type
ReqStatus = enum
Success, Error, ErrorFailure
RpcHttpServer* = ref object of RpcServer
servers: seq[StreamServer]
httpServers: seq[HttpServerRef]
proc sendAnswer(transp: StreamTransport, version: HttpVersion, code: HttpCode,
data: string = ""): Future[bool] {.async.} =
var answer = $version
answer.add(" ")
answer.add($code)
answer.add("\r\n")
answer.add("Date: " & httpDate() & "\r\n")
if len(data) > 0:
answer.add("Content-Type: application/json\r\n")
answer.add("Content-Length: " & $len(data) & "\r\n")
answer.add("\r\n")
if len(data) > 0:
answer.add(data)
try:
let res = await transp.write(answer)
return res == len(answer)
except CancelledError as exc: raise exc
except CatchableError:
return false
proc addHttpServer*(rpcServer: RpcHttpServer, address: TransportAddress) =
proc processClientRpc(rpcServer: RpcHttpServer): HttpProcessCallback {.closure.} =
return proc (req: RequestFence): Future[HttpResponseRef] {.async.} =
if req.isOk():
let request = req.get()
let body = await request.getBody()
proc validateRequest(transp: StreamTransport,
header: HttpRequestHeader): Future[ReqStatus] {.async.} =
if header.meth in {MethodPut, MethodDelete}:
# Request method is either PUT or DELETE.
debug "PUT/DELETE methods are not allowed", address = transp.remoteAddress()
return if await transp.sendAnswer(header.version, Http405):
Error
else:
ErrorFailure
let length = header.contentLength()
if length <= 0:
# request length could not be calculated.
debug "Content-Length is missing or 0", address = transp.remoteAddress()
return if await transp.sendAnswer(header.version, Http411):
Error
else:
ErrorFailure
if length > MaxHttpRequestSize:
# request length is more then `MaxHttpRequestSize`.
debug "Maximum size of request body reached",
address = transp.remoteAddress()
return if await transp.sendAnswer(header.version, Http413):
Error
else:
ErrorFailure
var ctype = header["Content-Type"]
# might be "application/json; charset=utf-8"
if "application/json" notin ctype.toLowerAscii():
# Content-Type header is not "application/json"
debug "Content type must be application/json",
address = transp.remoteAddress()
return if await transp.sendAnswer(header.version, Http415):
Error
else:
ErrorFailure
return Success
proc processClient(server: StreamServer,
transp: StreamTransport) {.async, gcsafe.} =
## Process transport data to the RPC server
var rpc = getUserData[RpcHttpServer](server)
var buffer = newSeq[byte](MaxHttpHeadersSize)
var header: HttpRequestHeader
var connection: string
debug "Received connection", address = $transp.remoteAddress()
while true:
try:
let hlenfut = transp.readUntil(addr buffer[0], MaxHttpHeadersSize,
HeadersMark)
let ores = await withTimeout(hlenfut, HttpHeadersTimeout)
if not ores:
# Timeout
debug "Timeout expired while receiving headers",
address = transp.remoteAddress()
discard await transp.sendAnswer(HttpVersion11, Http408)
await transp.closeWait()
break
else:
let hlen = hlenfut.read()
buffer.setLen(hlen)
header = buffer.parseRequest()
if header.failed():
# Header could not be parsed
debug "Malformed header received",
address = transp.remoteAddress()
discard await transp.sendAnswer(HttpVersion11, Http400)
await transp.closeWait()
break
except TransportLimitError:
# size of headers exceeds `MaxHttpHeadersSize`
debug "Maximum size of headers limit reached",
address = transp.remoteAddress()
discard await transp.sendAnswer(HttpVersion11, Http413)
await transp.closeWait()
break
except TransportIncompleteError:
# remote peer disconnected
debug "Remote peer disconnected", address = transp.remoteAddress()
await transp.closeWait()
break
except TransportOsError as exc:
debug "Problems with networking", address = transp.remoteAddress(),
error = exc.msg
await transp.closeWait()
break
except CatchableError as exc:
debug "Unknown exception", address = transp.remoteAddress(),
error = exc.msg
await transp.closeWait()
break
let vres = await validateRequest(transp, header)
if vres == Success:
trace "Received valid RPC request", address = $transp.remoteAddress()
# we need to get `Connection` header value before, because
# we are reusing `buffer`, and its value will be lost.
connection = header["Connection"]
let length = header.contentLength()
buffer.setLen(length)
try:
let blenfut = transp.readExactly(addr buffer[0], length)
let ores = await withTimeout(blenfut, HttpBodyTimeout)
if not ores:
# Timeout
debug "Timeout expired while receiving request body",
address = transp.remoteAddress()
discard await transp.sendAnswer(header.version, Http413)
await transp.closeWait()
break
else:
blenfut.read()
except TransportIncompleteError:
# remote peer disconnected
debug "Remote peer disconnected", address = transp.remoteAddress()
await transp.closeWait()
break
except TransportOsError as exc:
debug "Problems with networking", address = transp.remoteAddress(),
error = exc.msg
await transp.closeWait()
break
let future = rpc.route(cast[string](buffer))
let future = rpcServer.route(string.fromBytes(body))
yield future
if future.failed:
# rpc.route exception
debug "Internal error, while processing RPC call",
address = transp.remoteAddress()
let res = await transp.sendAnswer(header.version, Http503)
if not res:
await transp.closeWait()
break
debug "Internal error while processing JSON-RPC call"
return await request.respond(Http503, "Internal error while processing JSON-RPC call")
else:
var data = future.read()
let res = await transp.sendAnswer(header.version, Http200, data)
trace "RPC result has been sent", address = $transp.remoteAddress()
if not res:
await transp.closeWait()
break
elif vres == ErrorFailure:
debug "Remote peer disconnected", address = transp.remoteAddress()
await transp.closeWait()
break
if header.version in {HttpVersion09, HttpVersion10}:
debug "Disconnecting client", address = transp.remoteAddress()
await transp.closeWait()
break
let res = await request.respond(Http200, data)
trace "JSON-RPC result has been sent"
return res
else:
if connection == "close":
debug "Disconnecting client", address = transp.remoteAddress()
await transp.closeWait()
break
return dumbResponse()
debug "Finished connection", address = $transp.remoteAddress()
# Utility functions for setting up servers using stream transport addresses
proc addStreamServer*(server: RpcHttpServer, address: TransportAddress) =
let initialServerCount = len(rpcServer.httpServers)
try:
info "Starting JSON-RPC HTTP server", url = "http://" & $address
var transServer = createStreamServer(address, processClient,
{ReuseAddr}, udata = server)
server.servers.add(transServer)
var res = HttpServerRef.new(address, processClientRpc(rpcServer))
if res.isOk():
let httpServer = res.get()
rpcServer.httpServers.add(httpServer)
else:
raise newException(RpcBindError, "Unable to create server!")
except CatchableError as exc:
error "Failed to create server", address = $address,
message = exc.msg
if len(server.servers) == 0:
if len(rpcServer.httpServers) != initialServerCount + 1:
# Server was not bound, critical error.
raise newException(RpcBindError, "Unable to create server!")
proc addStreamServers*(server: RpcHttpServer,
proc addHttpServers*(server: RpcHttpServer,
addresses: openArray[TransportAddress]) =
for item in addresses:
server.addStreamServer(item)
server.addHttpServer(item)
proc addStreamServer*(server: RpcHttpServer, address: string) =
proc addHttpServer*(server: RpcHttpServer, address: string) =
## Create new server and assign it to addresses ``addresses``.
var
tas4: seq[TransportAddress]
@ -247,21 +80,22 @@ proc addStreamServer*(server: RpcHttpServer, address: string) =
discard
for r in tas4:
server.addStreamServer(r)
server.addHttpServer(r)
added.inc
if added == 0: # avoid ipv4 + ipv6 running together
for r in tas6:
server.addStreamServer(r)
server.addHttpServer(r)
added.inc
if added == 0:
# Addresses could not be resolved, critical error.
raise newException(RpcAddressUnresolvableError, "Unable to get address!")
proc addStreamServers*(server: RpcHttpServer, addresses: openArray[string]) =
proc addHttpServers*(server: RpcHttpServer, addresses: openArray[string]) =
for address in addresses:
server.addStreamServer(address)
server.addHttpServer(address)
proc addStreamServer*(server: RpcHttpServer, address: string, port: Port) =
proc addHttpServer*(server: RpcHttpServer, address: string, port: Port) =
var
tas4: seq[TransportAddress]
tas6: seq[TransportAddress]
@ -285,19 +119,19 @@ proc addStreamServer*(server: RpcHttpServer, address: string, port: Port) =
"Address " & address & " could not be resolved!")
for r in tas4:
server.addStreamServer(r)
server.addHttpServer(r)
added.inc
for r in tas6:
server.addStreamServer(r)
server.addHttpServer(r)
added.inc
if len(server.servers) == 0:
if len(server.httpServers) == 0:
# Server was not bound, critical error.
raise newException(RpcBindError,
"Could not setup server on " & address & ":" & $int(port))
proc new*(T: type RpcHttpServer): T =
T(router: RpcRouter.init(), servers: @[])
T(router: RpcRouter.init(), httpServers: @[])
proc new*(T: type RpcHttpServer, router: RpcRouter): T =
T(router: router, servers: @[])
@ -311,12 +145,12 @@ proc newRpcHttpServer*(router: RpcRouter): RpcHttpServer =
proc newRpcHttpServer*(addresses: openArray[TransportAddress]): RpcHttpServer =
## Create new server and assign it to addresses ``addresses``.
result = newRpcHttpServer()
result.addStreamServers(addresses)
result.addHttpServers(addresses)
proc newRpcHttpServer*(addresses: openArray[string]): RpcHttpServer =
## Create new server and assign it to addresses ``addresses``.
result = newRpcHttpServer()
result.addStreamServers(addresses)
result.addHttpServers(addresses)
proc newRpcHttpServer*(addresses: openArray[string], router: RpcRouter): RpcHttpServer =
## Create new server and assign it to addresses ``addresses``.
@ -330,22 +164,17 @@ proc newRpcHttpServer*(addresses: openArray[TransportAddress], router: RpcRouter
proc start*(server: RpcHttpServer) =
## Start the RPC server.
for item in server.servers:
debug "HTTP RPC server started", address = item.local
for item in server.httpServers:
debug "HTTP RPC server started" # (todo: fix this), address = item
item.start()
proc stop*(server: RpcHttpServer) =
proc stop*(server: RpcHttpServer) {.async.} =
## Stop the RPC server.
for item in server.servers:
debug "HTTP RPC server stopped", address = item.local
item.stop()
proc close*(server: RpcHttpServer) =
## Cleanup resources of RPC server.
for item in server.servers:
item.close()
for item in server.httpServers:
debug "HTTP RPC server stopped" # (todo: fix this), address = item.local
await item.stop()
proc closeWait*(server: RpcHttpServer) {.async.} =
## Cleanup resources of RPC server.
for item in server.servers:
for item in server.httpServers:
await item.closeWait()

View File

@ -0,0 +1,198 @@
import
stew/byteutils,
std/[strutils],
chronicles, httputils, chronos,
chronos/apps/http/shttpserver,
".."/[errors, server]
export server
logScope:
topics = "JSONRPC-HTTPS-SERVER"
type
ReqStatus = enum
Success, Error, ErrorFailure
RpcSecureHttpServer* = ref object of RpcServer
secureHttpServers: seq[SecureHttpServerRef]
proc addSecureHttpServer*(rpcServer: RpcSecureHttpServer,
address: TransportAddress,
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
) =
proc processClientRpc(rpcServer: RpcSecureHttpServer): HttpProcessCallback {.closure.} =
return proc (req: RequestFence): Future[HttpResponseRef] {.async.} =
if req.isOk():
let request = req.get()
let body = await request.getBody()
let future = rpcServer.route(string.fromBytes(body))
yield future
if future.failed:
debug "Internal error while processing JSON-RPC call"
return await request.respond(Http503, "Internal error while processing JSON-RPC call")
else:
var data = future.read()
let res = await request.respond(Http200, data)
trace "JSON-RPC result has been sent"
return res
else:
if req.error.code == Http408:
debug "Timeout error while processing JSON-RPC call"
return dumbResponse()
let initialServerCount = len(rpcServer.secureHttpServers)
try:
info "Starting JSON-RPC HTTPS server", url = "https://" & $address
var res = SecureHttpServerRef.new(address,
processClientRpc(rpcServer),
tlsPrivateKey,
tlsCertificate,
serverFlags = {Secure},
socketFlags = {ServerFlags.TcpNoDelay, ServerFlags.ReuseAddr}
)
if res.isOk():
let secureHttpServer = res.get()
rpcServer.secureHttpServers.add(secureHttpServer)
else:
raise newException(RpcBindError, "Unable to create server!")
except CatchableError as exc:
error "Failed to create server", address = $address,
message = exc.msg
if len(rpcServer.secureHttpServers) != initialServerCount + 1:
# Server was not bound, critical error.
raise newException(RpcBindError, "Unable to create server!")
proc addSecureHttpServers*(server: RpcSecureHttpServer,
addresses: openArray[TransportAddress],
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
) =
for item in addresses:
server.addSecureHttpServer(item, tlsPrivateKey, tlsCertificate)
proc addSecureHttpServer*(server: RpcSecureHttpServer,
address: string,
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
) =
## Create new server and assign it to addresses ``addresses``.
var
tas4: seq[TransportAddress]
tas6: seq[TransportAddress]
added = 0
# Attempt to resolve `address` for IPv4 address space.
try:
tas4 = resolveTAddress(address, AddressFamily.IPv4)
except CatchableError:
discard
# Attempt to resolve `address` for IPv6 address space.
try:
tas6 = resolveTAddress(address, AddressFamily.IPv6)
except CatchableError:
discard
for r in tas4:
server.addSecureHttpServer(r, tlsPrivateKey, tlsCertificate)
added.inc
if added == 0: # avoid ipv4 + ipv6 running together
for r in tas6:
server.addSecureHttpServer(r, tlsPrivateKey, tlsCertificate)
added.inc
if added == 0:
# Addresses could not be resolved, critical error.
raise newException(RpcAddressUnresolvableError, "Unable to get address!")
proc addSecureHttpServers*(server: RpcSecureHttpServer,
addresses: openArray[string],
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
) =
for address in addresses:
server.addSecureHttpServer(address, tlsPrivateKey, tlsCertificate)
proc addSecureHttpServer*(server: RpcSecureHttpServer,
address: string,
port: Port,
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
) =
var
tas4: seq[TransportAddress]
tas6: seq[TransportAddress]
added = 0
# Attempt to resolve `address` for IPv4 address space.
try:
tas4 = resolveTAddress(address, port, AddressFamily.IPv4)
except CatchableError:
discard
# Attempt to resolve `address` for IPv6 address space.
try:
tas6 = resolveTAddress(address, port, AddressFamily.IPv6)
except CatchableError:
discard
if len(tas4) == 0 and len(tas6) == 0:
# Address was not resolved, critical error.
raise newException(RpcAddressUnresolvableError,
"Address " & address & " could not be resolved!")
for r in tas4:
server.addSecureHttpServer(r, tlsPrivateKey, tlsCertificate)
added.inc
for r in tas6:
server.addSecureHttpServer(r, tlsPrivateKey, tlsCertificate)
added.inc
if len(server.secureHttpServers) == 0:
# Server was not bound, critical error.
raise newException(RpcBindError,
"Could not setup server on " & address & ":" & $int(port))
proc new*(T: type RpcSecureHttpServer): T =
T(router: RpcRouter.init(), secureHttpServers: @[])
proc newRpcSecureHttpServer*(): RpcSecureHttpServer =
RpcSecureHttpServer.new()
proc newRpcSecureHttpServer*(addresses: openArray[TransportAddress],
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
): RpcSecureHttpServer =
## Create new server and assign it to addresses ``addresses``.
result = newRpcSecureHttpServer()
result.addSecureHttpServers(addresses, tlsPrivateKey, tlsCertificate)
proc newRpcSecureHttpServer*(addresses: openArray[string],
tlsPrivateKey: TLSPrivateKey,
tlsCertificate: TLSCertificate
): RpcSecureHttpServer =
## Create new server and assign it to addresses ``addresses``.
result = newRpcSecureHttpServer()
result.addSecureHttpServers(addresses, tlsPrivateKey, tlsCertificate)
proc start*(server: RpcSecureHttpServer) =
## Start the RPC server.
for item in server.secureHttpServers:
debug "HTTPS RPC server started" # (todo: fix this), address = item
item.start()
proc stop*(server: RpcSecureHttpServer) {.async.} =
## Stop the RPC server.
for item in server.secureHttpServers:
debug "HTTPS RPC server stopped" # (todo: fix this), address = item.local
await item.stop()
proc closeWait*(server: RpcSecureHttpServer) {.async.} =
## Cleanup resources of RPC server.
for item in server.secureHttpServers:
await item.closeWait()

View File

@ -1,76 +1,15 @@
import unittest, json, strutils
import httputils, chronicles
import httputils
import ../json_rpc/[rpcserver, rpcclient]
const
TestsCount = 100
BufferSize = 8192
BigHeaderSize = 8 * 1024 + 1
BigBodySize = 128 * 1024 + 1
HeadersMark = @[byte(0x0D), byte(0x0A), byte(0x0D), byte(0x0A)]
const TestsCount = 100
Requests = [
"GET / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Length: 71\r\n" &
"Content-Type: text/html\r\n" &
"Connection: close\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}",
"BADHEADER\r\n\r\n",
"GET / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Type: application/json\r\n" &
"Connection: close\r\n" &
"\r\n",
"PUT / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Length: 71\r\n" &
"Content-Type: text/html\r\n" &
"Connection: close\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}",
"DELETE / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Length: 71\r\n" &
"Content-Type: text/html\r\n" &
"Connection: close\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}",
"GET / HTTP/0.9\r\n" &
"Host: status.im\r\n" &
"Content-Length: 71\r\n" &
"Content-Type: application/json\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}",
"GET / HTTP/1.0\r\n" &
"Host: status.im\r\n" &
"Content-Length: 71\r\n" &
"Content-Type: application/json\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}",
"GET / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Length: 71\r\n" &
"Content-Type: application/json\r\n" &
"Connection: close\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}",
"GET / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Length: 49\r\n" &
"Content-Type: application/json\r\n" &
"Connection: close\r\n" &
"\r\n" &
"{\"jsonrpc\":\"2.0\",\"method\":\"noParamsProc\",\"id\":67}",
"GET / HTTP/1.1\r\n" &
"Host: status.im\r\n" &
"Content-Length: 137438953472\r\n" &
"Content-Type: application/json\r\n" &
"Connection: close\r\n" &
"\r\n" &
"{128 gb Content-Length}",
]
proc simpleTest(address: string, port: Port): Future[bool] {.async.} =
var client = newRpcHttpClient()
await client.connect(address, port)
var r = await client.call("noParamsProc", %[])
if r.getStr == "Hello world":
result = true
proc continuousTest(address: string, port: Port): Future[int] {.async.} =
var client = newRpcHttpClient()
@ -82,76 +21,22 @@ proc continuousTest(address: string, port: Port): Future[int] {.async.} =
result += 1
await client.close()
proc customMessage(address: TransportAddress,
data: string,
expect: int): Future[bool] {.async.} =
var buffer = newSeq[byte](BufferSize)
var header: HttpResponseHeader
var transp = await connect(address)
defer: transp.close()
let wres = await transp.write(data)
doAssert(wres == len(data))
let rres = await transp.readUntil(addr buffer[0], BufferSize, HeadersMark)
doAssert(rres > 0)
buffer.setLen(rres)
header = parseResponse(buffer)
doAssert(header.success())
return header.code == expect
proc headerTest(address: string, port: Port): Future[bool] {.async.} =
var a = resolveTAddress(address, port)
var header = "GET / HTTP/1.1\r\n"
var i = 0
while len(header) <= BigHeaderSize:
header.add("Field" & $i & ": " & $i & "\r\n")
inc(i)
header.add("Content-Length: 71\r\n")
header.add("Content-Type: application/json\r\n")
header.add("Connection: close\r\n\r\n")
header.add("{\"jsonrpc\":\"2.0\",\"method\":\"myProc\",\"params\":[\"abc\", [1, 2, 3]],\"id\":67}")
return await customMessage(a[0], header, 413)
proc bodyTest(address: string, port: Port): Future[bool] {.async.} =
var body = repeat('B', BigBodySize)
var a = resolveTAddress(address, port)
var header = "GET / HTTP/1.1\r\n"
header.add("Content-Length: " & $len(body) & "\r\n")
header.add("Content-Type: application/json\r\n")
header.add("Connection: close\r\n\r\n")
header.add(body)
return await customMessage(a[0], header, 413)
proc disconTest(address: string, port: Port,
number: int, expect: int): Future[bool] {.async.} =
var a = resolveTAddress(address, port)
var buffer = newSeq[byte](BufferSize)
var header: HttpResponseHeader
var transp = await connect(a[0])
defer: transp.close()
let data = Requests[number]
let wres = await transp.write(data)
doAssert(wres == len(data))
let rres = await transp.readUntil(addr buffer[0], BufferSize, HeadersMark)
doAssert(rres > 0)
buffer.setLen(rres)
header = parseResponse(buffer)
doAssert(header.success())
if header.code != expect:
return false
let length = header.contentLength()
doAssert(length > 0)
buffer.setLen(length)
await transp.readExactly(addr buffer[0], len(buffer))
let left = await transp.read()
return len(left) == 0 and transp.atEof()
proc simpleTest(address: string, port: Port,
number: int, expect: int): Future[bool] {.async.} =
var a = resolveTAddress(address, port)
result = await customMessage(a[0], Requests[number], expect)
proc invalidTest(address: string, port: Port): Future[bool] {.async.} =
var client = newRpcHttpClient()
await client.connect(address, port)
var invalidA, invalidB: bool
try:
var r = await client.call("invalidProcA", %[])
discard r
except ValueError:
invalidA = true
try:
var r = await client.call("invalidProcB", %[1, 2, 3])
discard r
except ValueError:
invalidB = true
if invalidA and invalidB:
result = true
var httpsrv = newRpcHttpServer(["localhost:8545"])
@ -163,33 +48,13 @@ httpsrv.rpc("noParamsProc") do():
httpsrv.start()
suite "HTTP Server/HTTP Client RPC test suite":
suite "JSON-RPC test suite":
test "Simple RPC call":
check waitFor(simpleTest("localhost", Port(8545))) == true
test "Continuous RPC calls (" & $TestsCount & " messages)":
check waitFor(continuousTest("localhost", Port(8545))) == TestsCount
test "Wrong [Content-Type] test":
check waitFor(simpleTest("localhost", Port(8545), 0, 415)) == true
test "Bad request header test":
check waitFor(simpleTest("localhost", Port(8545), 1, 400)) == true
test "Zero [Content-Length] test":
check waitFor(simpleTest("localhost", Port(8545), 2, 411)) == true
test "PUT/DELETE methods test":
check:
waitFor(simpleTest("localhost", Port(8545), 3, 405)) == true
waitFor(simpleTest("localhost", Port(8545), 4, 405)) == true
test "Oversized headers test":
check waitFor(headerTest("localhost", Port(8545))) == true
test "Oversized request test":
check waitFor(bodyTest("localhost", Port(8545))) == true
test "HTTP/0.9 and HTTP/1.0 client test":
check:
waitFor(disconTest("localhost", Port(8545), 5, 200)) == true
waitFor(disconTest("localhost", Port(8545), 6, 200)) == true
test "[Connection]: close test":
check waitFor(disconTest("localhost", Port(8545), 7, 200)) == true
test "Omitted params test":
check waitFor(simpleTest("localhost", Port(8545), 8, 200)) == true
test "Big Content-Length":
check waitFor(simpleTest("localhost", Port(8545), 9, 413)) == true
test "Invalid RPC calls":
check waitFor(invalidTest("localhost", Port(8545))) == true
httpsrv.stop()
waitFor httpsrv.stop()
waitFor httpsrv.closeWait()

123
tests/testhttps.nim Normal file
View File

@ -0,0 +1,123 @@
import unittest, json, strutils
import httputils
import ../json_rpc/[rpcsecureserver, rpcclient]
import chronos/[streams/tlsstream, apps/http/httpcommon]
const TestsCount = 100
# 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-----
"""
proc simpleTest(address: string, port: Port): Future[bool] {.async.} =
var client = newRpcHttpClient(secure=true)
await client.connect(address, port, secure=true)
var r = await client.call("noParamsProc", %[])
if r.getStr == "Hello world":
result = true
proc continuousTest(address: string, port: Port): Future[int] {.async.} =
var client = newRpcHttpClient(secure=true)
result = 0
for i in 0..<TestsCount:
await client.connect(address, port, secure=true)
var r = await client.call("myProc", %[%"abc", %[1, 2, 3, i]])
if r.getStr == "Hello abc data: [1, 2, 3, " & $i & "]":
result += 1
await client.close()
proc invalidTest(address: string, port: Port): Future[bool] {.async.} =
var client = newRpcHttpClient(secure=true)
await client.connect(address, port, secure=true)
var invalidA, invalidB: bool
try:
var r = await client.call("invalidProcA", %[])
discard r
except ValueError:
invalidA = true
try:
var r = await client.call("invalidProcB", %[1, 2, 3])
discard r
except ValueError:
invalidB = true
if invalidA and invalidB:
result = true
let secureKey = TLSPrivateKey.init(HttpsSelfSignedRsaKey)
let secureCert = TLSCertificate.init(HttpsSelfSignedRsaCert)
var secureHttpSrv = newRpcSecureHttpServer(["localhost:8545"], secureKey, secureCert)
# Create RPC on server
secureHttpSrv.rpc("myProc") do(input: string, data: array[0..3, int]):
result = %("Hello " & input & " data: " & $data)
secureHttpSrv.rpc("noParamsProc") do():
result = %("Hello world")
secureHttpSrv.start()
suite "JSON-RPC test suite":
test "Simple RPC call":
check waitFor(simpleTest("localhost", Port(8545))) == true
test "Continuous RPC calls (" & $TestsCount & " messages)":
check waitFor(continuousTest("localhost", Port(8545))) == TestsCount
test "Invalid RPC calls":
check waitFor(invalidTest("localhost", Port(8545))) == true
waitFor secureHttpSrv.stop()
waitFor secureHttpSrv.closeWait()