Quick and dirty minimal implementation of CORS
This commit is contained in:
parent
668a2369f6
commit
d82574ba24
|
@ -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,
|
||||||
|
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue