Refactor static_server (#169)
This commit is contained in:
parent
4c06cb3204
commit
6cff2d8b05
1 changed files with 51 additions and 80 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue