diff --git a/lib/auth.js b/lib/auth.js index 4011505..2128aae 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -79,7 +79,7 @@ module.exports = application => { }; class Session { - constructor(token, cookie, sandbox, contextData = { token }) { + constructor(token, sandbox, contextData = { token }) { const contextHandler = { set: (data, key, value) => { const res = Reflect.set(data, key, value); @@ -88,30 +88,31 @@ module.exports = application => { } }; this.token = token; - this.cookie = cookie; this.sandbox = sandbox; this.data = contextData; this.context = new Proxy(contextData, contextHandler); } } - const start = (req, ip, userId) => { + const start = (client, userId) => { const token = generateToken(); - const host = parseHost(req.headers.host); + const host = parseHost(client.req.headers.host); + const ip = client.req.connection.remoteAddress; const cookie = `${TOKEN}=${token}; ${COOKIE_HOST}=${host}; HttpOnly`; const sandbox = getSandbox(); - const session = new Session(token, cookie, sandbox); + const session = new Session(token, sandbox); sessions.set(token, session); - cache.set(req, session); + cache.set(client.req, session); const data = JSON.stringify(session.data); db.insert('Session', { userId, token, ip, data }); + if (client.res) client.res.setHeader('Set-Cookie', cookie); return session; }; - const restore = async req => { - const cachedSession = cache.get(req); + const restore = async client => { + const cachedSession = cache.get(client.req); if (cachedSession) return cachedSession; - const { cookie } = req.headers; + const { cookie } = client.req.headers; if (!cookie) return null; const cookies = parseCookies(cookie); const token = cookies.token; @@ -122,18 +123,18 @@ module.exports = application => { if (record.data) { const data = JSON.parse(record.data); const sandbox = getSandbox(); - session = new Session(token, cookie, sandbox, data); + session = new Session(token, sandbox, data); sessions.set(token, session); } } if (!session) return null; - cache.set(req, session); + cache.set(client.req, session); return session; }; - const remove = (req, res, token) => { - const host = parseHost(req.headers.host); - res.setHeader('Set-Cookie', COOKIE_DELETE + host); + const remove = (client, token) => { + const host = parseHost(client.req.headers.host); + client.res.setHeader('Set-Cookie', COOKIE_DELETE + host); sessions.delete(token); db.delete('Session', { token }); }; diff --git a/lib/server.js b/lib/server.js index 7ce6c1f..8466254 100644 --- a/lib/server.js +++ b/lib/server.js @@ -9,6 +9,7 @@ const TRANSPORT = { http, https, ws: http, wss: https }; const MIME_TYPES = { html: 'text/html; charset=UTF-8', + json: 'application/json; charset=UTF-8', js: 'application/javascript; charset=UTF-8', css: 'text/css', png: 'image/png', @@ -46,10 +47,11 @@ const closeClients = () => { }; class Client { - constructor(req, res, application) { + constructor(req, res, application, connection) { this.req = req; this.res = res; this.application = application; + this.connection = connection; } static() { @@ -63,52 +65,58 @@ class Client { else this.error(404); } - error(status) { - if (this.res.finished) return; - this.res.writeHead(status, { 'Content-Type': 'text/plain' }); - this.res.end(`HTTP ${status}: ${http.STATUS_CODES[status]}`); - } - - async api() { - const { semaphore } = this.application.server; - try { - await semaphore.enter(); - } catch { - this.error(504); + error(status, err) { + const { application, req, res, connection } = this; + const reason = http.STATUS_CODES[status]; + const message = err ? err.stack : reason; + application.logger.error(`${req.url} - ${message} - ${status}`); + const result = JSON.stringify({ result: 'error', reason }); + if (connection) { + connection.send(result); return; } - const { req, res } = this; - const { url } = req; - const ip = req.connection.remoteAddress; - const name = url.substring(METHOD_OFFSET); - const session = await this.application.auth.restore(req); - const args = await receiveArgs(req); - const sandbox = session ? session.sandbox : undefined; - const context = session ? session.context : {}; + if (res.finished) return; + res.writeHead(status, { 'Content-Type': MIME_TYPES.json }); + res.end(result); + } + + async execute(method, args) { + const { application } = this; + const { semaphore } = application.server; + await semaphore.enter(); try { - const exp = this.application.runScript(name, sandbox); - const { method, access } = exp(context); - if (!session && access !== 'public') { - this.application.logger.error(`Forbidden ${url}`); - this.error(403); + const session = await application.auth.restore(this); + const sandbox = session ? session.sandbox : undefined; + const context = session ? session.context : {}; + const exp = application.runScript(method, sandbox); + const proc = exp(context); + if (!session && proc.access !== 'public') { semaphore.leave(); - return; + throw new Error(`Forbidden: ${method}`); } - const result = await method(args); - if (res.finished) { - semaphore.leave(); - return; + const result = await proc.method(args); + if (!session && proc.access === 'public') { + const session = application.auth.start(this, result.userId); + result.token = session.token; } - if (!session && access === 'public') { - const session = this.application.auth.start(req, ip, result.userId); - res.setHeader('Set-Cookie', session.cookie); - } - res.end(JSON.stringify(result)); + return JSON.stringify(result); + } finally { + semaphore.leave(); + } + } + + async rpc(method, args) { + const { res, connection } = this; + try { + const result = await this.execute(method, args); + if (connection) connection.send(result); + else res.end(result); } catch (err) { - this.application.logger.error(err.stack); - this.error(err.message === 'Not found' ? 404 : 500); + if (err.message === 'Not found') this.error(404); + else if (err.message === 'Semaphore timeout') this.error(504); + else if (err.message.startsWith('Forbidden:')) this.error(403); + else this.error(500, err); } - semaphore.leave(); } } @@ -134,47 +142,17 @@ const listener = application => (req, res) => { application.logger.log(`${method}\t${url}`); if (url.startsWith('/api/')) { - if (method === 'POST') client.api(); - else client.error(403); - } else { - client.static(); - } -}; - -const apiws = (application, connection, req) => async message => { - const { semaphore } = application.server; - const send = obj => connection.send(JSON.stringify(obj)); - try { - await semaphore.enter(); - } catch { - send({ result: 'error', reason: 'timeout' }); - return; - } - try { - const ip = req.connection.remoteAddress; - const { method: name, args } = JSON.parse(message); - const session = await application.auth.restore(req); - const sandbox = session ? session.sandbox : undefined; - const context = session ? session.context : {}; - const exp = application.runScript(name, sandbox); - const { method, access } = exp(context); - if (!session && access !== 'public') { - application.logger.error(`Forbidden: ${name}`); - send({ result: 'error', reason: 'forbidden' }); - semaphore.leave(); + if (method !== 'POST') { + client.error(403); return; } - const result = await method(args); - if (!session && access === 'public') { - const session = application.auth.start(req, ip, result.userId); - result.token = session.cookie; - } - send(result); - } catch (err) { - application.logger.error(err.stack); - send({ result: 'error' }); + receiveArgs(req).then(args => { + const method = url.substring(METHOD_OFFSET); + client.rpc(method, args); + }); + } else { + client.static(); } - semaphore.leave(); }; class Server { @@ -191,7 +169,11 @@ class Server { if (transport.startsWith('ws')) { this.ws = new WebSocket.Server({ server: this.instance }); this.ws.on('connection', (connection, req) => { - connection.on('message', apiws(application, connection, req)); + const client = new Client(req, null, application, connection); + connection.on('message', message => { + const { method, args } = JSON.parse(message); + client.rpc(method, args); + }); }); } this.instance.listen(port, host);