1. ienaga

    Posted

    ienaga
Changes in title
+Flash Advent Calendar 14日目 - WebSocketのハンドシェイクをエミュレートする -
Changes in tags
Changes in body
Source | HTML | Preview

FlashのSocketクラスの実装の時にすごくハマったので記事にしようと思います。
通常のJavaScriptではそもそも作る必要のない機能なので、笑い話としてみてもらえればと思います。

通常のWebSocketの流れ

const socket = new WebSocket("ws://example.com/chat");

// 通信が確率した時のイベント
socket.onopen = function (event) 
{
    alert("[open] Connection.");
};

// メッセージを受け取った時
socket.onmessage = function (event) 
{
    alert(`[message]: ${event.data}`);
};

はい。これで完了です。
凄く簡単に始めれます。

が、FlashのSocketクラスはブラウザがない事が前提なので
本来であればブラウザがやってくれるあれやこれやを自前でやる必要があります。

まず、Socket通信を確立する為に、ハンドシェイクを行う必要があります。

参考サイト
スクリーンショット 2020-12-16 23.23.18.png

サーバーに開始する為のリクエストを送ります。

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

HTTPバージョンは1.1以上で、メソッドはGETでなければだめです。
次に大切なのがbase64化されたSec-WebSocket-Key
このキーを元にリクエストに対応するレスポンスであることを確認します。

メッセージを送る度にヘッダーを生成して送ります。
ヘッダーの最後は改行が2個必須なのも注意点です。
(イメージしやすくする為に改行コードを追記してます。)

HTTP/1.1 101 Switching Protocols\n
Upgrade: websocket\n
Connection: Upgrade\n
Sec-WebSocket-Accept: hsBlbuDTkk24srzEOTBUlZAlC2g=\n
\n

Sec-WebSocket-Acceptの生成方法は
Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 (<=固定値)
をSHA1でhash化して最後にbase64化します。

まじか・・・

っという事で、自前でSHA1のハッシュ化するロジックを作成します。
ActionScriptのクラスを参考にしながら作っていきます。

/**
 * @param  {string} value
 * @return {string}
 * @public
 */
toBase64 = function (value)
{
    const buffer    = createBlocksFromString(value);
    const byteArray = hashBlocks(buffer);

    let hash = "";
    const length = byteArray.length;
    for (let idx = 0; idx < length; ++idx) {
        hash += String.fromCharCode(byteArray.readUnsignedByte());
    }

    return window.btoa(hash);
}

/**
 * @param  {string} value
 * @return 
 * @public
 */
createBlocksFromString = function (value)
{
    const blocks = [];

    const length = value.length * 8;
    for (let idx = 0; idx < length; idx += 8) {
        blocks[idx >> 5] |= (value.charCodeAt(idx / 8) & 255) << (24 - idx % 32);
    }

    // append padding and length
    blocks[length >> 5] |= 0x80 << (24 - length % 32);
    blocks[(((length + 64) >> 9) << 4) + 15] = length;

    return blocks;
}

const int32Array = new Int32Array(1);
/**
 * @param {number} value
 * @return {int}
 * @public
 */
toInt32 = function (value)
{
    int32Array[0] = value;
    return int32Array[0];
}

/**
 * @param  {array} blocks
 * @return {ByteArray}
 * @public
 */
hashBlocks = function (blocks)
{
    let a = 0;
    let b = 0;
    let c = 0;
    let d = 0;
    let e = 0;

    let h0 = 1732584193;
    let h1 = 4023233417;
    let h2 = 2562383102;
    let h3 = 271733878;
    let h4 = 3285377520;

    const tmp = new Util.$Array(80);
    let pos   = 0;
    let index = 0;

    const length = blocks.length;
    for (let idx = 0; idx < length; idx += 16) {

        a = h0;
        b = h1;
        c = toInt32(h2);
        d = h3;
        e = h4;

        pos = 0;
        for (; pos < 20; ++pos) {

            if (pos < 16) {

                tmp[pos] = blocks[idx + pos];

            } else {

                index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
                tmp[pos] = index << 1 | index >>> 31;

            }

            index = toInt32((a << 5 | a >>> 27) + (b & c | ~b & d) + e + toInt32(tmp[pos]) + 1518500249);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        for (; pos < 40; ++pos) {

            index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
            tmp[pos] = index << 1 | index >>> 31;
            index = toInt32((a << 5 | a >>> 27) + (b ^ c ^ d) + e + toInt32(tmp[pos]) + 1859775393);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        for (; pos < 60; ++pos) {

            index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
            tmp[pos] = index << 1 | index >>> 31;
            index = toInt32((a << 5 | a >>> 27) + (b & c | b & d | c & d) + e + toInt32(tmp[pos]) + 2400959708);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        for (; pos < 80; ++pos) {

            index = tmp[pos - 3] ^ tmp[pos - 8] ^ tmp[pos - 14] ^ tmp[pos - 16];
            tmp[pos] = index << 1 | index >>> 31;
            index = toInt32((a << 5 | a >>> 27) + (b ^ c ^ d) + e + toInt32(tmp[pos]) + 3395469782);

            e = d;
            d = c;
            c = b << 30 | b >>> 2;
            b = a;
            a = index;
        }

        h0 += a;
        h1 += b;
        h2 += c;
        h3 += d;
        h4 += e;

    }

    // ByteArrayクラスも自前作る必要があるのですが、ここはまたいつか・・・
    const byteArray = new ByteArray();
    byteArray.writeInt(h0);
    byteArray.writeInt(h1);
    byteArray.writeInt(h2);
    byteArray.writeInt(h3);
    byteArray.writeInt(h4);
    byteArray.position = 0;

    return byteArray;
}

ここまでできて、初めて通信のやとりとができる状態となります。

少し長くなってしまったので、今日はここまでにします。

明日は、ブラウザが行っているメッセージの暗号化をどうやって作るかを書こうかと思います。

本来はブラウザが全部やってくれてる事なので、本当に感謝です。。。