# Nimbus # Copyright (c) 2022 Status Research & Development GmbH # Licensed and distributed under either of # * MIT license (license terms in the root directory or at # https://opensource.org/licenses/MIT). # * Apache v2 license (license terms in the root directory or at # https://www.apache.org/licenses/LICENSE-2.0). # at your option. This file may not be copied, modified, or distributed except # according to those terms. import std/[uri], chronos, chronos/apps/http/[httptable, httpserver], json_rpc/rpcserver, httputils, websock/websock as ws {.push raises: [].} proc sameOrigin(a, b: Uri): bool = a.hostname == b.hostname and a.scheme == b.scheme and a.port == b.port proc containsOrigin(list: seq[Uri], origin: Uri): bool = for x in list: if x.sameOrigin(origin): return true const HookOK = HttpResponseRef(nil) proc httpCors*(allowedOrigins: seq[Uri]): HttpAuthHook = proc handler(req: HttpRequestRef): Future[HttpResponseRef] {.async.} = let origins = req.headers.getList("Origin") let everyOriginAllowed = allowedOrigins.len == 0 if origins.len > 1: return await req.respond(Http400, "Only a single Origin header must be specified") if origins.len == 0: # maybe not a CORS request return HookOK # this section shared by all http method let origin = parseUri(origins[0]) let resp = req.getResponse() if not everyOriginAllowed and not allowedOrigins.containsOrigin(origin): return await req.respond(Http403, "Origin not allowed") if everyOriginAllowed: resp.addHeader("Access-Control-Allow-Origin", "*") else: # The Vary: Origin header to must be set to prevent # potential cache poisoning attacks: # https://textslashplain.com/2018/08/02/cors-and-vary/ resp.addHeader("Vary", "Origin") resp.addHeader("Access-Control-Allow-Origin", origins[0]) let methods = req.headers.getList("Access-Control-Request-Method") # Check it this is preflight request # There are three conditions to identify proper preflight request: # - Origin header is present (we checked this earlier) # - It uses OPTIONS method # - It has Access-Control-Request-Method header if req.meth == MethodOptions and len(methods) > 0: # TODO: get actual methods supported by respective server # e.g. JSON-RPC, GRAPHQL, ENGINE-API resp.addHeader("Access-Control-Allow-Methods", "GET, POST") resp.addHeader("Vary", "Access-Control-Request-Method") # check headers let headers = req.headers.getString("Access-Control-Request-Headers", "?") if headers != "?": # TODO: get actual headers supported by each server? resp.addHeader("Access-Control-Allow-Headers", headers) resp.addHeader("Vary", "Access-Control-Request-Headers") # Response to preflight request should be in 200 range. return await req.respond(Http204) # other method such as POST or GET will fill # the rest of response in server return HookOK result = HttpAuthHook(handler) proc wsCors*(allowedOrigins: seq[Uri]): WsAuthHook = proc handler(req: ws.HttpRequest): Future[bool] {.async.} = # TODO: implement websock equivalent of # request.getResponse return true result = WsAuthHook(handler)