2024-06-28 10:34:57 +00:00
|
|
|
{.push raises: [].}
|
2024-02-29 08:48:14 +00:00
|
|
|
|
|
|
|
import
|
2024-05-16 20:29:11 +00:00
|
|
|
std/[options, strutils, re, net],
|
2024-07-09 11:14:28 +00:00
|
|
|
results,
|
2024-02-29 08:48:14 +00:00
|
|
|
chronicles,
|
|
|
|
chronos,
|
|
|
|
chronos/apps/http/httpserver
|
|
|
|
|
2024-03-15 23:08:47 +00:00
|
|
|
type OriginHandlerMiddlewareRef* = ref object of HttpServerMiddlewareRef
|
|
|
|
allowedOriginMatcher: Option[Regex]
|
|
|
|
everyOriginAllowed: bool
|
2024-02-29 08:48:14 +00:00
|
|
|
|
|
|
|
proc isEveryOriginAllowed(maybeAllowedOrigin: Option[string]): bool =
|
|
|
|
return maybeAllowedOrigin.isSome() and maybeAllowedOrigin.get() == "*"
|
|
|
|
|
|
|
|
proc compileOriginMatcher(maybeAllowedOrigin: Option[string]): Option[Regex] =
|
|
|
|
if maybeAllowedOrigin.isNone():
|
|
|
|
return none(Regex)
|
|
|
|
|
|
|
|
let allowedOrigin = maybeAllowedOrigin.get()
|
|
|
|
|
|
|
|
if (len(allowedOrigin) == 0):
|
|
|
|
return none(Regex)
|
|
|
|
|
|
|
|
try:
|
2024-03-15 23:08:47 +00:00
|
|
|
var matchOrigin: string
|
2024-02-29 08:48:14 +00:00
|
|
|
|
|
|
|
if allowedOrigin == "*":
|
|
|
|
matchOrigin = r".*"
|
|
|
|
return some(re(matchOrigin, {reIgnoreCase, reExtended}))
|
|
|
|
|
|
|
|
let allowedOrigins = allowedOrigin.split(",")
|
|
|
|
|
2024-03-15 23:08:47 +00:00
|
|
|
var matchExpressions: seq[string] = @[]
|
2024-02-29 08:48:14 +00:00
|
|
|
|
2024-03-15 23:08:47 +00:00
|
|
|
var prefix: string
|
2024-02-29 08:48:14 +00:00
|
|
|
for allowedOrigin in allowedOrigins:
|
|
|
|
if allowedOrigin.startsWith("http://"):
|
|
|
|
prefix = r"http:\/\/"
|
|
|
|
matchOrigin = allowedOrigin.substr(7)
|
|
|
|
elif allowedOrigin.startsWith("https://"):
|
|
|
|
prefix = r"https:\/\/"
|
|
|
|
matchOrigin = allowedOrigin.substr(8)
|
|
|
|
else:
|
|
|
|
prefix = r"https?:\/\/"
|
|
|
|
matchOrigin = allowedOrigin
|
|
|
|
|
|
|
|
matchOrigin = matchOrigin.replace(".", r"\.")
|
|
|
|
matchOrigin = matchOrigin.replace("*", ".*")
|
|
|
|
matchOrigin = matchOrigin.replace("?", ".?")
|
|
|
|
|
|
|
|
matchExpressions.add("^" & prefix & matchOrigin & "$")
|
|
|
|
|
|
|
|
let finalExpression = matchExpressions.join("|")
|
|
|
|
|
|
|
|
return some(re(finalExpression, {reIgnoreCase, reExtended}))
|
|
|
|
except RegexError:
|
|
|
|
var msg = getCurrentExceptionMsg()
|
2024-03-15 23:08:47 +00:00
|
|
|
error "Failed to compile regex", source = allowedOrigin, err = msg
|
2024-02-29 08:48:14 +00:00
|
|
|
return none(Regex)
|
|
|
|
|
2024-03-15 23:08:47 +00:00
|
|
|
proc originsMatch(
|
|
|
|
originHandler: OriginHandlerMiddlewareRef, requestOrigin: string
|
|
|
|
): bool =
|
2024-02-29 08:48:14 +00:00
|
|
|
if originHandler.allowedOriginMatcher.isNone():
|
|
|
|
return false
|
|
|
|
|
|
|
|
return requestOrigin.match(originHandler.allowedOriginMatcher.get())
|
|
|
|
|
|
|
|
proc originMiddlewareProc(
|
2024-03-15 23:08:47 +00:00
|
|
|
middleware: HttpServerMiddlewareRef,
|
|
|
|
reqfence: RequestFence,
|
|
|
|
nextHandler: HttpProcessCallback2,
|
|
|
|
): Future[HttpResponseRef] {.async: (raises: [CancelledError]).} =
|
2024-02-29 08:48:14 +00:00
|
|
|
if reqfence.isErr():
|
|
|
|
# Ignore request errors that detected before our middleware.
|
|
|
|
# Let final handler deal with it.
|
|
|
|
return await nextHandler(reqfence)
|
|
|
|
|
|
|
|
let self = OriginHandlerMiddlewareRef(middleware)
|
|
|
|
let request = reqfence.get()
|
|
|
|
var reqHeaders = request.headers
|
|
|
|
var response = request.getResponse()
|
|
|
|
|
|
|
|
if self.allowedOriginMatcher.isSome():
|
|
|
|
let origin = reqHeaders.getList("Origin")
|
|
|
|
try:
|
|
|
|
if origin.len == 1:
|
|
|
|
if self.everyOriginAllowed:
|
|
|
|
response.addHeader("Access-Control-Allow-Origin", "*")
|
2024-04-18 10:29:50 +00:00
|
|
|
response.addHeader("Access-Control-Allow-Headers", "Content-Type")
|
2024-02-29 08:48:14 +00:00
|
|
|
elif self.originsMatch(origin[0]):
|
|
|
|
# The Vary: Origin header to must be set to prevent
|
|
|
|
# potential cache poisoning attacks:
|
|
|
|
# https://textslashplain.com/2018/08/02/cors-and-vary/
|
|
|
|
response.addHeader("Vary", "Origin")
|
|
|
|
response.addHeader("Access-Control-Allow-Origin", origin[0])
|
2024-04-18 10:29:50 +00:00
|
|
|
response.addHeader("Access-Control-Allow-Headers", "Content-Type")
|
2024-02-29 08:48:14 +00:00
|
|
|
else:
|
|
|
|
return await request.respond(Http403, "Origin not allowed")
|
|
|
|
elif origin.len == 0:
|
|
|
|
discard
|
|
|
|
elif origin.len > 1:
|
2024-03-15 23:08:47 +00:00
|
|
|
return await request.respond(
|
|
|
|
Http400, "Only a single Origin header must be specified"
|
|
|
|
)
|
2024-02-29 08:48:14 +00:00
|
|
|
except HttpWriteError as exc:
|
|
|
|
# We use default error handler if we unable to send response.
|
|
|
|
return defaultResponse(exc)
|
|
|
|
|
|
|
|
# Calling next handler.
|
|
|
|
return await nextHandler(reqfence)
|
|
|
|
|
2024-03-15 23:08:47 +00:00
|
|
|
proc new*(
|
|
|
|
t: typedesc[OriginHandlerMiddlewareRef],
|
|
|
|
allowedOrigin: Option[string] = none(string),
|
|
|
|
): HttpServerMiddlewareRef =
|
|
|
|
let middleware = OriginHandlerMiddlewareRef(
|
|
|
|
allowedOriginMatcher: compileOriginMatcher(allowedOrigin),
|
|
|
|
everyOriginAllowed: isEveryOriginAllowed(allowedOrigin),
|
|
|
|
handler: originMiddlewareProc,
|
|
|
|
)
|
2024-02-29 08:48:14 +00:00
|
|
|
return HttpServerMiddlewareRef(middleware)
|