Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 15 additions & 14 deletions lib/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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 });
};
Expand Down
138 changes: 60 additions & 78 deletions lib/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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() {
Expand All @@ -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();
}
}

Expand All @@ -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 {
Expand All @@ -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);
Expand Down