|
|
|
@@ -0,0 +1,100 @@
|
|
|
|
|
import std/[json, nre, sequtils, strutils]
|
|
|
|
|
import cliutils, docopt, mummy, namespaced_logging
|
|
|
|
|
import webby/httpheaders
|
|
|
|
|
|
|
|
|
|
const USAGE = """
|
|
|
|
|
Usage:
|
|
|
|
|
short_url serve [options]
|
|
|
|
|
|
|
|
|
|
Options:
|
|
|
|
|
|
|
|
|
|
-c, --config <cfgFile> Use the given config file (defaults to ./short-url.cfg.json)
|
|
|
|
|
--debug Enable debug-level logging.
|
|
|
|
|
--trace Enable trace-level logging (takes precedence over --debug).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
const VERSION = "1.0.0"
|
|
|
|
|
const CT_TXT = "text/plain"
|
|
|
|
|
|
|
|
|
|
type
|
|
|
|
|
ShortUrlsConfig = object
|
|
|
|
|
mappings: seq[tuple[src, dst: string]]
|
|
|
|
|
port: int
|
|
|
|
|
cfg: CombinedConfig
|
|
|
|
|
|
|
|
|
|
Context = object
|
|
|
|
|
mappings: seq[tuple[src:string, regex: Regex, tmpl: string]]
|
|
|
|
|
logger: Logger
|
|
|
|
|
|
|
|
|
|
proc loadConfig(args: Table[string, Value]): ShortUrlsConfig =
|
|
|
|
|
let cfgFile =
|
|
|
|
|
if args["--config"]: $args["--config"]
|
|
|
|
|
else: findConfigFile("short-url.cfg.json")
|
|
|
|
|
|
|
|
|
|
result.cfg = initCombinedConfig(filename = cfgFile, docopt = args)
|
|
|
|
|
result.port = parseInt(result.cfg.getVal("port", "80"))
|
|
|
|
|
|
|
|
|
|
result.mappings = @[]
|
|
|
|
|
for (k, v) in pairs(result.cfg.getJson("mappings")):
|
|
|
|
|
result.mappings.add((k, v.getStr))
|
|
|
|
|
|
|
|
|
|
proc initMappings(m: seq[tuple[src, dst: string]]):
|
|
|
|
|
seq[tuple[src: string, regex: Regex, tmpl: string]] =
|
|
|
|
|
result = m.mapIt((it.src, re(it.src), it.dst))
|
|
|
|
|
|
|
|
|
|
proc makeHandler(mappings: seq[tuple[src,dst: string]], logSvc: LogService): RequestHandler =
|
|
|
|
|
let ctx = Context(
|
|
|
|
|
mappings: initMappings(mappings),
|
|
|
|
|
logger: threadLocalRef(logSvc).getLogger("pbm/short_url/response"))
|
|
|
|
|
|
|
|
|
|
return proc(req: Request) {.gcsafe.} =
|
|
|
|
|
|
|
|
|
|
if req.path == "/internal/health":
|
|
|
|
|
ctx.logger.info("Successful health check")
|
|
|
|
|
req.respond(200, @[("Content-Type", CT_TXT)], "healthy and happy")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
for (src, regex, tmpl) in ctx.mappings:
|
|
|
|
|
if match(req.uri, regex).isSome:
|
|
|
|
|
ctx.logger.debug(%*{
|
|
|
|
|
"msg": "found match",
|
|
|
|
|
"matching_pattern": %src,
|
|
|
|
|
"uri": %req.uri,
|
|
|
|
|
"tmpl": %tmpl })
|
|
|
|
|
|
|
|
|
|
req.respond(302,
|
|
|
|
|
toWebby(@[("Location", replace(req.uri, regex, tmpl))]),
|
|
|
|
|
"")
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
ctx.logger.debug(%*{
|
|
|
|
|
"msg": "no match found",
|
|
|
|
|
"uri": %req.uri,
|
|
|
|
|
"patterns": %mapIt(mappings, it.src) })
|
|
|
|
|
|
|
|
|
|
req.respond(404, @[("Content-Type", CT_TXT)], "not found")
|
|
|
|
|
|
|
|
|
|
when isMainModule:
|
|
|
|
|
var logSvc = initLogService()
|
|
|
|
|
try:
|
|
|
|
|
|
|
|
|
|
let llsvc = threadLocalRef(logSvc)
|
|
|
|
|
llsvc.addAppender(initConsoleLogAppender(formatter = formatJsonStructuredLog))
|
|
|
|
|
llsvc.setRootThreshold(lvlInfo)
|
|
|
|
|
let logger = llsvc.getLogger("pbm/short_url")
|
|
|
|
|
|
|
|
|
|
let args = docopt(USAGE, version = VERSION)
|
|
|
|
|
let cfg = loadConfig(args)
|
|
|
|
|
|
|
|
|
|
if args["--trace"]: logSvc.setRootThreshold(lvlAll)
|
|
|
|
|
elif args["--debug"]: logSvc.setRootThreshold(lvlDebug)
|
|
|
|
|
|
|
|
|
|
let server = newServer(makeHandler(cfg.mappings, logSvc))
|
|
|
|
|
logger.info("short_url v$# listening for requests on $#" %
|
|
|
|
|
[ VERSION, $cfg.port ])
|
|
|
|
|
server.serve(Port(cfg.port), address = "0.0.0.0")
|
|
|
|
|
|
|
|
|
|
except Exception:
|
|
|
|
|
let ex = getCurrentException()
|
|
|
|
|
threadLocalRef(logSvc).getLogger("pbm/short_url")
|
|
|
|
|
.fatal(msg = ex.msg, err = ex)
|