Refactor static_server (#169)

This commit is contained in:
Antonis Geralis 2021-01-08 18:07:19 +02:00 • committed by GitHub
commit 6cff2d8b05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -1,71 +1,63 @@
import net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5, import
logging, httpcore, asyncfile, options std/[net, os, strutils, uri, mimetypes, asyncnet, asyncdispatch, md5,
import asynchttpserver logging, httpcore, asyncfile, asynchttpserver, tables, times]
from cgi import decodeUrl from cgi import decodeUrl
import ws,tables,times import ws, dotenv
import dotenv
var logger = newConsoleLogger()
addHandler(logger)
when defined(release):
setLogFilter(lvlError)
type type
RawHeaders* = seq[tuple[key, val: string]] RawHeaders* = seq[tuple[key, val: string]]
proc toStr(headers: Option[RawHeaders]): string = proc toStr(headers: RawHeaders): string =
return $newHttpHeaders(headers.get(@({:}))) $newHttpHeaders(headers)
proc send( proc send(request: Request, code: HttpCode, headers: RawHeaders,
request: Request, code: HttpCode, headers: Option[RawHeaders], body: string body: string): Future[void] =
): Future[void] = return request.respond(code, body, newHttpHeaders(headers))
return request.respond(
code, body, newHttpHeaders(headers.get(@({:})))
)
proc statusContent(request: Request, status: HttpCode, content: string, proc statusContent(request: Request, status: HttpCode, content: string,
headers: Option[RawHeaders]): Future[void] = headers: RawHeaders): Future[void] =
try: try:
result = send(request, status, headers, content) result = send(request, status, headers, content)
when not defined(release): debug(" ", status, " ", toStr(headers))
logging.debug(" $1 $2" % [$status, toStr(headers)])
except: except:
logging.error("Could not send response: $1" % osErrorMsg(osLastError())) error("Could not send response: ", osErrorMsg(osLastError()))
proc sendStaticIfExists( proc sendStaticIfExists(req: Request, paths: seq[string]): Future[HttpCode] {.async.} =
req: Request, paths: seq[string]
): Future[HttpCode] {.async.} =
result = Http200 result = Http200
let mimes = newMimetypes() let mimes = newMimetypes()
for p in paths: for p in paths:
if fileExists(p): if fileExists(p):
if fpOthersRead notin getFilePermissions(p):
var fp = getFilePermissions(p)
if not fp.contains(fpOthersRead):
return Http403 return Http403
let fileSize = getFileSize(p) let fileSize = getFileSize(p)
let ext = p.splitFile.ext let extPos = searchExtPos(p)
let mimetype = mimes.getMimetype( let mimetype = mimes.getMimetype(
if ext.len > 0: ext[1 .. ^1] if extPos >= 0: p.substr(extPos + 1)
else: "" else: "")
)
if fileSize < 10_000_000: # 10 mb if fileSize < 10_000_000: # 10 mb
var file = readFile(p) var file = readFile(p)
var hashed = getMD5(file) var hashed = getMD5(file)
# If the user has a cached version of this file and it matches our # If the user has a cached version of this file and it matches our
# version, let them use it # version, let them use it
if req.headers.hasKey("If-None-Match") and req.headers["If-None-Match"] == hashed: if req.headers.getOrDefault("If-None-Match") == hashed:
await req.statusContent(Http304, "", none[RawHeaders]()) await req.statusContent(Http304, "", default(RawHeaders))
else: else:
await req.statusContent(Http200, file, some(@({ await req.statusContent(Http200, file, @{
"Content-Type": mimetype, "Content-Type": mimetype,
"ETag": hashed "ETag": hashed
}))) })
else: else:
let headers = @({ let headers = @{
"Content-Type": mimetype, "Content-Type": mimetype,
"Content-Length": $fileSize "Content-Length": $fileSize
}) }
await req.statusContent(Http200, "", some(headers)) await req.statusContent(Http200, "", headers)
var fileStream = newFutureStream[string]("sendStaticIfExists") var fileStream = newFutureStream[string]("sendStaticIfExists")
var file = openAsync(p, fmRead) var file = openAsync(p, fmRead)
# Let `readToStream` write file data into fileStream in the # Let `readToStream` write file data into fileStream in the
@ -80,38 +72,19 @@ proc sendStaticIfExists(
else: else:
break break
file.close() file.close()
return return
# If we get to here then no match could be found. # If we get to here then no match could be found.
return Http404 return Http404
proc handleFileRequest(req: Request): Future[HttpCode] {.async.} =
proc handleFileRequest(
req: Request
): Future[HttpCode] {.async.} =
# Find static file. # Find static file.
var reqPath = cgi.decodeUrl(req.url.path) var reqPath = cgi.decodeUrl(req.url.path)
var publicUrl = getEnv("publicUrl") var staticDir = getEnv("staticDir") # it's assumed a relative dir
var staticDir = getEnv("staticDir")
if not publicUrl.endsWith("/"):
publicUrl = publicUrl & "/"
reqPath = reqPath.substr(publicUrl.len)
let path = normalizedPath(
staticDir / reqPath
)
# Verify that this isn't outside our static dir.
var status = Http400 var status = Http400
let pathDir = path.splitFile.dir / "" var path = staticDir / reqPath
normalizePathEnd(path, false)
if pathDir.startsWith(publicUrl):
if dirExists(path): if dirExists(path):
status = await sendStaticIfExists( status = await sendStaticIfExists(req, @[path / "index.html", path / "index.htm"])
req,
@[path / "index.html", path / "index.htm"]
)
else: else:
status = await sendStaticIfExists(req, @[path]) status = await sendStaticIfExists(req, @[path])
return status return status
@ -119,10 +92,12 @@ proc handleFileRequest(
proc handleWs(req: Request) {.async.} = proc handleWs(req: Request) {.async.} =
var ws = await newWebSocket(req) var ws = await newWebSocket(req)
await ws.send("Welcome to simple echo server") await ws.send("Welcome to simple echo server")
var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable var files: Table[string, Time] = {"path": getLastModificationTime(".")}.toTable
let watchedFiles = [absolutePath "app.js", absolutePath "app.html"] let watchedFiles = [absolutePath "app.js", absolutePath "app.html"]
for path in watchedFiles: for path in watchedFiles:
files[path] = getLastModificationTime(path) files[path] = getLastModificationTime(path)
while ws.readyState == Open: while ws.readyState == Open:
await sleepAsync(500) await sleepAsync(500)
var changed = false var changed = false
@ -134,17 +109,13 @@ proc handleWs(req: Request) {.async.} =
await ws.send("refresh") await ws.send("refresh")
changed = false changed = false
proc serveStatic*() = proc serveStatic*() =
if fileExists("static.env"): if fileExists("static.env"):
var env: DotEnv var env: DotEnv
env = initDotEnv(getCurrentDir(), "static.env") env = initDotEnv(getCurrentDir(), "static.env")
env.overload() env.overload()
else: else:
loadEnvFromString(""" putEnv("staticDir", "assets/")
staticDir="./src/assets/"
publicUrl="public"
""")
var server = newAsyncHttpServer() var server = newAsyncHttpServer()
proc cb(req: Request) {.gcsafe, async.} = proc cb(req: Request) {.gcsafe, async.} =
@ -155,9 +126,9 @@ proc serveStatic*() =
elif req.url.path == "/app.js": elif req.url.path == "/app.js":
let file = absolutePath("app" & ".js") let file = absolutePath("app" & ".js")
if not file.fileExists: if not file.fileExists:
logging.error("$1 not exists!" % file ) error(file, " does not exist!")
if fpUserRead notin os.getFilePermissions(file): if fpUserRead notin os.getFilePermissions(file):
logging.error("Could not read $1!" % file ) error("Could not read ", file, "!")
await req.respond(Http200, readFile(file)) await req.respond(Http200, readFile(file))
else: else:
let status = await handleFileRequest(req) let status = await handleFileRequest(req)