litejs
Version:
Single-page application framework
344 lines (305 loc) • 9.61 kB
JavaScript
// AAAAAAAABBBBBBBBCCCCCCCC
// aaaaaabbbbbbccccccdddddd
// INSERT INTO device(ua, hash) WITH RECURSIVE cnt(x) AS (VALUES(1) UNION ALL SELECT x+1 FROM cnt WHERE x<1000000) SELECT 123,randomblob(18) FROM cnt;
// .timer on
// Run Time: real 0.001 user 0.000069 sys 0.000079
var crypto = require("crypto")
, log = require("../lib/log")("app:hello")
, util = require("../lib/util")
, digests = [ "sha1", "sha256" ]
, defaults = {
challenges: 'Bearer realm="app"',
hello: "/hello",
authType: "S",
devCookie: {
name: "a",
ttl: 5 * 365 * 24 * 60 * 60,
httpOnly: true
},
sesCookie: {
name: "b",
httpOnly: true
},
pepper: Buffer.from("Lcv9WvIPieSVuEYS", "base64"),
cluster: 0,
digest: "sha256",
bytes: 21,
iterations: 2
}
module.exports = function createHello(_opts) {
var hello = require(".")(Object.assign({}, defaults, _opts))
, opts = hello.options
opts.digestId = digests.indexOf(opts.digest)
opts.byteTimes = 0|(opts.bytes/3)
if (opts.digestId < 0) {
log.error("Invalid digest %s, should be one of: %s", opts.digest, digests)
throw Error("Invalid digest " + opts.digest)
}
opts.bytes = (1 + opts.byteTimes) * 3
opts.hashType = (opts.digestId << 4) | opts.byteTimes
hello.findHash = findHash
opts.devCookie.path = opts.sesCookie.path = opts.hello
return hello
.get(opts.hello, helloMw)
.put(opts.hello, helloMw)
.use(authMw)
.del(opts.hello, delMw)
function findHash(source, next) {
getByHash("device", source, opts, function(err, map, row) {
if (err) return next(true)
if (map.window) {
opts.db.get(
"SELECT l.id,l.user,w.session " +
"FROM window AS w,session AS s " +
"LEFT JOIN login AS l ON l.deleted IS NULL AND l.window=w.id " +
"WHERE s.id=w.session AND w.id=? AND s.device=?",
[map.window, map.device],
function(err, row) {
if (row) {
map.login = row.id
map.user = row.user
map.session = row.session
} else {
map.window = null
}
next(null, map)
}
)
} else {
next(null, map, row)
}
})
}
}
function helloMw(req, res, next, opts) {
var cookie
, hello = this
, body = req.body || {}
, time = req.date.getTime()
if (!body.user && body.email) {
body.user = body.email
}
// opts.db.text(".schema window_log_", function(err, text) { console.log("SCHEMA", text) })
hello.findHash(req.cookie(opts.devCookie), function(err, map) {
if (err) createDevice({})
else testSession(map)
})
function testSession(map) {
getById("session", testWindow, createSession, req.cookie(opts.sesCookie), map)
}
function testWindow(map, sess) {
if (sess.device !== map.device) return createSession(map)
if (body.a !== void 0) {
getById("window", testLogin, createWindow, body.a, map)
} else if (body.b !== void 0) {
createWindow(map)
} else {
opts.db.get(
"SELECT id FROM window WHERE session=? AND deleted IS NULL ORDER BY id DESC LIMIT 1",
[sess.id],
function(err, row) {
if (!row) return createWindow(map)
map.window = row.id
testLogin(map, row)
}
)
}
}
function testLogin(map, win) {
if (win.session !== map.session) return createWindow(map)
if (body.user != null) {
return hello.emit("auth", req, res, map, function() {
createLogin(map)
})
}
if (!map.window) {
return respond(map)
}
if (body.b !== void 0) {
}
opts.db.get("SELECT id,user FROM login WHERE window=? AND deleted IS NULL", [map.window], function(err, row) {
if (row) {
map.login = row.id
map.user = row.user
return respond(map)
}
opts.db.all("SELECT user FROM remember WHERE device=?", [map.device], function(err, rows) {
if (rows.length === 1 && rows[0]) {
map.user = rows[0].user
map.loginType = "remember"
return createLogin(map)
}
if (rows.length > 1) {
map.remember = rows
}
respond(map)
})
})
}
function createDevice(map) {
sub("device_ua", req.headers["user-agent"], function setHash(ua, errCount) {
errCount = errCount > 0 ? errCount + 1 : 1
if (errCount > 6) throw "to many errors"
crypto.randomBytes(opts.bytes, function(err, buf) {
if (err) {
log.error(err)
return setHash(ua, errCount)
}
buf[1] = opts.cluster
buf[4] = opts.hashType
buf[7] = opts.iterations // TODO:2019-02-11:lauri:Make it random above conf
crypto.pbkdf2(buf, opts.pepper, buf[7], opts.bytes, opts.digest, function(err, buf2) {
if (err) {
log.error(err)
return setHash(ua, errCount)
}
opts.db.insert("device(ua,hash)", [ua, buf2], function(err, row) {
if (err) {
log.error(err)
return setHash(ua, errCount)
}
log.debug("device created: %s", buf.toString("base64"))
res.cookie(opts.devCookie, cookie = buf.toString("base64"))
map.device = row.lastId
map.buf = buf
createSession(map)
})
})
})
})
}
function createSession(map) {
// opts.db.run("DELETE FROM session WHERE device=?", [map.device])
opts.db.run(
"UPDATE login SET deleted=? WHERE deleted IS NULL AND window IN " +
"(SELECT w.id FROM window AS w,session AS s WHERE s.id=w.session AND s.device=?)",
[time, map.device]
)
opts.db.run(
"UPDATE window SET deleted=? WHERE deleted IS NULL AND session IN (SELECT id FROM session WHERE device=?)",
[time, map.device]
)
opts.db.insert("session(device,ip)", [map.device, util.ip2buf(req.ip)], function(err, row) {
if (err) throw err
log.debug("session created: %s", row.lastId)
res.cookie(opts.sesCookie, row.lastId.toString(32))
map.session = row.lastId
createWindow(map)
})
}
function createWindow(map) {
var opener = (
body.b && parseInt(body.b, 32) ||
body.c !== req.headers.referer && body.c ||
null
)
opts.db.insert("window(created,session,opener)", [time, map.session, opener], function(err, row) {
if (err) return log.error("createWindow", err)
log.debug("window created: %s from %s", row.lastId, opener)
map.window = row.lastId
testLogin(map, {id: map.window, session: map.session})
})
}
function createLogin(map) {
opts.db.run("UPDATE login SET deleted=? WHERE window=? AND deleted IS NULL", [time, map.window])
if (!map.user) return respond(map)
sub("login_type", map.loginType, function(type) {
opts.db.insert("login(created,window,type,user)", [time, map.window, type, map.user], function(err, row) {
log.debug("login created: %s, %s", row.lastId, !!body.remember)
map.login = row.lastId
if (body.remember) {
opts.db.insert("remember(device,user,login)", [map.device, map.user, map.login])
}
respond(map)
})
})
}
function respond(map) {
var out = {}
, buf = Buffer.alloc(map.buf.length + 6, map.buf)
buf.writeUIntLE(map.window, map.buf.length, 6)
out.authorization = opts.authType + " " + buf.toString("base64").replace(/(?:AA)+$/g, "")
if (!body.a || parseInt(body.a, 32) !== map.window) {
out.a = map.window.toString(32)
}
hello.emit("result", req, res, out, map)
}
function sub(table, text, next) {
if (!text) return next(null)
opts.db.get("SELECT id FROM ? WHERE text=?", [table, text], function(err, row) {
if (row) return next(row.id)
opts.db.insert(table + "(text)", [text], function(err, row) {
next(row.lastId)
})
})
}
function getById(table, resolve, reject, source, map) {
var id = typeof source === "string" && parseInt(source, 32)
if (!id) return reject(map)
opts.db.get("SELECT * FROM ? WHERE id=?", [table, id], function(err, row) {
if (!row) return reject(map)
map[table] = row.id
resolve(map, row)
})
}
}
/*
* Authentication - login + password (who you are)
* Authorization - permissions (what you are allowed to do)
* Accounting - consumed resources (session statistics and usage for
* authorization control, billing, trend analysis,
* resource utilization, and capacity planning activities)
*/
function authMw(req, res, next, opts) {
var hello = this
, auth = req.headers["authorization"]
req.session = {}
if (auth !== void 0) {
// Authorization: <type> <credentials>
// Authorization: OAuth realm="Example", oauth_token="ad180jjd733klru7", oauth_version="1.0"
auth = auth.split(/\s+/)
if (auth[0] === "Basic") {
auth[1] = Buffer.from(auth[1], "base64").toString()
}
if (auth.length !== 2 || hello.emit("auth:" + auth[0], req, res, next, auth[1]) === 0) {
res.setHeader("WWW-Authenticate", opts.challenges)
res.sendStatus(401)
}
} else {
next()
}
}
function delMw(req, res, next, opts) {
var hello = this
, map = req.authMap
, time = req.date.getTime()
if (map && map.login) {
log.debug("logout %s", map.login)
hello.emit("logout", map)
opts.db.run("UPDATE login SET deleted=? WHERE id=?", [time, map.login])
opts.db.run("DELETE FROM remember WHERE device=? AND user=?", [map.device, map.user])
}
res.sendStatus(204)
}
function getByHash(table, source, opts, next) {
if (!source) return next(true)
var map = {}
, buf = map.buf = Buffer.from(source, "base64")
, digest = map.buf[4] >>> 4
, bytes = 3 * (1 + (map.buf[4] & 0xf))
, extra = map.buf.length - bytes
if (extra < 0 || extra > 6) {
return next(true)
}
if (extra > 0) {
map.window = buf.readUIntLE(bytes, extra)
buf = map.buf = map.buf.slice(0, bytes)
}
crypto.pbkdf2(buf, opts.pepper, buf[7], bytes, digests[digest], function(err, buf) {
opts.db.get("SELECT id FROM ? WHERE hash=?", [table, buf], function(err, row) {
if (!row) return next(true)
map[table] = row.id
next(null, map, row)
})
})
}