Add HTTP api
This commit is contained in:
94
src/apiserver.nim
Normal file
94
src/apiserver.nim
Normal file
@@ -0,0 +1,94 @@
|
||||
import asynchttpserver, asyncdispatch, json, strtabs, base64, strutils, strformat, options, tables
|
||||
import ../lib/dns, state
|
||||
|
||||
const headers = {"Content-type": "text/plain; charset=utf-8"}
|
||||
const authHeader = "authorization"
|
||||
|
||||
type
|
||||
NewRecordReq = object
|
||||
fqdn: string
|
||||
value: string
|
||||
|
||||
type
|
||||
Auth = tuple
|
||||
name: string
|
||||
password: string
|
||||
|
||||
proc handleAuth(req: Request, config: AppConfig): Option[Auth] =
|
||||
if not req.headers.hasKey(authHeader):
|
||||
return none(Auth)
|
||||
|
||||
let token = req.headers[authHeader].split(" ")[1]
|
||||
let credentials = decode(token).split(":")
|
||||
|
||||
let user = credentials[0]
|
||||
let pw = credentials[1]
|
||||
|
||||
if not config.users.hasKey(user) or config.users[user] != pw:
|
||||
return none(Auth)
|
||||
|
||||
return some((name: user, password: pw))
|
||||
|
||||
proc forbidden(req: Request): Future[void] =
|
||||
return req.respond(Http401, "forbidden", headers.newHttpHeaders())
|
||||
|
||||
proc ok(req: Request): Future[void] =
|
||||
return req.respond(Http200, "ok", headers.newHttpHeaders())
|
||||
|
||||
proc notFound(req: Request): Future[void] =
|
||||
return req.respond(Http404, "not found", headers.newHttpHeaders())
|
||||
|
||||
proc present(req: Request, auth: Auth, base: string): Future[void] {.async.} =
|
||||
let record = to(parseJson(req.body), NewRecordReq)
|
||||
let name = trimName(record.fqdn) & "." & auth.name & "." & base
|
||||
|
||||
addRecord(
|
||||
records,
|
||||
(name: name, dtype: DnsType.TXT),
|
||||
record.value
|
||||
)
|
||||
|
||||
await ok(req)
|
||||
|
||||
proc cleanup(req: Request, auth: Auth, base: string): Future[void] {.async.} =
|
||||
let record = to(parseJson(req.body), NewRecordReq)
|
||||
let name = trimName(record.fqdn) & "." & auth.name & "." & base
|
||||
|
||||
delRecord(
|
||||
records,
|
||||
(name: name, dtype: DnsType.TXT),
|
||||
record.value
|
||||
)
|
||||
|
||||
await ok(req)
|
||||
|
||||
proc serveApi*(config: AppConfig) {.async.} =
|
||||
let http = newAsyncHttpServer()
|
||||
|
||||
proc cb(req: Request) {.async.} =
|
||||
let auth = handleAuth(req, config)
|
||||
|
||||
if auth.isNone():
|
||||
await forbidden(req)
|
||||
return
|
||||
|
||||
let user = auth.unsafeGet()
|
||||
|
||||
if req.url.path == "/present":
|
||||
await present(req, user, config.base)
|
||||
elif req.url.path == "/cleanup":
|
||||
await cleanup(req, user, config.base)
|
||||
else:
|
||||
await notFound(req)
|
||||
|
||||
http.listen(config.apiPort)
|
||||
echo &"API listening on port {config.apiPort.int}"
|
||||
|
||||
while true:
|
||||
if http.shouldAcceptRequest():
|
||||
try:
|
||||
await http.acceptRequest(cb)
|
||||
except:
|
||||
continue
|
||||
else:
|
||||
await sleepAsync(500)
|
||||
35
src/dnsserver.nim
Normal file
35
src/dnsserver.nim
Normal file
@@ -0,0 +1,35 @@
|
||||
import asyncnet, asyncdispatch, nativesockets
|
||||
import strutils, options, tables, strformat
|
||||
import ../lib/dns, state
|
||||
|
||||
proc handleDnsRequest(records: RecordsTable, data: string): Option[string] =
|
||||
let msg = parseMessage(data)
|
||||
|
||||
if len(msg.questions) == 0:
|
||||
return
|
||||
|
||||
let question = msg.questions[0]
|
||||
let response = mkResponse(
|
||||
msg.header.id,
|
||||
question,
|
||||
records.getOrDefault((name: question.qname, dtype: question.qtype), @[])
|
||||
)
|
||||
|
||||
return some(packMessage(response))
|
||||
|
||||
proc serveDns*(config: AppConfig) {.async.} =
|
||||
let dns = newAsyncSocket(sockType=SockType.SOCK_DGRAM, protocol=Protocol.IPPROTO_UDP, buffered = false)
|
||||
dns.setSockOpt(OptReuseAddr, true)
|
||||
dns.bindAddr(config.dnsPort)
|
||||
|
||||
echo &"DNS listening on port {config.dnsPort.int}"
|
||||
|
||||
while true:
|
||||
try:
|
||||
let request = await dns.recvFrom(size=512)
|
||||
let response = handleDnsRequest(records, request.data)
|
||||
|
||||
if (response.isSome):
|
||||
await dns.sendTo(request.address, request.port, response.unsafeGet)
|
||||
except:
|
||||
continue
|
||||
42
src/state.nim
Normal file
42
src/state.nim
Normal file
@@ -0,0 +1,42 @@
|
||||
import tables, strtabs, sequtils, nativesockets
|
||||
import ../lib/dns
|
||||
|
||||
type
|
||||
RecordKey* = tuple
|
||||
name: string
|
||||
dtype: DnsType
|
||||
|
||||
type
|
||||
RecordsTable* = Table[RecordKey, seq[string]]
|
||||
|
||||
type
|
||||
AppConfig* = object
|
||||
users*: StringTableRef
|
||||
base*: string
|
||||
apiPort*: Port
|
||||
dnsPort*: Port
|
||||
|
||||
func trimName*(name: string): string =
|
||||
if name[^1] == '.':
|
||||
return name[0 .. ^2]
|
||||
else:
|
||||
return name
|
||||
|
||||
proc addRecord*(records: var RecordsTable, key: RecordKey, record: string) =
|
||||
if records.hasKey(key):
|
||||
records[key].add(record)
|
||||
else:
|
||||
records[key] = @[record]
|
||||
|
||||
proc delRecord*(records: var RecordsTable, key: RecordKey, record: string) =
|
||||
if not records.hasKey(key):
|
||||
return
|
||||
|
||||
records[key].keepItIf(it != record)
|
||||
|
||||
if len(records[key]) == 0:
|
||||
records.del(key)
|
||||
|
||||
# TODO: don't use a global for this
|
||||
var records* {.threadvar.}: RecordsTable
|
||||
records = initTable[RecordKey, seq[string]]()
|
||||
Reference in New Issue
Block a user