commit 8c3f48a0f3bd8fddb9fbdca1fc237a950564faa5 Author: Jonathan Bernard Date: Thu Mar 9 22:59:42 2023 -0600 Initial implementation. diff --git a/src/private/.cliconstants.nim.swp b/src/private/.cliconstants.nim.swp new file mode 100644 index 0000000..3601be0 Binary files /dev/null and b/src/private/.cliconstants.nim.swp differ diff --git a/src/private/cliconstants.nim b/src/private/cliconstants.nim new file mode 100644 index 0000000..89dedf5 --- /dev/null +++ b/src/private/cliconstants.nim @@ -0,0 +1,10 @@ +const TOCLERBE_VERSION* = "0.1.0" + +const USAGE* = """ +Usage: + toclerbe [options] + +Options: + + --debug Enable debug logging. +""" diff --git a/src/toclerbe.nim b/src/toclerbe.nim new file mode 100644 index 0000000..19d5665 --- /dev/null +++ b/src/toclerbe.nim @@ -0,0 +1,124 @@ +import std/[asyncdispatch, logging, os, strtabs, strutils] +import docopt, jester, zero_functional +import private/cliconstants + +from re import re, find + +type + ToClerbeCfg = object + apiKeys*: seq[string] + urlFilePath*: string + port*: int + urls*: StringTableRef + +proc parseUrlLine(line: string, lineNo: int): tuple[k, v: string] = + let pair = line.split("\t") + if pair.len != 2: + warn "Error loading URLs file. Line #" & $lineNo & + " does not contain exactly two values separated by \\t." + +proc loadUrlsFile(filename: string): StringTableRef = + result = newStringTable() + + var lineNo = 0 + for line in filename.lines: + lineNo += 1 + let pair = parseUrlLine(line, lineNo) + result[pair.k] = pair.v + +proc persist(cfg: ToClerbeCfg) = + var f: File + try: + f = open(cfg.urlFilePath, fmWrite) + for (key, val) in cfg.urls.pairs: f.writeLine(key & "\t" & val) + finally: + close(f) + +proc raiseEx(reason: string): void = raise newException(Exception, reason) + +template sendOptionsResp(allowedMethods: seq[HttpMethod]) = + + let corsHeaders = @{ + "Access-Control-Allow-Origin": $(headers(request)["Origin"]), + "Access-Control-Allow-Credentials": "true", + "Access-Control-Allow-Methods": (allowedMethods --> map($it)).join(", "), + "Access-Control-Allow-Headers": "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-CSRF-TOKEN" + } + + resp(Http200, corsHeaders, "") + +template checkAuth(cfg: ToClerbeCfg) = + ## Check this request for authentication and authorization information. + ## If the request is not authorized, this template sets up the 401 response + ## correctly. The calling context needs only to return from the route. + + var authed {.inject.} = false + + try: + if not headers(request).hasKey("Authorization"): + raiseEx "No auth token." + + let headerVal = headers(request)["Authorization"] + if not headerVal.startsWith("Bearer "): + raiseEx "Invalid Authentication type (only 'Bearer' is supported)." + + let presentedKey = headerVal[7..^1] + if not cfg.apiKeys.contains(presentedKey): + raiseEx "Invalid API key." + + authed = true + info "Authorized API key starting with " & presentedKey[0..4] + + except: + stderr.writeLine "Auth failed: " & getCurrentExceptionMsg() + halt( + Http401, + @{"Content-Type": "text/plain"}, + getCurrentExceptionMsg()) + +proc serveApi(cfg: ToClerbeCfg) = + + settings: + port = Port(cfg.port) + appName = "/" + + routes: + + options "/version": sendOptionsResp(@[HttpGet]) + get "/version": resp "to.cler.be v" & TOCLERBE_VERSION + + options "/api/create": sendOptionsResp(@[HttpPost]) + post "/api/create": + checkAuth(cfg) + try: + var lineNo = 0 + for line in request.body.lines: + lineNo += 1 + let pair = parseUrlLine(line, lineNo) + cfg.urls[pair.k] = pair.v + info "Added URL mapping: " & pair.k & " -> " & pair.v + cfg.persist() + resp(Http200, "") + except: + resp(Http500, getCurrentExceptionMsg()) + + get re".*": + if cfg.urls.contains(request.pathInfo): + resp(Http302, {"Location": cfg.urls[request.pathInfo]}, "") + else: resp(Http404, "") + +when isMainModule: + + let doc = USAGE + + logging.addHandler(newConsoleLogger(lvlDebug)) + + let args = docopt(doc, version = TOCLERBE_VERSION) + + let cfg = ToClerbeCfg( + apiKeys: getEnv("TOCLERBE_API_KEYS").split(";"), + urlFilePath: $args[""], + port: parseInt(getEnv("TOCLERBE_PORT", "9180")), + urls: loadUrlsFile($args[""])) + + serveApi(cfg) diff --git a/toclerbe.nimble b/toclerbe.nimble new file mode 100644 index 0000000..f5b693b --- /dev/null +++ b/toclerbe.nimble @@ -0,0 +1,16 @@ +# Package + +version = "0.1.0" +author = "Jonathan Bernard" +description = "Jonathan's custom URL shortener/bookmark service." +license = "MIT" +srcDir = "src" +bin = @["toclerbe"] + + +# Dependencies + +requires "nim >= 1.6.10" +requires @["docopt", "jester"] + +# Dependencies from git.jdb-software.com/nim-packages