dlovely-websocket
Version:
WebSocket For Dlovely
353 lines (352 loc) • 13.2 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.AppConnection = void 0;
const crypto_1 = __importDefault(require("crypto"));
const events_1 = require("events");
const common_1 = require("./common");
const frame = __importStar(require("./frame"));
const InStream_1 = require("./InStream");
const OutStream_1 = require("./OutStream");
class AppConnection extends events_1.EventEmitter {
token;
server;
socket;
key;
headers = {};
protocol;
protocols;
constructor(token, socket, server, onConnect) {
super();
this.token = token;
this.socket = socket;
this.server = server;
this.readyState = this.CONNECTING;
this.socket.on('readable', () => this.doRead());
this.socket.on('error', err => this.emit('error', err));
const onclose = () => {
if (this.readyState === this.CONNECTING ||
this.readyState === this.OPEN) {
this.emit('close', 1006, '');
}
this.readyState = this.CLOSED;
if (this.frameBuffer instanceof InStream_1.InStream) {
this.frameBuffer.end();
this.frameBuffer = '';
}
if (this.outStream instanceof OutStream_1.OutStream) {
this.outStream.end();
this.outStream = '';
}
};
this.socket.once('close', onclose);
this.socket.once('finish', onclose);
if (onConnect)
this.once('connect', onConnect);
}
extraParams = {};
sendSign(sign, data, callback) {
if (this.readyState === this.OPEN) {
if (!this.outStream) {
common_1.toast.log(`向连接[${this.token}]传出:${sign}${data
? typeof data === 'string'
? `\t${data}`
: Object.entries(data)
.map(val => `\r\n${val[0]}:\t|${val[1]}`)
.join('')
: ''}`);
return this.socket.write(frame.createTextFrame(JSON.stringify({ sign, data }), false), callback);
}
return this.emit('error', new Error('在发送完二进制帧之前,无法发送标签帧'));
}
return this.emit('error', new Error('无法向未打开的连接进行写入'));
}
sendText(str, callback) {
if (this.readyState === this.OPEN) {
if (!this.outStream) {
common_1.toast.log(`向连接[${this.token}]传出:${str}`);
return this.socket.write(frame.createTextFrame(str, false), callback);
}
return this.emit('error', new Error('在发送完二进制帧之前,无法发送文本帧'));
}
return this.emit('error', new Error('无法向未打开的连接进行写入'));
}
sendBinary(data, callback) {
if (this.readyState === this.OPEN) {
if (!this.outStream) {
common_1.toast.log(`向连接[${this.token}]传出:二进制流`, data);
return this.socket.write(frame.createBinaryFrame(data, false, true, true), callback);
}
return this.emit('error', new Error('在发送完之前的二进制帧之前,无法发送更多的二进制帧'));
}
return this.emit('error', new Error('无法向未打开的连接进行写入'));
}
send(data, callback) {
if (typeof data === 'string') {
this.sendText(data, callback);
}
else if (Buffer.isBuffer(data)) {
this.sendBinary(data, callback);
}
else {
throw new TypeError('数据应该是字符串或Buffer实例');
}
}
close(code, reason) {
if (this.readyState === this.OPEN) {
this.socket.write(frame.createCloseFrame(code, reason, false));
this.readyState = this.CLOSING;
}
else if (this.readyState !== this.CLOSED) {
this.socket.end();
this.readyState = this.CLOSED;
}
this.emit('close', code, reason);
}
buffer = Buffer.alloc(0);
doRead() {
const buffer = this.socket.read();
if (!buffer)
return;
this.buffer = Buffer.concat([this.buffer, buffer], this.buffer.length + buffer.length);
if (this.readyState === this.CONNECTING && !this.readHandshake())
return;
if (this.readyState !== this.CLOSED) {
let temp;
while ((temp = this.extractFrame()) === true) { }
if (temp === false)
this.close(1002);
else if (this.buffer.length > AppConnection.maxBufferLength)
this.close(1009);
}
}
readHandshake() {
if (this.buffer.length > AppConnection.maxBufferLength) {
this.socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
return false;
}
let i, found = false;
for (i = 0; i < this.buffer.length - 3; i++) {
if (this.buffer[i] === 13 &&
this.buffer[i + 2] === 13 &&
this.buffer[i + 1] === 10 &&
this.buffer[i + 3] === 10) {
found = true;
break;
}
}
if (!found)
return false;
const data = this.buffer
.slice(0, i + 4)
.toString()
.split('\r\n');
if (this.answerHandshake(data)) {
this.buffer = this.buffer.slice(i + 4);
this.readyState = this.OPEN;
this.emit('connect');
return true;
}
else {
this.socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
return false;
}
}
answerHandshake(lines) {
if (lines.length < 6)
return false;
if (!lines[0].match(/^GET (.+) HTTP\/\d\.\d$/i))
return false;
this.readHeaders(lines);
for (const head of [
'host',
'sec-websocket-key',
'upgrade',
'connection'
]) {
if (!this.headers[head])
return false;
}
if (this.headers.upgrade?.toLowerCase() !== 'websocket' ||
this.headers.connection
?.toLowerCase()
.split(/\s*,\s*/)
.indexOf('upgrade') === -1)
return false;
if (this.headers['sec-websocket-version'] !== '13')
return false;
this.key = this.headers['sec-websocket-key'];
if (this.headers['sec-websocket-protocol']) {
this.protocols = this.headers['sec-websocket-protocol']
?.split(',')
.map(each => each.trim());
if (this.server?._selectProtocol) {
this.protocol = this.server._selectProtocol(this, this.protocols);
}
}
const sha1 = crypto_1.default.createHash('sha1');
sha1.end(this.key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11');
const key = sha1.read().toString('base64');
const headers = {
upgrade: 'websocket',
connection: 'Upgrade',
'sec-websocket-accept': key
};
this.protocol && (headers['sec-websocket-protocol'] = this.protocol);
this.socket.write(buildRequest('HTTP/1.1 101 Switching Protocols', headers));
return true;
}
readHeaders(lines) {
lines.forEach(line => {
const match = line.match(/^([a-z-]+): (.+)$/i);
match && (this.headers[match[1].toLowerCase()] = match[2]);
});
}
frameBuffer = '';
outStream = '';
extractFrame() {
if (this.buffer.length < 2)
return;
let B = this.buffer[0];
const HB = B >> 4;
if (HB % 8)
return false;
const fin = HB === 8;
const opcode = B % 16;
if (![0, 1, 2, 8, 9, 10].includes(opcode))
return false;
if (opcode >= 8 && !fin)
return false;
B = this.buffer[1];
const hasMask = B >> 7;
if (this.server && !hasMask)
return false;
let len = B % 128;
let start = hasMask ? 6 : 2;
if (this.buffer.length < start + len)
return;
if (len === 126) {
len = this.buffer.readUInt16BE(2);
start += 2;
}
else if (len === 127) {
len =
this.buffer.readUInt32BE(2) * Math.pow(2, 32) +
this.buffer.readUInt32BE(6);
start += 8;
}
if (this.buffer.length < start + len)
return;
const payload = this.buffer.slice(start, start + len);
if (hasMask) {
const mask = this.buffer.slice(start - 4, start);
for (let i = 0; i < payload.length; i++) {
payload[i] ^= mask[i % 4];
}
}
this.buffer = this.buffer.slice(start + len);
return this.processFrame(fin, opcode, payload);
}
processFrame(fin, opcode, payload) {
if (opcode === 8) {
if (this.readyState === this.CLOSING)
this.socket.end();
else if (this.readyState === this.OPEN)
this.processCloseFrame(payload);
return true;
}
else if (opcode === 9) {
if (this.readyState === this.OPEN)
this.socket.write(frame.createPongFrame(payload.toString(), false));
return true;
}
else if (opcode === 10) {
this.emit('pong', payload.toString());
return true;
}
if (this.readyState !== this.OPEN)
return true;
if (opcode === 0 && !this.frameBuffer)
return false;
else if (opcode !== 0 && this.frameBuffer)
return false;
if (typeof this.frameBuffer === 'string') {
opcode = 1;
const payload_string = payload.toString();
this.frameBuffer = this.frameBuffer
? this.frameBuffer + payload_string
: payload_string;
if (fin) {
this.emit('text', this.frameBuffer);
this.frameBuffer = '';
}
}
else {
opcode = 2;
if (!this.frameBuffer) {
this.frameBuffer = new InStream_1.InStream();
this.emit('binary', this.frameBuffer);
}
this.frameBuffer.addData(payload);
if (fin) {
this.frameBuffer.end();
this.frameBuffer = '';
}
}
return true;
}
processCloseFrame(payload) {
let code, reason;
if (payload.length >= 2) {
code = payload.readUInt16BE(0);
reason = payload.slice(2).toString();
}
else {
code = 1005;
reason = '';
}
this.socket.write(frame.createCloseFrame(code, reason, false));
this.readyState = this.CLOSED;
this.emit('close', code, reason);
}
readyState;
CONNECTING = 0;
OPEN = 1;
CLOSING = 2;
CLOSED = 3;
static binaryFragmentation = 512 * 1024;
static maxBufferLength = 2 * 1024 * 1024;
}
exports.AppConnection = AppConnection;
function buildRequest(requestLine, headers) {
let headerString = requestLine + '\r\n';
for (const [key, val] of Object.entries(headers)) {
headerString += key + ': ' + val + '\r\n';
}
return headerString + '\r\n';
}