Compare commits
2 commits
641a978cbe
...
7fd4c589df
Author | SHA1 | Date | |
---|---|---|---|
|
7fd4c589df | ||
|
1b8dcd0a49 |
5 changed files with 189 additions and 23 deletions
|
@ -1,3 +1,13 @@
|
|||
const http = require('http');
|
||||
|
||||
const Wormhole = require('../index');
|
||||
|
||||
const wormhole = new Wormhole({ urls: [ '/hello' ] });
|
||||
const httpServer = http.createServer();
|
||||
|
||||
const wormhole = new Wormhole({ urls: [ '/hello' ], httpServer });
|
||||
|
||||
wormhole.on('connect', ({ socket, accept, reject }) => {
|
||||
accept();
|
||||
});
|
||||
|
||||
httpServer.listen(8080);
|
53
index.js
53
index.js
|
@ -1,4 +1,4 @@
|
|||
const http = require('http');
|
||||
const EventEmitter = require('events');
|
||||
|
||||
const { createLog } = require('./lib/logger');
|
||||
const handshake = require('./lib/handshake');
|
||||
|
@ -7,22 +7,33 @@ const Socket = require('./lib/Socket');
|
|||
|
||||
const handshakeLog = createLog([ 'Wormhole', 'Handshake' ]);
|
||||
|
||||
class Wormhole {
|
||||
constructor({ urls=[ '/bruh' ], port=8080 }) {
|
||||
this._urls = urls;
|
||||
this._port = port;
|
||||
class Wormhole extends EventEmitter {
|
||||
constructor({ urls=[ '/bruh' ], httpServer }) {
|
||||
super();
|
||||
|
||||
this._httpServer = http.createServer((req, res) => {
|
||||
this._urls = urls;
|
||||
this._httpServer = httpServer;
|
||||
|
||||
this._sockets = [];
|
||||
|
||||
this._httpServer.on('request', ((req, res) => {
|
||||
if (req.method === 'GET' && req.url && this._urls.includes(req.url)) {
|
||||
handshakeLog(`Got connection request to ${req.url} on port ${this._port}`);
|
||||
handshakeLog(`Got connection request to ${req.url}`);
|
||||
|
||||
let socket = new Socket({ socket: res.socket, initalState: constants.states.CONNECTING });
|
||||
|
||||
const failConnection = (status=400) => {
|
||||
socket._setConnectionState(constants.states.CLOSING);
|
||||
|
||||
res.writeHead(status);
|
||||
res.end();
|
||||
|
||||
socket._setConnectionState(constants.states.CLOSED);
|
||||
|
||||
console.trace();
|
||||
};
|
||||
|
||||
// TODO: check origin header
|
||||
const websocketKey = req.headers['sec-websocket-key'];
|
||||
const upgradeHeader = req.headers['upgrade'];
|
||||
const websocketVersion = req.headers['sec-websocket-version'];
|
||||
|
@ -32,9 +43,14 @@ class Wormhole {
|
|||
|
||||
const websocketAccept = handshake.generateWebsocketAcceptValue(websocketKey);
|
||||
|
||||
handshakeLog(websocketKey, websocketAccept);
|
||||
|
||||
if (websocketAccept) {
|
||||
const accept = () => {
|
||||
try {
|
||||
socket._setAccepted(true);
|
||||
} catch(e) {
|
||||
throw new Error('Tried to set socket fate (wether it is accept or not) more than once. Check if there are multiple listeners for the "connect" event or if you are somehow calling accept or reject multiple times.');
|
||||
}
|
||||
|
||||
res.writeHead(101, {
|
||||
'Upgrade': 'websocket',
|
||||
'Connection': 'Upgrade',
|
||||
|
@ -42,16 +58,27 @@ class Wormhole {
|
|||
});
|
||||
res.end();
|
||||
|
||||
const socket = new Socket({ socket: res.socket, initalState: 'CONNECTED' });
|
||||
socket._setConnectionState(constants.states.OPEN);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const reject = (status=403) => {
|
||||
try {
|
||||
socket._setAccepted(false);
|
||||
} catch(e) {
|
||||
throw new Error('Tried to set socket fate (wether it is accept or not) more than once. Check if there are multiple listeners for the "connect" event or if you are somehow calling accept or reject multiple times.');
|
||||
}
|
||||
|
||||
failConnection(status);
|
||||
};
|
||||
|
||||
return this.emit('connect', { socket, accept, reject });
|
||||
}
|
||||
|
||||
return failConnection();
|
||||
}
|
||||
});
|
||||
|
||||
this._httpServer.listen(port);
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
101
lib/Parser.js
Normal file
101
lib/Parser.js
Normal file
|
@ -0,0 +1,101 @@
|
|||
class WebsocketFrame {
|
||||
constructor() {
|
||||
// Just defining the structure of the WebsocketFrame,
|
||||
// I could actually just use a normal object but I
|
||||
// might need to add some methods to this in the future
|
||||
// so it's good enough
|
||||
|
||||
// 1 byte - first byte
|
||||
this.FIN = undefined;
|
||||
this.RSVx = undefined;
|
||||
this.Opcode = undefined;
|
||||
|
||||
// 1 byte - second byte
|
||||
this.MASK = undefined;
|
||||
this.PayloadLen = undefined;
|
||||
|
||||
// 4 bytes - masking key
|
||||
this.MaskingKey = undefined;
|
||||
|
||||
// (Extension Data + Application Data[PayloadLen]) (kind of) - payload data
|
||||
// TODO: Separate extension data from application data
|
||||
this.PayloadData = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const parseWebsocketFrame = (data) => {
|
||||
const firstByte = data.getUint8(0);
|
||||
const secondByte = data.getUint8(1);
|
||||
|
||||
const frame = new WebsocketFrame();
|
||||
|
||||
// 1 byte - first byte
|
||||
frame.FIN = (firstByte & 0x01); // 0x01[0b10000000] - Get most significant bit (FIN)
|
||||
frame.RSVx = (firstByte & 0x70) // 0x70[0b01110000] - Get the 3 bits after the most significant bit (RSVx)
|
||||
frame.Opcode = (firstByte & 0x0F) // 0xF[0b00001111] - Get the last 4 bits (Opcode)
|
||||
|
||||
// 1 byte - second byte
|
||||
frame.MASK = (secondByte & 0x01) // 0x01[0b10000000] - Get most significant bit (MASK)
|
||||
frame.PayloadLen = (secondByte & 0x7F) // 0x7F[0b01111111] - Get last 7 bits (Payload len)
|
||||
|
||||
let maskingKeyOffset = 2; // By default, theres a 2 byte offset. We will modify this in the cases below.
|
||||
|
||||
// Handle Payload len cases and set the masking key offset
|
||||
if (frame.PayloadLen === 126) {
|
||||
frame.PayloadLen = data.getUint16(2);
|
||||
maskingKeyOffset = 4; // 4 byte offset, because we also read 2 bytes with getUint16 above (2 bytes (normal size) + 2 bytes)
|
||||
} else if (frame.PayloadLen === 127) {
|
||||
frame.PayloadLen = data.getBigUint64(2);
|
||||
maskingKeyOffset = 10; // 10 byte offset, because we also read 8 bytes with getBigUint64 above (2 bytes (normal size) + 8 bytes)
|
||||
}
|
||||
|
||||
const maskingKeyEnd = maskingKeyOffset + 4; // (4 bytes because it is a 32 bit value)
|
||||
if (frame.MASK) frame.MaskingKey = data.buffer.slice(maskingKeyOffset, maskingKeyEnd); // Create a new buffer starting at the masking key offset and ending at the masking key end (duh).
|
||||
|
||||
// TODO: Separate extension data from application data
|
||||
frame.PayloadData = data.buffer.slice(maskingKeyEnd, maskingKeyEnd + frame.PayloadLen); // Create a new buffer that starts at the end of the masking key and ends at (the end of the masking key + payload length)
|
||||
|
||||
// TODO: implement unmasked string decoding
|
||||
if (frame.MASK) {
|
||||
switch (frame.Opcode) {
|
||||
case 1: { // Denotes a text frame
|
||||
const payloadDataView = new DataView(frame.PayloadData);
|
||||
const maskingKeyView = new DataView(frame.MaskingKey);
|
||||
|
||||
let decoded = '';
|
||||
|
||||
for (let i = 0; i < frame.PayloadData.byteLength; i++) {
|
||||
decoded += (String.fromCharCode(payloadDataView.getUint8(i) ^ maskingKeyView.getUint8(i % 4)));
|
||||
}
|
||||
|
||||
frame.DecodedPayloadData = decoded;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case 2: { // Denotes a binary frame
|
||||
// TODO: untested
|
||||
const payloadDataView = new DataView(frame.PayloadData);
|
||||
const maskingKeyView = new DataView(frame.MaskingKey);
|
||||
|
||||
const decoded = [];
|
||||
|
||||
for (let i = 0; i < frame.PayloadData.byteLength; i++) {
|
||||
decoded.push(payloadDataView.getUint8(i) ^ maskingKeyView.getUint8(i % 4));
|
||||
}
|
||||
|
||||
frame.DecodedPayloadData = decoded;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
WebsocketFrame,
|
||||
|
||||
parseWebsocketFrame
|
||||
};
|
|
@ -1,12 +1,34 @@
|
|||
const constants = require("./constants");
|
||||
const parser = require('./Parser');
|
||||
|
||||
class Socket {
|
||||
constructor({ initialState='CONNECTING', socket }) {
|
||||
this._state = initialState;
|
||||
this._socket = socket;
|
||||
this._accepted = false;
|
||||
this._fateDecided = false; // Wether the decision to accept or reject the socket was made
|
||||
|
||||
this._socket.on('data', (e) => {
|
||||
console.log(e.toString());
|
||||
if (this._state !== constants.states.OPEN) return;
|
||||
|
||||
this._decodePayload(e.buffer);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Socket.prototype._decodePayload = function(payload) {
|
||||
console.log(parser.parseWebsocketFrame(new DataView(payload)));
|
||||
};
|
||||
|
||||
Socket.prototype._setConnectionState = function(state) {
|
||||
this._state = state;
|
||||
};
|
||||
|
||||
Socket.prototype._setAccepted = function(state) {
|
||||
if (this._fateDecided) throw new Error('Tried to decide fate (wether socket is accepted or not) more than 1 time');
|
||||
|
||||
this._fateDecided = true;
|
||||
this._accepted = state;
|
||||
};
|
||||
|
||||
module.exports = Socket;
|
|
@ -1,5 +1,11 @@
|
|||
module.exports = {
|
||||
handshakeGUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
|
||||
upgradeHeaderRequirement: 'websocket',
|
||||
websocketVersionRequirement: '13'
|
||||
websocketVersionRequirement: '13',
|
||||
states: {
|
||||
CONNECTING: 'CONNECTING',
|
||||
OPEN: 'OPEN',
|
||||
CLOSING: 'CLOSING',
|
||||
CLOSED: 'CLOSED'
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue