Initial implementation.

This commit is contained in:
Jonathan Bernard 2023-03-09 22:59:42 -06:00
commit 8c3f48a0f3
4 changed files with 150 additions and 0 deletions

Binary file not shown.

View File

@ -0,0 +1,10 @@
const TOCLERBE_VERSION* = "0.1.0"
const USAGE* = """
Usage:
toclerbe <urlFile> [options]
Options:
--debug Enable debug logging.
"""

124
src/toclerbe.nim Normal file
View File

@ -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["<urlFile>"],
port: parseInt(getEnv("TOCLERBE_PORT", "9180")),
urls: loadUrlsFile($args["<urlFile>"]))
serveApi(cfg)

16
toclerbe.nimble Normal file
View File

@ -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