Skip to content

Instantly share code, notes, and snippets.

@lap00zza
Last active November 12, 2021 09:59
Show Gist options
  • Select an option

  • Save lap00zza/6b9878df14f0f8810b09a4fc9feb92a1 to your computer and use it in GitHub Desktop.

Select an option

Save lap00zza/6b9878df14f0f8810b09a4fc9feb92a1 to your computer and use it in GitHub Desktop.

Revisions

  1. lap00zza revised this gist Oct 29, 2018. 1 changed file with 115 additions and 61 deletions.
    176 changes: 115 additions & 61 deletions nanoWs-client.js
    Original file line number Diff line number Diff line change
    @@ -19,15 +19,30 @@ const { EventEmitter } = require("events");
    // ERRORS
    class ProtocolError extends Error {
    constructor() {
    super("only https, http, wss and ws are supported");
    super();
    this.name = "ProtocolError";
    this.message = "only https, http, wss and ws are supported";
    }
    }

    // ENUMS
    const DATA_TYPE = {
    TEXT: 0,
    BINARY: 1
    ///////////////////////
    // OPCODE Reference //
    ///////////////////////
    // prettier-ignore
    const OPCODE = {
    CONTINUE: 0x0,
    TEXT : 0x1,
    BINARY : 0x2,
    CLOSE : 0x8,
    PING : 0x9,
    PONG : 0xa
    };

    const SOCKET_STATE = {
    CONNECTING: 0,
    OPEN: 1,
    CLOSING: 2,
    CLOSED: 3
    };

    // prettier-ignore
    @@ -74,14 +89,15 @@ const maskPayload = payload => {

    /**
    * @param {String | Buffer} data
    * @param {DATA_TYPE} type
    * @param {OPCODE} type
    * @returns {Buffer}
    * @todo add support for length > 65535
    */
    const encodeFrame = (data, type) => {
    if (type === DATA_TYPE.TEXT) type = 0x1;
    else if (type === DATA_TYPE.BINARY) type = 0x2;
    else throw new TypeError();
    // if (type === OPCODE.TEXT);
    // else if (type === OPCODE.BINARY);
    // else throw new TypeError();

    // Buffer size (in bytes):
    // 2 for Headers (fin, opcode, mask, len)
    // :then maybe one of:
    @@ -136,6 +152,17 @@ class WebSocket extends EventEmitter {
    : http;
    this.SOCKET_STATE = "CONNECTING";
    this.getSocket();

    // @experimental
    // this.finBytes = 0;

    this._processing = false;
    this._buffers = [];
    this._remaining = Buffer.alloc(0);
    this._bufferedBytes = 0;
    // current frame. Why instance variable?
    // because a frame can span across chunks.
    this._frame = {};
    }

    static getHTTPHeaders() {
    @@ -174,7 +201,11 @@ class WebSocket extends EventEmitter {
    this.attachSocketEvents();
    this.SOCKET_STATE = "OPEN";
    this.emit("open");
    if (head) this.processChunk(head);
    if (head.length > 0) {
    this._buffers.push(head);
    this._bufferedBytes += head.length;
    this.processBuffers();
    }
    });
    request.on("error", e => {
    throw new Error(e);
    @@ -183,14 +214,16 @@ class WebSocket extends EventEmitter {
    }

    /**
    * We finally got out socket. Cool! Lets add a few listeners to make
    * We finally got our socket. Cool! Lets add a few listeners to make
    * the socket actually useful.
    * @see handleClose
    */
    attachSocketEvents() {
    this._socket.on("data", chunk => {
    if (this.debug) console.log("\x1b[35m%s\x1b[0m", "::raw::", chunk);
    this.processChunk(chunk);
    this._buffers.push(chunk);
    this._bufferedBytes += chunk.length;
    this.processBuffers();
    });
    this._socket.on("close", () => {
    if (this.debug) console.log("\x1b[31m%s\x1b[0m", "::closed::");
    @@ -199,78 +232,99 @@ class WebSocket extends EventEmitter {
    });
    }

    /**
    * A single chunk can contain multiple frames. We extract the
    * required bytes from the chunk.
    * @param {Buffer} chunk
    */
    processChunk(chunk) {
    getBytes(n) {
    if (this.debug) console.log("\x1b[31m%s\x1b[0m", `::GET:: ${n} bytes`);
    if (this._bufferedBytes === 0) return false;
    while (this._remaining.length < n) {
    if (this._buffers.length === 0) return false;
    const shifted = this._buffers.shift();
    // prettier-ignore
    if (this.debug)
    console.log("\x1b[35m::FILL REMAINING::", this._remaining, shifted, "\x1b[0m");
    this._remaining = Buffer.concat(
    [this._remaining, shifted],
    this._remaining.length + shifted.length
    );
    }
    this._bufferedBytes -= n;
    const bytes = this._remaining.slice(0, n);
    this._remaining = this._remaining.slice(n);
    return bytes;
    }

    processBuffers() {
    if (this._processing) return;
    if (this.debug)
    console.log("\x1b[32m%s\x1b[0m", "::extracting frames::");
    let offset = 0;
    while (offset <= chunk.length - 2) {
    console.log("\x1b[32m%s\x1b[0m", "::processing frames::");

    let bytes;
    while (true) {
    ///////////////////////
    // Get Frame Headers //
    ///////////////////////
    const frame = getFrameInfo(chunk.slice(offset, offset + 2));
    // const frame = getFrameInfo(chunk.slice(offset, offset + 2));
    // NOTE: FIN because it is part of headers
    if (!this._frame.hasOwnProperty("FIN")) {
    if (!(bytes = this.getBytes(2))) break;
    this._frame = getFrameInfo(bytes);
    if (this.debug) console.log("HEAD", this._frame);
    // Control Frames have PAYLOAD_LENGTH = 0
    if (this._frame.LEN === 0) {
    this.handleFrame(this._frame);
    this._frame = {}; // reset frame
    continue;
    }
    }

    //////////////////////////////
    // Calculate Payload Length //
    //////////////////////////////
    let payloadLength = 0;
    let payloadOffset = 2;
    if (frame.LEN <= 125) {
    payloadLength = frame.LEN;
    } else if (frame.LEN === 126) {
    // read the next 16 bits (2bytes) as an unsigned int
    payloadLength = (chunk[offset + 2] << 8) | chunk[offset + 3];
    payloadOffset += 2;
    } else {
    // prettier-ignore
    // read the next 64 bits (8bytes) as an unsigned int
    payloadLength =
    (chunk[offset + 2] << 56) | (chunk[offset + 3] << 48) |
    (chunk[offset + 4] << 40) | (chunk[offset + 5] << 32) |
    (chunk[offset + 6] << 24) | (chunk[offset + 7] << 16) |
    (chunk[offset + 8] << 8) | chunk[offset + 9];
    payloadOffset += 8;
    if (!this._frame.hasOwnProperty("PAYLOAD_LENGTH")) {
    if (this._frame.LEN <= 125) {
    this._frame.PAYLOAD_LENGTH = this._frame.LEN;
    } else if (this._frame.LEN === 126) {
    if (!(bytes = this.getBytes(2))) break;
    this._frame.PAYLOAD_LENGTH = (bytes[0] << 8) | bytes[1];
    } else {
    // prettier-ignore
    // read the next 64 bits (8bytes) as an unsigned int
    // payloadLength =
    // (chunk[offset + 2] << 56) | (chunk[offset + 3] << 48) |
    // (chunk[offset + 4] << 40) | (chunk[offset + 5] << 32) |
    // (chunk[offset + 6] << 24) | (chunk[offset + 7] << 16) |
    // (chunk[offset + 8] << 8) | chunk[offset + 9];
    // payloadOffset += 8;
    }
    if (this.debug) console.log("LEN", this._frame);
    }

    //////////////////////////////////////////
    // Add Payload information and dispatch //
    //////////////////////////////////////////
    frame.PAYLOAD_LENGTH = payloadLength;
    frame.PAYLOAD = chunk.slice(
    offset + payloadOffset,
    offset + payloadOffset + payloadLength
    );
    this.handleFrame(frame);

    // And off we go again
    offset += payloadOffset + payloadLength;
    if (!this._frame.hasOwnProperty("PAYLOAD")) {
    if (!(bytes = this.getBytes(this._frame.PAYLOAD_LENGTH))) break;
    this._frame.PAYLOAD = bytes;
    this.handleFrame(this._frame);
    this._frame = {}; // reset frame
    }
    }
    this._processing = false;
    }

    handleFrame(frame) {
    if (this.debug) console.log("\x1b[34m%s\x1b[0m", "::FRAME::", frame);
    //////////////////////
    // OPCODE Reference //
    //////////////////////
    // 0x0 Continue
    // 0x1 Text
    // 0x2 Binary
    // 0x8 Close
    // 0x9 Ping
    // 0xA Pong
    // if (!frame.FIN) this.finBytes += frame.PAYLOAD_LENGTH;
    // console.log("::finbytes::", this.finBytes);

    switch (frame.OPCODE) {
    // NOTE: StringDecoder will be useful in Continue
    case 0x1:
    case OPCODE.TEXT:
    this.handleTextData(frame.PAYLOAD.toString());
    break;
    case 0x8:
    case OPCODE.CLOSE:
    this.handleClose();
    break;
    case 0x9:
    case OPCODE.PING:
    console.log("::PING::");
    // TODO: https://tools.ietf.org/html/rfc6455#section-5.5.2
    break;
    @@ -301,7 +355,7 @@ class WebSocket extends EventEmitter {
    throw new Error("Websocket needs to be open.");
    if (typeof data !== "string")
    throw new TypeError("data must be string");
    this._socket.write(encodeFrame(data, DATA_TYPE.TEXT));
    this._socket.write(encodeFrame(data, OPCODE.TEXT));
    }

    close() {
  2. lap00zza revised this gist Oct 24, 2018. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions nanoWs-client.js
    Original file line number Diff line number Diff line change
    @@ -208,7 +208,7 @@ class WebSocket extends EventEmitter {
    if (this.debug)
    console.log("\x1b[32m%s\x1b[0m", "::extracting frames::");
    let offset = 0;
    while (offset < chunk.length) {
    while (offset <= chunk.length - 2) {
    ///////////////////////
    // Get Frame Headers //
    ///////////////////////
    @@ -247,7 +247,7 @@ class WebSocket extends EventEmitter {
    this.handleFrame(frame);

    // And off we go again
    offset += 2 + payloadLength;
    offset += payloadOffset + payloadLength;
    }
    }

  3. lap00zza renamed this gist Oct 16, 2018. 1 changed file with 89 additions and 50 deletions.
    139 changes: 89 additions & 50 deletions ws_client.js → nanoWs-client.js
    Original file line number Diff line number Diff line change
    @@ -16,9 +16,6 @@ const { EventEmitter } = require("events");
    // const zlib = require("zlib");
    // const { StringDecoder } = require("string_decoder");

    // for logging console outputs
    let DEBUG = true;

    // ERRORS
    class ProtocolError extends Error {
    constructor() {
    @@ -27,6 +24,12 @@ class ProtocolError extends Error {
    }
    }

    // ENUMS
    const DATA_TYPE = {
    TEXT: 0,
    BINARY: 1
    };

    // prettier-ignore
    /**
    * Decodes a websocket data frame header (byte 1 and byte 2) according
    @@ -70,30 +73,53 @@ const maskPayload = payload => {
    };

    /**
    * @param {String} data
    * @param {String | Buffer} data
    * @param {DATA_TYPE} type
    * @returns {Buffer}
    * @todo add support for length > 125
    * @todo add support for length > 65535
    */
    const encodeFrame = data => {
    if (typeof data !== "string") throw new TypeError("data must be string");
    if (data.length > 125)
    throw new Error("Not equipped to handled > 125 length");
    // Buffer size reference:
    const encodeFrame = (data, type) => {
    if (type === DATA_TYPE.TEXT) type = 0x1;
    else if (type === DATA_TYPE.BINARY) type = 0x2;
    else throw new TypeError();
    // Buffer size (in bytes):
    // 2 for Headers (fin, opcode, mask, len)
    // :then maybe one of:
    // - 2 for extended length if 125 < len <= 65535
    // - 8 for extended length if len > 65535
    // 4 for MASKING KEY
    // rest is for payload length
    const _b = Buffer.alloc(2);
    _b[0] = 0b10000001;
    _b[1] = 0b10000000 | data.length;
    const masked = maskPayload(Buffer.from(data));
    return Buffer.concat([_b, masked.key, masked.payload], 2 + 4 + data.length);
    if (data.length <= 0x7d /* 125 */) {
    const _b = Buffer.alloc(2);
    _b[0] = 0b10000000 | type;
    _b[1] = 0b10000000 | data.length;
    const masked = maskPayload(Buffer.from(data));
    return Buffer.concat(
    [_b, masked.key, masked.payload],
    2 + 4 + data.length
    );
    } else if (data.length <= 0xffff /* 65535 */) {
    const _b = Buffer.alloc(2 + 2);
    _b[0] = 0b10000001;
    _b[1] = 0b11111110; // LEN must be 126
    _b[2] = (0b1111111100000000 & data.length) >> 8;
    _b[3] = 0b0000000011111111 & data.length;
    const masked = maskPayload(Buffer.from(data));
    return Buffer.concat(
    [_b, masked.key, masked.payload],
    2 + 2 + 4 + data.length
    );
    } else {
    throw new RangeError("Not equipped to handled > 65535 length");
    }
    };

    class WebSocket extends EventEmitter {
    /**
    * @param {String} url
    * @param {String} url the websocket url
    * @param {Boolean} [debug=false] log raw socket events
    */
    constructor(url) {
    constructor(url, debug = false) {
    const _url = new URL(url);
    if (!["https:", "http:", "wss:", "ws:"].includes(_url.protocol))
    throw new ProtocolError();
    @@ -102,6 +128,7 @@ class WebSocket extends EventEmitter {

    super();
    this.url = _url;
    this.debug = debug;
    this._socket = undefined;
    this.httpModule =
    _url.protocol === "https:" || _url.protocol === "wss:"
    @@ -126,26 +153,33 @@ class WebSocket extends EventEmitter {
    * A websocket starts off as a normal HTTP request. This is the
    * handshake part. Once our connection is upgraded, the actual
    * data transfer can start.
    *
    * NOTE: not chaining request coz the stack trace gets a bit messy.
    */
    getSocket() {
    this.httpModule
    .request(this.url, WebSocket.getHTTPHeaders())
    .on("response", res => {
    console.log("::response::");
    res.pipe(process.stdout);
    })
    // This is what we are interested in. Remember
    // socket (net.Socket) is a Duplex stream.
    .on("upgrade", (res, socket, head) => {
    console.log("::upgrade::");
    if (DEBUG) console.log("\x1b[35m%s\x1b[0m", "::head::", head);
    this._socket = socket;
    this.attachSocketEvents();
    this.SOCKET_STATE = "OPEN";
    this.emit("open");
    if (head) this.processChunk(head);
    })
    .end();
    const request = this.httpModule.request(
    this.url,
    WebSocket.getHTTPHeaders()
    );
    request.on("response", res => {
    if (this.debug) console.log("::response::");
    res.pipe(process.stdout);
    });
    // This is what we are interested in. Remember
    // socket (net.Socket) is a Duplex stream.
    request.on("upgrade", (res, socket, head) => {
    if (this.debug) console.log("::upgrade::");
    if (this.debug) console.log("\x1b[35m%s\x1b[0m", "::head::", head);
    this._socket = socket;
    this.attachSocketEvents();
    this.SOCKET_STATE = "OPEN";
    this.emit("open");
    if (head) this.processChunk(head);
    });
    request.on("error", e => {
    throw new Error(e);
    });
    request.end();
    }

    /**
    @@ -155,11 +189,11 @@ class WebSocket extends EventEmitter {
    */
    attachSocketEvents() {
    this._socket.on("data", chunk => {
    if (DEBUG) console.log("\x1b[35m%s\x1b[0m", "::raw::", chunk);
    if (this.debug) console.log("\x1b[35m%s\x1b[0m", "::raw::", chunk);
    this.processChunk(chunk);
    });
    this._socket.on("close", () => {
    if (DEBUG) console.log("\x1b[31m%s\x1b[0m", "::closed::");
    if (this.debug) console.log("\x1b[31m%s\x1b[0m", "::closed::");
    this.SOCKET_STATE = "CLOSED";
    this.emit("close");
    });
    @@ -171,16 +205,17 @@ class WebSocket extends EventEmitter {
    * @param {Buffer} chunk
    */
    processChunk(chunk) {
    if (DEBUG) console.log("\x1b[32m%s\x1b[0m", "::extracting frames::");
    if (this.debug)
    console.log("\x1b[32m%s\x1b[0m", "::extracting frames::");
    let offset = 0;
    while (offset < chunk.length) {
    ///////////////////////////
    // Get Frame Headers
    ///////////////////////////
    ///////////////////////
    // Get Frame Headers //
    ///////////////////////
    const frame = getFrameInfo(chunk.slice(offset, offset + 2));

    //////////////////////////////
    // Calculate Payload Length
    // Calculate Payload Length //
    //////////////////////////////
    let payloadLength = 0;
    let payloadOffset = 2;
    @@ -201,9 +236,9 @@ class WebSocket extends EventEmitter {
    payloadOffset += 8;
    }

    ///////////////////////////////////////////
    // Add Payload information and dispatch
    ///////////////////////////////////////////
    //////////////////////////////////////////
    // Add Payload information and dispatch //
    //////////////////////////////////////////
    frame.PAYLOAD_LENGTH = payloadLength;
    frame.PAYLOAD = chunk.slice(
    offset + payloadOffset,
    @@ -217,14 +252,13 @@ class WebSocket extends EventEmitter {
    }

    handleFrame(frame) {
    if (DEBUG) console.log("\x1b[34m%s\x1b[0m", "::FRAME::", frame);
    if (this.debug) console.log("\x1b[34m%s\x1b[0m", "::FRAME::", frame);
    //////////////////////
    // OPCODE Reference //
    //////////////////////
    // 0x0 Continue
    // 0x1 Text
    // 0x2 Binary
    //
    // 0x8 Close
    // 0x9 Ping
    // 0xA Pong
    @@ -236,19 +270,24 @@ class WebSocket extends EventEmitter {
    case 0x8:
    this.handleClose();
    break;
    case 0x9:
    console.log("::PING::");
    // TODO: https://tools.ietf.org/html/rfc6455#section-5.5.2
    break;
    }
    }

    handleTextData(text) {
    if (DEBUG) console.log("\x1b[33m%s\x1b[0m", "::emit message::", text);
    if (this.debug)
    console.log("\x1b[33m%s\x1b[0m", "::emit message::", text);
    this.emit("message", text);
    }

    handleClose() {
    // For now lets close the connection by sending
    // <Buffer 0b10001000 0b00000000>
    // ^^^^
    if (DEBUG) console.log("\x1b[31m%s\x1b[0m", "::closing:: 0x8");
    if (this.debug) console.log("\x1b[31m%s\x1b[0m", "::closing:: 0x8");
    this.SOCKET_STATE = "CLOSING";
    this._socket.write(Buffer.from([0b10001000, 0b00000000], 2));
    }
    @@ -262,7 +301,7 @@ class WebSocket extends EventEmitter {
    throw new Error("Websocket needs to be open.");
    if (typeof data !== "string")
    throw new TypeError("data must be string");
    this._socket.write(encodeFrame(data));
    this._socket.write(encodeFrame(data, DATA_TYPE.TEXT));
    }

    close() {
  4. lap00zza revised this gist Oct 14, 2018. 1 changed file with 22 additions and 5 deletions.
    27 changes: 22 additions & 5 deletions ws_client.js
    Original file line number Diff line number Diff line change
    @@ -9,7 +9,7 @@
    */

    const crypto = require("crypto");
    const URL = require("url");
    const { URL } = require("url");
    const https = require("https");
    const http = require("http");
    const { EventEmitter } = require("events");
    @@ -19,6 +19,14 @@ const { EventEmitter } = require("events");
    // for logging console outputs
    let DEBUG = true;

    // ERRORS
    class ProtocolError extends Error {
    constructor() {
    super("only https, http, wss and ws are supported");
    this.name = "ProtocolError";
    }
    }

    // prettier-ignore
    /**
    * Decodes a websocket data frame header (byte 1 and byte 2) according
    @@ -86,12 +94,20 @@ class WebSocket extends EventEmitter {
    * @param {String} url
    */
    constructor(url) {
    const protocol = URL.parse(url).protocol;
    const _url = new URL(url);
    if (!["https:", "http:", "wss:", "ws:"].includes(_url.protocol))
    throw new ProtocolError();
    if (_url.protocol === "wss:") _url.protocol = "https:";
    if (_url.protocol === "ws:") _url.protocol = "http:";

    super();
    this.url = url;
    this.url = _url;
    this._socket = undefined;
    this.httpModule =
    _url.protocol === "https:" || _url.protocol === "wss:"
    ? https
    : http;
    this.SOCKET_STATE = "CONNECTING";
    this.httpModule = protocol === "https:" ? https : http;
    this.getSocket();
    }

    @@ -112,7 +128,8 @@ class WebSocket extends EventEmitter {
    * data transfer can start.
    */
    getSocket() {
    this.httpModule.request(this.url, WebSocket.getHTTPHeaders())
    this.httpModule
    .request(this.url, WebSocket.getHTTPHeaders())
    .on("response", res => {
    console.log("::response::");
    res.pipe(process.stdout);
  5. lap00zza revised this gist Oct 14, 2018. 1 changed file with 17 additions and 12 deletions.
    29 changes: 17 additions & 12 deletions ws_client.js
    Original file line number Diff line number Diff line change
    @@ -9,10 +9,12 @@
    */

    const crypto = require("crypto");
    const URL = require("url");
    const https = require("https");
    const http = require("http");
    const zlib = require("zlib");
    const { EventEmitter } = require("events");
    // const zlib = require("zlib");
    // const { StringDecoder } = require("string_decoder");

    // for logging console outputs
    let DEBUG = true;
    @@ -84,10 +86,12 @@ class WebSocket extends EventEmitter {
    * @param {String} url
    */
    constructor(url) {
    const protocol = URL.parse(url).protocol;
    super();
    this.url = url;
    this._socket = undefined;
    this.SOCKET_STATE = "CONNECTING";
    this.httpModule = protocol === "https:" ? https : http;
    this.getSocket();
    }

    @@ -108,7 +112,7 @@ class WebSocket extends EventEmitter {
    * data transfer can start.
    */
    getSocket() {
    http.request(this.url, WebSocket.getHTTPHeaders())
    this.httpModule.request(this.url, WebSocket.getHTTPHeaders())
    .on("response", res => {
    console.log("::response::");
    res.pipe(process.stdout);
    @@ -197,17 +201,18 @@ class WebSocket extends EventEmitter {

    handleFrame(frame) {
    if (DEBUG) console.log("\x1b[34m%s\x1b[0m", "::FRAME::", frame);
    /**
    * OPCODE Reference
    * 0x0 Continue
    * 0x1 Text
    * 0x2 Binary
    * ---
    * 0x8 Close
    * 0x9 Ping
    * 0xA Pong
    */
    //////////////////////
    // OPCODE Reference //
    //////////////////////
    // 0x0 Continue
    // 0x1 Text
    // 0x2 Binary
    //
    // 0x8 Close
    // 0x9 Ping
    // 0xA Pong
    switch (frame.OPCODE) {
    // NOTE: StringDecoder will be useful in Continue
    case 0x1:
    this.handleTextData(frame.PAYLOAD.toString());
    break;
  6. lap00zza revised this gist Oct 13, 2018. No changes.
  7. lap00zza created this gist Oct 13, 2018.
    251 changes: 251 additions & 0 deletions ws_client.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,251 @@
    /**
    * nanoWS
    * A very basic websocket client. Its does not fully cover rfc6455 so
    * it must not used for any real world work. I wrote it to find out
    * how websockets actually worked.
    *
    * @licence MIT
    * @author Jewel Mahanta <[email protected]>
    */

    const crypto = require("crypto");
    const https = require("https");
    const http = require("http");
    const zlib = require("zlib");
    const { EventEmitter } = require("events");

    // for logging console outputs
    let DEBUG = true;

    // prettier-ignore
    /**
    * Decodes a websocket data frame header (byte 1 and byte 2) according
    * to [rfc6455](https://tools.ietf.org/html/rfc6455#section-5.2)
    * @param {Buffer} header - The first 2 bytes of frame
    */
    const getFrameInfo = (header) => {
    // BYTE 1
    const FIN = header[0] & 0b10000000;
    const RESV1 = header[0] & 0b01000000; /* no-use */
    const RESV2 = header[0] & 0b00100000; /* no-use */
    const RESV3 = header[0] & 0b00010000; /* no-use */
    const OPCODE = header[0] & 0b00001111;
    // BYTE 2
    const MASK = header[1] & 0b10000000;
    const LEN = header[1] & 0b01111111;
    return {
    FIN: !!FIN,
    OPCODE,
    MASK: !!MASK,
    LEN
    };
    };

    /**
    * Payload masking according to
    * [rfc6455](https://tools.ietf.org/html/rfc6455#section-5.3)
    * @param {Buffer} payload
    */
    const maskPayload = payload => {
    const maskingKey = crypto.randomBytes(4);
    const maskedPayload = Buffer.alloc(payload.length);
    for (let i = 0; i < payload.length; i++) {
    const j = i % 4;
    maskedPayload[i] = payload[i] ^ maskingKey[j];
    }
    return {
    payload: maskedPayload,
    key: maskingKey
    };
    };

    /**
    * @param {String} data
    * @returns {Buffer}
    * @todo add support for length > 125
    */
    const encodeFrame = data => {
    if (typeof data !== "string") throw new TypeError("data must be string");
    if (data.length > 125)
    throw new Error("Not equipped to handled > 125 length");
    // Buffer size reference:
    // 2 for Headers (fin, opcode, mask, len)
    // 4 for MASKING KEY
    // rest is for payload length
    const _b = Buffer.alloc(2);
    _b[0] = 0b10000001;
    _b[1] = 0b10000000 | data.length;
    const masked = maskPayload(Buffer.from(data));
    return Buffer.concat([_b, masked.key, masked.payload], 2 + 4 + data.length);
    };

    class WebSocket extends EventEmitter {
    /**
    * @param {String} url
    */
    constructor(url) {
    super();
    this.url = url;
    this._socket = undefined;
    this.SOCKET_STATE = "CONNECTING";
    this.getSocket();
    }

    static getHTTPHeaders() {
    return {
    headers: {
    Upgrade: "websocket",
    Connection: "Upgrade",
    "Sec-WebSocket-Key": crypto.randomBytes(16).toString("hex"),
    "Sec-WebSocket-Version": 13
    }
    };
    }

    /**
    * A websocket starts off as a normal HTTP request. This is the
    * handshake part. Once our connection is upgraded, the actual
    * data transfer can start.
    */
    getSocket() {
    http.request(this.url, WebSocket.getHTTPHeaders())
    .on("response", res => {
    console.log("::response::");
    res.pipe(process.stdout);
    })
    // This is what we are interested in. Remember
    // socket (net.Socket) is a Duplex stream.
    .on("upgrade", (res, socket, head) => {
    console.log("::upgrade::");
    if (DEBUG) console.log("\x1b[35m%s\x1b[0m", "::head::", head);
    this._socket = socket;
    this.attachSocketEvents();
    this.SOCKET_STATE = "OPEN";
    this.emit("open");
    if (head) this.processChunk(head);
    })
    .end();
    }

    /**
    * We finally got out socket. Cool! Lets add a few listeners to make
    * the socket actually useful.
    * @see handleClose
    */
    attachSocketEvents() {
    this._socket.on("data", chunk => {
    if (DEBUG) console.log("\x1b[35m%s\x1b[0m", "::raw::", chunk);
    this.processChunk(chunk);
    });
    this._socket.on("close", () => {
    if (DEBUG) console.log("\x1b[31m%s\x1b[0m", "::closed::");
    this.SOCKET_STATE = "CLOSED";
    this.emit("close");
    });
    }

    /**
    * A single chunk can contain multiple frames. We extract the
    * required bytes from the chunk.
    * @param {Buffer} chunk
    */
    processChunk(chunk) {
    if (DEBUG) console.log("\x1b[32m%s\x1b[0m", "::extracting frames::");
    let offset = 0;
    while (offset < chunk.length) {
    ///////////////////////////
    // Get Frame Headers
    ///////////////////////////
    const frame = getFrameInfo(chunk.slice(offset, offset + 2));

    //////////////////////////////
    // Calculate Payload Length
    //////////////////////////////
    let payloadLength = 0;
    let payloadOffset = 2;
    if (frame.LEN <= 125) {
    payloadLength = frame.LEN;
    } else if (frame.LEN === 126) {
    // read the next 16 bits (2bytes) as an unsigned int
    payloadLength = (chunk[offset + 2] << 8) | chunk[offset + 3];
    payloadOffset += 2;
    } else {
    // prettier-ignore
    // read the next 64 bits (8bytes) as an unsigned int
    payloadLength =
    (chunk[offset + 2] << 56) | (chunk[offset + 3] << 48) |
    (chunk[offset + 4] << 40) | (chunk[offset + 5] << 32) |
    (chunk[offset + 6] << 24) | (chunk[offset + 7] << 16) |
    (chunk[offset + 8] << 8) | chunk[offset + 9];
    payloadOffset += 8;
    }

    ///////////////////////////////////////////
    // Add Payload information and dispatch
    ///////////////////////////////////////////
    frame.PAYLOAD_LENGTH = payloadLength;
    frame.PAYLOAD = chunk.slice(
    offset + payloadOffset,
    offset + payloadOffset + payloadLength
    );
    this.handleFrame(frame);

    // And off we go again
    offset += 2 + payloadLength;
    }
    }

    handleFrame(frame) {
    if (DEBUG) console.log("\x1b[34m%s\x1b[0m", "::FRAME::", frame);
    /**
    * OPCODE Reference
    * 0x0 Continue
    * 0x1 Text
    * 0x2 Binary
    * ---
    * 0x8 Close
    * 0x9 Ping
    * 0xA Pong
    */
    switch (frame.OPCODE) {
    case 0x1:
    this.handleTextData(frame.PAYLOAD.toString());
    break;
    case 0x8:
    this.handleClose();
    break;
    }
    }

    handleTextData(text) {
    if (DEBUG) console.log("\x1b[33m%s\x1b[0m", "::emit message::", text);
    this.emit("message", text);
    }

    handleClose() {
    // For now lets close the connection by sending
    // <Buffer 0b10001000 0b00000000>
    // ^^^^
    if (DEBUG) console.log("\x1b[31m%s\x1b[0m", "::closing:: 0x8");
    this.SOCKET_STATE = "CLOSING";
    this._socket.write(Buffer.from([0b10001000, 0b00000000], 2));
    }

    /**
    * @param {String} data
    * @todo add support for binary data
    */
    send(data) {
    if (this.SOCKET_STATE !== "OPEN")
    throw new Error("Websocket needs to be open.");
    if (typeof data !== "string")
    throw new TypeError("data must be string");
    this._socket.write(encodeFrame(data));
    }

    close() {
    this.handleClose();
    }
    }

    module.exports = WebSocket;