diff --git a/json_rpc.nimble b/json_rpc.nimble index d1a244d..576bf77 100644 --- a/json_rpc.nimble +++ b/json_rpc.nimble @@ -7,6 +7,7 @@ skipDirs = @["tests"] ### Dependencies requires "nim >= 1.2.0", + "stew", "nimcrypto", "stint", "chronos", diff --git a/json_rpc/clients/httpclient.nim b/json_rpc/clients/httpclient.nim index b305dfb..b0296d8 100644 --- a/json_rpc/clients/httpclient.nim +++ b/json_rpc/clients/httpclient.nim @@ -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 = - T( - maxBodySize: maxBodySize, - httpSession: HttpSessionRef.new(), - ) +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) + \ No newline at end of file diff --git a/json_rpc/errors.nim b/json_rpc/errors.nim index b277a30..02acae6 100644 --- a/json_rpc/errors.nim +++ b/json_rpc/errors.nim @@ -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 diff --git a/json_rpc/rpcsecureserver.nim b/json_rpc/rpcsecureserver.nim new file mode 100644 index 0000000..dde4a63 --- /dev/null +++ b/json_rpc/rpcsecureserver.nim @@ -0,0 +1,3 @@ +import server +import servers/[socketserver, shttpserver] +export server, socketserver, shttpserver \ No newline at end of file diff --git a/json_rpc/servers/httpserver.nim b/json_rpc/servers/httpserver.nim index dda738a..df955d6 100644 --- a/json_rpc/servers/httpserver.nim +++ b/json_rpc/servers/httpserver.nim @@ -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 + 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: - 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)) - 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 + var data = future.read() + let res = await request.respond(Http200, data) + trace "JSON-RPC result has been sent" + return res 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 + return dumbResponse() - if header.version in {HttpVersion09, HttpVersion10}: - debug "Disconnecting client", address = transp.remoteAddress() - await transp.closeWait() - break - else: - if connection == "close": - debug "Disconnecting client", address = transp.remoteAddress() - await transp.closeWait() - break - - 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) - added.inc - for r in tas6: - server.addStreamServer(r) + server.addHttpServer(r) added.inc + if added == 0: # avoid ipv4 + ipv6 running together + for r in tas6: + 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() diff --git a/json_rpc/servers/shttpserver.nim b/json_rpc/servers/shttpserver.nim new file mode 100644 index 0000000..ab3e7f8 --- /dev/null +++ b/json_rpc/servers/shttpserver.nim @@ -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() \ No newline at end of file diff --git a/tests/testhttp.nim b/tests/testhttp.nim index f4ef91e..cb42643 100644 --- a/tests/testhttp.nim +++ b/tests/testhttp.nim @@ -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() diff --git a/tests/testhttps.nim b/tests/testhttps.nim new file mode 100644 index 0000000..0a7202e --- /dev/null +++ b/tests/testhttps.nim @@ -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..