680 lines
21 KiB
TypeScript
680 lines
21 KiB
TypeScript
import { EventEmitter } from 'events'
|
|
|
|
import { ServerEvent } from './constants'
|
|
import { Namespace } from './namespace'
|
|
import { Client } from './client'
|
|
import { Parser } from './parser'
|
|
import { PacketTypes, SubPacketTypes } from './types'
|
|
import { Packet } from './packet'
|
|
import { Socket } from './socket'
|
|
import { Adapter } from './adapter'
|
|
import { Transport } from '../transport'
|
|
import { ParentNamespace } from './parent-namespace'
|
|
|
|
interface EngineOptions {
|
|
/**
|
|
* how many ms without a pong packet to consider the connection closed
|
|
* @default 5000
|
|
*/
|
|
pingTimeout: number
|
|
/**
|
|
* how many ms before sending a new ping packet
|
|
* @default 25000
|
|
*/
|
|
pingInterval: number
|
|
/**
|
|
* how many ms before an uncompleted transport upgrade is cancelled
|
|
* @default 10000
|
|
*/
|
|
upgradeTimeout: number
|
|
/**
|
|
* how many bytes or characters a message can be, before closing the session (to avoid DoS).
|
|
* @default 1e5 (100 KB)
|
|
*/
|
|
maxHttpBufferSize: number
|
|
/**
|
|
* A function that receives a given handshake or upgrade request as its first parameter,
|
|
* and can decide whether to continue or not. The second argument is a function that needs
|
|
* to be called with the decided information: fn(err, success), where success is a boolean
|
|
* value where false means that the request is rejected, and err is an error code.
|
|
*/
|
|
// allowRequest: (
|
|
// req: http.IncomingMessage,
|
|
// fn: (err: string | null | undefined, success: boolean) => void
|
|
// ) => void
|
|
/**
|
|
* the low-level transports that are enabled
|
|
* @default ["polling", "websocket"]
|
|
*/
|
|
// transports: Transport[]
|
|
/**
|
|
* whether to allow transport upgrades
|
|
* @default true
|
|
*/
|
|
allowUpgrades: boolean
|
|
/**
|
|
* parameters of the WebSocket permessage-deflate extension (see ws module api docs). Set to false to disable.
|
|
* @default false
|
|
*/
|
|
perMessageDeflate: boolean | object
|
|
/**
|
|
* parameters of the http compression for the polling transports (see zlib api docs). Set to false to disable.
|
|
* @default true
|
|
*/
|
|
httpCompression: boolean | object
|
|
/**
|
|
* what WebSocket server implementation to use. Specified module must
|
|
* conform to the ws interface (see ws module api docs). Default value is ws.
|
|
* An alternative c++ addon is also available by installing uws module.
|
|
*/
|
|
wsEngine: string
|
|
/**
|
|
* an optional packet which will be concatenated to the handshake packet emitted by Engine.IO.
|
|
*/
|
|
initialPacket: any
|
|
/**
|
|
* configuration of the cookie that contains the client sid to send as part of handshake response headers. This cookie
|
|
* might be used for sticky-session. Defaults to not sending any cookie.
|
|
* @default false
|
|
*/
|
|
// cookie: CookieSerializeOptions | boolean
|
|
/**
|
|
* the options that will be forwarded to the cors module
|
|
*/
|
|
// cors: CorsOptions
|
|
}
|
|
|
|
interface AttachOptions {
|
|
/**
|
|
* name of the path to capture
|
|
* @default "/engine.io"
|
|
*/
|
|
path: string
|
|
/**
|
|
* destroy unhandled upgrade requests
|
|
* @default true
|
|
*/
|
|
destroyUpgrade: boolean
|
|
/**
|
|
* milliseconds after which unhandled requests are ended
|
|
* @default 1000
|
|
*/
|
|
destroyUpgradeTimeout: number
|
|
}
|
|
|
|
interface EngineAttachOptions extends EngineOptions, AttachOptions { }
|
|
|
|
interface ServerOptions extends EngineAttachOptions {
|
|
event?: EventEmitter
|
|
root?: string
|
|
/**
|
|
* name of the path to capture
|
|
* @default "/socket.io"
|
|
*/
|
|
path: string
|
|
/**
|
|
* whether to serve the client files
|
|
* @default true
|
|
*/
|
|
serveClient: boolean
|
|
/**
|
|
* the adapter to use
|
|
* @default the in-memory adapter (https://github.com/socketio/socket.io-adapter)
|
|
*/
|
|
adapter: any
|
|
/**
|
|
* the parser to use
|
|
* @default the default parser (https://github.com/socketio/socket.io-parser)
|
|
*/
|
|
parser: any
|
|
/**
|
|
* how many ms before a client without namespace is closed
|
|
* @default 45000
|
|
*/
|
|
connectTimeout: number
|
|
}
|
|
|
|
interface WebSocketServer extends EventEmitter {
|
|
close(): void
|
|
}
|
|
|
|
class Server {
|
|
public readonly sockets: Namespace
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_parser: Parser
|
|
private readonly encoder
|
|
|
|
/**
|
|
* @private
|
|
*/
|
|
_nsps: Map<string, Namespace>
|
|
private parentNsps: Map<
|
|
| string
|
|
| RegExp
|
|
| ((
|
|
name: string,
|
|
query: object,
|
|
fn: (err: Error, success: boolean) => void
|
|
) => void),
|
|
ParentNamespace
|
|
> = new Map();
|
|
private _adapter: Adapter
|
|
// private _serveClient: boolean;
|
|
private eio
|
|
private engine: { ws: any }
|
|
private _path: string
|
|
private clientPathRegex: RegExp
|
|
/**
|
|
* @private
|
|
*/
|
|
_connectTimeout: number
|
|
|
|
options: Partial<ServerOptions>
|
|
private websocketServer: WebSocketServer
|
|
private allClients: Map<string, Client>
|
|
|
|
constructor(instance: any, options: Partial<ServerOptions>) {
|
|
if (!instance) { throw new Error('instance can\'t be undefiend!') }
|
|
this.options = Object.assign({
|
|
event: new EventEmitter(),
|
|
path: '/socket.io',
|
|
root: root + '/wwwroot',
|
|
serveClient: false,
|
|
connectTimeout: 45000,
|
|
wsEngine: process.env.EIO_WS_ENGINE || "ws",
|
|
pingTimeout: 5000,
|
|
pingInterval: 25000,
|
|
upgradeTimeout: 10000,
|
|
maxHttpBufferSize: 1e6,
|
|
transports: 'websocket',
|
|
allowUpgrades: true,
|
|
httpCompression: {
|
|
threshold: 1024
|
|
},
|
|
cors: false
|
|
}, options)
|
|
this.initServerConfig()
|
|
this.sockets = this.of('/')
|
|
this.selectServerImpl(instance)
|
|
this.initServer()
|
|
}
|
|
/**
|
|
* Sets/gets whether client code is being served.
|
|
*
|
|
* @param {Boolean} v - whether to serve client code
|
|
* @return {Server|Boolean} self when setting or value when getting
|
|
* @public
|
|
*/
|
|
public serveClient(v: boolean): Server
|
|
public serveClient(): boolean
|
|
public serveClient(v?: boolean): Server | boolean {
|
|
throw new Error("Method not implemented.")
|
|
}
|
|
/**
|
|
* Executes the middleware for an incoming namespace not already created on the server.
|
|
*
|
|
* @param {String} name - name of incoming namespace
|
|
* @param {Object} auth - the auth parameters
|
|
* @param {Function} fn - callback
|
|
*
|
|
* @private
|
|
*/
|
|
_checkNamespace(
|
|
name: string,
|
|
auth: object,
|
|
fn: (nsp: Namespace) => void
|
|
) {
|
|
// if (this.parentNsps.size === 0) return fn(false)
|
|
|
|
// const keysIterator = this.parentNsps.keys()
|
|
|
|
// const run = () => {
|
|
// let nextFn = keysIterator.next()
|
|
// if (nextFn.done) {
|
|
// return fn(false)
|
|
// }
|
|
// nextFn.value(name, auth, (err, allow) => {
|
|
// if (err || !allow) {
|
|
// run()
|
|
// } else {
|
|
// fn(this.parentNsps.get(nextFn.value).createChild(name))
|
|
// }
|
|
// })
|
|
// }
|
|
fn(undefined)
|
|
}
|
|
/**
|
|
* Sets the client serving path.
|
|
*
|
|
* @param {String} v pathname
|
|
* @return {Server|String} self when setting or value when getting
|
|
* @public
|
|
*/
|
|
path(): string
|
|
path(v: string): Server
|
|
path(v?: any): string | Server {
|
|
if (!arguments.length) return this._path
|
|
|
|
this._path = v.replace(/\/$/, "")
|
|
|
|
const escapedPath = this._path.replace(/[-\/\\^$*+?.()|[\]{}]/g, "\\$&")
|
|
this.clientPathRegex = new RegExp(
|
|
"^" +
|
|
escapedPath +
|
|
"/socket\\.io(\\.min|\\.msgpack\\.min)?\\.js(\\.map)?$"
|
|
)
|
|
return this
|
|
}
|
|
/**
|
|
* Set the delay after which a client without namespace is closed
|
|
* @param v
|
|
* @public
|
|
*/
|
|
public connectTimeout(v: number): Server
|
|
public connectTimeout(): number
|
|
public connectTimeout(v?: number): Server | number {
|
|
if (v === undefined) return this._connectTimeout
|
|
this._connectTimeout = v
|
|
return this
|
|
}
|
|
/**
|
|
* Sets the adapter for rooms.
|
|
*
|
|
* @param {Adapter} v pathname
|
|
* @return {Server|Adapter} self when setting or value when getting
|
|
* @public
|
|
*/
|
|
public adapter(): any
|
|
public adapter(v: any)
|
|
public adapter(v?): Server | any {
|
|
if (!arguments.length) return this._adapter
|
|
this._adapter = v
|
|
for (const nsp of this._nsps.values()) {
|
|
nsp._initAdapter()
|
|
}
|
|
return this
|
|
}
|
|
// /**
|
|
// * Attaches socket.io to a server or port.
|
|
// *
|
|
// * @param {http.Server|Number} srv - server or port
|
|
// * @param {Object} opts - options passed to engine.io
|
|
// * @return {Server} self
|
|
// * @public
|
|
// */
|
|
// public listen(srv: http.Server, opts?: Partial<ServerOptions>): Server
|
|
// public listen(srv: number, opts?: Partial<ServerOptions>): Server
|
|
// public listen(srv: any, opts: Partial<ServerOptions> = {}): Server {
|
|
// return this.attach(srv, opts)
|
|
// }
|
|
|
|
// /**
|
|
// * Attaches socket.io to a server or port.
|
|
// *
|
|
// * @param {http.Server|Number} srv - server or port
|
|
// * @param {Object} opts - options passed to engine.io
|
|
// * @return {Server} self
|
|
// * @public
|
|
// */
|
|
// public attach(srv: http.Server, opts?: Partial<ServerOptions>): Server
|
|
// public attach(port: number, opts?: Partial<ServerOptions>): Server
|
|
// public attach(srv: any, opts: Partial<ServerOptions> = {}): Server {
|
|
// if ("function" == typeof srv) {
|
|
// const msg =
|
|
// "You are trying to attach socket.io to an express " +
|
|
// "request handler function. Please pass a http.Server instance."
|
|
// throw new Error(msg)
|
|
// }
|
|
|
|
// // handle a port as a string
|
|
// if (Number(srv) == srv) {
|
|
// srv = Number(srv)
|
|
// }
|
|
|
|
// if ("number" == typeof srv) {
|
|
// debug("creating http server and binding to %d", srv)
|
|
// const port = srv
|
|
// srv = http.createServer((req, res) => {
|
|
// res.writeHead(404)
|
|
// res.end()
|
|
// })
|
|
// srv.listen(port)
|
|
// }
|
|
|
|
// // set engine.io path to `/socket.io`
|
|
// opts.path = opts.path || this._path
|
|
|
|
// this.initEngine(srv, opts)
|
|
|
|
// return this
|
|
// }
|
|
// /**
|
|
// * Initialize engine
|
|
// *
|
|
// * @param srv - the server to attach to
|
|
// * @param opts - options passed to engine.io
|
|
// * @private
|
|
// */
|
|
// private initEngine(srv: http.Server, opts: Partial<EngineAttachOptions>) {
|
|
// // initialize engine
|
|
// debug("creating engine.io instance with opts %j", opts)
|
|
// this.eio = engine.attach(srv, opts)
|
|
|
|
// // attach static file serving
|
|
// if (this._serveClient) this.attachServe(srv)
|
|
|
|
// // Export http server
|
|
// this.httpServer = srv
|
|
|
|
// // bind to engine events
|
|
// this.bind(this.eio)
|
|
// }
|
|
|
|
// /**
|
|
// * Attaches the static file serving.
|
|
// *
|
|
// * @param {Function|http.Server} srv http server
|
|
// * @private
|
|
// */
|
|
// private attachServe(srv) {
|
|
// debug("attaching client serving req handler")
|
|
|
|
// const evs = srv.listeners("request").slice(0)
|
|
// srv.removeAllListeners("request")
|
|
// srv.on("request", (req, res) => {
|
|
// if (this.clientPathRegex.test(req.url)) {
|
|
// this.serve(req, res)
|
|
// } else {
|
|
// for (let i = 0; i < evs.length; i++) {
|
|
// evs[i].call(srv, req, res)
|
|
// }
|
|
// }
|
|
// })
|
|
// }
|
|
// /**
|
|
// * Handles a request serving of client source and map
|
|
// *
|
|
// * @param {http.IncomingMessage} req
|
|
// * @param {http.ServerResponse} res
|
|
// * @private
|
|
// */
|
|
// private serve(req: http.IncomingMessage, res: http.ServerResponse) {
|
|
// const filename = req.url.replace(this._path, "")
|
|
// const isMap = dotMapRegex.test(filename)
|
|
// const type = isMap ? "map" : "source"
|
|
|
|
// // Per the standard, ETags must be quoted:
|
|
// // https://tools.ietf.org/html/rfc7232#section-2.3
|
|
// const expectedEtag = '"' + clientVersion + '"'
|
|
|
|
// const etag = req.headers["if-none-match"]
|
|
// if (etag) {
|
|
// if (expectedEtag == etag) {
|
|
// debug("serve client %s 304", type)
|
|
// res.writeHead(304)
|
|
// res.end()
|
|
// return
|
|
// }
|
|
// }
|
|
|
|
// debug("serve client %s", type)
|
|
|
|
// res.setHeader("Cache-Control", "public, max-age=0")
|
|
// res.setHeader(
|
|
// "Content-Type",
|
|
// "application/" + (isMap ? "json" : "javascript")
|
|
// )
|
|
// res.setHeader("ETag", expectedEtag)
|
|
|
|
// if (!isMap) {
|
|
// res.setHeader("X-SourceMap", filename.substring(1) + ".map")
|
|
// }
|
|
// Server.sendFile(filename, req, res)
|
|
// }
|
|
|
|
// /**
|
|
// * @param filename
|
|
// * @param req
|
|
// * @param res
|
|
// * @private
|
|
// */
|
|
// private static sendFile(
|
|
// filename: string,
|
|
// req: http.IncomingMessage,
|
|
// res: http.ServerResponse
|
|
// ) {
|
|
// const readStream = createReadStream(
|
|
// path.join(__dirname, "../client-dist/", filename)
|
|
// )
|
|
// const encoding = accepts(req).encodings(["br", "gzip", "deflate"])
|
|
|
|
// const onError = err => {
|
|
// if (err) {
|
|
// res.end()
|
|
// }
|
|
// }
|
|
|
|
// switch (encoding) {
|
|
// case "br":
|
|
// res.writeHead(200, { "content-encoding": "br" })
|
|
// readStream.pipe(createBrotliCompress()).pipe(res)
|
|
// pipeline(readStream, createBrotliCompress(), res, onError)
|
|
// break
|
|
// case "gzip":
|
|
// res.writeHead(200, { "content-encoding": "gzip" })
|
|
// pipeline(readStream, createGzip(), res, onError)
|
|
// break
|
|
// case "deflate":
|
|
// res.writeHead(200, { "content-encoding": "deflate" })
|
|
// pipeline(readStream, createDeflate(), res, onError)
|
|
// break
|
|
// default:
|
|
// res.writeHead(200)
|
|
// pipeline(readStream, res, onError)
|
|
// }
|
|
// }
|
|
|
|
// /**
|
|
// * Binds socket.io to an engine.io instance.
|
|
// *
|
|
// * @param {engine.Server} engine engine.io (or compatible) server
|
|
// * @return {Server} self
|
|
// * @public
|
|
// */
|
|
// public bind(engine): Server {
|
|
// this.engine = engine
|
|
// this.engine.on("connection", this.onconnection.bind(this))
|
|
// return this
|
|
// }
|
|
/**
|
|
* Called with each incoming transport connection.
|
|
*
|
|
* @param {engine.Socket} conn
|
|
* @return {Server} self
|
|
* @private
|
|
*/
|
|
private onconnection(conn): Server {
|
|
console.debug(`incoming connection with id ${conn.id}`)
|
|
let client = new Client(this, conn)
|
|
this.allClients.set(conn.id, client)
|
|
return this
|
|
}
|
|
// of(nsp: string): Namespace {
|
|
// if (!this._nsps.has(nsp)) {
|
|
// console.debug(`create Namespace ${nsp}`)
|
|
// this._nsps.set(nsp, new Namespace(this, nsp))
|
|
// }
|
|
// return this._nsps.get(nsp)
|
|
// }
|
|
/**
|
|
* Looks up a namespace.
|
|
*
|
|
* @param {String|RegExp|Function} name nsp name
|
|
* @param {Function} [fn] optional, nsp `connection` ev handler
|
|
* @public
|
|
*/
|
|
public of(
|
|
name:
|
|
| string
|
|
| RegExp
|
|
| ((
|
|
name: string,
|
|
query: object,
|
|
fn: (err: Error, success: boolean) => void
|
|
) => void),
|
|
fn?: (socket: Socket) => void
|
|
) {
|
|
if (typeof name === "function" || name instanceof RegExp) {
|
|
const parentNsp = new ParentNamespace(this)
|
|
console.debug(`initializing parent namespace ${parentNsp.name}`)
|
|
if (typeof name === "function") {
|
|
this.parentNsps.set(name, parentNsp)
|
|
} else {
|
|
this.parentNsps.set(
|
|
(nsp, conn, next) => next(null, (name as RegExp).test(nsp)),
|
|
parentNsp
|
|
)
|
|
}
|
|
if (fn) {
|
|
// @ts-ignore
|
|
parentNsp.on("connect", fn)
|
|
}
|
|
return parentNsp
|
|
}
|
|
if (String(name)[0] !== "/") name = "/" + name
|
|
let nsp = this._nsps.get(name)
|
|
if (!nsp) {
|
|
console.debug(`initializing namespace ${name}`)
|
|
nsp = new Namespace(this, name)
|
|
this._nsps.set(name, nsp)
|
|
}
|
|
if (fn) nsp.on("connect", fn)
|
|
return nsp
|
|
}
|
|
close(fn?: () => void): void {
|
|
this.clients.length
|
|
for (const client of this.allClients.values()) {
|
|
client._disconnect()
|
|
}
|
|
|
|
// this.engine.close()
|
|
this.websocketServer.close()
|
|
|
|
// if (this.httpServer) {
|
|
// this.httpServer.close(fn)
|
|
// } else {
|
|
fn && fn()
|
|
// }
|
|
}
|
|
on(event: "connection", listener: (socket: Socket) => void): Namespace
|
|
on(event: "connect", listener: (socket: Socket) => void): Namespace
|
|
on(event: string, listener: Function): Namespace
|
|
on(event: any, listener: any): Namespace {
|
|
return this.sockets.on(event, listener)
|
|
}
|
|
to(room: string): Namespace {
|
|
return this.sockets.to(room)
|
|
}
|
|
in(room: string): Namespace {
|
|
return this.sockets.in(room)
|
|
}
|
|
use(fn: (socket: Socket, fn: (err?: any) => void) => void): Namespace {
|
|
return this.sockets.use(fn)
|
|
}
|
|
emit(event: string, ...args: any[]): Namespace {
|
|
// @ts-ignore
|
|
return this.sockets.emit(event, ...args)
|
|
}
|
|
send(...args: any[]): Namespace {
|
|
return this.sockets.send(...args)
|
|
}
|
|
write(...args: any[]): Namespace {
|
|
return this.sockets.write(...args)
|
|
}
|
|
clients(...args: any[]): Namespace {
|
|
return this.sockets.clients(args[0])
|
|
}
|
|
compress(...args: any[]): Namespace {
|
|
return this.sockets.compress(args[0])
|
|
}
|
|
// ===============================
|
|
private initServerConfig() {
|
|
this.allClients = new Map()
|
|
this._nsps = new Map()
|
|
this.connectTimeout(this.options.connectTimeout || 45000)
|
|
this._parser = this.options.parser || new Parser()
|
|
this.adapter(this.options.adapter || Adapter)
|
|
}
|
|
private selectServerImpl(instance: any) {
|
|
let WebSocketServerImpl = undefined
|
|
if (instance.class.name.startsWith('io.netty.channel')) {
|
|
WebSocketServerImpl = require("../netty").NettyWebSocketServer
|
|
} else {
|
|
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
|
|
}
|
|
this.websocketServer = new WebSocketServerImpl(instance, this.options)
|
|
}
|
|
private initServer() {
|
|
this.websocketServer.on(ServerEvent.connect, (transport: Transport) => {
|
|
this.onconnection(transport)
|
|
})
|
|
this.websocketServer.on(ServerEvent.message, (transport: Transport, text) => {
|
|
if (this.allClients.has(transport.id)) {
|
|
let client = this.allClients.get(transport.id)
|
|
client.onPacket(this._parser.decode(text))
|
|
} else {
|
|
console.error(`unknow transport ${transport.id} reciver message ${text}`)
|
|
}
|
|
})
|
|
this.websocketServer.on(ServerEvent.disconnect, (transport: Transport, reason) => {
|
|
if (this.allClients.has(transport.id)) {
|
|
this.allClients.get(transport.id).onclose(reason)
|
|
this.allClients.delete(transport.id)
|
|
} else {
|
|
console.error(`unknow transport ${transport?.id} disconnect cause ${reason}`)
|
|
}
|
|
})
|
|
this.websocketServer.on(ServerEvent.error, (transport: Transport, cause) => {
|
|
if (this.allClients.has(transport?.id)) {
|
|
let client = this.allClients.get(transport?.id)
|
|
if (client.listeners(ServerEvent.error).length) {
|
|
client.emit(ServerEvent.error, cause)
|
|
} else {
|
|
console.error(`client ${client.id} cause error: ${cause}`)
|
|
console.ex(cause)
|
|
}
|
|
} else {
|
|
console.error(`unknow transport ${transport?.id} cause error: ${cause}`)
|
|
console.ex(cause)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Expose main namespace (/).
|
|
*/
|
|
|
|
const emitterMethods = Object.keys(EventEmitter.prototype).filter(function (
|
|
key
|
|
) {
|
|
return typeof EventEmitter.prototype[key] === "function"
|
|
})
|
|
|
|
emitterMethods.forEach(function (fn) {
|
|
Server.prototype[fn] = function () {
|
|
return this.sockets[fn].apply(this.sockets, arguments)
|
|
}
|
|
})
|
|
|
|
export {
|
|
Server,
|
|
Socket,
|
|
Client,
|
|
Namespace,
|
|
ServerOptions
|
|
}
|