Initial implementation.
This commit is contained in:
commit
8c3f48a0f3
BIN
src/private/.cliconstants.nim.swp
Normal file
BIN
src/private/.cliconstants.nim.swp
Normal file
Binary file not shown.
10
src/private/cliconstants.nim
Normal file
10
src/private/cliconstants.nim
Normal 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
124
src/toclerbe.nim
Normal 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
16
toclerbe.nimble
Normal 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
|
Loading…
x
Reference in New Issue
Block a user