UNPKG

9.46 kBJavaScriptView Raw
1
2// AAAAAAAABBBBBBBBCCCCCCCC
3// aaaaaabbbbbbccccccdddddd
4// 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;
5// .timer on
6// Run Time: real 0.001 user 0.000069 sys 0.000079
7
8var crypto = require("crypto")
9, log = require("../lib/log")("app:hello")
10, util = require("../lib/util")
11, digests = [ "sha1", "sha256" ]
12, defaults = {
13 challenges: 'Bearer realm="app"',
14 hello: "/hello",
15 authType: "S",
16 devCookie: {
17 name: "a",
18 ttl: 5 * 365 * 24 * 60 * 60,
19 httpOnly: true
20 },
21 sesCookie: {
22 name: "b",
23 httpOnly: true
24 },
25 pepper: Buffer.from("Lcv9WvIPieSVuEYS", "base64"),
26 cluster: 0,
27 digest: "sha256",
28 bytes: 21,
29 iterations: 2
30}
31
32
33module.exports = function createHello(_opts) {
34 var hello = require(".")(Object.assign({}, defaults, _opts))
35 , opts = hello.options
36
37 opts.digestId = digests.indexOf(opts.digest)
38 opts.byteTimes = 0|(opts.bytes/3)
39
40 if (opts.digestId < 0) {
41 log.error("Invalid digest %s, should be one of: %s", opts.digest, digests)
42 throw Error("Invalid digest " + opts.digest)
43 }
44
45 opts.bytes = (1 + opts.byteTimes) * 3
46 opts.hashType = (opts.digestId << 4) | opts.byteTimes
47
48 hello.findHash = findHash
49
50 opts.devCookie.path = opts.sesCookie.path = opts.hello
51
52 return hello
53 .get(opts.hello, helloMw)
54 .put(opts.hello, helloMw)
55 .use(authMw)
56 .del(opts.hello, delMw)
57
58 function findHash(source, next) {
59 getByHash("device", source, opts, function(err, map, row) {
60 if (err) return next(true)
61 if (map.window) {
62 opts.db.get(
63 "SELECT l.id,l.user,w.session " +
64 "FROM window AS w,session AS s " +
65 "LEFT JOIN login AS l ON l.deleted IS NULL AND l.window=w.id " +
66 "WHERE s.id=w.session AND w.id=? AND s.device=?",
67 [map.window, map.device],
68 function(err, row) {
69 if (row) {
70 map.login = row.id
71 map.user = row.user
72 map.session = row.session
73 } else {
74 map.window = null
75 }
76 next(null, map)
77 }
78 )
79 } else {
80 next(null, map, row)
81 }
82 })
83 }
84}
85
86function helloMw(req, res, next, opts) {
87 var cookie
88 , hello = this
89 , body = req.body || {}
90 , time = req.date.getTime()
91
92 if (!body.user && body.email) {
93 body.user = body.email
94 }
95
96 // opts.db.text(".schema window_log_", function(err, text) { console.log("SCHEMA", text) })
97
98 hello.findHash(req.cookie(opts.devCookie), function(err, map) {
99 if (err) createDevice({})
100 else testSession(map)
101 })
102
103 function testSession(map) {
104 getById("session", testWindow, createSession, req.cookie(opts.sesCookie), map)
105 }
106
107 function testWindow(map, sess) {
108 if (sess.device !== map.device) return createSession(map)
109 if (body.a !== void 0) {
110 getById("window", testLogin, createWindow, body.a, map)
111 } else if (body.b !== void 0) {
112 createWindow(map)
113 } else {
114 opts.db.get(
115 "SELECT id FROM window WHERE session=? AND deleted IS NULL ORDER BY id DESC LIMIT 1",
116 [sess.id],
117 function(err, row) {
118 if (!row) return createWindow(map)
119 map.window = row.id
120 testLogin(map, row)
121 }
122 )
123 }
124 }
125
126 function testLogin(map) {
127 if (body.user != null) {
128 return hello.emit("auth", req, res, map, function() {
129 createLogin(map)
130 })
131 }
132 if (!map.window) {
133 return respond(map)
134 }
135 if (body.b !== void 0) {
136 }
137 opts.db.get("SELECT id,user FROM login WHERE window=? AND deleted IS NULL", [map.window], function(err, row) {
138 if (row) {
139 map.login = row.id
140 map.user = row.user
141 return respond(map)
142 }
143 opts.db.all("SELECT user FROM remember WHERE device=?", [map.device], function(err, rows) {
144 if (rows.length === 1 && rows[0]) {
145 map.user = rows[0].user
146 map.loginType = "remember"
147 return createLogin(map)
148 }
149 if (rows.length > 1) {
150 map.remember = rows
151 }
152 respond(map)
153 })
154 })
155 }
156
157 function createDevice(map) {
158 sub("device_ua", req.headers["user-agent"], function setHash(ua, errCount) {
159 errCount = errCount > 0 ? errCount + 1 : 1
160 if (errCount > 6) throw "to many errors"
161 crypto.randomBytes(opts.bytes, function(err, buf) {
162 if (err) {
163 log.error(err)
164 return setHash(ua, errCount)
165 }
166 buf[1] = opts.cluster
167 buf[4] = opts.hashType
168 buf[7] = opts.iterations // TODO:2019-02-11:lauri:Make it random above conf
169 crypto.pbkdf2(buf, opts.pepper, buf[7], opts.bytes, opts.digest, function(err, buf2) {
170 if (err) {
171 log.error(err)
172 return setHash(ua, errCount)
173 }
174 opts.db.insert("device(ua,hash)", [ua, buf2], function(err, row) {
175 if (err) {
176 log.error(err)
177 return setHash(ua, errCount)
178 }
179 log.debug("device created: %s", buf.toString("base64"))
180 res.cookie(opts.devCookie, cookie = buf.toString("base64"))
181 map.device = row.lastId
182 map.buf = buf
183 createSession(map)
184 })
185 })
186 })
187 })
188 }
189
190 function createSession(map) {
191 // opts.db.run("DELETE FROM session WHERE device=?", [map.device])
192 opts.db.run(
193 "UPDATE login SET deleted=? WHERE deleted IS NULL AND window IN " +
194 "(SELECT w.id FROM window AS w,session AS s WHERE s.id=w.session AND s.device=?)",
195 [time, map.device]
196 )
197 opts.db.run(
198 "UPDATE window SET deleted=? WHERE deleted IS NULL AND session IN (SELECT id FROM session WHERE device=?)",
199 [time, map.device]
200 )
201 opts.db.insert("session(device,ip)", [map.device, util.ip2buf(req.ip)], function(err, row) {
202 if (err) throw err
203 log.debug("session created: %s", row.lastId)
204 res.cookie(opts.sesCookie, row.lastId.toString(32))
205 map.session = row.lastId
206 createWindow(map)
207 })
208 }
209
210 function createWindow(map) {
211 var opener = (
212 body.b && parseInt(body.b, 32) ||
213 body.c !== req.headers.referer && body.c ||
214 null
215 )
216 opts.db.insert("window(created,session,opener)", [time, map.session, opener], function(err, row) {
217 log.debug("window created: %s from %s", row.lastId, opener)
218 map.window = row.lastId
219 testLogin(map)
220 })
221 }
222
223 function createLogin(map) {
224 opts.db.run("UPDATE login SET deleted=? WHERE window=? AND deleted IS NULL", [time, map.window])
225 if (!map.user) return respond(map)
226 sub("login_type", map.loginType, function(type) {
227 opts.db.insert("login(created,window,type,user)", [time, map.window, type, map.user], function(err, row) {
228 log.debug("login created: %s, %s", row.lastId, !!body.remember)
229 map.login = row.lastId
230 if (body.remember) {
231 opts.db.insert("remember(device,user,login)", [map.device, map.user, map.login])
232 }
233 respond(map)
234 })
235 })
236 }
237
238 function respond(map) {
239 var out = {}
240 , buf = Buffer.alloc(map.buf.length + 6, map.buf)
241 buf.writeUIntLE(map.window, map.buf.length, 6)
242 out.authorization = opts.authType + " " + buf.toString("base64").replace(/(?:AA)+$/g, "")
243
244 if (!body.a || parseInt(body.a, 32) !== map.window) {
245 out.a = map.window.toString(32)
246 }
247 hello.emit("result", req, res, out, map)
248 }
249
250 function sub(table, text, next) {
251 if (!text) return next(null)
252 opts.db.get("SELECT id FROM ? WHERE text=?", [table, text], function(err, row) {
253 if (row) return next(row.id)
254 opts.db.insert(table + "(text)", [text], function(err, row) {
255 next(row.lastId)
256 })
257 })
258 }
259
260 function getById(table, resolve, reject, source, map) {
261 var id = typeof source === "string" && parseInt(source, 32)
262 if (!id) return reject(map)
263 opts.db.get("SELECT * FROM ? WHERE id=?", [table, id], function(err, row) {
264 if (!row) return reject(map)
265 map[table] = row.id
266 resolve(map, row)
267 })
268 }
269}
270
271
272/*
273 * Authentication - login + password (who you are)
274 * Authorization - permissions (what you are allowed to do)
275 * Accounting - consumed resources (session statistics and usage for
276 * authorization control, billing, trend analysis,
277 * resource utilization, and capacity planning activities)
278 */
279
280function authMw(req, res, next, opts) {
281 var hello = this
282 , auth = req.headers["authorization"]
283 req.session = {}
284
285 if (auth !== void 0) {
286 // Authorization: <type> <credentials>
287 // Authorization: OAuth realm="Example", oauth_token="ad180jjd733klru7", oauth_version="1.0"
288 auth = auth.split(/\s+/)
289
290 if (auth[0] === "Basic") {
291 auth[1] = Buffer.from(auth[1], "base64").toString()
292 }
293 if (auth.length !== 2 || hello.emit("auth:" + auth[0], req, res, next, auth[1]) === 0) {
294 res.setHeader("WWW-Authenticate", opts.challenges)
295 res.sendStatus(401)
296 }
297 } else {
298 next()
299 }
300}
301
302function delMw(req, res, next, opts) {
303 var hello = this
304 , map = req.authMap
305 , time = req.date.getTime()
306
307 if (map && map.login) {
308 log.debug("logout %s", map.login)
309 hello.emit("logout", map)
310 opts.db.run("UPDATE login SET deleted=? WHERE id=?", [time, map.login])
311 opts.db.run("DELETE FROM remember WHERE device=? AND user=?", [map.device, map.user])
312 }
313 res.sendStatus(204)
314}
315
316function getByHash(table, source, opts, next) {
317 if (!source) return next(true)
318 var map = {}
319 , buf = map.buf = Buffer.from(source, "base64")
320 , digest = map.buf[4] >>> 4
321 , bytes = 3 * (1 + (map.buf[4] & 0xf))
322 , extra = map.buf.length - bytes
323
324 if (extra < 0 || extra > 6) {
325 return next(true)
326 }
327
328 if (extra > 0) {
329 map.window = buf.readUIntLE(bytes, extra)
330 buf = map.buf = map.buf.slice(0, bytes)
331 }
332
333 crypto.pbkdf2(buf, opts.pepper, buf[7], bytes, digests[digest], function(err, buf) {
334 opts.db.get("SELECT id FROM ? WHERE hash=?", [table, buf], function(err, row) {
335 if (!row) return next(true)
336 map[table] = row.id
337 next(null, map, row)
338 })
339 })
340}
341