<commit_before>'use strict';

const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const Cryptiles = require('@hapi/cryptiles');
const Hoek = require('@hapi/hoek');
const Iron = require('@hapi/iron');
const Teamwork = require('@hapi/teamwork');

const internals = {
    version: '2'
};

exports = module.exports = internals.Socket = function (ws, req, listener) {

    this._ws = ws;
    this._cookies = req.headers.cookie;
    this._listener = listener;
    this._helloed = false;
    this._pinged = true;
    this._processingCount = 0;
    this._subscriptions = {};
    this._packets = [];
    this._sending = false;
    this._removed = new Teamwork.Team();

    this.server = this._listener._server;
    this.id = this._listener._generateId();
    this.app = {};
    this.auth = {
        isAuthenticated: false,
        credentials: null,
        artifacts: null,
        _error: null,
        _initialAuthTimeout: null,
        _verified: 0
    };

    this.info = {
        remoteAddress: req.socket.remoteAddress,
        remotePort: req.socket.remotePort,
        'x-forwarded-for': req.headers['x-forwarded-for']
    };

    if (this._listener._settings.auth &&
        this._listener._settings.auth.timeout) {

        this.auth._initialAuthTimeout = setTimeout(() => this.disconnect(), this._listener._settings.auth.timeout);
    }

    this._ws.on('message', (message) => this._onMessage(message));
};

internals.Socket.prototype.disconnect = function () {

    clearTimeout(this.auth._initialAuthTimeout);
    this.auth._initialAuthTimeout = null;

    this._ws.close();
    return this._removed;
};

internals.Socket.prototype.send = function (message) {

    const response = {
        type: 'update',
        message
    };

    return this._send(response);
};

internals.Socket.prototype.publish = function (path, update) {

    const message = {
        type: 'pub',
        path,
        message: update
    };

    return this._send(message);
};

internals.Socket.prototype.revoke = async function (path, update, options = {}) {

    await this._unsubscribe(path);

    const message = {
        type: 'revoke',
        path
    };

    if (update !== null) {                  // Allow sending falsy values
        message.message = update;
    }

    if (options.ignoreClose &&
        !this.isOpen()) {

        return Promise.resolve();
    }

    return this._send(message);
};

internals.Socket.prototype.isOpen = function () {

    return this._ws.readyState === 1;
};

internals.Socket.prototype._send = function (message, options) {

    options = options || {};

    if (!this.isOpen()) {                                           // Open
        return Promise.reject(Boom.internal('Socket not open'));
    }

    try {
        var string = JSON.stringify(message);
        if (options.replace) {
            Object.keys(options.replace).forEach((token) => {

                string = string.replace(`"${token}"`, options.replace[token]);
            });
        }
    }
    catch (err) {
        this.server.log(['nes', 'serialization', 'error'], message.type);

        if (message.id) {
            return this._error(Boom.internal('Failed serializing message'), message);
        }

        return Promise.reject(err);
    }

    const team = new Teamwork.Team();
    this._packets.push({ message: string, type: message.type, team });
    this._flush();
    return team.work;
};

internals.Socket.prototype._flush = async function () {

    if (this._sending ||
        !this._packets.length) {

        return;
    }

    this._sending = true;

    const packet = this._packets.shift();
    let messages = [packet.message];

    // Break message into smaller chunks

    const maxChunkChars = this._listener._settings.payload.maxChunkChars;
    if (maxChunkChars &&
        packet.message.length > maxChunkChars) {

        messages = [];
        const parts = Math.ceil(packet.message.length / maxChunkChars);
        for (let i = 0; i < parts; ++i) {
            const last = (i === parts - 1);
            const prefix = (last ? '!' : '+');
            messages.push(prefix + packet.message.slice(i * maxChunkChars, (i + 1) * maxChunkChars));
        }
    }

    for (let i = 0; i < messages.length; ++i) {
        const message = messages[i];

        const team = new Teamwork.Team();
        this._ws.send(message, (err) => team.attend(err));
        try {
            await team.work;
        }
        catch (err) {
            var error = err;
            break;
        }

        if (packet.type !== 'ping') {
            this._pinged = true;                        // Consider the connection valid if send() was successful
        }
    }

    this._sending = false;
    packet.team.attend(error);

    setImmediate(() => this._flush());
};

internals.Socket.prototype._active = function () {

    return this._pinged ||
        this._sending ||
        this._processingCount;
};

internals.Socket.prototype._error = function (err, request) {

    err = Boom.boomify(err);

    const message = Hoek.clone(err.output);
    delete message.payload.statusCode;
    message.headers = this._filterHeaders(message.headers);

    if (request) {
        message.type = request.type;
        message.id = request.id;
    }

    if (err.path) {
        message.path = err.path;
    }

    return this._send(message);
};

internals.Socket.prototype._onMessage = async function (message) {

    try {
        var request = JSON.parse(message);
    }
    catch (err) {
        return this._error(Boom.badRequest('Cannot parse message'));
    }

    this._pinged = true;
    ++this._processingCount;

    try {
        var { response, options } = await this._lifecycle(request);
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        var error = err;
    }

    try {
        if (error) {
            await this._error(error, request);
        }
        else if (response) {
            await this._send(response, options);
        }
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        this.disconnect();
    }

    --this._processingCount;
};

internals.Socket.prototype._lifecycle = async function (request) {

    if (!request.type) {
        throw Boom.badRequest('Cannot parse message');
    }

    if (!request.id) {
        throw Boom.badRequest('Message missing id');
    }

    await this._verifyAuth();

    // Initialization and Authentication

    if (request.type === 'ping') {
        return {};
    }

    if (request.type === 'hello') {
        return this._processHello(request);
    }

    if (!this._helloed) {
        throw Boom.badRequest('Connection is not initialized');
    }

    if (request.type === 'reauth') {
        return this._processReauth(request);
    }

    // Endpoint request

    if (request.type === 'request') {
        return this._processRequest(request);
    }

    // Custom message request

    if (request.type === 'message') {
        return this._processMessage(request);
    }

    // Subscriptions

    if (request.type === 'sub') {
        return this._processSubscription(request);
    }

    if (request.type === 'unsub') {
        return this._processUnsub(request);
    }

    // Unknown

    throw Boom.badRequest('Unknown message type');
};

internals.Socket.prototype._processHello = async function (request) {

    if (this._helloed) {
        throw Boom.badRequest('Connection already initialized');
    }

    if (request.version !== internals.version) {
        throw Boom.badRequest('Incorrect protocol version (expected ' + internals.version + ' but received ' + (request.version || 'none') + ')');
    }

    this._helloed = true;                   // Prevents the client from reusing the socket if erred (leaves socket open to ensure client gets the error response)
    await this._authenticate(request);

    if (this._listener._settings.onConnection) {
        await this._listener._settings.onConnection(this);
    }

    if (request.subs) {
        for (let i = 0; i < request.subs.length; ++i) {
            const path = request.subs[i];
            try {
                await this._listener._subscribe(path, this);
            }
            catch (err) {
                err.path = path;
                throw err;
            }
        }
    }

    const response = {
        type: 'hello',
        id: request.id,
        heartbeat: this._listener._settings.heartbeat,
        socket: this.id
    };

    return { response };
};

internals.Socket.prototype._processReauth = async function (request) {

    try {
        await this._authenticate(request);
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        setTimeout(() => this.disconnect()); // disconnect after sending the response to the client
        throw err;
    }

    const response = {
        type: 'reauth',
        id: request.id
    };

    return { response };
};

internals.Socket.prototype._processRequest = async function (request) {

    let method = request.method;
    if (!method) {
        throw Boom.badRequest('Message missing method');
    }

    let path = request.path;
    if (!path) {
        throw Boom.badRequest('Message missing path');
    }

    if (request.headers &&
        internals.caseInsensitiveKey(request.headers, 'authorization')) {

        throw Boom.badRequest('Cannot include an Authorization header');
    }

    if (path[0] !== '/') {              // Route id
        const route = this.server.lookup(path);
        if (!route) {
            throw Boom.notFound();
        }

        path = route.path;
        method = route.method;

        if (method === '*') {
            throw Boom.badRequest('Cannot use route id with wildcard method route config');
        }
    }

    if (this._listener._settings.auth &&
        path === this._listener._settings.auth.endpoint) {

        throw Boom.notFound();
    }

    const shot = {
        method,
        url: path,
        payload: request.payload,
        headers: request.headers,
        auth: this.auth.isAuthenticated ? this.auth : null,
        validate: false,
        plugins: {
            nes: {
                socket: this
            }
        }
    };

    const res = await this.server.inject(shot);

    const response = {
        type: 'request',
        id: request.id,
        statusCode: res.statusCode,
        payload: res.result,
        headers: this._filterHeaders(res.headers)
    };

    const options = {};
    if (typeof res.result === 'string' &&
        res.headers['content-type'] &&
        res.headers['content-type'].match(/^application\/json/)) {

        const token = Cryptiles.randomString(32);
        options.replace = { [token]: res.result };
        response.payload = token;
    }

    return { response, options };
};

internals.Socket.prototype._processMessage = async function (request) {

    if (!this._listener._settings.onMessage) {
        throw Boom.notImplemented();
    }

    const message = await this._listener._settings.onMessage(this, request.message);
    const response = {
        type: 'message',
        id: request.id,
        message
    };

    return { response };
};

internals.Socket.prototype._processSubscription = async function (request) {

    await this._listener._subscribe(request.path, this);

    const response = {
        type: 'sub',
        id: request.id,
        path: request.path
    };

    return { response };
};

internals.Socket.prototype._processUnsub = async function (request) {

    await this._unsubscribe(request.path);

    const response = {
        type: 'unsub',
        id: request.id
    };

    return { response };
};

internals.Socket.prototype._unsubscribe = function (path) {

    const sub = this._subscriptions[path];
    if (sub) {
        delete this._subscriptions[path];
        return sub.remove(this, path);
    }
};

internals.Socket.prototype._authenticate = async function (request) {

    await this._authByCookie();

    if (!request.auth &&
        !this.auth.isAuthenticated &&
        this._listener._authRequired()) {

        throw Boom.unauthorized('Connection requires authentication');
    }

    if (request.auth &&
        request.type !== 'reauth' &&
        this.auth.isAuthenticated) {        // Authenticated using a cookie during upgrade

        throw Boom.badRequest('Connection already authenticated');
    }

    clearTimeout(this.auth._initialAuthTimeout);
    this.auth._initialAuthTimeout = null;

    if (request.auth) {
        const config = this._listener._settings.auth;
        if (config.type === 'direct') {
            const route = this.server.lookup(config.id);
            request.auth.headers = request.auth.headers || {};
            request.auth.headers['x-forwarded-for'] = this.info['x-forwarded-for'];
            const res = await this.server.inject({ url: route.path, method: 'auth', headers: request.auth.headers, remoteAddress: this.info.remoteAddress, allowInternals: true, validate: false });
            if (res.statusCode !== 200) {
                throw Boom.unauthorized(res.result.message);
            }

            if (!res.result.credentials) {
                return;
            }

            this._setCredentials(res.result);
            return;
        }

        try {
            const auth = await Iron.unseal(request.auth, config.password, config.iron || Iron.defaults);
            this._setCredentials(auth);
        }
        catch (err) {
            throw Boom.unauthorized('Invalid token');
        }
    }
};

internals.Socket.prototype._authByCookie = async function () {

    const config = this._listener._settings.auth;
    if (!config) {
        return;
    }

    const cookies = this._cookies;
    if (!cookies) {
        return;
    }

    try {
        var { states } = await this.server.states.parse(cookies);
    }
    catch (err) {
        throw Boom.unauthorized('Invalid nes authentication cookie');
    }

    const auth = states[config.cookie];
    if (auth) {
        this._setCredentials(auth);
    }
};

internals.Socket.prototype._setCredentials = function (auth) {

    this.auth.isAuthenticated = true;
    this.auth.credentials = auth.credentials;
    this.auth.artifacts = auth.artifacts;
    this.auth.strategy = auth.strategy;

    this.auth._verified = Date.now();

    this._listener._sockets.auth(this);
};

internals.Socket.prototype._verifyAuth = async function () {

    const now = Date.now();

    if (!this._listener._settings.auth.minAuthVerifyInterval ||
        now - this.auth._verified < this._listener._settings.auth.minAuthVerifyInterval) {

        return;
    }

    this.auth._verified = now;

    try {
        await this.server.auth.verify(this);
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        setImmediate(() => this.disconnect());
        throw Boom.forbidden('Credential verification failed');
    }
};

internals.Socket.prototype._filterHeaders = function (headers) {

    const filter = this._listener._settings.headers;
    if (!filter) {
        return undefined;
    }

    if (filter === '*') {
        return headers;
    }

    const filtered = {};
    const fields = Object.keys(headers);
    for (let i = 0; i < fields.length; ++i) {
        const field = fields[i];
        if (filter.indexOf(field.toLowerCase()) !== -1) {
            filtered[field] = headers[field];
        }
    }

    return filtered;
};

internals.caseInsensitiveKey = function (object, key) {

    const keys = Object.keys(object);
    for (let i = 0; i < keys.length; ++i) {
        const current = keys[i];
        if (key === current.toLowerCase()) {
            return object[current];
        }
    }

    return undefined;
};
<commit_msg>fix: pass remoteAddress when injecting request<commit_after>'use strict';

const Boom = require('@hapi/boom');
const Bounce = require('@hapi/bounce');
const Cryptiles = require('@hapi/cryptiles');
const Hoek = require('@hapi/hoek');
const Iron = require('@hapi/iron');
const Teamwork = require('@hapi/teamwork');

const internals = {
    version: '2'
};

exports = module.exports = internals.Socket = function (ws, req, listener) {

    this._ws = ws;
    this._cookies = req.headers.cookie;
    this._listener = listener;
    this._helloed = false;
    this._pinged = true;
    this._processingCount = 0;
    this._subscriptions = {};
    this._packets = [];
    this._sending = false;
    this._removed = new Teamwork.Team();

    this.server = this._listener._server;
    this.id = this._listener._generateId();
    this.app = {};
    this.auth = {
        isAuthenticated: false,
        credentials: null,
        artifacts: null,
        _error: null,
        _initialAuthTimeout: null,
        _verified: 0
    };

    this.info = {
        remoteAddress: req.socket.remoteAddress,
        remotePort: req.socket.remotePort,
        'x-forwarded-for': req.headers['x-forwarded-for']
    };

    if (this._listener._settings.auth &&
        this._listener._settings.auth.timeout) {

        this.auth._initialAuthTimeout = setTimeout(() => this.disconnect(), this._listener._settings.auth.timeout);
    }

    this._ws.on('message', (message) => this._onMessage(message));
};

internals.Socket.prototype.disconnect = function () {

    clearTimeout(this.auth._initialAuthTimeout);
    this.auth._initialAuthTimeout = null;

    this._ws.close();
    return this._removed;
};

internals.Socket.prototype.send = function (message) {

    const response = {
        type: 'update',
        message
    };

    return this._send(response);
};

internals.Socket.prototype.publish = function (path, update) {

    const message = {
        type: 'pub',
        path,
        message: update
    };

    return this._send(message);
};

internals.Socket.prototype.revoke = async function (path, update, options = {}) {

    await this._unsubscribe(path);

    const message = {
        type: 'revoke',
        path
    };

    if (update !== null) {                  // Allow sending falsy values
        message.message = update;
    }

    if (options.ignoreClose &&
        !this.isOpen()) {

        return Promise.resolve();
    }

    return this._send(message);
};

internals.Socket.prototype.isOpen = function () {

    return this._ws.readyState === 1;
};

internals.Socket.prototype._send = function (message, options) {

    options = options || {};

    if (!this.isOpen()) {                                           // Open
        return Promise.reject(Boom.internal('Socket not open'));
    }

    try {
        var string = JSON.stringify(message);
        if (options.replace) {
            Object.keys(options.replace).forEach((token) => {

                string = string.replace(`"${token}"`, options.replace[token]);
            });
        }
    }
    catch (err) {
        this.server.log(['nes', 'serialization', 'error'], message.type);

        if (message.id) {
            return this._error(Boom.internal('Failed serializing message'), message);
        }

        return Promise.reject(err);
    }

    const team = new Teamwork.Team();
    this._packets.push({ message: string, type: message.type, team });
    this._flush();
    return team.work;
};

internals.Socket.prototype._flush = async function () {

    if (this._sending ||
        !this._packets.length) {

        return;
    }

    this._sending = true;

    const packet = this._packets.shift();
    let messages = [packet.message];

    // Break message into smaller chunks

    const maxChunkChars = this._listener._settings.payload.maxChunkChars;
    if (maxChunkChars &&
        packet.message.length > maxChunkChars) {

        messages = [];
        const parts = Math.ceil(packet.message.length / maxChunkChars);
        for (let i = 0; i < parts; ++i) {
            const last = (i === parts - 1);
            const prefix = (last ? '!' : '+');
            messages.push(prefix + packet.message.slice(i * maxChunkChars, (i + 1) * maxChunkChars));
        }
    }

    for (let i = 0; i < messages.length; ++i) {
        const message = messages[i];

        const team = new Teamwork.Team();
        this._ws.send(message, (err) => team.attend(err));
        try {
            await team.work;
        }
        catch (err) {
            var error = err;
            break;
        }

        if (packet.type !== 'ping') {
            this._pinged = true;                        // Consider the connection valid if send() was successful
        }
    }

    this._sending = false;
    packet.team.attend(error);

    setImmediate(() => this._flush());
};

internals.Socket.prototype._active = function () {

    return this._pinged ||
        this._sending ||
        this._processingCount;
};

internals.Socket.prototype._error = function (err, request) {

    err = Boom.boomify(err);

    const message = Hoek.clone(err.output);
    delete message.payload.statusCode;
    message.headers = this._filterHeaders(message.headers);

    if (request) {
        message.type = request.type;
        message.id = request.id;
    }

    if (err.path) {
        message.path = err.path;
    }

    return this._send(message);
};

internals.Socket.prototype._onMessage = async function (message) {

    try {
        var request = JSON.parse(message);
    }
    catch (err) {
        return this._error(Boom.badRequest('Cannot parse message'));
    }

    this._pinged = true;
    ++this._processingCount;

    try {
        var { response, options } = await this._lifecycle(request);
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        var error = err;
    }

    try {
        if (error) {
            await this._error(error, request);
        }
        else if (response) {
            await this._send(response, options);
        }
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        this.disconnect();
    }

    --this._processingCount;
};

internals.Socket.prototype._lifecycle = async function (request) {

    if (!request.type) {
        throw Boom.badRequest('Cannot parse message');
    }

    if (!request.id) {
        throw Boom.badRequest('Message missing id');
    }

    await this._verifyAuth();

    // Initialization and Authentication

    if (request.type === 'ping') {
        return {};
    }

    if (request.type === 'hello') {
        return this._processHello(request);
    }

    if (!this._helloed) {
        throw Boom.badRequest('Connection is not initialized');
    }

    if (request.type === 'reauth') {
        return this._processReauth(request);
    }

    // Endpoint request

    if (request.type === 'request') {
        return this._processRequest(request);
    }

    // Custom message request

    if (request.type === 'message') {
        return this._processMessage(request);
    }

    // Subscriptions

    if (request.type === 'sub') {
        return this._processSubscription(request);
    }

    if (request.type === 'unsub') {
        return this._processUnsub(request);
    }

    // Unknown

    throw Boom.badRequest('Unknown message type');
};

internals.Socket.prototype._processHello = async function (request) {

    if (this._helloed) {
        throw Boom.badRequest('Connection already initialized');
    }

    if (request.version !== internals.version) {
        throw Boom.badRequest('Incorrect protocol version (expected ' + internals.version + ' but received ' + (request.version || 'none') + ')');
    }

    this._helloed = true;                   // Prevents the client from reusing the socket if erred (leaves socket open to ensure client gets the error response)
    await this._authenticate(request);

    if (this._listener._settings.onConnection) {
        await this._listener._settings.onConnection(this);
    }

    if (request.subs) {
        for (let i = 0; i < request.subs.length; ++i) {
            const path = request.subs[i];
            try {
                await this._listener._subscribe(path, this);
            }
            catch (err) {
                err.path = path;
                throw err;
            }
        }
    }

    const response = {
        type: 'hello',
        id: request.id,
        heartbeat: this._listener._settings.heartbeat,
        socket: this.id
    };

    return { response };
};

internals.Socket.prototype._processReauth = async function (request) {

    try {
        await this._authenticate(request);
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        setTimeout(() => this.disconnect()); // disconnect after sending the response to the client
        throw err;
    }

    const response = {
        type: 'reauth',
        id: request.id
    };

    return { response };
};

internals.Socket.prototype._processRequest = async function (request) {

    let method = request.method;
    if (!method) {
        throw Boom.badRequest('Message missing method');
    }

    let path = request.path;
    if (!path) {
        throw Boom.badRequest('Message missing path');
    }

    if (request.headers &&
        internals.caseInsensitiveKey(request.headers, 'authorization')) {

        throw Boom.badRequest('Cannot include an Authorization header');
    }

    if (path[0] !== '/') {              // Route id
        const route = this.server.lookup(path);
        if (!route) {
            throw Boom.notFound();
        }

        path = route.path;
        method = route.method;

        if (method === '*') {
            throw Boom.badRequest('Cannot use route id with wildcard method route config');
        }
    }

    if (this._listener._settings.auth &&
        path === this._listener._settings.auth.endpoint) {

        throw Boom.notFound();
    }

    const shot = {
        method,
        url: path,
        payload: request.payload,
        headers: request.headers,
        auth: this.auth.isAuthenticated ? this.auth : null,
        validate: false,
        plugins: {
            nes: {
                socket: this
            }
        },
        remoteAddress: this.info.remoteAddress
    };

    const res = await this.server.inject(shot);

    const response = {
        type: 'request',
        id: request.id,
        statusCode: res.statusCode,
        payload: res.result,
        headers: this._filterHeaders(res.headers)
    };

    const options = {};
    if (typeof res.result === 'string' &&
        res.headers['content-type'] &&
        res.headers['content-type'].match(/^application\/json/)) {

        const token = Cryptiles.randomString(32);
        options.replace = { [token]: res.result };
        response.payload = token;
    }

    return { response, options };
};

internals.Socket.prototype._processMessage = async function (request) {

    if (!this._listener._settings.onMessage) {
        throw Boom.notImplemented();
    }

    const message = await this._listener._settings.onMessage(this, request.message);
    const response = {
        type: 'message',
        id: request.id,
        message
    };

    return { response };
};

internals.Socket.prototype._processSubscription = async function (request) {

    await this._listener._subscribe(request.path, this);

    const response = {
        type: 'sub',
        id: request.id,
        path: request.path
    };

    return { response };
};

internals.Socket.prototype._processUnsub = async function (request) {

    await this._unsubscribe(request.path);

    const response = {
        type: 'unsub',
        id: request.id
    };

    return { response };
};

internals.Socket.prototype._unsubscribe = function (path) {

    const sub = this._subscriptions[path];
    if (sub) {
        delete this._subscriptions[path];
        return sub.remove(this, path);
    }
};

internals.Socket.prototype._authenticate = async function (request) {

    await this._authByCookie();

    if (!request.auth &&
        !this.auth.isAuthenticated &&
        this._listener._authRequired()) {

        throw Boom.unauthorized('Connection requires authentication');
    }

    if (request.auth &&
        request.type !== 'reauth' &&
        this.auth.isAuthenticated) {        // Authenticated using a cookie during upgrade

        throw Boom.badRequest('Connection already authenticated');
    }

    clearTimeout(this.auth._initialAuthTimeout);
    this.auth._initialAuthTimeout = null;

    if (request.auth) {
        const config = this._listener._settings.auth;
        if (config.type === 'direct') {
            const route = this.server.lookup(config.id);
            request.auth.headers = request.auth.headers || {};
            request.auth.headers['x-forwarded-for'] = this.info['x-forwarded-for'];
            const res = await this.server.inject({ url: route.path, method: 'auth', headers: request.auth.headers, remoteAddress: this.info.remoteAddress, allowInternals: true, validate: false });
            if (res.statusCode !== 200) {
                throw Boom.unauthorized(res.result.message);
            }

            if (!res.result.credentials) {
                return;
            }

            this._setCredentials(res.result);
            return;
        }

        try {
            const auth = await Iron.unseal(request.auth, config.password, config.iron || Iron.defaults);
            this._setCredentials(auth);
        }
        catch (err) {
            throw Boom.unauthorized('Invalid token');
        }
    }
};

internals.Socket.prototype._authByCookie = async function () {

    const config = this._listener._settings.auth;
    if (!config) {
        return;
    }

    const cookies = this._cookies;
    if (!cookies) {
        return;
    }

    try {
        var { states } = await this.server.states.parse(cookies);
    }
    catch (err) {
        throw Boom.unauthorized('Invalid nes authentication cookie');
    }

    const auth = states[config.cookie];
    if (auth) {
        this._setCredentials(auth);
    }
};

internals.Socket.prototype._setCredentials = function (auth) {

    this.auth.isAuthenticated = true;
    this.auth.credentials = auth.credentials;
    this.auth.artifacts = auth.artifacts;
    this.auth.strategy = auth.strategy;

    this.auth._verified = Date.now();

    this._listener._sockets.auth(this);
};

internals.Socket.prototype._verifyAuth = async function () {

    const now = Date.now();

    if (!this._listener._settings.auth.minAuthVerifyInterval ||
        now - this.auth._verified < this._listener._settings.auth.minAuthVerifyInterval) {

        return;
    }

    this.auth._verified = now;

    try {
        await this.server.auth.verify(this);
    }
    catch (err) {
        Bounce.rethrow(err, 'system');
        setImmediate(() => this.disconnect());
        throw Boom.forbidden('Credential verification failed');
    }
};

internals.Socket.prototype._filterHeaders = function (headers) {

    const filter = this._listener._settings.headers;
    if (!filter) {
        return undefined;
    }

    if (filter === '*') {
        return headers;
    }

    const filtered = {};
    const fields = Object.keys(headers);
    for (let i = 0; i < fields.length; ++i) {
        const field = fields[i];
        if (filter.indexOf(field.toLowerCase()) !== -1) {
            filtered[field] = headers[field];
        }
    }

    return filtered;
};

internals.caseInsensitiveKey = function (object, key) {

    const keys = Object.keys(object);
    for (let i = 0; i < keys.length; ++i) {
        const current = keys[i];
        if (key === current.toLowerCase()) {
            return object[current];
        }
    }

    return undefined;
};
<|endoftext|>


<commit_before>var iostream = require('socket.io');

var Statics = {
    io_ref: null,
    routers: {}
};

var func_params = function (func) {
    var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
        params = /([^\s,]+)/g,
        func_str = func.toString().replace(comments, ''),
        start = func_str.indexOf('(') + 1,
        end = func_str.indexOf(')'),
        head = func_str.slice(start, end),
        result = head.match(params);
    return (result) ? result : [];
};

var power_set = function (list) {
    var set = [],
        listSize = list.length,
        combinationsCount = (1 << listSize);
    for (var i = 1; i < combinationsCount; i++, set.push(combination))
        for (var j = 0, combination = []; j < listSize; j++)
            if ((i & (1 << j)))
                combination.push(list[j]);
    return set;
};

var Router = function () {
    var Handler = function (func) {
        var params = func_params(func),
            p = params.filter(function (v) { return (v[0] !== '$'); }),
            o = params.filter(function (v) { return (v[0] === '$'); }),
            o = (function (a) {
                for (var i in a) a[i] = a[i].substr(1); return a;
            }(o)),
            pset = power_set(o),
            possible = (p.length > 0) ? [p.sort().join(' ')] : [];
        for (var i in pset) {
            possible.push(p.concat(pset[i]).sort().join(' '));
        }
        return {
            params: params,
            keys: possible,
            func: func,
            execute: function (socket, data) {
                var k, p = [];
                if (data) {
                    for (var i in this.params) {
                        k = this.params[i];
                        if (k[0] === '$') k = k.substr(1);
                        if (!data[k]) p.push(undefined);
                        else p.push(data[k]);
                    }
                }
                this.func.apply(socket, p);
            }
        };
    };
    return {
        _handlers: {},
        getIO: function () { return Statics.io_ref; },
        add: function (func) {
            var h = new Handler(func);
            if (!this._add_handler(h)) {
                throw new Error('Cannot add socket handler for path' +
                                ' that already exists.');
            }
        },
        addException: function (func) {
            if (this._handlers.except) {
                throw new Error('Exception handler already added for event.');
            }
            this._handlers.except = new Handler(func);
        },
        run: function (socket, data) {
            var h, k;
            if (!data || typeof(data) !== 'object' ||
                    Object.keys(data).length < 1) {
                h = this._handlers.do;
            }
            else {
                h = this._handlers[Object.keys(data.sort()).join(' ')];
            }
            if (!h) {
                h = this._handlers.except;
            }
            if (!this._run_handler(h, socket, data)) {
                throw new Error('Uncaught event passed: ' + JSON.stringify(data));
            }
        },
        _run_handler: function (h, socket, data) {
            if (!h) return false;
            h.execute(socket, data);
            return true;
        },
        _add_handler: function (h) {
            if (h.params.length === 0) {
                if (this._handlers.do) return false;
                this._handlers.do = h;
            }
            else {
                for (var i in h.keys) {
                    if (h.keys[i]) {
                        if (this._handlers[h.keys[i]]) return false;
                        this._handlers[h.keys[i]] = h;
                    }
                }
            }
            return true;
        }
    };
};

var init = function (server, middleware) {
    Statics.io_ref = iostream(server);
    Statics.io_ref.use(middleware);
    Statics.io_ref.on('connection', function (socket) {
        var f = function (event) {
                return function (data) {
                    Statics.routers[event].run(socket, data);
                };
            };
        if (Statics.routers['connection']) {
            // Connection Handler
            Statics.routers['connection'].run(socket);
        }
        // All other handlers
        for (var event in Statics.routers) {
            if (event !== 'connection') {
                socket.on(event, f(event));
            }
        }
    });
};

var use = function (event, router) {
    Statics.routers[event] = router;
};

module.exports = {
    Router: Router,
    initialize: init,
    use: use
};<commit_msg>[Fix] Bad array logic.<commit_after>var iostream = require('socket.io');

var Statics = {
    io_ref: null,
    routers: {}
};

var func_params = function (func) {
    var comments = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg,
        params = /([^\s,]+)/g,
        func_str = func.toString().replace(comments, ''),
        start = func_str.indexOf('(') + 1,
        end = func_str.indexOf(')'),
        head = func_str.slice(start, end),
        result = head.match(params);
    return (result) ? result : [];
};

var power_set = function (list) {
    var set = [],
        listSize = list.length,
        combinationsCount = (1 << listSize);
    for (var i = 1; i < combinationsCount; i++, set.push(combination))
        for (var j = 0, combination = []; j < listSize; j++)
            if ((i & (1 << j)))
                combination.push(list[j]);
    return set;
};

var Router = function () {
    var Handler = function (func) {
        var params = func_params(func),
            p = params.filter(function (v) { return (v[0] !== '$'); }),
            o = params.filter(function (v) { return (v[0] === '$'); }),
            o = (function (a) {
                for (var i in a) a[i] = a[i].substr(1); return a;
            }(o)),
            pset = power_set(o),
            possible = (p.length > 0) ? [p.sort().join(' ')] : [];
        for (var i in pset) {
            possible.push(p.concat(pset[i]).sort().join(' '));
        }
        return {
            params: params,
            keys: possible,
            func: func,
            execute: function (socket, data) {
                var k, p = [];
                if (data) {
                    for (var i in this.params) {
                        k = this.params[i];
                        if (k[0] === '$') k = k.substr(1);
                        if (!data[k]) p.push(undefined);
                        else p.push(data[k]);
                    }
                }
                this.func.apply(socket, p);
            }
        };
    };
    return {
        _handlers: {},
        getIO: function () { return Statics.io_ref; },
        add: function (func) {
            var h = new Handler(func);
            if (!this._add_handler(h)) {
                throw new Error('Cannot add socket handler for path' +
                                ' that already exists.');
            }
        },
        addException: function (func) {
            if (this._handlers.except) {
                throw new Error('Exception handler already added for event.');
            }
            this._handlers.except = new Handler(func);
        },
        run: function (socket, data) {
            var h, k;
            if (!data || typeof(data) !== 'object' ||
                    Object.keys(data).length < 1) {
                h = this._handlers.do;
            }
            else {
                h = this._handlers[Object.keys().sort().join(' ')];
            }
            if (!h) {
                h = this._handlers.except;
            }
            if (!this._run_handler(h, socket, data)) {
                throw new Error('Uncaught event passed: ' + JSON.stringify(data));
            }
        },
        _run_handler: function (h, socket, data) {
            if (!h) return false;
            h.execute(socket, data);
            return true;
        },
        _add_handler: function (h) {
            if (h.params.length === 0) {
                if (this._handlers.do) return false;
                this._handlers.do = h;
            }
            else {
                for (var i in h.keys) {
                    if (h.keys[i]) {
                        if (this._handlers[h.keys[i]]) return false;
                        this._handlers[h.keys[i]] = h;
                    }
                }
            }
            return true;
        }
    };
};

var init = function (server, middleware) {
    Statics.io_ref = iostream(server);
    Statics.io_ref.use(middleware);
    Statics.io_ref.on('connection', function (socket) {
        var f = function (event) {
                return function (data) {
                    Statics.routers[event].run(socket, data);
                };
            };
        if (Statics.routers['connection']) {
            // Connection Handler
            Statics.routers['connection'].run(socket);
        }
        // All other handlers
        for (var event in Statics.routers) {
            if (event !== 'connection') {
                socket.on(event, f(event));
            }
        }
    });
};

var use = function (event, router) {
    Statics.routers[event] = router;
};

module.exports = {
    Router: Router,
    initialize: init,
    use: use
};<|endoftext|>


<commit_before>const module_name = __filename.slice(__dirname.length + 1, -3);

const amp    = require('amp-message');
const zeromq = require('zeromq');

// Output log message to kodi notification and log.msg
function kodi_log(message) {
	kodi.notify(module_name, message);

	log.msg({
		src : module_name,
		msg : message,
	});
}

// Handle incoming messages
function on_data(data) {
	switch (data.event) {
		case 'bus-rx'            : bus_data.receive(data.data); break;
		case 'host-data'         : on_host_data(data);          break;
		case 'host-data-request' : host_data.send();            break;
		case 'status-request'    : socket.status_tx(data.data); break;

			// case 'log-bus'     : log.bus   (data.message); break;
			// case 'log-msg'     : log.socket(data);         break; // Broken, fix this
			// case 'bus-tx'      :
			// case 'lcd-color'   :
			// case 'lcd-command' :
			// case 'lcd-text'    :
			// default            :
	}

	return;

	log.socket({
		method : 'rx',
		type   : data.host.type,
		event  : data.event,
		string : data.host.host.short,
	});
}

// Handle incoming host-data messages
function on_host_data(data) {
	return;

	log.socket({
		method : 'rx',
		type   : data.data.type,
		event  : data.data.host.short+' => temp',
		string : data.data.temperature+'c',
	});
}

// Send data over zeroMQ
function send(event, data) {
	// Don't bother sending anything if we're not connected
	if (status.server.connected === false) {
		log.msg({
			src : module_name,
			msg : 'Server not connected, cannot send message',
		});

		return;
	}

	let message = {
		host  : status.system,
		event : event,
		data  : data,
	};

	// Encode object as AMP message
	let amp_message = new amp;
	amp_message.push(message);

	// Send AMP message over zeroMQ
	socket.interfaces[app_type].send([ app_type+'-tx', amp_message.toBuffer() ]);

	return;

	log.socket({
		method : 'tx',
		type   : status.system.type,
		event  : event,
		string : '',
	});
}

// Decode AMP-encoded messages from zeroMQ
function decode_amp(message) {
	let data = new amp(message).shift();
	// console.log(JSON.stringify(data, null, 2));

	// Parse received data
	on_data(data);
}

// Configure event handlers
function event_config(pass) {
	Object.keys(socket.interfaces).forEach((interface) => {
		// When a peer connects
		socket.interfaces[interface].on('connect', () => {
			log.module({
				src : module_name,
				msg : 'Peer connected',
			});
		});

		// When a peer disconnects
		socket.interfaces[interface].on('disconnect', () => {
			log.module({
				src : module_name,
				msg : 'Peer disconnected',
			});
		});

		socket.interfaces[interface].on('error', (error) => {
			log.module({
				src : module_name,
				msg : 'Error '+error,
			});
		});

		socket.interfaces[interface].on('message', (topic, message) => {
			log.module({
				src : module_name,
				msg : topic+' message received via '+interface,
			});

			// Decode AMP message
			decode_amp(message);
		});
	});

	socket.interfaces[app_type].on('bind', () => {
		update.status('server.connected', true);

		update.status('server.connecting',   false);
		update.status('server.reconnecting', false);

		// Send this host's data to zeroMQ clients to update them
		host_data.send();

		if (app_type == 'client') {
			// Refresh all data on first connect if enabled
			// This should ACTUALLY be more event-driven and reside in modules/IKE.js
			setTimeout(() => {
				switch (config.options.obc_refresh_on_start) {
					case true : IKE.obc_refresh(); break;
					default   : IKE.request('ignition');
				}
			}, 250);
		}
	});

	socket.interfaces[app_type].on('unbind', () => {
		update.status('server.connected', false);

		if (app_type == 'client') {
			// Reset basic vars
			json.status_reset_basic(() => {});
		}
	});

	log.module({ src : module_name, msg : 'Initialized event listeners' });

	pass();
	return true;
}

// Return full zeroMQ URL from config data
function get_url(interface) {
	let zmq_conf = config.zeromq;
	return zmq_conf.proto+'://'+zmq_conf.urls[interface]+':'+zmq_conf.ports[interface];
}

// Return zeroMQ socket type from config data
function get_type(interface) {
	if (interface === app_type) return 'pub';
	return 'sub';
}

// Initialize zeroMQ server
function init(pass, fail) {
	log.msg({ src : module_name, msg : 'Initializing' });

	// Setup events
	event_config(() => {
		// Bind server
		socket.interfaces[app_type].bind(get_url(app_type), (error) => {
			if (error) {
				log.module({
					src : module_name,
					msg : 'zeroMQ server bind '+error,
				});

				fail();
				return false;
			}

			// Connect interfaces and subscribe to topics
			Object.keys(socket.interfaces).forEach((interface) => {
				if (interface != app_type) {
					socket.interfaces[interface].connect(get_url(interface));
					socket.interfaces[interface].subscribe(interface+'-tx');
					socket.interfaces[interface].subscribe('daemon-tx');
					socket.interfaces[interface].subscribe('client-tx');
					socket.interfaces[interface].subscribe('bus-rx');
					log.module({ src : module_name, msg : interface+' connected' });
				}
			})

			log.module({ src : module_name, msg : 'Initialized' });

			pass();
			return true;
		});
	});
}

// Terminate zeroMQ server
function term(pass, fail) {
	log.msg({ src : module_name, msg : 'Terminating' });

	// Disconnect interfaces
	Object.keys(socket.interfaces).forEach((interface) => {
		if (interface != app_type) {
			socket.interfaces[interface].disconnect(get_url(interface));
			log.module({ src : module_name, msg : interface+' disconnected' });
		}
	});

	if (!status.server.connected) {
		log.module({
			src : module_name,
			msg : 'zeroMQ server already unbound',
		});

		pass();
		return true;
	}

	// Unbind server
	socket.interfaces[app_type].unbind(get_url(app_type), (error) => {
		if (error) {
			log.module({
				src : module_name,
				msg : 'zeroMQ server unbind '+error,
			});

			fail();
			return false;
		}

		log.msg({ src : module_name, msg : 'Terminated' });

		pass();
		return true;
	});
}

// Initialize websocket
function init_old_ws(init_callback = null) {
	socket.io.on('connect', () => {
	});

	socket.io.on('connect_error', (error) => {
		status.server.connected = false;
		if (status.server.reconnecting === false) {
			kodi_log('Connect error: '+error.description.code);
		}
	});

	socket.io.on('connect_timeout', () => {
		status.server.connected = false;
	});

	socket.io.on('reconnect', (number) => {
		status.server.connected  = true;
		status.server.connecting = false;

		// Request ignition on reconnect
		IKE.request('ignition');
	});

	socket.io.on('reconnect_attempt', () => {
		status.server.connected    = false;
		status.server.connecting   = true;
		status.server.reconnecting = true;
	});

	socket.io.on('reconnecting', (number) => {
		status.server.connected    = false;
		status.server.connecting   = true;
		status.server.reconnecting = true;
	});

	socket.io.on('reconnect_error', (error) => {
		status.server.connected = false;
	});

	socket.io.on('reconnect_failed', () => {
		status.server.connected    = false;
		status.server.connecting   = false;
		status.server.reconnecting = false;
	});

	socket.io.on('pong', (number) => {
		status.server.connected    = true;
		status.server.connecting   = false;
		status.server.reconnecting = false;

		// Only display message if the value changed
		if (status.server.latency !== number) {
			// Only display message if the value is greater than 10, or was previously greater than 10
			if (number > 10 || status.server.latency > 10) {
				log.msg({
					src : module_name,
					msg : 'Latency '+number+'ms',
				});
			}

			status.server.latency = number;
		}
	});
}

module.exports = {
	interfaces : {
		can0   : zeromq.socket(get_type('can0')),
		can1   : zeromq.socket(get_type('can1')),
		client : zeromq.socket(get_type('client')),
		daemon : zeromq.socket(get_type('daemon')),
		dbus   : zeromq.socket(get_type('dbus')),
		ibus   : zeromq.socket(get_type('ibus')),
		kbus   : zeromq.socket(get_type('kbus')),
		lcd    : zeromq.socket(get_type('lcd')),
	},

	// Start/stop functions
	init : (pass, fail) => { return init(pass, fail); },
	term : (pass, fail) => { return term(pass, fail); },

	// Data sender-wrapper
	send : (event, data) => { send(event, data); },

	// Send USB LCD commands/text to bmwd
	lcd_color_tx   : (data) => { send('lcd-color',   data); },
	lcd_command_tx : (data) => { send('lcd-command', data); },
	lcd_text_tx    : (data) => { send('lcd-text',    data); },

	// Send vehicle bus data to bmwi instance
	bus_tx : (bus, data) => {
		send('bus-tx', {
			bus  : bus,
			data : data,
		});
	},

	// Send status data object for use by other zeroMQ clients
	status_tx : (module) => {
		// If the entire status object was requested
		if (module == 'all') {
			log.msg({
				src : module_name,
				msg : 'Sending full status',
			});

			send('status', status);
			host_data.send();
			return;
		}

		log.module({
			src : module_name,
			msg : 'Sending \''+module+'\' status',
		});

		let msg = {};
		msg[module] = status[module];
		send('status', msg);
	},

	// Send bus log messages to bmwd
	log_bus : (data) => {
		return;
		send('log-bus', data);
	},

	// Send app log messages to bmwd
	log_msg : (data) => {
		return;
		send('log-msg', data);
	},
};
<commit_msg>Update<commit_after>const module_name = __filename.slice(__dirname.length + 1, -3);

const amp    = require('amp-message');
const zeromq = require('zeromq');

// Output log message to kodi notification and log.msg
function kodi_log(message) {
	kodi.notify(module_name, message);

	log.msg({
		src : module_name,
		msg : message,
	});
}

// Handle incoming messages
function on_data(data) {
	console.dir(data, { showHidden: true, depth: null, colors: true});

	switch (data.event) {
		case 'bus-rx'            : bus_data.receive(data.data); break;
		case 'host-data'         : on_host_data(data);          break;
		case 'host-data-request' : host_data.send();            break;
		case 'status-request'    : socket.status_tx(data.data); break;

			// case 'log-bus'     : log.bus   (data.message); break;
			// case 'log-msg'     : log.socket(data);         break; // Broken, fix this
			// case 'bus-tx'      :
			// case 'lcd-color'   :
			// case 'lcd-command' :
			// case 'lcd-text'    :
			// default            :
	}

	return;

	log.socket({
		method : 'rx',
		type   : data.host.type,
		event  : data.event,
		string : data.host.host.short,
	});
}

// Handle incoming host-data messages
function on_host_data(data) {
	return;

	log.socket({
		method : 'rx',
		type   : data.data.type,
		event  : data.data.host.short+' => temp',
		string : data.data.temperature+'c',
	});
}

// Send data over zeroMQ
function send(event, data) {
	// Don't bother sending anything if we're not connected
	if (status.server.connected === false) {
		log.msg({
			src : module_name,
			msg : 'Server not connected, cannot send message',
		});

		return;
	}

	let message = {
		host  : status.system,
		event : event,
		data  : data,
	};

	// Encode object as AMP message
	let amp_message = new amp;
	amp_message.push(message);

	// Send AMP message over zeroMQ
	socket.interfaces[app_type].send([ app_type+'-tx', amp_message.toBuffer() ]);

	return;

	log.socket({
		method : 'tx',
		type   : status.system.type,
		event  : event,
		string : '',
	});
}

// Decode AMP-encoded messages from zeroMQ
function decode_amp(message) {
	let data = new amp(message).shift();
	// console.log(JSON.stringify(data, null, 2));

	// Parse received data
	on_data(data);
}

// Configure event handlers
function event_config(pass) {
	Object.keys(socket.interfaces).forEach((interface) => {
		// When a peer connects
		socket.interfaces[interface].on('connect', () => {
			log.module({
				src : module_name,
				msg : 'Peer connected',
			});
		});

		// When a peer disconnects
		socket.interfaces[interface].on('disconnect', () => {
			log.module({
				src : module_name,
				msg : 'Peer disconnected',
			});
		});

		socket.interfaces[interface].on('error', (error) => {
			log.module({
				src : module_name,
				msg : 'Error '+error,
			});
		});

		socket.interfaces[interface].on('message', (topic, message) => {
			log.module({
				src : module_name,
				msg : topic+' message received via '+interface,
			});

			// Decode AMP message
			decode_amp(message);
		});
	});

	socket.interfaces[app_type].on('bind', () => {
		update.status('server.connected', true);

		update.status('server.connecting',   false);
		update.status('server.reconnecting', false);

		// Send this host's data to zeroMQ clients to update them
		host_data.send();

		if (app_type == 'client') {
			// Refresh all data on first connect if enabled
			// This should ACTUALLY be more event-driven and reside in modules/IKE.js
			setTimeout(() => {
				switch (config.options.obc_refresh_on_start) {
					case true : IKE.obc_refresh(); break;
					default   : IKE.request('ignition');
				}
			}, 250);
		}
	});

	socket.interfaces[app_type].on('unbind', () => {
		update.status('server.connected', false);

		if (app_type == 'client') {
			// Reset basic vars
			json.status_reset_basic(() => {});
		}
	});

	log.module({ src : module_name, msg : 'Initialized event listeners' });

	pass();
	return true;
}

// Return full zeroMQ URL from config data
function get_url(interface) {
	let zmq_conf = config.zeromq;
	return zmq_conf.proto+'://'+zmq_conf.urls[interface]+':'+zmq_conf.ports[interface];
}

// Return zeroMQ socket type from config data
function get_type(interface) {
	if (interface === app_type) return 'pub';
	return 'sub';
}

// Initialize zeroMQ server
function init(pass, fail) {
	log.msg({ src : module_name, msg : 'Initializing' });

	// Setup events
	event_config(() => {
		// Bind server
		socket.interfaces[app_type].bind(get_url(app_type), (error) => {
			if (error) {
				log.module({
					src : module_name,
					msg : 'zeroMQ server bind '+error,
				});

				fail();
				return false;
			}

			// Connect interfaces and subscribe to topics
			Object.keys(socket.interfaces).forEach((interface) => {
				if (interface != app_type) {
					socket.interfaces[interface].connect(get_url(interface));
					socket.interfaces[interface].subscribe(interface+'-tx');
					socket.interfaces[interface].subscribe('daemon-tx');
					socket.interfaces[interface].subscribe('client-tx');
					socket.interfaces[interface].subscribe('bus-rx');
					log.module({ src : module_name, msg : interface+' connected' });
				}
			})

			log.module({ src : module_name, msg : 'Initialized' });

			pass();
			return true;
		});
	});
}

// Terminate zeroMQ server
function term(pass, fail) {
	log.msg({ src : module_name, msg : 'Terminating' });

	// Disconnect interfaces
	Object.keys(socket.interfaces).forEach((interface) => {
		if (interface != app_type) {
			socket.interfaces[interface].disconnect(get_url(interface));
			log.module({ src : module_name, msg : interface+' disconnected' });
		}
	});

	if (!status.server.connected) {
		log.module({
			src : module_name,
			msg : 'zeroMQ server already unbound',
		});

		pass();
		return true;
	}

	// Unbind server
	socket.interfaces[app_type].unbind(get_url(app_type), (error) => {
		if (error) {
			log.module({
				src : module_name,
				msg : 'zeroMQ server unbind '+error,
			});

			fail();
			return false;
		}

		log.msg({ src : module_name, msg : 'Terminated' });

		pass();
		return true;
	});
}

// Initialize websocket
function init_old_ws(init_callback = null) {
	socket.io.on('connect', () => {
	});

	socket.io.on('connect_error', (error) => {
		status.server.connected = false;
		if (status.server.reconnecting === false) {
			kodi_log('Connect error: '+error.description.code);
		}
	});

	socket.io.on('connect_timeout', () => {
		status.server.connected = false;
	});

	socket.io.on('reconnect', (number) => {
		status.server.connected  = true;
		status.server.connecting = false;

		// Request ignition on reconnect
		IKE.request('ignition');
	});

	socket.io.on('reconnect_attempt', () => {
		status.server.connected    = false;
		status.server.connecting   = true;
		status.server.reconnecting = true;
	});

	socket.io.on('reconnecting', (number) => {
		status.server.connected    = false;
		status.server.connecting   = true;
		status.server.reconnecting = true;
	});

	socket.io.on('reconnect_error', (error) => {
		status.server.connected = false;
	});

	socket.io.on('reconnect_failed', () => {
		status.server.connected    = false;
		status.server.connecting   = false;
		status.server.reconnecting = false;
	});

	socket.io.on('pong', (number) => {
		status.server.connected    = true;
		status.server.connecting   = false;
		status.server.reconnecting = false;

		// Only display message if the value changed
		if (status.server.latency !== number) {
			// Only display message if the value is greater than 10, or was previously greater than 10
			if (number > 10 || status.server.latency > 10) {
				log.msg({
					src : module_name,
					msg : 'Latency '+number+'ms',
				});
			}

			status.server.latency = number;
		}
	});
}

module.exports = {
	interfaces : {
		can0   : zeromq.socket(get_type('can0')),
		can1   : zeromq.socket(get_type('can1')),
		client : zeromq.socket(get_type('client')),
		daemon : zeromq.socket(get_type('daemon')),
		dbus   : zeromq.socket(get_type('dbus')),
		ibus   : zeromq.socket(get_type('ibus')),
		kbus   : zeromq.socket(get_type('kbus')),
		lcd    : zeromq.socket(get_type('lcd')),
	},

	// Start/stop functions
	init : (pass, fail) => { return init(pass, fail); },
	term : (pass, fail) => { return term(pass, fail); },

	// Data sender-wrapper
	send : (event, data) => { send(event, data); },

	// Send USB LCD commands/text to bmwd
	lcd_color_tx   : (data) => { send('lcd-color',   data); },
	lcd_command_tx : (data) => { send('lcd-command', data); },
	lcd_text_tx    : (data) => { send('lcd-text',    data); },

	// Send vehicle bus data to bmwi instance
	bus_tx : (bus, data) => {
		send('bus-tx', {
			bus  : bus,
			data : data,
		});
	},

	// Send status data object for use by other zeroMQ clients
	status_tx : (module) => {
		// If the entire status object was requested
		if (module == 'all') {
			log.msg({
				src : module_name,
				msg : 'Sending full status',
			});

			send('status', status);
			host_data.send();
			return;
		}

		log.module({
			src : module_name,
			msg : 'Sending \''+module+'\' status',
		});

		let msg = {};
		msg[module] = status[module];
		send('status', msg);
	},

	// Send bus log messages to bmwd
	log_bus : (data) => {
		return;
		send('log-bus', data);
	},

	// Send app log messages to bmwd
	log_msg : (data) => {
		return;
		send('log-msg', data);
	},
};
<|endoftext|>


<commit_before>var gzip = require('zlib').gzip;
var url = require('url');
var testSuffix = /\.[a-zA-Z0-9]+$/;
var wrabbit = require("wrabbit");

// the index client
var client;

// index document http headers
var resHeaders = {
    'Cache-Control': 'public, max-age=31536000',
    'Vary': 'Accept-Encoding',
    'Content-Type': 'text/html; charset=utf-8',
    'Content-Encoding': 'gzip',
    'Server': 'JCES'
};

/**
 * Handle public files.
 *
 * @public
 * @param {array} The url paht splitted into an array.
 * @param {object} The http request object.
 * @param {object} The http respone object.
 */
exports.public = function (pathname, req, res) {
    var self = this;

    engine.file.get(pathname, function (err, file) {

        if (err) {
            res.writeHead(404, {'Content-Type': 'text/plain; charset=utf-8'});
            res.end('File "' + pathname + '" not found.');
            return;
        }

        res.writeHead(200, file.http);
        res.end(file.data);
    });
};

/**
 * Proxy an external script and append the engine wrapping code.
 *
 * @public
 * @param {object} The link object.
 */
exports.externalFile = function (link) {
    var url = link.path.join('/');
    wrabbit.wrapUrl(url, link.query.w === "1" ? "E('" + url + "', function (require, module, exports, global, engine) {" : "", function (err, res) {
        if (err) {
            link.res.writeHead(res);
            return link.res.end(err);
        }

        link.res.writeHead(200, {
            "Content-type": "application/javascript"
        });
        link.res.end(res);
    });
};

/**
 * Send the index document to the client.
 *
 * @private
 * @param {object} The http response object.
 * @param {boolean} Remove session if true.
 */
exports.client = function (pathname, req, res) {

    var removeSid = req._sidInvalid;

    // remove sid
    if (removeSid) {
        resHeaders['Set-Cookie'] = 'sid=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
    } else if (resHeaders['Set-Cookie']) {
        delete resHeaders['Set-Cookie'];
    }

    if (client) {
        res.writeHead(200, resHeaders);
        return res.end(client);
    }

    // get the engine client and core modules paths with fingerprints
    // TODO combine the client files in production mode
    var loadContext = {
        _id: engine.operation_id,
        _base: engine.root + 'lib/client/',
        components: {
            scripts: ['engine.js', 'flow.js', 'utils.js',  'link.js']
        }
    };

    engine.file.prepareComponents(loadContext, function (err, files) {

        // send the error in the browser
        if (err) {
            res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8'});
            res.end(err.toString());
            return;
        }

        // get script files array
        files = files.components.scripts;

        // load engine client in a browser
        var script = '';

        // load the core engine modules
        script += '';

        // zip and send index document
        gzip(
            '<!DOCTYPE html>' +
                '<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">' +
                '<link rel="icon" href="/favicon.ico"/>' +
                script +
            '',
            function (err, data) {

                client = data;
                resHeaders['Content-Length'] = client.length;

                resHeaders['Last-Modified'] = new Date().toString();

                res.writeHead(200, resHeaders);
                res.end(client);
            }
        );
    });
};

/**
 * Get the client side module and engine scripts.
 *
 * @public
 * @param {object} The link object.
 */
exports.moduleFile = function (link) {
    var self = this;

    // the module name must be almost alphanumeric
    if (link.pathname.replace(/[^a-z0-9\/\.\-_@\|]|\.\.\//gi, '') !== link.pathname) {
        link.headers = {'content-type': 'text/plain'};
        return link.end(400, 'Incorrect data in module request URL');
    }

    // get client engine files
    if (link.path[0] === engine.operation_id) {

        var file = {
            path: link.path[1],
            base: engine.root + 'lib/client/'
        };

        if (link.path[0] !== 'engine.js') {
            file.wrap = engine.operation_id + '/' + link.path[1];
        }

        // save compressed/compiled script in cache and send it to the client
        engine.file.get(file, function (err, script) {

            if (err) {
                link.headers = {'content-type': 'text/plain'};
                link.end(404, err.toString());
                return;
            }

            link.headers = script.http;
            link.end(200, script.data);
        });

        return;
    }

    // get module package
    var modulePackage = engine.modules.get(link.path[0]);

    // check if module exists
    if (!modulePackage || !modulePackage.components) {
        link.headers = {'content-type': 'text/plain'};
        return link.end(404, 'File not found.');
    }

    var file = {
        path: link.path.slice(1).join('/'),
        base: modulePackage._base,
        wrap: link.path[0] + '/'
    };

    // get files for access ceck
    var files;
    switch (link.pathname.split('.').pop()) {
        case 'js':
            files = modulePackage.components.scripts;
            break;
        case 'css':
            files = modulePackage.components.styles;
            break;
        case 'html':
        case 'htm':
            files = modulePackage.components.markup;
            break;
        default:
          file.base += (modulePackage.public || 'public') + '/';
    }

    // check file access
    if (files) {

        var found;
        for (var i = 0; i < files.length; ++i) {

            if (files[i] === file.wrap + file.path) {
                found = true;
                break;
            }
        }

        if (!found) {
            link.headers = {'content-type': 'text/plain'};
            return link.end(403, 'Forbidden to access, this file is.');
        }
    }

    // save compressed/compiled script in cache and send it to the client
    engine.file.get(file, function (err, script) {

        if (err) {
            link.headers = {'content-type': 'text/plain'};
            return link.end(404, err.toString());
        }

        link.headers = script.http;
        link.end(200, script.data);
    });
};

/**
 * Get a HTML snippet.
 *
 * @public
 * @param {object} The link object.
 */
exports.markup = function (link) {
    var self = this;

    link.data(function (err, path) {

        // ignore errors
        if (err) {
            return;
        }

        var file = {
            path: path,
            base: engine.paths.app_markup,
            noCompression: true
        };

        if (path[0] !== '/') {
            // split path and get module id
            var moduleId = (path = path.split('/')).shift();

            // create path without module id
            file.path = path.join('/');

            // get module package
            var modulePackage = engine.modules.get(moduleId);

            // read the file from module base
            if (!modulePackage) {
                err = 'No module package found for module: ' + moduleId;
                console.error('lib/static.js#221', err.toString());
                return link.send(err);
            }

            // set base dir to module base
            file.base = modulePackage._base;
        }

        // send html snippet to client
        engine.file.get(file, function (err, file) {

            if (err || !file && !file.data) {
                err = err|| 'HTML snippet not found.';
                console.error('lib/static.js#229', err.toString());
                return link.send(err);
            }

            link.send(err, [file.ext, file.data.toString('utf8')]);
        });
    });
}
<commit_msg>Use the stream method in wrabbit<commit_after>var gzip = require('zlib').gzip;
var url = require('url');
var testSuffix = /\.[a-zA-Z0-9]+$/;
var wrabbit = require("wrabbit");

// the index client
var client;

// index document http headers
var resHeaders = {
    'Cache-Control': 'public, max-age=31536000',
    'Vary': 'Accept-Encoding',
    'Content-Type': 'text/html; charset=utf-8',
    'Content-Encoding': 'gzip',
    'Server': 'JCES'
};

/**
 * Handle public files.
 *
 * @public
 * @param {array} The url paht splitted into an array.
 * @param {object} The http request object.
 * @param {object} The http respone object.
 */
exports.public = function (pathname, req, res) {
    var self = this;

    engine.file.get(pathname, function (err, file) {

        if (err) {
            res.writeHead(404, {'Content-Type': 'text/plain; charset=utf-8'});
            res.end('File "' + pathname + '" not found.');
            return;
        }

        res.writeHead(200, file.http);
        res.end(file.data);
    });
};

/**
 * Proxy an external script and append the engine wrapping code.
 *
 * @public
 * @param {object} The link object.
 */
exports.externalFile = function (link) {
    var url = link.path.join('/');
    wrabbit.stream(url, link.query.w === "1" ? "E('" + url + "', function (require, module, exports, global, engine) {" : "", link.res, function (err, res) {
        if (err) {
            link.res.writeHead(400);
            return link.res.end(err.toString());
        }

        link.res.writeHead(200, {
            "Content-type": "application/javascript"
        });
        link.res.end(res);
    });
};

/**
 * Send the index document to the client.
 *
 * @private
 * @param {object} The http response object.
 * @param {boolean} Remove session if true.
 */
exports.client = function (pathname, req, res) {

    var removeSid = req._sidInvalid;

    // remove sid
    if (removeSid) {
        resHeaders['Set-Cookie'] = 'sid=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
    } else if (resHeaders['Set-Cookie']) {
        delete resHeaders['Set-Cookie'];
    }

    if (client) {
        res.writeHead(200, resHeaders);
        return res.end(client);
    }

    // get the engine client and core modules paths with fingerprints
    // TODO combine the client files in production mode
    var loadContext = {
        _id: engine.operation_id,
        _base: engine.root + 'lib/client/',
        components: {
            scripts: ['engine.js', 'flow.js', 'utils.js',  'link.js']
        }
    };

    engine.file.prepareComponents(loadContext, function (err, files) {

        // send the error in the browser
        if (err) {
            res.writeHead(500, {'Content-Type': 'text/plain; charset=utf-8'});
            res.end(err.toString());
            return;
        }

        // get script files array
        files = files.components.scripts;

        // load engine client in a browser
        var script = '';

        // load the core engine modules
        script += '';

        // zip and send index document
        gzip(
            '<!DOCTYPE html>' +
                '<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">' +
                '<link rel="icon" href="/favicon.ico"/>' +
                script +
            '',
            function (err, data) {

                client = data;
                resHeaders['Content-Length'] = client.length;

                resHeaders['Last-Modified'] = new Date().toString();

                res.writeHead(200, resHeaders);
                res.end(client);
            }
        );
    });
};

/**
 * Get the client side module and engine scripts.
 *
 * @public
 * @param {object} The link object.
 */
exports.moduleFile = function (link) {
    var self = this;

    // the module name must be almost alphanumeric
    if (link.pathname.replace(/[^a-z0-9\/\.\-_@\|]|\.\.\//gi, '') !== link.pathname) {
        link.headers = {'content-type': 'text/plain'};
        return link.end(400, 'Incorrect data in module request URL');
    }

    // get client engine files
    if (link.path[0] === engine.operation_id) {

        var file = {
            path: link.path[1],
            base: engine.root + 'lib/client/'
        };

        if (link.path[0] !== 'engine.js') {
            file.wrap = engine.operation_id + '/' + link.path[1];
        }

        // save compressed/compiled script in cache and send it to the client
        engine.file.get(file, function (err, script) {

            if (err) {
                link.headers = {'content-type': 'text/plain'};
                link.end(404, err.toString());
                return;
            }

            link.headers = script.http;
            link.end(200, script.data);
        });

        return;
    }

    // get module package
    var modulePackage = engine.modules.get(link.path[0]);

    // check if module exists
    if (!modulePackage || !modulePackage.components) {
        link.headers = {'content-type': 'text/plain'};
        return link.end(404, 'File not found.');
    }

    var file = {
        path: link.path.slice(1).join('/'),
        base: modulePackage._base,
        wrap: link.path[0] + '/'
    };

    // get files for access ceck
    var files;
    switch (link.pathname.split('.').pop()) {
        case 'js':
            files = modulePackage.components.scripts;
            break;
        case 'css':
            files = modulePackage.components.styles;
            break;
        case 'html':
        case 'htm':
            files = modulePackage.components.markup;
            break;
        default:
          file.base += (modulePackage.public || 'public') + '/';
    }

    // check file access
    if (files) {

        var found;
        for (var i = 0; i < files.length; ++i) {

            if (files[i] === file.wrap + file.path) {
                found = true;
                break;
            }
        }

        if (!found) {
            link.headers = {'content-type': 'text/plain'};
            return link.end(403, 'Forbidden to access, this file is.');
        }
    }

    // save compressed/compiled script in cache and send it to the client
    engine.file.get(file, function (err, script) {

        if (err) {
            link.headers = {'content-type': 'text/plain'};
            return link.end(404, err.toString());
        }

        link.headers = script.http;
        link.end(200, script.data);
    });
};

/**
 * Get a HTML snippet.
 *
 * @public
 * @param {object} The link object.
 */
exports.markup = function (link) {
    var self = this;

    link.data(function (err, path) {

        // ignore errors
        if (err) {
            return;
        }

        var file = {
            path: path,
            base: engine.paths.app_markup,
            noCompression: true
        };

        if (path[0] !== '/') {
            // split path and get module id
            var moduleId = (path = path.split('/')).shift();

            // create path without module id
            file.path = path.join('/');

            // get module package
            var modulePackage = engine.modules.get(moduleId);

            // read the file from module base
            if (!modulePackage) {
                err = 'No module package found for module: ' + moduleId;
                console.error('lib/static.js#221', err.toString());
                return link.send(err);
            }

            // set base dir to module base
            file.base = modulePackage._base;
        }

        // send html snippet to client
        engine.file.get(file, function (err, file) {

            if (err || !file && !file.data) {
                err = err|| 'HTML snippet not found.';
                console.error('lib/static.js#229', err.toString());
                return link.send(err);
            }

            link.send(err, [file.ext, file.data.toString('utf8')]);
        });
    });
}
<|endoftext|>


<commit_before>var gzip = require('zlib').gzip;
var url = require('url');
var wrabbit = require("wrabbit");
var File = require('./file');
var Cache = require('./cache');
var Composition = require('./composition');

var ModuleCache = Cache('modules');

// the index client
var client;

// index document http headers
var resHeaders = {
    'Cache-Control': 'public, max-age=31536000',
    'Vary': 'Accept-Encoding',
    'Content-Type': 'text/html; charset=utf-8',
    'Content-Encoding': 'gzip',
    'Server': 'JCES'
};

/**
 * Send the index document to the client.
 *
 * @private
 * @param {object} The http response object.
 * @param {boolean} Remove session if true.
 */
exports.client = function (stream) {

    var pathname = stream.pathname;
    var req = stream.req;
    var res = stream.res;

    stream.headers = resHeaders;

    // remove sid
    if (req._sidInvalid) {
        resHeaders['Set-Cookie'] = 'sid=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
    } else if (resHeaders['Set-Cookie']) {
        delete resHeaders['Set-Cookie'];
    }

    if (client) {
        return stream.end(200, client);
    }

    // get the engine client and core modules paths with fingerprints
    var loadContext = {
        _id: engine.operation_id,
        _base: engine.root + 'lib/client/',
        combine: 'client.js',
        components: {
            module: [
                'resource.js',
                'utils.js',
                'stream.js',
                'socket.js',
                'flow.js',
                'instance.js',
                'transform.js',
                'engine.js'
            ]
        }
    };

    File.prepareComponents(loadContext, function (err, files) {

        // send the error in the browser
        if (err) {
            stream.headers = {'Content-Type': 'text/plain; charset=utf-8'};
            return stream.end(500, err.toString());
        }

        // get script files array
        files = files.components.module;

        var script = '';

        // load single files for better debugging
        // TOOD combine files in production mode
        for (var i = 0; i < files.length; ++i) {
            script += '';
        }

        // compress client
        gzip(
            '<!DOCTYPE html>' +
                '<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">' +
                '<link rel="icon" href="/favicon.ico"/>' +
                script +
            '',
            function (err, data) {

                // send the error in the browser
                if (err) {
                    stream.headers = {'Content-Type': 'text/plain; charset=utf-8'};
                    return stream.end(500, err.toString());
                }

                client = data;
                resHeaders['Content-Length'] = client.length;

                // send client
                stream.end(200, client);
            }
        );
    });
};

/**
 * Handle public files.
 *
 * @public
 * @param {array} The url paht splitted into an array.
 * @param {object} The http request object.
 * @param {object} The http respone object.
 */
exports.public = function (stream) {
    var self = this;

    // TODO stream file
    File.get(stream.pathname, stream.query.w, function (err, file) {

        if (err) {
            stream.headers = {'Content-Type': 'text/plain; charset=utf-8'};
            stream.end(404, 'File "' + stream.pathname + '" not found.');
            return;
        }

        stream.headers = file.http;
        stream.end(200, file.data);
    });
};

/**
 * Proxy an external script and append the engine wrapping code.
 *
 * @public
 * @param {object} The link object.
 */
exports.wrap = function (stream) {
    var url = stream.path.join('/');
    wrabbit.stream(url, stream.query.w === "1" ? "E('" + url + "', function (require, module, exports, global, engine) {" : "", stream.res);
};

/**
 * Load an module instance over websoockets.
 *
 * @public
 * @param {object} The link object.
 */
exports.composition = function (stream) {
    var self = this;
    var role = stream.socket.session[engine.session_role];

    // receive data
    stream.data(function (err, instance) {

        // load entrypoint module instance with hostname
        if (!instance) {

            instance = stream.socket.upgradeReq.headers.host.split(':')[0].replace(/\./g, '_');

            // get the public version of the entrypoint module instance
            if (!role) {
                instance += '.pub';
            }
        }

        // get composition
        Composition(instance, role, function (err, composition) {

            if (err) {
                return stream.write(err.toString());
            }

            if (!composition.module) {
                return stream.write('Composition has no client module!');
            }

            // write composition to client
            stream.write(null, composition.client);
        });
    });
};

/**
 * Get the client side module and engine scripts.
 *
 * @public
 * @param {object} The link object.
 */
exports.file = function (stream) {
    var self = this;

    // the module name must be almost alphanumeric
    if (stream.pathname.replace(/[^a-z0-9\/\.\-_@\|]|\.\.\//gi, '') !== stream.pathname) {
        stream.headers = {'content-type': 'text/plain'};
        return stream.end(400, 'Incorrect data in module request URL');
    }

    var file;

    // get client engine files
    if (stream.path[0] === engine.operation_id) {

        file = {
            path: stream.path[1],
            base: engine.root + 'lib/client/'
        };

        if (stream.path[0] !== 'engine.js') {
            file.wrap = engine.operation_id + '/' + stream.path[1];
        }

        // save compressed/compiled script in cache and send it to the client
        File.get(file, true, function (err, script) {

            if (err) {
                stream.headers = {'content-type': 'text/plain'};
                stream.end(404, err.toString());
                return;
            }

            stream.headers = script.http;
            stream.end(200, script.data);
        });

        return;
    }

    // get module package
    var modulePackage = ModuleCache.get(stream.path[0]);

    // check if module exists
    if (!modulePackage || !modulePackage.components) {
        stream.headers = {'content-type': 'text/plain'};
        return stream.end(404, 'File not found.');
    }

    file = {
        path: stream.path.slice(1).join('/'),
        base: modulePackage._base,
        wrap: stream.path[0] + '/'
    };

    // get files for access ceck
    var files;
    switch (stream.pathname.split('.').pop()) {
        case 'js':
            files = modulePackage.components.module;
            break;
        case 'css':
            files = modulePackage.components.styles;
            break;
        case 'html':
        case 'htm':
            files = modulePackage.components.markup;
            break;
        default:
          file.base += (modulePackage.public || 'public') + '/';
    }

    // check file access
    if (files) {

        var found;
        for (var i = 0; i < files.length; ++i) {

            if (files[i] === file.wrap + file.path) {
                found = true;
                break;
            }
        }

        if (!found) {
            stream.headers = {'content-type': 'text/plain'};
            return stream.end(403, 'Forbidden to access, this file is.');
        }
    }

    // save compressed/compiled script in cache and send it to the client
    File.get(file, stream.query.w, function (err, script) {

        if (err) {
            stream.headers = {'content-type': 'text/plain'};
            return stream.end(404, err.toString());
        }

        stream.headers = script.http;
        stream.end(200, script.data);
    });
};

/**
 * Get a HTML snippet.
 *
 * @public
 * @param {object} The link object.
 */
exports.markup = function (stream) {
    var self = this;

    stream.data(function (err, path) {

        // ignore errors
        if (err) {
            return;
        }

        var file = {
            path: path,
            base: engine.paths.app_markup,
            noCompression: true
        };

        if (path[0] !== '/') {
            // split path and get module id
            var moduleId = (path = path.split('/')).shift();

            // create path without module id
            file.path = path.join('/');

            // get module package
            var modulePackage = ModuleCache.get(moduleId);

            // read the file from module base
            if (!modulePackage) {
                err = 'No module package found for module: ' + moduleId;
                console.error('lib/static.js#221', err.toString());
                return stream.write(err);
            }

            // set base dir to module base
            file.base = modulePackage._base;
        }

        // send html snippet to client
        File.get(file, false, function (err, file) {

            if (err || !file && !file.data) {
                err = err|| 'HTML snippet not found.';
                console.error('lib/static.js#229', err.toString());
                return stream.write(err);
            }

            stream.write(err, [file.ext, file.data.toString('utf8')]);
        });
    });
};
<commit_msg>better client composition checks<commit_after>var gzip = require('zlib').gzip;
var url = require('url');
var wrabbit = require("wrabbit");
var File = require('./file');
var Cache = require('./cache');
var Composition = require('./composition');

var ModuleCache = Cache('modules');

// the index client
var client;

// index document http headers
var resHeaders = {
    'Cache-Control': 'public, max-age=31536000',
    'Vary': 'Accept-Encoding',
    'Content-Type': 'text/html; charset=utf-8',
    'Content-Encoding': 'gzip',
    'Server': 'JCES'
};

/**
 * Send the index document to the client.
 *
 * @private
 * @param {object} The http response object.
 * @param {boolean} Remove session if true.
 */
exports.client = function (stream) {

    var pathname = stream.pathname;
    var req = stream.req;
    var res = stream.res;

    stream.headers = resHeaders;

    // remove sid
    if (req._sidInvalid) {
        resHeaders['Set-Cookie'] = 'sid=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT';
    } else if (resHeaders['Set-Cookie']) {
        delete resHeaders['Set-Cookie'];
    }

    if (client) {
        return stream.end(200, client);
    }

    // get the engine client and core modules paths with fingerprints
    var loadContext = {
        _id: engine.operation_id,
        _base: engine.root + 'lib/client/',
        combine: 'client.js',
        components: {
            module: [
                'resource.js',
                'utils.js',
                'stream.js',
                'socket.js',
                'flow.js',
                'instance.js',
                'transform.js',
                'engine.js'
            ]
        }
    };

    File.prepareComponents(loadContext, function (err, files) {

        // send the error in the browser
        if (err) {
            stream.headers = {'Content-Type': 'text/plain; charset=utf-8'};
            return stream.end(500, err.toString());
        }

        // get script files array
        files = files.components.module;

        var script = '';

        // load single files for better debugging
        // TOOD combine files in production mode
        for (var i = 0; i < files.length; ++i) {
            script += '';
        }

        // compress client
        gzip(
            '<!DOCTYPE html>' +
                '<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">' +
                '<link rel="icon" href="/favicon.ico"/>' +
                script +
            '',
            function (err, data) {

                // send the error in the browser
                if (err) {
                    stream.headers = {'Content-Type': 'text/plain; charset=utf-8'};
                    return stream.end(500, err.toString());
                }

                client = data;
                resHeaders['Content-Length'] = client.length;

                // send client
                stream.end(200, client);
            }
        );
    });
};

/**
 * Handle public files.
 *
 * @public
 * @param {array} The url paht splitted into an array.
 * @param {object} The http request object.
 * @param {object} The http respone object.
 */
exports.public = function (stream) {
    var self = this;

    // TODO stream file
    File.get(stream.pathname, stream.query.w, function (err, file) {

        if (err) {
            stream.headers = {'Content-Type': 'text/plain; charset=utf-8'};
            stream.end(404, 'File "' + stream.pathname + '" not found.');
            return;
        }

        stream.headers = file.http;
        stream.end(200, file.data);
    });
};

/**
 * Proxy an external script and append the engine wrapping code.
 *
 * @public
 * @param {object} The link object.
 */
exports.wrap = function (stream) {
    var url = stream.path.join('/');
    wrabbit.stream(url, stream.query.w === "1" ? "E('" + url + "', function (require, module, exports, global, engine) {" : "", stream.res);
};

/**
 * Load an module instance over websoockets.
 *
 * @public
 * @param {object} The link object.
 */
exports.composition = function (stream) {
    var self = this;
    var role = stream.socket.session[engine.session_role];

    // receive data
    stream.data(function (err, instance) {

        // load entrypoint module instance with hostname
        if (!instance) {

            instance = stream.socket.upgradeReq.headers.host.split(':')[0].replace(/\./g, '_');

            // get the public version of the entrypoint module instance
            if (!role) {
                instance += '.pub';
            }
        }

        // get composition
        Composition(instance, role, function (err, composition) {

            if (err) {
                return stream.write(err.toString());
            }

            if (!composition.module || !composition.client || !composition.client.module) {
                return stream.write('Incomplete client composition!');
            }

            // write composition to client
            stream.write(null, composition.client);
        });
    });
};

/**
 * Get the client side module and engine scripts.
 *
 * @public
 * @param {object} The link object.
 */
exports.file = function (stream) {
    var self = this;

    // the module name must be almost alphanumeric
    if (stream.pathname.replace(/[^a-z0-9\/\.\-_@\|]|\.\.\//gi, '') !== stream.pathname) {
        stream.headers = {'content-type': 'text/plain'};
        return stream.end(400, 'Incorrect data in module request URL');
    }

    var file;

    // get client engine files
    if (stream.path[0] === engine.operation_id) {

        file = {
            path: stream.path[1],
            base: engine.root + 'lib/client/'
        };

        if (stream.path[0] !== 'engine.js') {
            file.wrap = engine.operation_id + '/' + stream.path[1];
        }

        // save compressed/compiled script in cache and send it to the client
        File.get(file, true, function (err, script) {

            if (err) {
                stream.headers = {'content-type': 'text/plain'};
                stream.end(404, err.toString());
                return;
            }

            stream.headers = script.http;
            stream.end(200, script.data);
        });

        return;
    }

    // get module package
    var modulePackage = ModuleCache.get(stream.path[0]);

    // check if module exists
    if (!modulePackage || !modulePackage.components) {
        stream.headers = {'content-type': 'text/plain'};
        return stream.end(404, 'File not found.');
    }

    file = {
        path: stream.path.slice(1).join('/'),
        base: modulePackage._base,
        wrap: stream.path[0] + '/'
    };

    // get files for access ceck
    var files;
    switch (stream.pathname.split('.').pop()) {
        case 'js':
            files = modulePackage.components.module;
            break;
        case 'css':
            files = modulePackage.components.styles;
            break;
        case 'html':
        case 'htm':
            files = modulePackage.components.markup;
            break;
        default:
          file.base += (modulePackage.public || 'public') + '/';
    }

    // check file access
    if (files) {

        var found;
        for (var i = 0; i < files.length; ++i) {

            if (files[i] === file.wrap + file.path) {
                found = true;
                break;
            }
        }

        if (!found) {
            stream.headers = {'content-type': 'text/plain'};
            return stream.end(403, 'Forbidden to access, this file is.');
        }
    }

    // save compressed/compiled script in cache and send it to the client
    File.get(file, stream.query.w, function (err, script) {

        if (err) {
            stream.headers = {'content-type': 'text/plain'};
            return stream.end(404, err.toString());
        }

        stream.headers = script.http;
        stream.end(200, script.data);
    });
};

/**
 * Get a HTML snippet.
 *
 * @public
 * @param {object} The link object.
 */
exports.markup = function (stream) {
    var self = this;

    stream.data(function (err, path) {

        // ignore errors
        if (err) {
            return;
        }

        var file = {
            path: path,
            base: engine.paths.app_markup,
            noCompression: true
        };

        if (path[0] !== '/') {
            // split path and get module id
            var moduleId = (path = path.split('/')).shift();

            // create path without module id
            file.path = path.join('/');

            // get module package
            var modulePackage = ModuleCache.get(moduleId);

            // read the file from module base
            if (!modulePackage) {
                err = 'No module package found for module: ' + moduleId;
                console.error('lib/static.js#221', err.toString());
                return stream.write(err);
            }

            // set base dir to module base
            file.base = modulePackage._base;
        }

        // send html snippet to client
        File.get(file, false, function (err, file) {

            if (err || !file && !file.data) {
                err = err|| 'HTML snippet not found.';
                console.error('lib/static.js#229', err.toString());
                return stream.write(err);
            }

            stream.write(err, [file.ext, file.data.toString('utf8')]);
        });
    });
};
<|endoftext|>