nodelink / src /ws.js
flameface's picture
Upload 25 files
b58c6cb verified
import EventEmitter from 'node:events'
import crypto from 'node:crypto'
import { Buffer } from 'node:buffer'
const TLS_MAX_SEND_SIZE = 2 ** 14
const CONTINUE_HEADER_LENGTH = 2
function parseFrameHeader(buffer) {
let startIndex = 2
const opcode = buffer[0] & 0b00001111
const fin = (buffer[0] & 0b10000000) === 0b10000000
const isMasked = (buffer[1] & 0x80) === 0x80
let payloadLength = buffer[1] & 0b01111111
if (payloadLength === 126) {
startIndex += 2
payloadLength = buffer.readUInt16BE(2)
} else if (payloadLength === 127) {
const buf = buffer.subarray(startIndex, startIndex + 8)
payloadLength = buf.readUInt32BE(0) * Math.pow(2, 32) + buf.readUInt32BE(4)
startIndex += 8
}
let mask = null
if (isMasked) {
mask = buffer.subarray(startIndex, startIndex + 4)
startIndex += 4
buffer = buffer.subarray(startIndex, startIndex + payloadLength)
for (let i = 0; i < buffer.length; i++) {
buffer[i] ^= mask[i & 3]
}
} else {
buffer = buffer.subarray(startIndex, startIndex + payloadLength)
}
return {
opcode,
fin,
buffer,
payloadLength
}
}
class WebsocketConnection extends EventEmitter {
constructor(req, socket, head, addHeaders) {
super()
this.req = req
this.socket = socket
socket.setNoDelay()
socket.setKeepAlive(true)
if (head.length !== 0) socket.unshift(head)
const headers = [
'HTTP/1.1 101 Switching Protocols',
'Upgrade: websocket',
'Connection: Upgrade',
'Sec-WebSocket-Accept: ' + crypto.createHash('sha1').update(req.headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').digest('base64'),
'Sec-WebSocket-Version: 13',
]
if (addHeaders) {
for (const [key, value] of Object.entries(addHeaders)) {
headers.push(`${key}: ${value}`)
}
}
socket.write(headers.join('\r\n') + '\r\n\r\n')
socket.on('data', (data) => {
const headers = parseFrameHeader(data)
switch (headers.opcode) {
case 0x0: {
this.cachedData.push(headers.buffer)
if (headers.fin) {
this.emit('message', Buffer.concat(this.cachedData).toString())
this.cachedData = []
}
break
}
case 0x1: {
this.emit('message', headers.buffer.toString())
break
}
case 0x2: {
throw new Error('Binary data is not supported.')
break
}
case 0x8: {
if (headers.buffer.length === 0) {
this.emit('close', 1006, '')
} else {
const code = headers.buffer.readUInt16BE(0)
const reason = headers.buffer.subarray(2).toString('utf-8')
this.emit('close', code, reason)
}
socket.end()
socket.removeAllListeners()
break
}
case 0x9: {
const pong = Buffer.allocUnsafe(2)
pong[0] = 0x8a
pong[1] = 0x00
this.socket.write(pong)
break
}
case 0x10: {
this.emit('pong')
}
}
if (headers.buffer.length > headers.payloadLength)
this.socket.unshift(headers.buffer)
})
req.on('error', (err) => {
socket.destroy()
this.emit('close', 1006, `Error: ${err.message}`)
socket.removeAllListeners()
})
socket.on('error', (err) => {
socket.destroy()
this.emit('close', 1006, `Error: ${err.message}`)
socket.removeAllListeners()
})
socket.on('end', () => {
socket.end()
this.emit('close', 1006, '')
socket.removeAllListeners()
})
}
send(data) {
const payload = Buffer.from(data, 'utf-8')
if (payload.length + CONTINUE_HEADER_LENGTH > TLS_MAX_SEND_SIZE) {
for (let i = 0; i < payload.length; i += TLS_MAX_SEND_SIZE) {
const buffer = payload.subarray(i, i + TLS_MAX_SEND_SIZE)
const length = Buffer.byteLength(buffer)
let header = null
if (i === 0) {
header = this.makeFHeader({ len: length, fin: false, opcode: 0x1 })
}
else if (i + TLS_MAX_SEND_SIZE >= payload.length) {
header = this.makeFHeader({ len: length, fin: true, opcode: 0x0 })
}
else {
header = this.makeFHeader({ len: length, fin: false, opcode: 0x0 })
}
this.socket.write(Buffer.concat([ header, buffer ]))
}
return true
} else {
return this.sendFrame(payload, { len: payload.length, fin: true, opcode: 0x01 })
}
}
destroy() {
this.socket.destroy()
this.socket = null
this.req = null
}
makeFHeader(options) {
let payloadStartIndex = 2
let payloadLength = options.len
if (options.len >= 65536) {
payloadStartIndex += 8
payloadLength = 127
} else if (options.len > 125) {
payloadStartIndex += 2
payloadLength = 126
}
const header = Buffer.allocUnsafe(payloadStartIndex)
header[0] = options.fin ? options.opcode | 0x80 : options.opcode
header[1] = payloadLength
if (payloadLength === 126) {
header.writeUInt16BE(options.len, 2)
} else if (payloadLength === 127) {
header[2] = header[3] = 0
header.writeUIntBE(options.len, 4, 6)
}
return header
}
sendFrame(data, options) {
const header = this.makeFHeader(options)
if (this.socket) this.socket.write(Buffer.concat([ header, data ]))
return true
}
close(code, reason) {
const data = Buffer.allocUnsafe(2 + Buffer.byteLength(reason || 'normal close'))
data.writeUInt16BE(code || 1000)
data.write(reason || 'normal close', 2)
this.sendFrame(data, { len: data.length, fin: true, opcode: 0x08 })
return true
}
}
class WebSocketServer extends EventEmitter {
constructor() {
super()
}
handleUpgrade(req, socket, head, headers, callback) {
const connection = new WebsocketConnection(req, socket, head, headers)
if (!socket.readable || !socket.writable) return socket.destroy()
callback(connection)
}
}
export { WebSocketServer }