-
-
Notifications
You must be signed in to change notification settings - Fork 42
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
WIP: Implement class Frame #477
base: master
Are you sure you want to change the base?
Changes from all commits
1f26327
ede8de9
7df26af
509564f
302bd63
e67f5bd
c474f02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -17,29 +17,109 @@ const OPCODE_SHORT = 0x81; | |||||
const LEN_16_BIT = 126; | ||||||
const MAX_16_BIT = 65536; | ||||||
const LEN_64_BIT = 127; | ||||||
const MAX_64_BIT_PAYLOAD = Number.MAX_SAFE_INTEGER; | ||||||
const HEAD_LENGTH = 2; | ||||||
const EXTENDED_PAYLOAD_16_LENGTH = 2; | ||||||
const EXTENDED_PAYLOAD_64_LENGTH = 8; | ||||||
|
||||||
const calcOffset = (frame, length) => { | ||||||
if (length < LEN_16_BIT) return [2, 6]; | ||||||
if (length === LEN_16_BIT) return [4, 8]; | ||||||
return [10, 14]; | ||||||
}; | ||||||
class Frame { | ||||||
#frame = null; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
#masked = null; | ||||||
#dataOffset = null; | ||||||
#mask = null; | ||||||
#length = null; | ||||||
|
||||||
const parseFrame = (frame) => { | ||||||
const length = frame[1] ^ 0x80; | ||||||
const [maskOffset, dataOffset] = calcOffset(frame, length); | ||||||
const mask = frame.subarray(maskOffset, maskOffset + MASK_LENGTH); | ||||||
const data = frame.subarray(dataOffset); | ||||||
return { mask, data }; | ||||||
}; | ||||||
constructor(frame) { | ||||||
this.#frame = frame; | ||||||
const length = this.#frame[1] & 0x7f; | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because it doesn't have to work that way, pass a reference to the buffer with WebSocket frame to the constructor. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Or use static method 'from()' to create new instance |
||||||
this.#masked = (frame[1] & 0x80) === 0x80; | ||||||
if (length < 126) { | ||||||
this.#dataOffset = HEAD_LENGTH + MASK_LENGTH; | ||||||
this.#mask = frame.subarray(HEAD_LENGTH, HEAD_LENGTH + MASK_LENGTH); | ||||||
this.#length = length; | ||||||
} else if (length === 126) { | ||||||
this.#dataOffset = HEAD_LENGTH + EXTENDED_PAYLOAD_16_LENGTH + MASK_LENGTH; | ||||||
this.#mask = frame.subarray( | ||||||
HEAD_LENGTH + EXTENDED_PAYLOAD_16_LENGTH, | ||||||
HEAD_LENGTH + EXTENDED_PAYLOAD_16_LENGTH + MASK_LENGTH, | ||||||
); | ||||||
this.#length = this.#frame.readUInt16BE(2); | ||||||
} else { | ||||||
this.#dataOffset = HEAD_LENGTH + EXTENDED_PAYLOAD_64_LENGTH + MASK_LENGTH; | ||||||
this.#mask = frame.subarray( | ||||||
HEAD_LENGTH + EXTENDED_PAYLOAD_64_LENGTH, | ||||||
HEAD_LENGTH + EXTENDED_PAYLOAD_64_LENGTH + MASK_LENGTH, | ||||||
); | ||||||
this.#length = (frame.readUInt32BE(2) << 32) + frame.readUInt32BE(4); | ||||||
} | ||||||
} | ||||||
|
||||||
const unmask = (buffer, mask) => { | ||||||
const data = Buffer.allocUnsafe(buffer.length); | ||||||
buffer.copy(data); | ||||||
for (let i = 0; i < data.length; i++) { | ||||||
data[i] ^= mask[i & 3]; | ||||||
unmask() { | ||||||
if (!this.#masked) return; | ||||||
for (let i = 0; i < this.#length; ++i) { | ||||||
this.#frame[this.#dataOffset + i] ^= this.#mask[i & 3]; | ||||||
} | ||||||
this.#masked = false; | ||||||
} | ||||||
|
||||||
toString() { | ||||||
return this.#frame.toString( | ||||||
'utf8', | ||||||
this.#dataOffset, | ||||||
this.#dataOffset + this.#length, | ||||||
); | ||||||
} | ||||||
|
||||||
get frame() { | ||||||
return this.#frame; | ||||||
} | ||||||
return data; | ||||||
}; | ||||||
|
||||||
static from(data) { | ||||||
if (Buffer.isBuffer(data)) { | ||||||
if (data.length === 0) throw new Error('Empty frame!'); | ||||||
if ((data[1] & 0x80) !== 0x80) throw new Error('1002: protocol error'); | ||||||
// | ||||||
// The maximum safe integer in JavaScript is 2^53 - 1. An error is returned | ||||||
// if payload length is greater than this number. | ||||||
// | ||||||
if ((data[2] & 0x7f) === 127) { | ||||||
const upperInt = data.readUInt32BE(2); | ||||||
if (upperInt > MAX_64_BIT_PAYLOAD >> 32) | ||||||
throw new Error( | ||||||
'1009: Unsupported WebSocket frame: payload length > 2^53 - 1', | ||||||
); | ||||||
} | ||||||
return new Frame(data); | ||||||
} | ||||||
|
||||||
if (typeof data === 'string') { | ||||||
if (data.length === 0) throw new Error('Empty string!'); | ||||||
const payload = Buffer.from(data); | ||||||
const length = payload.length; | ||||||
let meta = Buffer.alloc(2); | ||||||
meta[0] = 0x81; // FIN = 1, RSV = 0b000, opcode = 0b0001 (text frame) | ||||||
if (length < LEN_16_BIT) { | ||||||
meta[1] = length; | ||||||
} else if (length < MAX_16_BIT) { | ||||||
const len = Buffer.alloc(2); | ||||||
len.writeUint16BE(length, 0); | ||||||
meta[1] = LEN_16_BIT; | ||||||
meta = Buffer.concat([meta, len]); | ||||||
} else if (length < MAX_64_BIT_PAYLOAD) { | ||||||
const len = Buffer.alloc(8); | ||||||
len.writeBigUInt64BE(BigInt(length), 0); | ||||||
meta[1] = LEN_64_BIT; | ||||||
this.meta = Buffer.concat([meta, len]); | ||||||
} else { | ||||||
throw new Error('string is too long to encode in one frame!'); | ||||||
} | ||||||
const frame = Buffer.concat([meta, payload]); | ||||||
return new Frame(Buffer.from(frame)); | ||||||
} | ||||||
|
||||||
throw new Error('Unsupported'); | ||||||
} | ||||||
} | ||||||
|
||||||
class Connection { | ||||||
constructor(socket) { | ||||||
|
@@ -56,34 +136,18 @@ class Connection { | |||||
} | ||||||
|
||||||
send(text) { | ||||||
const data = Buffer.from(text); | ||||||
let meta = Buffer.alloc(2); | ||||||
const length = data.length; | ||||||
meta[0] = OPCODE_SHORT; | ||||||
if (length < LEN_16_BIT) { | ||||||
meta[1] = length; | ||||||
} else if (length < MAX_16_BIT) { | ||||||
const len = Buffer.from([(length & 0xff00) >> 8, length & 0x00ff]); | ||||||
meta = Buffer.concat([meta, len]); | ||||||
meta[1] = LEN_16_BIT; | ||||||
} else { | ||||||
const len = Buffer.alloc(8); | ||||||
len.writeBigInt64BE(BigInt(length), 0); | ||||||
meta = Buffer.concat([meta, len]); | ||||||
meta[1] = LEN_64_BIT; | ||||||
} | ||||||
const frame = Buffer.concat([meta, data]); | ||||||
this.socket.write(frame); | ||||||
const frame = Frame.from(text); | ||||||
this.socket.write(frame.frame); | ||||||
} | ||||||
|
||||||
receive(data) { | ||||||
console.log('data: ', data[0], data.length); | ||||||
if (data[0] !== OPCODE_SHORT) return; | ||||||
const frame = parseFrame(data); | ||||||
const msg = unmask(frame.data, frame.mask); | ||||||
const text = msg.toString(); | ||||||
this.send(`Echo "${text}"`); | ||||||
const frame = Frame.from(data); | ||||||
frame.unmask(); | ||||||
const text = frame.toString(); | ||||||
console.log('Message:', text); | ||||||
this.send(`Echo "${text}"`); | ||||||
} | ||||||
|
||||||
accept(key) { | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typically in a constructor you just assign some props for instance. Make some configuration. But you don't do some complex logic of how inputs should be mapped correctly to Frame class props. This is not the responsibility of the constructor. So we can create
from
method that will take this responsibility. See in the example below. Also we should not handle encode or decode logic in constructor. Because when you create frame with string or buffer there is no difference.new Frame('1234')
andnew Frame(buffer)
. Code becomes implicit and less readable. Instead you can doFrame.from(data).decode()
andFrame.from(data).decode()
. This makes clear what happens in this code. See in the example below. So, please use interface described in example below.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, with the factory, yeah, that's probably better. But creating a text frame of a protocol from a string and just saving the finished frame in a class field is not the same action. Then it is probably better to just accept a ready frame by constructor and create a protocol frame from input data by static method. Thanks.