litejs
Version:
Full-stack web framework in a tiny package
546 lines (477 loc) • 15.6 kB
JavaScript
var defaultOpts = {
accept: {
"application/json;filename=;select=;space=": function(data, negod) {
return JSON.stringify(
data,
negod.select ? negod.select.split(",") : null,
+negod.space || negod.space
)
},
// RFC 4180 optional parameters: charset, header
"text/csv;br=\"\r\n\";delimiter=\",\";fields=;filename=;header=;NULL=;select=": require("./csv.js").encode,
"application/sql;fields=;filename=;NULL=NULL;select=;table=table": function(data, negod) {
negod.re = /\D/
negod.br = "),\n("
negod.prefix = "INSERT INTO " +
negod.table + (negod.fields ? " (" + negod.fields + ")" : "") + " VALUES ("
negod.postfix = ");"
return require("./csv.js").encode(data, negod)
}
},
catch: sendError,
charset: "UTF-8",
compress: {
"br": "createBrotliCompress",
"deflate;q=0.1": "createDeflate",
"gzip;q=0.2": "createGzip"
},
error: {
"URIError": { code: 400 }
},
exitTime: 5000,
ipHeader: "x-forwarded-for",
protoHeader: "x-forwarded-proto",
log: console,
maxURILength: 2000,
method: {
DELETE: "del",
GET: "get",
PATCH: "patch",
POST: "post",
PUT: "put"
},
mime: {
css: "text/css",
csv: "text/csv",
cur: "image/vnd.microsoft.icon",
doc: "application/msword",
epub: "application/epub+zip",
gif: "image/gif",
gz: "application/x-gzip",
htm: "text/html",
html: "text/html",
ico: "image/x-icon",
jar: "application/java-archive",
jpeg: "image/jpeg",
jpg: "image/jpeg",
js: "text/javascript",
json: "application/json",
m3u: "audio/x-mpegurl",
manifest: "text/cache-manifest",
midi: "audio/midi",
mjs: "text/javascript",
mp3: "audio/mpeg",
mp4: "video/mp4",
mpeg: "video/mpeg",
mpg: "video/mpeg",
mpga: "audio/mpeg",
pdf: "application/pdf",
pgp: "application/pgp",
png: "image/png",
ppz: "application/vnd.ms-powerpoint",
ps: "application/postscript",
rar: "application/x-rar-compressed",
rtf: "text/rtf",
sh: "application/x-sh",
sql: "application/sql",
svg: "image/svg+xml",
tgz: "application/x-tar-gz",
tiff: "image/tiff",
ttf: "font/ttf",
txt: "text/plain",
wav: "audio/x-wav",
weba: "audio/webm",
webm: "video/webm",
webp: "image/webp",
woff2: "font/woff2",
woff: "font/woff",
xls: "application/vnd.ms-excel",
xlw: "application/vnd.ms-excel",
xml: "text/xml",
zip: "application/zip"
},
rangeSize: 500 * 1024,
status: Object.assign({}, require("http").STATUS_CODES),
http: {
port: 8080
}
}
, fs = require("fs")
, content = require("./content")
, event = require("./event")
, util = require("./util")
, cookieRe = /[^!#-~]|[%,;\\]/g
, rangeRe = /^bytes=(\d*)-(\d*)$/
, tmpDate = new Date()
module.exports = createServer
createServer.getCookie = getCookie
createServer.setCookie = setCookie
function createServer(opts) {
opts = app.opts = util.deepAssign({defaults: defaultOpts}, defaultOpts, opts)
var uses = []
event.asEmitter(app)
if (!opts._accept) opts._accept = require("./accept").accept(opts.accept)
if (!opts._compress) opts._compress = opts.compress && require("./accept").accept(opts.compress)
Object.keys(opts.method).forEach(function(method) {
app[opts.method[method]] = use.bind(app, method)
})
Object.keys(opts.status).forEach(function(code) {
if (code > 399 && !opts.error[opts.status[code]]) opts.error[opts.status[code]] = { code: +code }
})
app.listen = listen
app.use = use.bind(app, null)
return app
function app(req, res, _next) {
var oldPath, oldUrl
, tryCatch = true
, usePos = 0
, forwarded = req.headers[opts.ipHeader]
, reqMethod = req.method === "HEAD" ? "GET" : req.method
, socket = req.socket || {}
if (!res.send) {
req.date = new Date()
req.ip = forwarded ? forwarded.trim().split(/[\s,]+/)[0] : socket.remoteAddress
req.opts = res.opts = opts
req.protocol = forwarded && trustedProxy(socket.remoteAddress) ? req.headers[opts.protoHeader] : socket.encrypted ? "https" : "http"
req.publicUrl = opts.publicUrl || req.protocol + "://" + req.headers.host
req.res = res
req.secure = req.protocol === "https"
res.isHead = req.method === "HEAD"
res.req = req
res.send = send
res.sendStatus = sendStatus
// IE8-10 accept 2083 chars in URL
// Sitemaps protocol has a limit of 2048 characters in a URL
// Google SERP tool wouldn't cope with URLs longer than 1855 chars
if (req.url.length > opts.maxURILength) {
return opts.catch("URI Too Long", req, res, opts)
}
req.content = content
req.cookie = getCookie
req.originalUrl = req.url
res.cookie = setCookie
res.link = setLink
res.sendFile = sendFile
}
next()
function next(err) {
if (err) {
return opts.catch(err, req, res, opts)
}
var method = uses[usePos]
, path = uses[usePos + 1]
, pos = usePos += 3
if (method && method !== reqMethod || path && path !== req.url.slice(0, path.length)) {
next()
} else if (uses[pos - 1] === void 0) {
if (typeof _next === "function") {
_next()
} else {
opts.catch(404, req, res, opts)
}
} else {
method = uses[pos - 1]
if (path) {
oldPath = req.baseUrl
oldUrl = req.url
req.baseUrl = path
req.url = req.url === path ? "/" : req.url.slice(path.slice(-1) === "/" ? path.length - 1 : path.length)
}
if (tryCatch === true) {
tryCatch = false
try {
method.call(app, req, res, path ? nextPath : next, opts)
} catch(e) {
return opts.catch(e, req, res, opts)
}
} else {
method.call(app, req, res, path ? nextPath : next, opts)
}
if (pos === usePos) {
tryCatch = true
}
}
}
function nextPath(e) {
req.baseUrl = oldPath
req.url = oldUrl
next(e)
}
}
function use(method, path) {
var arr = Array.from(arguments)
, len = arr.length
, i = 2
if (typeof path === "function") {
path = null
i = 1
}
for (; i < len; ) {
if (typeof arr[i] !== "function") throw Error("Not a function")
uses.push(method, path, arr[i++])
}
return app
}
function trustedProxy(ip) {
return !Array.isArray(opts.trustProxy) || opts.trustProxy.some(util.ipInNet.bind(null, ip))
}
}
function send(body, opts_) {
var tmp
, res = this
, reqHead = res.req.headers
, resHead = {}
, negod = res.opts._accept(reqHead.accept || reqHead["content-type"] || "*/*")
, opts = Object.assign({
statusCode: res.statusCode,
mimeType: negod.rule
}, res.opts, negod, opts_)
, outStream = opts.stream || res
if (!negod.match) {
return res.sendStatus(406) // Not Acceptable
}
tmp = util.num(opts.cache && opts.sendfile && opts.cache[opts.sendfile], opts.maxAge)
if (typeof tmp === "number") {
// max-age=N is relative to the time of the request
resHead["Cache-Control"] = tmp > 0 ? "public, max-age=" + (0|(tmp/1000)) : "no-cache, max-age=0"
}
if (opts.mtime && opts.mtime > Date.parse(reqHead["if-modified-since"])) {
return res.sendStatus(304)
}
if (typeof body !== "string") {
negod.select = opts && opts.select || res.req.url.split("$select")[1] || ""
body = negod.o(body, negod)
}
resHead["Content-Type"] = opts.mimeType + (
opts.charset && opts.mimeType.slice(0, 5) === "text/" ? "; charset=" + opts.charset : ""
)
if (opts.size >= 0) {
resHead["Content-Length"] = opts.size
if (opts.size > opts.rangeSize) {
resHead["Accept-Ranges"] = "bytes"
if ((tmp = reqHead.range && !reqHead["if-range"] && rangeRe.exec(reqHead.range))) {
opts.start = tmp[1] ? +tmp[1] : tmp[2] ? opts.size - tmp[2] - 1 : 0
opts.end = tmp[1] && tmp[2] ? +tmp[2] : opts.size - 1
if (opts.start > opts.end || opts.end >= opts.size) {
opts.start = 0
opts.end = opts.size - 1
} else {
opts.statusCode = 206
resHead["Content-Length"] = opts.end - opts.start
resHead["Content-Range"] = "bytes " + opts.start + "-" + opts.end + "/" + opts.size
}
}
}
}
if (opts.filename) {
resHead["Content-Disposition"] = "attachment; filename=" + (
typeof opts.filename === "function" ? opts.filename() : opts.filename
)
}
negod = opts._compress && opts._compress(reqHead["accept-encoding"])
if (negod && negod.match) {
// Server may choose not to compress the body, if:
// - data is already compressed (some image format)
// - server is overloaded and cannot afford the computational overhead.
// Microsoft recommends not to compress if a server uses more than 80% of its computational power.
delete resHead["Content-Length"]
resHead["Content-Encoding"] = negod.match
resHead.Vary = "Accept-Encoding"
outStream = typeof negod.o === "string" ? require("zlib")[negod.o]() : negod.o()
outStream.pipe(res)
}
if (opts.headers) Object.assign(resHead, opts.headers["*"], opts.headers[res.req.originalUrl])
res.writeHead(opts.statusCode || 200, resHead)
if (res.isHead) {
return res.end()
}
if (opts.sendfile) {
fs.createReadStream(opts.sendfile, {start: opts.start, end: opts.end}).pipe(outStream)
} else {
outStream.end(body)
}
}
function sendFile(file, opts_, next_) {
var res = this
, opts = typeof opts_ === "function" ? (next_ = opts_) && {} : opts_ || {}
, next = typeof next_ === "function" ? next_ : function(code) {
sendStatus.call(res, code)
}
fs.stat(file, function(err, stat) {
if (err) return next(404)
if (stat.isDirectory()) return next(403)
opts.mtime = stat.mtime
opts.size = stat.size
opts.filename = opts.download === true ? file.split("/").pop() : opts.download
opts.mimeType = res.opts.mime[ file.split(".").pop() ] || "application/octet-stream"
opts.sendfile = file
send.call(res, file, opts)
})
}
function sendStatus(code, message) {
var res = this
res.statusCode = code
if (code > 199 && code !== 204 && code !== 205 && code !== 304) {
message = (message || res.opts.status[code] || code) + "\n"
res.setHeader("Content-Length", message.length)
res.setHeader("Content-Type", "text/plain")
if (!res.isHead) {
res.write(message)
}
}
res.end()
}
function sendError(e, req, res, opts) {
var map = typeof e === "string" ? opts.error[e] || { message: e } : typeof e === "number" ? { code: e } : e
, error = {
id: Math.random().toString(36).slice(2, 10),
time: req.date,
code: map.code || e.code || 500
}
res.statusCode = error.code
res.statusMessage = error.message = opts.status[error.code] || "Error " + error.code
res.send(error)
opts.log.error(
(Array.isArray(e.stack) ? e.stack.join("\n") : e.stack || (e.name || "Error") + ": " + error.message).replace("Error:", "Error:" + error.id)
)
}
function setLink(rel, url) {
var res = this
, existing = res.getHeader("link") || []
if (!Array.isArray(existing)) {
existing = [ existing ]
}
existing.push("<" + encodeURI(url) + ">; rel=\"" + rel + "\"")
res.setHeader("Link", existing)
}
function listen() {
var exiting
, server = this
, opts = server.opts
process.on("uncaughtException", function(e) {
if (opts.log) opts.log.error(
"\nUNCAUGHT EXCEPTION!\n" +
(Array.isArray(e.stack) ? e.message + "\n" + e.stack.join("\n") : e.stack || (e.name || "Error") + ": " + (e.message || e))
)
else throw e
;(opts.exit || exit).call(server, 1)
})
process.on("SIGINT", function() {
if (exiting) {
opts.log.info("\nKilling from SIGINT (got Ctrl-C twice)")
return process.exit()
}
exiting = true
opts.log.info("\nGracefully shutting down from SIGINT (Ctrl-C)")
;(opts.exit || exit).call(server, 0)
})
process.on("SIGTERM", function() {
opts.log.info("Gracefully shutting down from SIGTERM (kill)")
;(opts.exit || exit).call(server, 0)
})
process.on("SIGHUP", function() {
opts.log.info("Reloading configuration from SIGHUP")
server.listen(true)
server.emit("reload")
})
server.listen = opts.listen || function() {
["http", "https", "http2"].forEach(createNet)
}
server.listen()
return server
function createNet(proto) {
if (server["_" + proto]) server["_" + proto].close()
var map = opts[proto]
if (!map || !map.port) return
var net = server["_" + proto] = (
proto === "http" ?
require(proto).createServer(map.redirect ? forceHttps : server) :
require(proto).createSecureServer(map, map.redirect ? forceHttps : server)
)
.listen(map.port, map.host || "0.0.0.0", function() {
var addr = this.address()
opts.log.info("Listen %s at %s:%s", proto, addr.address, addr.port)
this.on("close", function() {
opts.log.info("Stop listening %s at %s:%s", proto, addr.address, addr.port)
})
if (map.noDelay) this.on("connection", setNoDelay)
})
if (map.sessionReuse) {
var sessionStore = {}
net
.on("newSession", function(id, data, cb) {
sessionStore[id] = data
cb()
})
.on("resumeSession", function(id, cb) {
cb(null, sessionStore[id] || null)
})
}
}
function setNoDelay(socket) {
if (socket.setNoDelay) socket.setNoDelay(true)
}
function forceHttps(req, res) {
// Safari 5 and IE9 drop the original URI's fragment if a HTTP/3xx redirect occurs.
// If the Location header on the response specifies a fragment, it is used.
// IE10+, Chrome 11+, Firefox 4+, and Opera will all "reattach" the original URI's fragment after following a 3xx redirection.
var port = opts.https && opts.https.port || 8443
, host = (req.headers.host || "localhost").split(":")[0]
, url = "https://" + (port === 443 ? host : host + ":" + port) + req.url
res.writeHead(301, {"Content-Type": "text/html", "Location": url})
res.end("Redirecting to <a href=\"" + url + "\">" + url + "</a>")
}
function exit(code) {
var softKill = util.wait(function() {
opts.log.info("Everything closed cleanly")
process.exit(code)
}, 1)
server.emit("beforeExit", softKill)
try {
if (server._http) server._http.close(softKill.wait()).unref()
if (server._https) server._https.close(softKill.wait()).unref()
if (server._http2) server._http2.close(softKill.wait()).unref()
} catch(e) {}
setTimeout(function() {
opts.log.warn("Kill (timeout)")
process.exit(code)
}, opts.exitTime).unref()
softKill()
}
}
function setCookie(opts, value) {
var res = this
, existing = res.getHeader("set-cookie") || ""
, name = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
, cookie = (name + "=" + value).replace(cookieRe, encodeURIComponent) +
(opts.maxAge ? "; Expires=" + (tmpDate.setTime(opts.maxAge < 1 ? 0 : Date.now() + util.num(opts.maxAge)), tmpDate).toUTCString() : "") +
(opts.path ? "; Path=" + opts.path : "") +
(opts.domain ? "; Domain=" + opts.domain : "") +
(opts.secure ? "; Secure" : "") +
(opts.httpOnly ? "; HttpOnly" : "") +
(opts.sameSite ? "; SameSite=" + opts.sameSite : "")
if (Array.isArray(existing)) {
for (var i = existing.length; i--; ) if (overRideCookie(name, existing[i])) existing.splice(i, 1)
existing.push(cookie)
} else {
res.setHeader("Set-Cookie", overRideCookie(name, existing) ? cookie : [ existing, cookie ])
}
}
function overRideCookie(name, cookie) {
return !cookie || cookie.slice(0, name.length + 1) === name + "="
}
function getCookie(opts) {
var req = this
, name = (typeof opts === "string" ? (opts = { name: opts }) : opts).name
, junks = ("; " + req.headers.cookie).split("; " + name + "=")
if (junks.length > 2) {
req.res.setHeader("Clear-Site-Data", "\"cookies\"")
req.opts.log.warn("%s %s %s Cookie fixation: %s", req.ip, req.method, req.originalUrl, req.headers.cookie)
} else try {
junks = decodeURIComponent((junks[1] || "").split(";")[0])
if (!opts.re || opts.re.test(junks)) return junks
} catch(e) {
req.opts.log.warn("Invalid cookie '%s' in: %s", name, req.headers.cookie)
}
return ""
}