Files
ms/packages/websocket/src/engine.io/server.ts
2021-08-03 16:59:43 +08:00

691 lines
23 KiB
TypeScript

const qs = require("querystring")
const parse = require("url").parse
// const base64id = require("base64id")
import transports from './transports'
import { EventEmitter } from 'events'
// const EventEmitter = require("events").EventEmitter
import { Socket } from './socket'
// const debug = require("debug")("engine")
const debug = function (...args) { }
// const cookieMod = require("cookie")
// const DEFAULT_WS_ENGINE = require("ws").Server;
import { WebSocketServer } from '../server'
import { Transport } from './transport'
const DEFAULT_WS_ENGINE = WebSocketServer
import { Request } from '../server/request'
import { WebSocketClient } from '../server/client'
export class Server extends EventEmitter {
public static errors = {
UNKNOWN_TRANSPORT: 0,
UNKNOWN_SID: 1,
BAD_HANDSHAKE_METHOD: 2,
BAD_REQUEST: 3,
FORBIDDEN: 4,
UNSUPPORTED_PROTOCOL_VERSION: 5
}
public static errorMessages = {
0: "Transport unknown",
1: "Session ID unknown",
2: "Bad handshake method",
3: "Bad request",
4: "Forbidden",
5: "Unsupported protocol version"
}
private clients = {}
private clientsCount = 0
public opts: any
private corsMiddleware: any
private ws: any
private perMessageDeflate: any
constructor(opts: any = {}) {
super()
this.opts = Object.assign(
{
wsEngine: DEFAULT_WS_ENGINE,
pingTimeout: 20000,
pingInterval: 25000,
upgradeTimeout: 10000,
maxHttpBufferSize: 1e6,
transports: Object.keys(transports),
allowUpgrades: true,
httpCompression: {
threshold: 1024
},
cors: false,
allowEIO3: false
},
opts
)
// if (opts.cookie) {
// this.opts.cookie = Object.assign(
// {
// name: "io",
// path: "/",
// httpOnly: opts.cookie.path !== false,
// sameSite: "lax"
// },
// opts.cookie
// )
// }
// if (this.opts.cors) {
// this.corsMiddleware = require("cors")(this.opts.cors)
// }
// if (opts.perMessageDeflate) {
// this.opts.perMessageDeflate = Object.assign(
// {
// threshold: 1024
// },
// opts.perMessageDeflate
// )
// }
// this.init()
}
// /**
// * Initialize websocket server
// *
// * @api private
// */
// init() {
// if (!~this.opts.transports.indexOf("websocket")) return
// if (this.ws) this.ws.close()
// this.ws = new this.opts.wsEngine({
// noServer: true,
// clientTracking: false,
// perMessageDeflate: this.opts.perMessageDeflate,
// maxPayload: this.opts.maxHttpBufferSize
// })
// if (typeof this.ws.on === "function") {
// this.ws.on("headers", (headersArray, req) => {
// // note: 'ws' uses an array of headers, while Engine.IO uses an object (response.writeHead() accepts both formats)
// // we could also try to parse the array and then sync the values, but that will be error-prone
// const additionalHeaders = {}
// const isInitialRequest = !req._query.sid
// if (isInitialRequest) {
// this.emit("initial_headers", additionalHeaders, req)
// }
// this.emit("headers", additionalHeaders, req)
// Object.keys(additionalHeaders).forEach(key => {
// headersArray.push(`${key}: ${additionalHeaders[key]}`)
// })
// })
// }
// }
/**
* Returns a list of available transports for upgrade given a certain transport.
*
* @return {Array}
* @api public
*/
upgrades(transport): Array<any> {
if (!this.opts.allowUpgrades) return []
return transports[transport].upgradesTo || []
}
// /**
// * Verifies a request.
// *
// * @param {http.IncomingMessage}
// * @return {Boolean} whether the request is valid
// * @api private
// */
// verify(req, upgrade, fn) {
// // transport check
// const transport = req._query.transport
// if (!~this.opts.transports.indexOf(transport)) {
// debug('unknown transport "%s"', transport)
// return fn(Server.errors.UNKNOWN_TRANSPORT, { transport })
// }
// // 'Origin' header check
// const isOriginInvalid = checkInvalidHeaderChar(req.headers.origin)
// if (isOriginInvalid) {
// const origin = req.headers.origin
// req.headers.origin = null
// debug("origin header invalid")
// return fn(Server.errors.BAD_REQUEST, {
// name: "INVALID_ORIGIN",
// origin
// })
// }
// // sid check
// const sid = req._query.sid
// if (sid) {
// if (!this.clients.hasOwnProperty(sid)) {
// debug('unknown sid "%s"', sid)
// return fn(Server.errors.UNKNOWN_SID, {
// sid
// })
// }
// const previousTransport = this.clients[sid].transport.name
// if (!upgrade && previousTransport !== transport) {
// debug("bad request: unexpected transport without upgrade")
// return fn(Server.errors.BAD_REQUEST, {
// name: "TRANSPORT_MISMATCH",
// transport,
// previousTransport
// })
// }
// } else {
// // handshake is GET only
// if ("GET" !== req.method) {
// return fn(Server.errors.BAD_HANDSHAKE_METHOD, {
// method: req.method
// })
// }
// if (!this.opts.allowRequest) return fn()
// return this.opts.allowRequest(req, (message, success) => {
// if (!success) {
// return fn(Server.errors.FORBIDDEN, {
// message
// })
// }
// fn()
// })
// }
// fn()
// }
/**
* Prepares a request by processing the query string.
*
* @api private
*/
prepare(req) {
// try to leverage pre-existing `req._query` (e.g: from connect)
if (!req._query) {
req._query = ~req.url.indexOf("?") ? qs.parse(parse(req.url).query) : {}
}
}
/**
* Closes all clients.
*
* @api public
*/
close() {
debug("closing all open clients")
for (let i in this.clients) {
if (this.clients.hasOwnProperty(i)) {
this.clients[i].close(true)
}
}
if (this.ws) {
debug("closing webSocketServer")
this.ws.close()
// don't delete this.ws because it can be used again if the http server starts listening again
}
return this
}
// /**
// * Handles an Engine.IO HTTP request.
// *
// * @param {http.IncomingMessage} request
// * @param {http.ServerResponse|http.OutgoingMessage} response
// * @api public
// */
// handleRequest(req, res) {
// debug('handling "%s" http request "%s"', req.method, req.url)
// this.prepare(req)
// req.res = res
// const callback = (errorCode, errorContext) => {
// if (errorCode !== undefined) {
// this.emit("connection_error", {
// req,
// code: errorCode,
// message: Server.errorMessages[errorCode],
// context: errorContext
// })
// abortRequest(res, errorCode, errorContext)
// return
// }
// if (req._query.sid) {
// debug("setting new request for existing client")
// this.clients[req._query.sid].transport.onRequest(req)
// } else {
// const closeConnection = (errorCode, errorContext) =>
// abortRequest(res, errorCode, errorContext)
// this.handshake(req._query.transport, req, closeConnection)
// }
// }
// if (this.corsMiddleware) {
// this.corsMiddleware.call(null, req, res, () => {
// this.verify(req, false, callback)
// })
// } else {
// this.verify(req, false, callback)
// }
// }
/**
* generate a socket id.
* Overwrite this method to generate your custom socket id
*
* @param {Object} request object
* @api public
*/
generateId(req) {
return req.id
}
/**
* Handshakes a new client.
*
* @param {String} transport name
* @param {Object} request object
* @param {Function} closeConnection
*
* @api private
*/
// @java-patch sync handshake
handshake(transportName, req, closeConnection: (code: number) => void) {
console.debug('engine.io server handshake transport', transportName, 'from', req.url)
const protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
if (protocol === 3 && !this.opts.allowEIO3) {
debug("unsupported protocol version")
this.emit("connection_error", {
req,
code: Server.errors.UNSUPPORTED_PROTOCOL_VERSION,
message:
Server.errorMessages[Server.errors.UNSUPPORTED_PROTOCOL_VERSION],
context: {
protocol
}
})
closeConnection(Server.errors.UNSUPPORTED_PROTOCOL_VERSION)
return
}
let id
try {
id = this.generateId(req)
} catch (e) {
console.debug("error while generating an id")
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "ID_GENERATION_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
console.debug('engine.io server handshaking client "' + id + '"')
try {
var transport: Transport = new transports[transportName](req)
if ("websocket" !== transportName) {
throw new Error('Unsupport polling at MiaoScript!')
}
// if ("polling" === transportName) {
// transport.maxHttpBufferSize = this.opts.maxHttpBufferSize
// transport.httpCompression = this.opts.httpCompression
// } else if ("websocket" === transportName) {
transport.perMessageDeflate = this.opts.perMessageDeflate
// }
if (req._query && req._query.b64) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
} catch (e) {
console.ex(e)
this.emit("connection_error", {
req,
code: Server.errors.BAD_REQUEST,
message: Server.errorMessages[Server.errors.BAD_REQUEST],
context: {
name: "TRANSPORT_HANDSHAKE_ERROR",
error: e
}
})
closeConnection(Server.errors.BAD_REQUEST)
return
}
console.debug(`engine.io server create socket ${id} from transport ${transport.name} protocol ${protocol}`)
const socket = new Socket(id, this, transport, req, protocol)
transport.on("headers", (headers, req) => {
const isInitialRequest = !req._query.sid
if (isInitialRequest) {
if (this.opts.cookie) {
headers["Set-Cookie"] = [
// cookieMod.serialize(this.opts.cookie.name, id, this.opts.cookie)
]
}
this.emit("initial_headers", headers, req)
}
this.emit("headers", headers, req)
})
transport.onRequest(req)
this.clients[id] = socket
this.clientsCount++
socket.once("close", () => {
delete this.clients[id]
this.clientsCount--
})
this.emit("connection", socket)
}
// /**
// * Handles an Engine.IO HTTP Upgrade.
// *
// * @api public
// */
// handleUpgrade(req, socket, upgradeHead) {
// this.prepare(req)
// this.verify(req, true, (errorCode, errorContext) => {
// if (errorCode) {
// this.emit("connection_error", {
// req,
// code: errorCode,
// message: Server.errorMessages[errorCode],
// context: errorContext
// })
// abortUpgrade(socket, errorCode, errorContext)
// return
// }
// const head = Buffer.from(upgradeHead) // eslint-disable-line node/no-deprecated-api
// upgradeHead = null
// // delegate to ws
// this.ws.handleUpgrade(req, socket, head, websocket => {
// this.onWebSocket(req, socket, websocket)
// })
// })
// }
/**
* Called upon a ws.io connection.
*
* @param {ws.Socket} websocket
* @api private
*/
onWebSocket(req: Request, socket, websocket: WebSocketClient) {
websocket.on("error", onUpgradeError)
if (
transports[req._query.transport] !== undefined &&
!transports[req._query.transport].prototype.handlesUpgrades
) {
console.debug("transport doesnt handle upgraded requests")
websocket.close()
return
}
// get client id
const id = req._query.sid
// keep a reference to the ws.Socket
req.websocket = websocket
if (id) {
const client = this.clients[id]
if (!client) {
console.debug("upgrade attempt for closed client")
websocket.close()
} else if (client.upgrading) {
console.debug("transport has already been trying to upgrade")
websocket.close()
} else if (client.upgraded) {
console.debug("transport had already been upgraded")
websocket.close()
} else {
console.debug("upgrading existing transport")
// transport error handling takes over
websocket.removeListener("error", onUpgradeError)
const transport = new transports[req._query.transport](req)
if (req._query && req._query.b64) {
transport.supportsBinary = false
} else {
transport.supportsBinary = true
}
transport.perMessageDeflate = this.perMessageDeflate
client.maybeUpgrade(transport)
}
} else {
// transport error handling takes over
websocket.removeListener("error", onUpgradeError)
// const closeConnection = (errorCode, errorContext) =>
// abortUpgrade(socket, errorCode, errorContext)
this.handshake(req._query.transport, req, () => { })
}
function onUpgradeError() {
console.debug("websocket error before upgrade")
// websocket.close() not needed
}
}
/**
* Captures upgrade requests for a http.Server.
*
* @param {http.Server} server
* @param {Object} options
* @api public
*/
attach(server, options: any = {}) {
// let path = (options.path || "/engine.io").replace(/\/$/, "")
// const destroyUpgradeTimeout = options.destroyUpgradeTimeout || 1000
// // normalize path
// path += "/"
// function check(req) {
// return path === req.url.substr(0, path.length)
// }
// cache and clean up listeners
// const listeners = server.listeners("request").slice(0)
// server.removeAllListeners("request")
server.on("close", this.close.bind(this))
// server.on("listening", this.init.bind(this))
// @java-patch transfer to Netty Server
server.on("connect", (request: Request, websocket: WebSocketClient) => {
console.debug('Engine.IO connect client from', request.url)
this.prepare(request)
this.onWebSocket(request, undefined, websocket)
})
// set server as ws server
this.ws = server
// // add request handler
// server.on("request", (req, res) => {
// if (check(req)) {
// debug('intercepting request for path "%s"', path)
// this.handleRequest(req, res)
// } else {
// let i = 0
// const l = listeners.length
// for (; i < l; i++) {
// listeners[i].call(server, req, res)
// }
// }
// })
// if (~this.opts.transports.indexOf("websocket")) {
// server.on("upgrade", (req, socket, head) => {
// if (check(req)) {
// this.handleUpgrade(req, socket, head)
// } else if (false !== options.destroyUpgrade) {
// // default node behavior is to disconnect when no handlers
// // but by adding a handler, we prevent that
// // and if no eio thing handles the upgrade
// // then the socket needs to die!
// setTimeout(function () {
// if (socket.writable && socket.bytesWritten <= 0) {
// return socket.end()
// }
// }, destroyUpgradeTimeout)
// }
// })
// }
}
}
// /**
// * Close the HTTP long-polling request
// *
// * @param res - the response object
// * @param errorCode - the error code
// * @param errorContext - additional error context
// *
// * @api private
// */
// function abortRequest(res, errorCode, errorContext) {
// const statusCode = errorCode === Server.errors.FORBIDDEN ? 403 : 400
// const message =
// errorContext && errorContext.message
// ? errorContext.message
// : Server.errorMessages[errorCode]
// res.writeHead(statusCode, { "Content-Type": "application/json" })
// res.end(
// JSON.stringify({
// code: errorCode,
// message
// })
// )
// }
// /**
// * Close the WebSocket connection
// *
// * @param {net.Socket} socket
// * @param {string} errorCode - the error code
// * @param {object} errorContext - additional error context
// *
// * @api private
// */
// function abortUpgrade(socket, errorCode, errorContext: any = {}) {
// socket.on("error", () => {
// debug("ignoring error from closed connection")
// })
// if (socket.writable) {
// const message = errorContext.message || Server.errorMessages[errorCode]
// const length = Buffer.byteLength(message)
// socket.write(
// "HTTP/1.1 400 Bad Request\r\n" +
// "Connection: close\r\n" +
// "Content-type: text/html\r\n" +
// "Content-Length: " +
// length +
// "\r\n" +
// "\r\n" +
// message
// )
// }
// socket.destroy()
// }
// module.exports = Server
/* eslint-disable */
// /**
// * From https://github.com/nodejs/node/blob/v8.4.0/lib/_http_common.js#L303-L354
// *
// * True if val contains an invalid field-vchar
// * field-value = *( field-content / obs-fold )
// * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
// * field-vchar = VCHAR / obs-text
// *
// * checkInvalidHeaderChar() is currently designed to be inlinable by v8,
// * so take care when making changes to the implementation so that the source
// * code size does not exceed v8's default max_inlined_source_size setting.
// **/
// // prettier-ignore
// const validHdrChars = [
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15
// 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ...
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
// 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255
// ]
// function checkInvalidHeaderChar(val) {
// val += ""
// if (val.length < 1) return false
// if (!validHdrChars[val.charCodeAt(0)]) {
// debug('invalid header, index 0, char "%s"', val.charCodeAt(0))
// return true
// }
// if (val.length < 2) return false
// if (!validHdrChars[val.charCodeAt(1)]) {
// debug('invalid header, index 1, char "%s"', val.charCodeAt(1))
// return true
// }
// if (val.length < 3) return false
// if (!validHdrChars[val.charCodeAt(2)]) {
// debug('invalid header, index 2, char "%s"', val.charCodeAt(2))
// return true
// }
// if (val.length < 4) return false
// if (!validHdrChars[val.charCodeAt(3)]) {
// debug('invalid header, index 3, char "%s"', val.charCodeAt(3))
// return true
// }
// for (let i = 4; i < val.length; ++i) {
// if (!validHdrChars[val.charCodeAt(i)]) {
// debug('invalid header, index "%i", char "%s"', i, val.charCodeAt(i))
// return true
// }
// }
// return false
// }