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