1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | var 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 |
|
33 | module.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 |
|
86 | function 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 |
|
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
|
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 |
|
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 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 |
|
282 | function authMw(req, res, next, opts) {
|
283 | var hello = this
|
284 | , auth = req.headers["authorization"]
|
285 | req.session = {}
|
286 |
|
287 | if (auth !== void 0) {
|
288 |
|
289 |
|
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 |
|
304 | function 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 |
|
318 | function 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 |
|