родной websocket api NodeJS для больших сообщений? - PullRequest
0 голосов
/ 20 апреля 2020

Я читал статью о написании сокет-сервера с нуля , и он в основном работает с небольшими фреймами / пакетами, но когда я пытаюсь отправить около 2 КБ данных, я получаю эту ошибку:

internal/buffer.js:77
  throw new ERR_OUT_OF_RANGE(type || 'offset',
  ^
RangeError [ERR_OUT_OF_RANGE]: The value of "offset" is out of range. It must be >= 0 and <= 7. Receive
d 8
    at boundsError (internal/buffer.js:77:9)
    at Buffer.readUInt8 (internal/buffer.js:243:5)
    at pm (/home/users/me/main.js:277:24)
    at Socket.<anonymous> (/home/users/me/main.js:149:15)
    at Socket.emit (events.js:315:20)
    at addChunk (_stream_readable.js:297:12)
    at readableAddChunk (_stream_readable.js:273:9)
    at Socket.Readable.push (_stream_readable.js:214:10)
    at TCP.onStreamRead (internal/stream_base_commons.js:186:23) {
  code: 'ERR_OUT_OF_RANGE'
}

Вот мой серверный код (некоторые детали были изменены для безопасности, но здесь он полностью для номеров строк и т. Д. c.), Но важной частью здесь является функция pm [= parseMessage] ( внизу):

let http = require('http'),
    ch   = require("child_process"),
    crypto = require("crypto"),
    fs = require("fs"),
    password = fs.readFileSync(“./secretPasswordFile.txt”),
    callbacks = {

    CHANGEDforSecUrITY(m, cs) {
        if(m.password === password) {
            if(m.command) {
                try {
                    cs.my = ch.exec(
                        m.command,
                        (
                            err,
                            stdout,
                            stderr
                        ) => {
                            cs.write(ans(s({
                                err,
                                stdout,
                                stderr
                            })));
                        }
                    );
                } catch(e) {
                    cs.write(ans(
                        s({
                            error: e.toString()
                        })
                    ))
                }
            }
            if(m.exit) {
                console.log("LOL", cs.my);
                if(cs.my && typeof cs.my.kill === "function") {
                    cs.my.kill();
                    console.log(cs.my, "DID?");
                }
            }
            cs.write(
                ans(
                    s({
                    hi: 2,
                    youSaid:m
                }))


            )
        } else {
            cs.write(ans(s({
                hey: "wrong password!!"
            })))
        }


        console.log("hi!",m)
    }
    },
    banned = [
    "61.19.71.84"
    ],
    server = http.createServer(
    (q,r)=> {
        if(banned.includes(q.connection.remoteAddress)) {
            r.end("Hey man, " + q.connection.remoteAddress, 
                "I know you're there!!");
        } else {
            ch.exec(`sudo "$(which node)" -p "console.log(4)"`)
            console.log(q.url)
            console.log(q.connection.remoteAddress,q.connection.remotePort)        
            let path = q.url.substring(1)
            q.url == "/" && 
                (path = "index.html")
            q.url == "/secret" &&
                (path = "../main.js")
            fs.readFile(
                "./static/" + path,
                (er, f) => {
                    if(er) {
                        r.end("<h2>404!!</h2>");    

                    } else {
                        r.end(f);
                    }
                }
            )
        }
    }
    )
server.listen(
    process.env.PORT || 80, 
    c=> {
        console.log(c,"helo!!!")
        server.on("upgrade", (req, socket) => {
            if(req.headers["upgrade"] !== "websocket") {
                socket.end("HTTP/1.1 400 Bad Request");
                return;
            }

            let key = req.headers["sec-websocket-key"];
            if(key) {
                let hash = gav(key)
                let headers = [
                    "HTTP/1.1 101 Web Socket Protocol Handshake",
                    "Upgrade: WebSocket",
                    "Connection: Upgrade",
                    `Sec-WebSocket-Accept: ${hash}`
                ];
                let protocol = req.headers[
                    "sec-websocket-protocol"
                ];
                let protocols = (
                    protocol &&
                    protocol.split(",")
                    .map(s => s.trim())
                    || []
                );
                protocols.includes("json") &&
                    headers
                    .push("Sec-WebSocket-Protocol: json");
                let headersStr = (
                    headers.join("\r\n") + 
                    "\r\n\r\n"


                )


                console.log(
                    "Stuff happening",
                    req.headers,
                    headersStr
                );
                fs.writeFileSync("static/logs.txt",headersStr);
                socket.write(
                    headersStr
                );


                socket.write(ans(JSON.stringify(
                    {
                        hello: "world!!!"
                    }
                )))

            }

            socket.on("data", buf => {
                let msg = pm(buf);
                console.log("HEY MAN!",msg)
                if(msg) {
                    console.log("GOT!",msg);
                    for(let k in msg) {
                        if(callbacks[k]) {
                            callbacks[k](
                                msg[k],
                                socket
                            )
                        }
                    }
                } else {
                    console.log("nope");
                }
            });
        });

    }
)

function pm(buf) {
    /*
     *structure of first byte:
         1: if its the last frame in buffer
         2 - 4: reserved bits
         5 - 8: a number which shows what type of message it is. Chart:

             "0": means we continue
             "1": means this frame contains text
             "2": means this is binary
             "0011"(3) - "0111" (11): reserved values
             "1000"(8): means connection closed
             "1001"(9): ping (checking for response)
             "1010"(10): pong (response verified)
             "1010"(11) - "1111"(15): reserved for "control" frames
     structure of second byte:
        1: is it "masked"
        2 - 8: length of payload, if less than 126.
            if 126, 2 additional bytes are added
            if 127 (or more), 6 additional bytes added (total 8)

     * */
    const myFirstByte = buf.readUInt8(0);

    const isThisFinalFrame = isset(myFirstByte,7) //first bit

    const [
        reserved1,
        reserved2,
        reserved3
    ] = [
        isset(myFirstByte, 6),
        isset(myFirstByte, 5),
        isset(myFirstByte, 4) //reserved bits 
    ]

    const opcode = myFirstByte & parseInt("1111",2); //checks last 4 bits

    //check if closed connection ("1000"(8))
    if(opcode == parseInt("1000", 2))
        return null; //shows that connection closed

    //look for text frame ("0001"(1))
    if(opcode == parseInt("0001",2)) {
        const theSecondByte = buf.readUInt8(1);

        const isMasked = isset(theSecondByte, 7) //1st bit from left side

        let currentByteOffset = 2; //we are theSecondByte now, so 2

        let payloadLength = theSecondByte & 127; //chcek up to 7 bits

        if(payloadLength > 125) {
            if(payloadLength === 126) {
                payloadLength = buf.readUInt16BE(
                    currentByteOffset
                ) //read next two bytes from position
                currentByteOffset += 2; //now we left off at 
                //the fourth byte, so thats where we are

            } else {
                //if only the second byte is full,
                //that shows that there are 6 more 
                //bytes to hold the length 
                const right = buf.readUInt32BE(
                    currentByteOffset
                );
                const left = buf.readUInt32BE(
                    currentByteOffset + 4 //the 8th byte ??
                );

                throw new Error("brutal " + currentByteOffset);

            }
        }

        //if we have masking byte set to 1, get masking key
        //
        //


        //now that we have the lengths
        //and possible masks, read the rest 
        //of the bytes, for actual data
        const data = Buffer.alloc(payloadLength); 

        if(isMasked) {
            //can't just copy it,
            //have to do some stuff with
            //the masking key and this thing called
            //"XOR" to the data. Complicated
            //formulas, llook into later
            //
            let maskingBytes = Buffer.allocUnsafe(4);
            buf.copy(
                maskingBytes,
                0,
                currentByteOffset,
                currentByteOffset + 4
            );
            currentByteOffset += 4;
            for(
                let i = 0;
                i < payloadLength;
                ++i
            ) {

                const source = buf.readUInt8(
                    currentByteOffset++
                );

                //now mask the source with masking byte
                data.writeUInt8(
                    source ^ maskingBytes[i & 3],
                    i
                );
            }
        } else {
            //just copy bytes directly to our buffer
            buf.copy(
                data,
                0,
                currentByteOffset++
            );
        }

        //at this point we have the actual data, so make a json
        //
        const json = data.toString("utf8");
        return p(json);
    } else {
        return "LOL IDK?!";
    }
}

function p(str) {
    try {
        return JSON.parse(str);
    } catch(e){
        return str
    }
}

function s(ob) {
    try {
        return JSON.stringify(ob);
    } catch(e) {
        return e.toString();
    }
}

function ans(str) {
    const byteLength = Buffer.byteLength(str);

    const lengthByteCount = byteLength < 126 ? 0 : 2;
    const payloadLength = lengthByteCount === 0 ? byteLength : 126;

    const buffer = Buffer.alloc(
        2 +
        lengthByteCount + 
        byteLength
    );

    buffer.writeUInt8(
        parseInt("10000001",2), //opcode is "1", at firstbyte
        0
    );

    buffer.writeUInt8(payloadLength, 1); //at second byte

    let currentByteOffset = 2; //already wrote second byte by now

    if(lengthByteCount > 0) {
        buffer.writeUInt16BE(
            byteLength,
            2 //more length at 3rd byte position
        );
        currentByteOffset += lengthByteCount; //which is 2 more bytes
        //of length, since not supporting more than that
    }

    buffer.write(str, currentByteOffset); //the rest of the bytes
    //are the actual data, see chart in function pm
    //
    return buffer;
}

function gav(ak) {
    return crypto
    .createHash("sha1")
    .update(ak +'258EAFA5-E914-47DA-95CA-C5AB0DC85B11', "binary")
    .digest("base64")
}

function isset(b, k) {
    return !!(
        b >>> k & 1
    )
}

Учитывая, что эта ошибка не происходит с меньшими пакетами, я полагаю обоснованное предположение, что это из-за ограничений кода здесь, как указано в официальной документации РФ C :

5.4. Фрагментация

Основная задача фрагментации - разрешить отправку сообщения неизвестного размера при запуске сообщения без необходимости буферизации этого сообщения. Если сообщения не могут быть фрагментированы, тогда конечной точке
придется буферизовать все сообщение, чтобы его длина могла быть подсчитана до
до отправки первого байта. При фрагментации сервер или посредник
могут выбрать буфер разумного размера, а когда
буфер заполнен, записать фрагмент в сеть.

Дополнительный вариант использования для фрагментации предназначен для мультиплексирования. где нежелательно, чтобы большое сообщение в одном логическом канале
монополизировало выходной канал, поэтому мультиплексирование должно быть свободным для разделения сообщения на более мелкие фрагменты, чтобы лучше использовать выходной канал. (Обратите внимание, что расширение мультиплексирования не описано в этом документе.)

Если расширение не указывает иное, кадры не имеют семантического значения. Посредник может объединять и / или разбивать кадры, если клиент и сервер не согласовали расширения
или если согласованы некоторые расширения
, но посредник понял все согласованные расширения
и знает, как объединить и / или разделенные кадры
при наличии этих расширений. Одним из следствий этого является то, что в отсутствие расширений отправители и получатели не должны зависеть от
наличия определенных c границ кадра.

Следующие правила применяются к фрагментации:

o Нефрагментированное сообщение состоит из одного кадра с установленным битом FIN (раздел 5.2) и кода операции, отличного от 0.

o Фрагментированное сообщение состоит из одного кадра с очищенным битом FIN и кода операции, отличного от 0, за которым следует ноль или более кадров с очищенным битом FIN и кодом операции, установленным в 0, и завершается одним кадром с установленным битом FIN и кодом операции 0. Фрагментированное сообщение концептуально эквивалентно одному большему сообщению, полезная нагрузка которого равен объединению полезных нагрузок фрагментов по порядку; однако при наличии расширений это может не выполняться, так как расширение определяет интерпретацию «данных расширения». Например, «Данные расширения» могут присутствовать только в начале первого фрагмента и применяться к последующим фрагментам, или могут присутствовать «Данные расширения», присутствующие в каждом из фрагментов, которые применяются только к этому конкретному фрагменту. При отсутствии «данных расширения» следующий пример демонстрирует, как работает фрагментация.

  EXAMPLE: For a text message sent as three fragments, the first
  fragment would have an opcode of 0x1 and a FIN bit clear, the
  second fragment would have an opcode of 0x0 and a FIN bit clear,
  and the third fragment would have an opcode of 0x0 and a FIN bit
  that is set.

o Контрольные кадры (см. Раздел 5.5) МОГУТ быть введены в середине фрагментированного сообщения. Сами контрольные кадры НЕ ДОЛЖНЫ быть фрагментированными.

o Фрагменты сообщения ДОЛЖНЫ быть доставлены получателю в порядке, отправленном отправителем. o Фрагменты одного сообщения НЕ ДОЛЖНЫ чередоваться между фрагментами другого сообщения, если не было согласовано расширение, которое может интерпретировать перемежение.

o Конечная точка ДОЛЖНА быть способна обрабатывать контрольные кадры в середине фрагментированного сообщения.

o Отправитель МОЖЕТ создавать фрагменты любого размера для неуправляемых сообщений.

o Клиенты и серверы ДОЛЖНЫ поддерживать получение как фрагментированных, так и нефрагментированных сообщений.

o Поскольку кадры управления не могут быть фрагментированы, посредник НЕ ДОЛЖЕН пытаться изменить фрагментацию кадра управления.

o Посредник НЕ ДОЛЖЕН изменить фрагментацию сообщения, если используются какие-либо зарезервированные битовые значения, а значение этих значений неизвестно посреднику.

o Посредник НЕ ДОЛЖЕН изменять фрагментацию любого сообщения в контексте соединения, где расширения были согласованы, и посредник не знает о семантике согласованных расширений. Точно так же посредник, который не видел рукопожатие WebSocket (и не был уведомлен о его содержимом), который привел к соединению WebSocket, НЕ ДОЛЖЕН изменять фрагментацию любого сообщения такого соединения.

o Как следствие из этих правил все фрагменты сообщения имеют один и тот же тип, как установлено кодом операции первого фрагмента. Поскольку контрольные кадры не могут быть фрагментированы, тип для всех фрагментов в сообщении ДОЛЖЕН быть либо текстовым, двоичным, либо одним из зарезервированных кодов операций.

ПРИМЕЧАНИЕ. Если контрольные кадры не могут быть вставлены, задержка пинга например, будет очень длинным, если за большим сообщением.
Следовательно, требование обработки кадров управления в середине фрагментированного сообщения
.

ПРИМЕЧАНИЕ РЕАЛИЗАЦИИ: При отсутствии какого-либо Расширение, получатель
не должен буферизовать весь кадр для его обработки. Для примера
, если используется потоковый API, часть кадра может быть
доставлена ​​приложению. Однако обратите внимание, что это предположение
может не выполняться для всех будущих расширений WebSocket.

В словах статья выше :

Выравнивание Node.js буферов сокетов с фреймами сообщений WebSocket

Node.js данные сокетов (я говорю о net .Socket в данном случае, а не WebSockets) получаются в буферизованных фрагментах. Они разделены друг от друга, независимо от того, где начинаются или заканчиваются ваши фреймы WebSocket!

Это означает, что если ваш сервер получает большие сообщения, фрагментированные в несколько фреймов WebSocket, или получает большое количество сообщений в быстрой последовательности, нет никакой гарантии, что каждый буфер данных, полученный сокетом Node.js, будет выровнен с началом и концом байтовых данных, составляющих данный кадр.

Итак, при разборе каждого буфера, полученного сокет, вам нужно отслеживать, где заканчивается один кадр и где начинается следующий. Вам нужно быть уверенным, что вы получили все байты данных для кадра - прежде чем вы сможете безопасно использовать данные этого кадра.

Возможно, один кадр заканчивается посередине через тот же буфер в который начинается следующий кадр. Также возможно, что кадр разделен на несколько буферов, которые будут получены последовательно.

Следующая диаграмма является преувеличенной иллюстрацией проблемы. В большинстве случаев кадры имеют тенденцию помещаться внутри буфера. Из-за способа поступления данных вы часто обнаруживаете, что кадр начинается и заканчивается в соответствии с началом и концом буфера сокета. Но на это нельзя полагаться во всех случаях, и это необходимо учитывать при реализации. enter image description here Для правильной реализации может потребоваться некоторая работа.

Для реализации базового c, описанной ниже, я пропустил любой код для обработки больших сообщений или сообщений, разбитых на несколько кадров .

Итак, моя проблема здесь в том, что в статье пропущен фрагментарный код, что мне нужно знать ... но в этой RF C документации , некоторые примеры фрагментированных и нефрагментированных пакетов даются:

5.6. Кадры данных

Кадры данных (например, неуправляемые кадры) идентифицируются с помощью кодов операций
, где самый значимый бит кода операции равен 0. В настоящее время определенные коды операций для кадров данных включают в себя 0x1 (текст), 0x2 ( Binary). Коды операций
0x3-0x7 зарезервированы для дальнейших неуправляемых кадров, которые еще должны быть определены
.

Кадры данных переносят данные прикладного уровня и / или уровня расширения. Код операции определяет интерпретацию данных:

Текст

  The "Payload data" is text data encoded as UTF-8.  Note that a
  particular text frame might include a partial UTF-8 sequence;
  however, the whole message MUST contain valid UTF-8.  Invalid
  UTF-8 in reassembled messages is handled as described in
  Section 8.1.

Двоичный

  The "Payload data" is arbitrary binary data whose interpretation
  is solely up to the application layer.

5.7. Примеры

o Однокадровое немаскированное текстовое сообщение

  *  0x81 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains "Hello")

o Однокадровое маскированное текстовое сообщение

  *  0x81 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58
     (contains "Hello")

o Фрагментированное немаскированное текстовое сообщение

  *  0x01 0x03 0x48 0x65 0x6c (contains "Hel")

  *  0x80 0x02 0x6c 0x6f (contains "lo")

o Запрос Ping без маскировки и ответ Ping с маской

  *  0x89 0x05 0x48 0x65 0x6c 0x6c 0x6f (contains a body of "Hello",
     but the contents of the body are arbitrary)

  *  0x8a 0x85 0x37 0xfa 0x21 0x3d 0x7f 0x9f 0x4d 0x51 0x58
     (contains a body of "Hello", matching the body of the ping)

o Двоичное сообщение размером 256 байт в одном немаскированном кадре

  *  0x82 0x7E 0x0100 [256 bytes of binary data]

o Двоичное сообщение объемом 64 КБ в одном немаскированном виде frame

  *  0x82 0x7F 0x0000000000010000 [65536 bytes of binary data]

Таким образом, может показаться, что это пример фрагмента.

Также этот представляется уместным:

6,2. Получение данных

Для получения данных WebSocket конечная точка прослушивает основное сетевое соединение
. Входящие данные ДОЛЖНЫ быть проанализированы как кадры WebSocket, как определено в разделе 5.2. Если кадр управления (раздел 5.5) получен
, кадр ДОЛЖЕН обрабатываться, как определено в разделе 5.5. После получения
фрейма данных (раздел 5.6) конечная точка ДОЛЖНА записать
/ тип / данных, как определено кодом операции (frame-opcode) из
Раздел 5.2. «Данные приложения» из этого кадра определяются как
/ data / сообщения. Если фрейм содержит нефрагментированное сообщение
(раздел 5.4), говорят, что получено сообщение WebSocket
получено
с типом / типом / и данными / данными /. Если кадр является частью
фрагментированного сообщения, «Данные приложения» последующих фреймов данных
объединяются для формирования / data /. Когда последний фрагмент получен, как указано битом FIN (frame-fin), говорят, что A
Сообщение WebSocket было получено
с данными / данными / (состоящими из
конкатенации) «данных приложения» фрагментов) и типа / типа / (отмеченных в первом кадре фрагментированного сообщения).
Последующие кадры данных ДОЛЖНЫ интерпретироваться как принадлежащие новому
сообщению WebSocket.

Расширения (раздел 9) МОГУТ изменить семантику того, как считываются данные, в частности, включая то, что включает в себя границу сообщения.
Расширения, в дополнение к добавлению «данных расширения» перед
«данными приложения» «в полезной нагрузке МОЖЕТ также изменить« данные приложения
»(например, сжав их).

Проблема:

Я не знаю, как проверить для фрагментов и выравнивания их с буферами узла, как упомянуто в статье, я могу только читать очень маленькие количества буфера.

Как я могу проанализировать большие порции данных, используя методы фрагментации, упомянутые в документации RF C, и расположение буферов nodeJS со ссылкой на (но не объяснено) в статье?

...