presidium-websocket
Version:
Presidium WebSocket client and server for Node.js
229 lines (201 loc) • 5.04 kB
JavaScript
const events = require('events')
const zlib = require('zlib')
const encodeWebSocketFrame = require('./encodeWebSocketFrame')
/**
* @name ServerWebsocket
*
* @docs
* ```coffeescript [specscript]
* new ServerWebsocket(socket net.Socket, options {
* maxMessageLength: number
* }) -> websocket ServerWebsocket
* ```
*/
class ServerWebsocket extends events.EventEmitter {
constructor(socket, options) {
super()
this._socket = socket
this._perMessageDeflate = socket._perMessageDeflate
this.on('error', () => {
this.destroy()
})
this._socket.on('error', error => {
this.emit('error', error)
})
this._maxMessageLength = options.maxMessageLength
this._socketBufferLength = options.socketBufferLength
this._continuationPayloads = []
this.readyState = 0 // CONNECTING
}
/**
* @name connect
*
* @docs
* ```coffeescript [specscript]
* websocket.connect() -> ()
* ```
*/
connect() {
this.emit('error', new Error('server WebSocket instances cannot use the connect method'))
}
/**
* @name send
*
* @docs
* ```coffeescript [specscript]
* websocket.send(payload Buffer|string) -> ()
* ```
*/
async send(payload) {
let buffer = null
let opcode = null
if (Buffer.isBuffer(payload)) {
buffer = payload
opcode = 0x2
} else if (ArrayBuffer.isView(payload)) {
buffer = Buffer.from(payload.buffer)
opcode = 0x2
} else if (typeof payload == 'string') {
buffer = Buffer.from(payload, 'utf8')
opcode = 0x1
} else {
this.emit('error', new TypeError('send can only process binary or text frames'))
return undefined
}
let compressed = false
if (this._perMessageDeflate && buffer.length > 0) {
try {
const compressedPayload = zlib.deflateRawSync(buffer)
if (
compressedPayload.length >= 4 &&
compressedPayload.slice(-4).equals(Buffer.from([0x00, 0x00, 0xff, 0xff]))
) {
buffer = compressedPayload.slice(0, -4)
} else {
buffer = compressedPayload
}
compressed = true
} catch (error) {
this.emit('error', error)
return undefined
}
}
if (buffer.length <= this._maxMessageLength) { // unfragmented
this._socket.write(encodeWebSocketFrame.call(
this,
buffer,
opcode,
false,
true,
compressed
))
} else { // fragmented
let index = 0
let fragment = buffer.slice(0, this._maxMessageLength)
this._socket.write(encodeWebSocketFrame.call(
this,
fragment,
opcode,
false,
false,
compressed
))
// continuation frames
index += this._maxMessageLength
while (index < payload.length) {
const fin = index + this._maxMessageLength >= payload.length
fragment = buffer.slice(index, index + this._maxMessageLength)
this._socket.write(encodeWebSocketFrame.call(
this,
fragment,
0x0,
false,
fin,
compressed
))
index += this._maxMessageLength
}
}
return undefined
}
/**
* @name sendClose
*
* @docs
* Sends close frame to client
*
* ```coffeescript [specscript]
* websocket.sendClose(payload Buffer|string) -> ()
* ```
*/
sendClose(payload = Buffer.from([])) {
if (!Buffer.isBuffer(payload)) {
payload = Buffer.from(payload)
}
this._socket.write(encodeWebSocketFrame.call(this, payload, 0x8)) // close frame
this.sentClose = true
}
/**
* @name sendPing
*
* @docs
* Sends "ping" to client
*
* ```coffeescript [specscript]
* websocket.sendPing(payload Buffer|string) -> ()
* ```
*/
sendPing(payload = Buffer.from([])) {
if (!Buffer.isBuffer(payload)) {
payload = Buffer.from(payload)
}
this._socket.write(encodeWebSocketFrame.call(this, payload, 0x9)) // ping frame
}
/**
* @name sendPong
*
* @docs
* Sends "pong" back to client
*
* ```coffeescript [specscript]
* websocket.sendPong(payload Buffer|string) -> ()
* ```
*/
sendPong(payload = Buffer.from([])) {
if (!Buffer.isBuffer(payload)) {
payload = Buffer.from(payload)
}
this._socket.write(encodeWebSocketFrame.call(this, payload, 0xA)) // pong frame
}
/**
* @name close
*
* @docs
* Closes the websocket
*
* ```coffeescript [specscript]
* websocket.close() -> ()
* ```
*/
close(payload = Buffer.from([])) {
this.readyState = 2 // CLOSING
this.sendClose(payload)
}
/**
* @name destroy
*
* @docs
* Closes the websocket
*
* ```coffeescript [specscript]
* websocket.destroy(payload Buffer|string) -> ()
* ```
*/
destroy(payload) {
this._socket.destroy()
this.closed = true
this.readyState = 3 // CLOSED
this.emit('close', payload)
}
}
module.exports = ServerWebsocket