nxkit
Version:
This is a collection of tools, independent of any other libraries
280 lines (279 loc) • 11.3 kB
JavaScript
"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;