UNPKG

nxkit

Version:

This is a collection of tools, independent of any other libraries

280 lines (279 loc) 11.3 kB
"use strict"; /* ***** BEGIN LICENSE BLOCK ***** * Distributed under the BSD license: * * Copyright (c) 2015, xuewen.chu * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of xuewen.chu nor the * names of its contributors may be used to endorse or promote products * derived from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL xuewen.chu BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * ***** END LICENSE BLOCK ***** */ function __export(m) { for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; } Object.defineProperty(exports, "__esModule", { value: true }); const util_1 = require("../util"); const service_1 = require("../service"); const wsservice = require("./service"); const errno_1 = require("../errno"); const buffer_1 = require("../buffer"); const uuid_1 = require("../hash/uuid"); const crypto = require("crypto"); const url = require("url"); const _conv_1 = require("./_conv"); __export(require("./_conv")); const parser_1 = require("./parser"); const data_1 = require("./data"); class WSConversation extends _conv_1.ConversationBasic { /** * @arg {http.ServerRequest} req * @arg {String} bind_services */ constructor(req, upgradeHead, bind_services) { super(); this.m_token = uuid_1.default(); var server = req.socket.server; this.server = server.__wrap__; this.request = req; this.socket = req.socket; // initialize this._initialize(bind_services).catch(err => { this.close(); this._safeDestroy(); // 关闭连接 // console.warn(err); }); } _safeDestroy() { try { if (this.socket) this.socket.destroy(); // 关闭连接 } catch (err) { console.warn(err); } } async _initialize(bind_services) { var self = this; var services = bind_services.split(','); util_1.default.assert(services[0], 'Bind Service undefined'); self.socket.pause(); if (!self.__initialize()) return self._safeDestroy(); // 关闭连接 util_1.default.assert(!self.m_isOpen); self.m_isOpen = true; self.onClose.on(function () { util_1.default.assert(self.m_isOpen); console.log('WS conv close'); self.m_isOpen = false; // self.request = null; // self.socket = null; // self.token = ''; // self.onOpen.off(); try { for (var s of Object.values(self.m_handles)) { s.destroy(); } self.server.onWSConversationClose.trigger(self); } catch (err) { console.error(err); } // self.server = null; util_1.default.nextTick(() => self.onClose.off()); }); self.onOpen.trigger({}); self.server.onWSConversationOpen.trigger(self); try { await self.bindServices(services); } catch (err) { await util_1.default.sleep(5e3); // delay 5s throw err; } self.socket.resume(); } __initialize() { if (!this._handshakes()) { return false; } var self = this; var socket = this.socket; var parser = new parser_1.PacketParser(); socket.setNoDelay(true); socket.setTimeout(0); socket.setKeepAlive(true, _conv_1.KEEP_ALIVE_TIME); socket.on('timeout', () => self.close()); socket.on('end', () => self.close()); socket.on('close', () => self.close()); socket.on('data', (e) => parser.add(buffer_1.default.from(e))); socket.on('error', e => (console.error('web socket error:', e), self.close())); socket.on('drain', () => (self.m_overflow = false, self.onDrain.trigger({}))); parser.onText.on(e => self.handlePacket(e.data, true /*isText*/)); parser.onData.on(e => self.handlePacket(e.data, false)); parser.onPing.on(e => self.handlePing(e.data)); parser.onPong.on(e => self.handlePong(e.data)); parser.onClose.on(e => self.close()); parser.onError.on(e => (console.error('web socket parser error:', e.data), self.close())); return true; } _handshakes() { var req = this.request; var key = req.headers['sec-websocket-key']; var origin = req.headers['sec-websocket-origin'] || ''; // var location = (socket.encrypted ? 'wss' : 'ws') + '://' + req.headers.host + req.url; var upgrade = req.headers.upgrade; if (!upgrade || upgrade.toLowerCase() !== 'websocket') { console.error('connection invalid'); return false; } if (!this.verifyOrigin(origin)) { console.error('connection invalid: origin mismatch'); return false; } if (!key) { console.error('connection invalid: received no key'); return false; } try { this._upgrade(); } catch (err) { console.error(err); return false; } return true; } _upgrade() { // calc key var key = this.request.headers['sec-websocket-key']; var shasum = crypto.createHash('sha1'); shasum.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'); key = shasum.digest('base64'); var headers = [ 'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', 'Session-Token: ' + this.token, 'Sec-WebSocket-Accept: ' + key, ]; this.socket.write(headers.concat('', '').join('\r\n')); } /** * verifies the origin of a request. * @param {String} origin * @return {Boolean} */ verifyOrigin(origin) { var origins = this.server.origins; if (origin == 'null') { origin = '*'; } if (origins.indexOf('*:*') != -1) { return true; } if (origin) { try { var parts = url.parse(origin); var ok = ~origins.indexOf(parts.hostname + ':' + parts.port) || ~origins.indexOf(parts.hostname + ':*') || ~origins.indexOf('*:' + parts.port); if (!ok) { console.warn('illegal origin: ' + origin); } return ok; } catch (ex) { console.warn('error parsing origin'); } } else { console.warn('origin missing from websocket call, yet required by config'); } return false; } /** * @func bindService() 绑定服务 */ async bindServices(services) { var self = this; for (var name of services) { var cls = service_1.default.get(name); util_1.default.assert(cls, name + ' not found'); util_1.default.assert(util_1.default.equalsClass(wsservice.WSService, cls), name + ' Service type is not correct'); util_1.default.assert(!(name in self.m_handles), 'Service no need to repeat binding'); console.log('SW requestAuth', this.request.url); var ser = new cls(self); var ok = await util_1.default.timeout(ser.requestAuth({ service: name, action: '' }), 2e4); util_1.default.assert(ok, errno_1.default.ERR_REQUEST_AUTH_FAIL); self.m_isGzip = ser.headers['use-gzip'] == 'on'; console.log('SER Loading', this.request.url); await util_1.default.timeout(ser.load(), 2e4); if (!self.m_default_service) self.m_default_service = name; self.m_handles[name] = ser; self.m_services_count++; ser.m_loaded = true; // TODO ptinate visit ser.name = name; // TODO ptinate visit 设置服务名称 await util_1.default.sleep(200); // TODO 在同一个node进程中同时开启多个服务时socket无法写入 ser._trigger('Load', { token: this.token }).catch((e) => console.error(e)); console.log('SER Load', this.request.url); } } send(data) { util_1.default.assert(this.isOpen, errno_1.default.ERR_CONNECTION_CLOSE_STATUS); return _conv_1.ConversationBasic.write(this, parser_1.sendDataPacket, [this.socket, data]); } ping() { util_1.default.assert(this.isOpen, errno_1.default.ERR_CONNECTION_CLOSE_STATUS); // return _Conversation.write(this, sendPingPacket, [this.socket]); // TODO Browser does not support standard Ping and Pong API, So the extension protocol is used here return _conv_1.ConversationBasic.write(this, parser_1.sendDataPacket, [this.socket, data_1.PING_BUFFER]); } pong() { util_1.default.assert(this.isOpen, errno_1.default.ERR_CONNECTION_CLOSE_STATUS); // return _Conversation.write(this, sendPongPacket, [this.socket]); // TODO Browser does not support standard Ping and Pong API, So the extension protocol is used here return _conv_1.ConversationBasic.write(this, parser_1.sendDataPacket, [this.socket, data_1.PONG_BUFFER]); } close() { if (this.isOpen) { var socket = this.socket; socket.removeAllListeners('timeout'); socket.removeAllListeners('end'); socket.removeAllListeners('close'); socket.removeAllListeners('error'); socket.removeAllListeners('data'); socket.removeAllListeners('drain'); try { if (socket.writable) socket.end(); } catch (err) { console.error(err); } this.onClose.trigger({}); console.log('Hybi Conversation Close'); } } } exports.WSConversation = WSConversation;