Compare commits

..

2 commits

Author SHA1 Message Date
hippoz
7fd4c589df
implement basic parsing 2021-02-02 10:17:49 +02:00
hippoz
1b8dcd0a49
add connect handler that decides wether socket is accepted or not 2021-01-29 02:40:52 +02:00
5 changed files with 189 additions and 23 deletions

View file

@ -1,3 +1,13 @@
const http = require('http');
const Wormhole = require('../index'); 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);

View file

@ -1,4 +1,4 @@
const http = require('http'); const EventEmitter = require('events');
const { createLog } = require('./lib/logger'); const { createLog } = require('./lib/logger');
const handshake = require('./lib/handshake'); const handshake = require('./lib/handshake');
@ -7,22 +7,33 @@ const Socket = require('./lib/Socket');
const handshakeLog = createLog([ 'Wormhole', 'Handshake' ]); const handshakeLog = createLog([ 'Wormhole', 'Handshake' ]);
class Wormhole { class Wormhole extends EventEmitter {
constructor({ urls=[ '/bruh' ], port=8080 }) { constructor({ urls=[ '/bruh' ], httpServer }) {
this._urls = urls; super();
this._port = port;
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)) { 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) => { const failConnection = (status=400) => {
socket._setConnectionState(constants.states.CLOSING);
res.writeHead(status); res.writeHead(status);
res.end(); res.end();
socket._setConnectionState(constants.states.CLOSED);
console.trace(); console.trace();
}; };
// TODO: check origin header
const websocketKey = req.headers['sec-websocket-key']; const websocketKey = req.headers['sec-websocket-key'];
const upgradeHeader = req.headers['upgrade']; const upgradeHeader = req.headers['upgrade'];
const websocketVersion = req.headers['sec-websocket-version']; const websocketVersion = req.headers['sec-websocket-version'];
@ -32,26 +43,42 @@ class Wormhole {
const websocketAccept = handshake.generateWebsocketAcceptValue(websocketKey); const websocketAccept = handshake.generateWebsocketAcceptValue(websocketKey);
handshakeLog(websocketKey, websocketAccept);
if (websocketAccept) { if (websocketAccept) {
res.writeHead(101, { const accept = () => {
'Upgrade': 'websocket', try {
'Connection': 'Upgrade', socket._setAccepted(true);
'Sec-WebSocket-Accept': websocketAccept } 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.end(); }
res.writeHead(101, {
'Upgrade': 'websocket',
'Connection': 'Upgrade',
'Sec-WebSocket-Accept': websocketAccept
});
res.end();
socket._setConnectionState(constants.states.OPEN);
return true;
};
const socket = new Socket({ socket: res.socket, initalState: 'CONNECTED' }); 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 true; return this.emit('connect', { socket, accept, reject });
} }
return failConnection(); return failConnection();
} }
}); }));
this._httpServer.listen(port);
} }
} }

101
lib/Parser.js Normal file
View 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
};

View file

@ -1,12 +1,34 @@
const constants = require("./constants");
const parser = require('./Parser');
class Socket { class Socket {
constructor({ initialState='CONNECTING', socket }) { constructor({ initialState='CONNECTING', socket }) {
this._state = initialState; this._state = initialState;
this._socket = socket; 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) => { 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; module.exports = Socket;

View file

@ -1,5 +1,11 @@
module.exports = { module.exports = {
handshakeGUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', handshakeGUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
upgradeHeaderRequirement: 'websocket', upgradeHeaderRequirement: 'websocket',
websocketVersionRequirement: '13' websocketVersionRequirement: '13',
states: {
CONNECTING: 'CONNECTING',
OPEN: 'OPEN',
CLOSING: 'CLOSING',
CLOSED: 'CLOSED'
}
} }