UNPKG

9.61 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("../log")("server: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, win) {
127 if (win.session !== map.session) return createWindow(map)
128 if (body.user != null) {
129 return hello.emit("auth", req, res, map, function() {
130 createLogin(map)
131 })
132 }
133 if (!map.window) {
134 return respond(map)
135 }
136 if (body.b !== void 0) {
137 }
138 opts.db.get("SELECT id,user FROM login WHERE window=? AND deleted IS NULL", [map.window], function(err, row) {
139 if (row) {
140 map.login = row.id
141 map.user = row.user
142 return respond(map)
143 }
144 opts.db.all("SELECT user FROM remember WHERE device=?", [map.device], function(err, rows) {
145 if (rows.length === 1 && rows[0]) {
146 map.user = rows[0].user
147 map.loginType = "remember"
148 return createLogin(map)
149 }
150 if (rows.length > 1) {
151 map.remember = rows
152 }
153 respond(map)
154 })
155 })
156 }
157
158 function createDevice(map) {
159 sub("device_ua", req.headers["user-agent"], function setHash(ua, errCount) {
160 errCount = errCount > 0 ? errCount + 1 : 1
161 if (errCount > 6) throw "to many errors"
162 crypto.randomBytes(opts.bytes, function(err, buf) {
163 if (err) {
164 log.error(err)
165 return setHash(ua, errCount)
166 }
167 buf[1] = opts.cluster
168 buf[4] = opts.hashType
169 buf[7] = opts.iterations // TODO:2019-02-11:lauri:Make it random above conf
170 crypto.pbkdf2(buf, opts.pepper, buf[7], opts.bytes, opts.digest, function(err, buf2) {
171 if (err) {
172 log.error(err)
173 return setHash(ua, errCount)
174 }
175 opts.db.insert("device(ua,hash)", [ua, buf2], function(err, row) {
176 if (err) {
177 log.error(err)
178 return setHash(ua, errCount)
179 }
180 log.debug("device created: %s", buf.toString("base64"))
181 res.cookie(opts.devCookie, cookie = buf.toString("base64"))
182 map.device = row.lastId
183 map.buf = buf
184 createSession(map)
185 })
186 })
187 })
188 })
189 }
190
191 function createSession(map) {
192 // opts.db.run("DELETE FROM session WHERE device=?", [map.device])
193 opts.db.run(
194 "UPDATE login SET deleted=? WHERE deleted IS NULL AND window IN " +
195 "(SELECT w.id FROM window AS w,session AS s WHERE s.id=w.session AND s.device=?)",
196 [time, map.device]
197 )
198 opts.db.run(
199 "UPDATE window SET deleted=? WHERE deleted IS NULL AND session IN (SELECT id FROM session WHERE device=?)",
200 [time, map.device]
201 )
202 opts.db.insert("session(device,ip)", [map.device, util.ip2buf(req.ip)], function(err, row) {
203 if (err) throw err
204 log.debug("session created: %s", row.lastId)
205 res.cookie(opts.sesCookie, row.lastId.toString(32))
206 map.session = row.lastId
207 createWindow(map)
208 })
209 }
210
211 function createWindow(map) {
212 var opener = (
213 body.b && parseInt(body.b, 32) ||
214 body.c !== req.headers.referer && body.c ||
215 null
216 )
217 opts.db.insert("window(created,session,opener)", [time, map.session, opener], function(err, row) {
218 if (err) return log.error("createWindow", err)
219 log.debug("window created: %s from %s", row.lastId, opener)
220 map.window = row.lastId
221 testLogin(map, {id: map.window, session: map.session})
222 })
223 }
224
225 function createLogin(map) {
226 opts.db.run("UPDATE login SET deleted=? WHERE window=? AND deleted IS NULL", [time, map.window])
227 if (!map.user) return respond(map)
228 sub("login_type", map.loginType, function(type) {
229 opts.db.insert("login(created,window,type,user)", [time, map.window, type, map.user], function(err, row) {
230 log.debug("login created: %s, %s", row.lastId, !!body.remember)
231 map.login = row.lastId
232 if (body.remember) {
233 opts.db.insert("remember(device,user,login)", [map.device, map.user, map.login])
234 }
235 respond(map)
236 })
237 })
238 }
239
240 function respond(map) {
241 var out = {}
242 , buf = Buffer.alloc(map.buf.length + 6, map.buf)
243 buf.writeUIntLE(map.window, map.buf.length, 6)
244 out.authorization = opts.authType + " " + buf.toString("base64").replace(/(?:AA)+$/g, "")
245
246 if (!body.a || parseInt(body.a, 32) !== map.window) {
247 out.a = map.window.toString(32)
248 }
249 hello.emit("result", req, res, out, map)
250 }
251
252 function sub(table, text, next) {
253 if (!text) return next(null)
254 opts.db.get("SELECT id FROM ? WHERE text=?", [table, text], function(err, row) {
255 if (row) return next(row.id)
256 opts.db.insert(table + "(text)", [text], function(err, row) {
257 next(row.lastId)
258 })
259 })
260 }
261
262 function getById(table, resolve, reject, source, map) {
263 var id = typeof source === "string" && parseInt(source, 32)
264 if (!id) return reject(map)
265 opts.db.get("SELECT * FROM ? WHERE id=?", [table, id], function(err, row) {
266 if (!row) return reject(map)
267 map[table] = row.id
268 resolve(map, row)
269 })
270 }
271}
272
273
274/*
275 * Authentication - login + password (who you are)
276 * Authorization - permissions (what you are allowed to do)
277 * Accounting - consumed resources (session statistics and usage for
278 * authorization control, billing, trend analysis,
279 * resource utilization, and capacity planning activities)
280 */
281
282function authMw(req, res, next, opts) {
283 var hello = this
284 , auth = req.headers["authorization"]
285 req.session = {}
286
287 if (auth !== void 0) {
288 // Authorization: <type> <credentials>
289 // Authorization: OAuth realm="Example", oauth_token="ad180jjd733klru7", oauth_version="1.0"
290 auth = auth.split(/\s+/)
291
292 if (auth[0] === "Basic") {
293 auth[1] = Buffer.from(auth[1], "base64").toString()
294 }
295 if (auth.length !== 2 || hello.emit("auth:" + auth[0], req, res, next, auth[1]) === 0) {
296 res.setHeader("WWW-Authenticate", opts.challenges)
297 res.sendStatus(401)
298 }
299 } else {
300 next()
301 }
302}
303
304function delMw(req, res, next, opts) {
305 var hello = this
306 , map = req.authMap
307 , time = req.date.getTime()
308
309 if (map && map.login) {
310 log.debug("logout %s", map.login)
311 hello.emit("logout", map)
312 opts.db.run("UPDATE login SET deleted=? WHERE id=?", [time, map.login])
313 opts.db.run("DELETE FROM remember WHERE device=? AND user=?", [map.device, map.user])
314 }
315 res.sendStatus(204)
316}
317
318function getByHash(table, source, opts, next) {
319 if (!source) return next(true)
320 var map = {}
321 , buf = map.buf = Buffer.from(source, "base64")
322 , digest = map.buf[4] >>> 4
323 , bytes = 3 * (1 + (map.buf[4] & 0xf))
324 , extra = map.buf.length - bytes
325
326 if (extra < 0 || extra > 6) {
327 return next(true)
328 }
329
330 if (extra > 0) {
331 map.window = buf.readUIntLE(bytes, extra)
332 buf = map.buf = map.buf.slice(0, bytes)
333 }
334
335 crypto.pbkdf2(buf, opts.pepper, buf[7], bytes, digests[digest], function(err, buf) {
336 opts.db.get("SELECT id FROM ? WHERE hash=?", [table, buf], function(err, row) {
337 if (!row) return next(true)
338 map[table] = row.id
339 next(null, map, row)
340 })
341 })
342}
343