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) {
|
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
|
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 |
|
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 |
|
274 |
|
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 | function authMw(req, res, next, opts) {
|
281 | var hello = this
|
282 | , auth = req.headers["authorization"]
|
283 | req.session = {}
|
284 |
|
285 | if (auth !== void 0) {
|
286 |
|
287 |
|
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 |
|
302 | function 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 |
|
316 | function 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 |
|