Quick and dirty minimal implementation of CORS

This commit is contained in:
Zahary Karadjov 2022-02-12 00:13:08 +02:00 committed by zah
parent 668a2369f6
commit d82574ba24
3 changed files with 72 additions and 10 deletions

View File

@ -6,7 +6,7 @@
# Licensed under either of # Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2) # Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT) # MIT license (LICENSE-MIT)
import chronos/apps import chronos/apps, chronos/apps/http/httpclient
import stew/[results, byteutils] import stew/[results, byteutils]
export results, apps export results, apps
@ -72,11 +72,11 @@ proc response*(t: typedesc[RestApiResponse], data: ByteChar,
else: else:
block: block:
var default: seq[byte] var default: seq[byte]
if len(data) > 0: ContentBody(contentType: contentType,
ContentBody(contentType: contentType, data: toBytes(data)) data: if len(data) > 0: toBytes(data) else: default)
else:
ContentBody(contentType: contentType, data: default) RestApiResponse(kind: RestApiResponseKind.Content,
RestApiResponse(kind: RestApiResponseKind.Content, status: status, status: status,
content: content) content: content)
proc redirect*(t: typedesc[RestApiResponse], status: HttpCode = Http307, proc redirect*(t: typedesc[RestApiResponse], status: HttpCode = Http307,

View File

@ -6,8 +6,10 @@
# Licensed under either of # Licensed under either of
# Apache License, version 2.0, (LICENSE-APACHEv2) # Apache License, version 2.0, (LICENSE-APACHEv2)
# MIT license (LICENSE-MIT) # MIT license (LICENSE-MIT)
import chronos, chronos/apps/http/[httpcommon, httptable]
import std/[macros, options] import std/[macros, options]
import chronos, chronos/apps/http/[httpcommon, httptable, httpclient]
import httputils
import stew/bitops2 import stew/bitops2
import btrees import btrees
import common, segpath, macrocommon import common, segpath, macrocommon
@ -46,13 +48,22 @@ type
RestRouter* = object RestRouter* = object
patternCallback*: PatternCallback patternCallback*: PatternCallback
routes*: BTree[SegmentedPath, RestRouteItem] routes*: BTree[SegmentedPath, RestRouteItem]
allowedOrigin*: Option[string]
proc init*(t: typedesc[RestRouter], proc init*(t: typedesc[RestRouter],
patternCallback: PatternCallback): RestRouter {.raises: [Defect].} = patternCallback: PatternCallback,
allowedOrigin = none(string)): RestRouter {.raises: [Defect].} =
doAssert(not(isNil(patternCallback)), doAssert(not(isNil(patternCallback)),
"Pattern validation callback must not be nil") "Pattern validation callback must not be nil")
RestRouter(patternCallback: patternCallback, RestRouter(patternCallback: patternCallback,
routes: initBTree[SegmentedPath, RestRouteItem]()) routes: initBTree[SegmentedPath, RestRouteItem](),
allowedOrigin: allowedOrigin)
proc optionsRequestHandler(request: HttpRequestRef,
pathParams: HttpTable,
queryParams: HttpTable,
body: Option[ContentBody]): Future[RestApiResponse] {.async.} =
return RestApiResponse.response("", Http200)
proc addRoute*(rr: var RestRouter, request: HttpMethod, path: string, proc addRoute*(rr: var RestRouter, request: HttpMethod, path: string,
flags: set[RestRouterFlag], handler: RestApiCallback) {. flags: set[RestRouterFlag], handler: RestApiCallback) {.
@ -65,6 +76,25 @@ proc addRoute*(rr: var RestRouter, request: HttpMethod, path: string,
let item = RestRouteItem(kind: RestRouteKind.Handler, let item = RestRouteItem(kind: RestRouteKind.Handler,
path: spath, flags: flags, callback: handler) path: spath, flags: flags, callback: handler)
rr.routes.add(spath, item) rr.routes.add(spath, item)
if rr.allowedOrigin.isSome:
let
optionsPath = SegmentedPath.init(
MethodOptions, path, rr.patternCallback)
optionsRoute = rr.routes.getOrDefault(
optionsPath, RestRouteItem(kind: RestRouteKind.None))
case route.kind
of RestRouteKind.None:
let optionsHandler = RestRouteItem(kind: RestRouteKind.Handler,
path: optionsPath,
flags: {RestRouterFlag.Raw},
callback: optionsRequestHandler)
rr.routes.add(optionsPath, optionsHandler)
else:
# This may happen if we use the same URL path in separate GET and
# POST handlers. Reusing the previously installed OPTIONS handler
# is perfectly fine.
discard
else: else:
raiseAssert("The route is already in the routing table") raiseAssert("The route is already in the routing table")

View File

@ -28,6 +28,16 @@ proc getContentBody*(r: HttpRequestRef): Future[Option[ContentBody]] {.async.} =
let cbody = ContentBody(contentType: cres.get(), data: data) let cbody = ContentBody(contentType: cres.get(), data: data)
return some[ContentBody](cbody) return some[ContentBody](cbody)
proc originsMatch(requestOrigin, allowedOrigin: string): bool =
if allowedOrigin.startsWith("http://") or allowedOrigin.startsWith("https://"):
requestOrigin == allowedOrigin
elif requestOrigin.startsWith("http://"):
requestOrigin.toOpenArray(7, requestOrigin.len - 1) == allowedOrigin
elif requestOrigin.startsWith("https://"):
requestOrigin.toOpenArray(8, requestOrigin.len - 1) == allowedOrigin
else:
false
proc processRestRequest*[T](server: T, proc processRestRequest*[T](server: T,
rf: RequestFence): Future[HttpResponseRef] {. rf: RequestFence): Future[HttpResponseRef] {.
gcsafe, async.} = gcsafe, async.} =
@ -98,14 +108,36 @@ proc processRestRequest*[T](server: T,
uri = $request.uri uri = $request.uri
return await request.respond(Http410) return await request.respond(Http410)
of RestApiResponseKind.Content: of RestApiResponseKind.Content:
let headers = HttpTable.init([("Content-Type", var headers = HttpTable.init([("Content-Type",
restRes.content.contentType)]) restRes.content.contentType)])
if server.router.allowedOrigin.isSome:
let origin = request.headers.getList("Origin")
let everyOriginAllowed = server.router.allowedOrigin.get == "*"
if origin.len == 1:
if everyOriginAllowed:
headers.add("Access-Control-Allow-Origin", "*")
elif originsMatch(origin[0], server.router.allowedOrigin.get):
# The Vary: Origin header to must be set to prevent
# potential cache poisoning attacks:
# https://textslashplain.com/2018/08/02/cors-and-vary/
headers.add("Vary", "Origin")
headers.add("Access-Control-Allow-Origin", origin[0])
else:
return await request.respond(Http403, "Origin not allowed")
elif origin.len > 1:
return await request.respond(Http400,
"Only a single Origin header must be specified")
elif not everyOriginAllowed:
return await request.respond(Http403,
"Service can be used only from CORS-enabled clients")
debug "Received response from handler", debug "Received response from handler",
status = restRes.status.toInt(), status = restRes.status.toInt(),
meth = $request.meth, peer = $request.remoteAddress(), meth = $request.meth, peer = $request.remoteAddress(),
uri = $request.uri, uri = $request.uri,
content_type = restRes.content.contentType, content_type = restRes.content.contentType,
content_size = len(restRes.content.data) content_size = len(restRes.content.data)
return await request.respond(restRes.status, return await request.respond(restRes.status,
restRes.content.data, headers) restRes.content.data, headers)
of RestApiResponseKind.Error: of RestApiResponseKind.Error: