Compare commits

...

22 Commits

Author SHA1 Message Date
0f418f39df v0.16.3 2021-08-05 14:39:53 +08:00
75cb430230 feat: 优化WebSocket客户端 支持WSS
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-05 14:35:17 +08:00
6aedd8e680 v0.16.2 2021-08-03 17:59:49 +08:00
cd31f80805 feat: 更新插件管理模块
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-03 17:56:11 +08:00
08ba1c1a98 chore: 备份插件
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-03 17:31:06 +08:00
d5c2a825fc chore: 优化部分逻辑
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-03 17:30:53 +08:00
b36b63277f feat: 更新WebSocket 优化逻辑
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-08-03 16:59:43 +08:00
7d02194ac7 v0.16.1 2021-07-14 15:48:56 +08:00
ce4ad6f046 chore: 更新依赖版本
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-07-14 15:48:42 +08:00
24691a9ce8 feat: 修改默认超时时间
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-07-14 15:46:32 +08:00
9fa13f49cd v0.16.0 2021-07-10 18:28:43 +08:00
64a698089b chore: update depends 2021-07-10 18:28:22 +08:00
53843b65d2 feat: backup plugins
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-07-10 18:15:54 +08:00
5f00431e8b feat: 优化XHR相关功能
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-07-10 18:14:55 +08:00
c947ff7a14 v0.15.0 2021-06-22 15:58:29 +08:00
be2988fc58 backup: plugins
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-06-22 15:52:27 +08:00
3beed64319 feat: remove some debug output
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-06-22 15:51:50 +08:00
23c7cb955a feat: add openjdk nashorn shim
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-06-22 15:51:28 +08:00
3be1f78a14 refactor: optimize config & plugin load
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-06-22 15:51:13 +08:00
d919fa07fc feat: bukkit chat support 1.17
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-06-22 15:24:08 +08:00
d8fd7b0a7d feat: update depends
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-06-22 15:23:47 +08:00
907f9ed03f feat: add push script
Signed-off-by: MiaoWoo <admin@yumc.pw>
2021-03-27 15:29:22 +08:00
107 changed files with 7184 additions and 2686 deletions

View File

@@ -1,5 +1,5 @@
{
"version": "0.14.1",
"version": "0.16.3",
"useWorkspaces": true,
"npmClient": "yarn",
"packages": [

View File

@@ -12,7 +12,7 @@
"build": "lerna run build --scope=\"@ccms/!(plugins)\"",
"build:plugins": "lerna run build --scope=\"@ccms/plugins\"",
"ug": "yarn upgrade-interactive --latest",
"np": "lerna exec \"npm publish --access=public --registry https://registry.npmjs.org\" --scope=\"@ccms/!(client|plugins)\"",
"np": "./script/push.sh",
"lsp": "npm login --registry=https://registry.npmjs.org --scope=@ccms",
"lp": "lerna publish --registry https://registry.npmjs.org"
},

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/amqp",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript amqp package",
"keywords": [
"miaoscript",
@@ -19,17 +19,17 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"@javatypes/amqp-client": "^0.0.3",
"@javatypes/spring-amqp": "^0.0.3",
"@javatypes/spring-rabbit": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/api",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -19,9 +19,9 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0",
"@ccms/polyfill": "^0.14.1",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0",
"@ccms/polyfill": "^0.16.2",
"base64-js": "^1.5.1",
"source-map-builder": "^0.0.7"
},
@@ -29,6 +29,6 @@
"@types/base64-js": "^1.3.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -5,7 +5,7 @@ import * as base64 from 'base64-js'
const Arrays = Java.type('java.util.Arrays')
const Level = Java.type('java.util.logging.Level')
const Paths = Java.type('java.nio.file.Paths')
const ignoreLogPrefix = ['java.', 'javax.', 'sun.', 'net.minecraft.', 'org.bukkit.', 'jdk.nashorn.', 'io.netty.', 'org.spongepowered.', 'org.apache', 'org.springframework']
const ignoreLogPrefix = ['java.', 'javax.', 'sun.', 'net.minecraft.', 'org.bukkit.', 'jdk.nashorn.', 'org.openjdk.nashorn', 'io.netty.', 'org.spongepowered.', 'org.apache', 'org.springframework']
enum LogLevel {
ALL,
@@ -19,7 +19,7 @@ enum LogLevel {
}
export class MiaoScriptConsole implements Console {
Console: NodeJS.ConsoleConstructor
Console: any
memory: any
private static sourceMaps: { [key: string]: SourceMapBuilder } = {}
@@ -156,7 +156,7 @@ export class MiaoScriptConsole implements Console {
let className = trace.className
var fileName = trace.fileName as string
var lineNumber = trace.lineNumber
if (className.startsWith('jdk.nashorn.internal.scripts')) {
if (className.startsWith('jdk.nashorn.internal.scripts') || className.startsWith('org.openjdk.nashorn.internal.scripts')) {
className = className.substr(className.lastIndexOf('$') + 1)
var { fileName, lineNumber } = this.readSourceMap(fileName, lineNumber)
if (fileName.startsWith(root)) { fileName = fileName.split(root)[1] }
@@ -190,7 +190,7 @@ export class MiaoScriptConsole implements Console {
countReset(label?: string): void {
throw new Error("Method not implemented.")
}
dir(obj: any, options?: NodeJS.InspectOptions): void {
dir(obj: any, options?: any): void {
throw new Error("Method not implemented.")
}
dirxml(...data: any[]): void {

View File

@@ -16,15 +16,28 @@ export namespace server {
* Runtime Server Instance
*/
export const ServerInstance = Symbol("ServerInstance")
export interface NativePlugin {
name: string
version: string
authors?: string | string[]
enable: boolean
depends?: string[]
softDepends?: string[]
/**
* 插件本体
*/
origin: any
[key: string]: any
}
@injectable()
export abstract class NativePluginManager {
list(): any[] {
list(): NativePlugin[] {
throw new Error("Method not implemented.")
}
has(name: string): boolean {
return true
}
get(name: string): any {
get(name: string): NativePlugin {
throw new Error("Method not implemented.")
}
load(name: string): boolean {

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/bukkit",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript bukkit package",
"keywords": [
"miaoscript",
@@ -22,11 +22,11 @@
"@javatypes/spigot-api": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
}
}

View File

@@ -15,6 +15,7 @@ let downgrade = false
* 获取NMS版本
*/
let nmsVersion = undefined
let nmsSubVersion = undefined
/**
* 获取NMS类
*/
@@ -49,15 +50,23 @@ function remapFieldName(clazz: any, origin: string, test: string) {
function init() {
//@ts-ignore
nmsVersion = org.bukkit.Bukkit.server.class.name.split('.')[3]
nmsSubVersion = nmsVersion.split("_")[1]
try {
RemapUtils = Java.type('catserver.server.remapper.RemapUtils')
} catch (ex) {
}
let nmsChatSerializerClass = nmsCls(nmsVersion.split("_")[1] > 7 ? "IChatBaseComponent$ChatSerializer" : "ChatSerializer")
let nmsChatSerializerClass = undefined
if (nmsSubVersion < 8) {
nmsChatSerializerClass = nmsCls("ChatSerializer")
} else if (nmsSubVersion < 17) {
nmsChatSerializerClass = nmsCls("IChatBaseComponent$ChatSerializer")
} else {
nmsChatSerializerClass = base.getClass('net.minecraft.network.chat.IChatBaseComponent$ChatSerializer')
}
let nmsChatSerializerMethod = remapMethod(nmsChatSerializerClass, 'a', 'func_150699_a', base.getClass('java.lang.String'))
nmsChatSerializerMethodName = nmsChatSerializerMethod.getName()
ChatSerializer = Java.type(nmsChatSerializerClass.getName())
let packetTypeClass = nmsCls("PacketPlayOutChat")
let packetTypeClass = nmsSubVersion < 17 ? nmsCls("PacketPlayOutChat") : base.getClass('net.minecraft.network.protocol.game.PacketPlayOutChat')
PacketPlayOutChat = Java.type(packetTypeClass.getName())
let packetTypeConstructor: { parameterTypes: any[] }
let constructors = packetTypeClass.constructors
@@ -75,9 +84,14 @@ function init() {
if (nmsChatMessageTypeClass.isEnum()) {
chatMessageTypes = nmsChatMessageTypeClass.getEnumConstants()
}
let playerConnectionField = remapFieldName(nmsCls('EntityPlayer'), 'playerConnection', 'field_71135_a')
let playerConnectionField = undefined
if (nmsSubVersion < 17) {
playerConnectionField = remapFieldName(nmsCls('EntityPlayer'), 'playerConnection', 'field_71135_a')
} else {
playerConnectionField = base.getClass('net.minecraft.server.level.EntityPlayer').getField('b')
}
playerConnectionFieldName = playerConnectionField.getName()
sendPacketMethodName = remapMethod(playerConnectionField.getType(), 'sendPacket', 'func_179290_a', nmsCls('Packet')).getName()
sendPacketMethodName = remapMethod(playerConnectionField.getType(), 'sendPacket', 'func_179290_a', nmsSubVersion < 17 ? nmsCls('Packet') : base.getClass('net.minecraft.network.protocol.Packet')).getName()
}
function json(sender: { name: string }, json: string) {

View File

@@ -1,12 +1,36 @@
import { server } from '@ccms/api'
const Bukkit = org.bukkit.Bukkit
const Bukkit: typeof org.bukkit.Bukkit = Java.type('org.bukkit.Bukkit')
export class BukkitNativePluginManager extends server.NativePluginManager {
has(name: string) {
return !!this.get(name)
private bukkitPluginManager: org.bukkit.plugin.PluginManager
constructor() {
super()
this.bukkitPluginManager = Bukkit.getPluginManager()
}
get(name: string) {
return Bukkit.getPluginManager().getPlugin(name)
list(): server.NativePlugin[] {
return Java.from(this.bukkitPluginManager.getPlugins()).map(plugin => this.convert(plugin))
}
has(name: string): boolean {
return !!this.bukkitPluginManager.getPlugin(name)
}
get(name: string): server.NativePlugin {
return this.convert(this.bukkitPluginManager.getPlugin(name))
}
private convert(plugin: org.bukkit.plugin.Plugin): server.NativePlugin {
if (!plugin) return plugin as any
let desc = plugin.getDescription()
return {
name: plugin.getName(),
version: desc.getVersion(),
authors: Java.from(desc.getAuthors()),
depends: Java.from(desc.getDepend()),
softDepends: Java.from(desc.getSoftDepend()),
enable: plugin.isEnabled(),
origin: plugin
}
}
}

View File

@@ -21,11 +21,11 @@ export class BukkitTaskManager extends task.TaskManager {
export class BukkitTask extends task.Task {
submit0(...args: any[]): task.Cancelable {
let run = new BukkitRunnable({ run: () => this.run(...args) })
let funcName = `runTask${this.interval ? 'Timer' : 'Later'}${this.isAsync ? 'Asynchronously' : ''}`
let suffix = this.isAsync ? 'Asynchronously' : ''
if (this.interval) {
return run[funcName](base.getInstance(), this.laterTime, this.interval)
return run[`runTaskTimer${suffix}`](base.getInstance(), this.laterTime, this.interval)
} else {
return run[funcName](base.getInstance(), this.laterTime)
return run[`runTaskLater${suffix}`](base.getInstance(), this.laterTime)
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/bungee",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript bungee package",
"keywords": [
"miaoscript",
@@ -22,11 +22,11 @@
"@javatypes/bungee-api": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
}
}

View File

@@ -3,10 +3,34 @@ import { server } from '@ccms/api'
let Bungee: net.md_5.bungee.api.ProxyServer = base.getInstance().getProxy()
export class BungeeNativePluginManager extends server.NativePluginManager {
has(name: string) {
return !!this.get(name)
private bungeePluginManager: net.md_5.bungee.api.plugin.PluginManager
constructor() {
super()
this.bungeePluginManager = Bungee.getPluginManager()
}
get(name: string) {
return Bungee.getPluginManager().getPlugin(name)
list(): server.NativePlugin[] {
return Java.from(this.bungeePluginManager.getPlugins()).map(plugin => this.convert(plugin))
}
has(name: string): boolean {
return !!this.bungeePluginManager.getPlugin(name)
}
get(name: string): server.NativePlugin {
return this.convert(this.bungeePluginManager.getPlugin(name))
}
private convert(plugin: net.md_5.bungee.api.plugin.Plugin): server.NativePlugin {
if (!plugin) return plugin as any
let desc = plugin.getDescription()
return {
name: desc.getName(),
version: desc.getVersion(),
authors: [desc.getAuthor()],
depends: Java.from(desc.getDepends()) as any,
softDepends: Java.from(desc.getSoftDepends()) as any,
enable: true,
origin: plugin
}
}
}

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@ccms/client",
"version": "0.14.1",
"version": "0.16.1",
"description": "MiaoScript client package",
"keywords": [
"miaoscript",
@@ -22,11 +22,11 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"minecraft-protocol": "^1.24.1"
"minecraft-protocol": "^1.25.0"
},
"devDependencies": {
"@types/node": "^14.14.37",
"@types/node": "^16.3.2",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/common",
"version": "0.14.1",
"version": "0.16.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -19,11 +19,11 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"@javatypes/jdk": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"gitHead": "562e2d00175c9d3a99c8b672aa07e6d92706a027"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/compile",
"version": "0.14.0",
"version": "0.16.0",
"description": "MiaoScript compile package",
"keywords": [
"miaoscript",
@@ -21,6 +21,6 @@
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/container",
"version": "0.14.0",
"version": "0.16.0",
"description": "MiaoScript container package",
"keywords": [
"miaoscript",
@@ -19,13 +19,13 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"inversify": "^5.0.5",
"inversify": "^5.1.1",
"inversify-binding-decorators": "^4.0.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/core",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -21,11 +21,11 @@
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0"
},
"gitHead": "781524f83e52cad26d7c480513e3c525df867121"
}

View File

@@ -1,7 +1,7 @@
let containerStartTime = Date.now()
console.i18n("ms.core.ioc.initialize", { scope: global.scope })
import { plugin, server, task, constants } from '@ccms/api'
import { DefaultContainer as container, inject, provideSingleton, ContainerInstance, buildProviderModule, Autowired } from '@ccms/container'
import { DefaultContainer as container, provideSingleton, ContainerInstance, buildProviderModule, Autowired } from '@ccms/container'
console.i18n("ms.core.ioc.completed", { scope: global.scope, time: (Date.now() - containerStartTime) / 1000 })
import http from '@ccms/common/dist/http'

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/database",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript database package",
"keywords": [
"miaoscript",
@@ -22,10 +22,10 @@
"@javatypes/spring-jdbc": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/i18n",
"version": "0.14.1",
"version": "0.16.0",
"description": "MiaoScript i18n package",
"keywords": [
"miaoscript",
@@ -19,14 +19,14 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@types/js-yaml": "^4.0.0",
"@ccms/nashorn": "^0.16.0",
"@types/js-yaml": "^4.0.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"js-yaml": "^4.0.0"
"js-yaml": "^4.1.0"
},
"gitHead": "781524f83e52cad26d7c480513e3c525df867121"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/keyvalue",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript keyvalue package",
"keywords": [
"miaoscript",
@@ -19,18 +19,18 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"@javatypes/amqp-client": "^0.0.3",
"@javatypes/spring-amqp": "^0.0.3",
"@javatypes/spring-rabbit": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"gitHead": "2589633069d24f646ac09261b1b2304c21d4ea75"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/nashorn",
"version": "0.14.0",
"version": "0.16.0",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -22,6 +22,6 @@
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/nodejs",
"version": "0.14.0",
"version": "0.16.0",
"description": "MiaoScript nodejs package",
"keywords": [
"miaoscript",
@@ -19,10 +19,11 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"tslib": "^2.3.0",
"typescript": "^4.3.5"
},
"gitHead": "781524f83e52cad26d7c480513e3c525df867121"
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/nukkit",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript nukkit package",
"keywords": [
"miaoscript",
@@ -22,11 +22,11 @@
"@javatypes/nukkit-api": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
}
}

View File

@@ -3,10 +3,34 @@ import { server } from '@ccms/api'
let Nukkit: cn.nukkit.Server = base.getInstance().getServer()
export class NukkitNativePluginManager extends server.NativePluginManager {
private nukkitPluginManager: cn.nukkit.plugin.PluginManager
constructor() {
super()
this.nukkitPluginManager = Nukkit.getPluginManager()
}
list(): server.NativePlugin[] {
return Java.from(this.nukkitPluginManager.getPlugins().values()).map(plugin => this.convert(plugin))
}
has(name: string) {
return !!this.get(name)
return !!this.nukkitPluginManager.getPlugin(name)
}
get(name: string) {
return Nukkit.getPluginManager().getPlugin(name)
return this.convert(this.nukkitPluginManager.getPlugin(name))
}
private convert(plugin: cn.nukkit.plugin.Plugin): server.NativePlugin {
if (!plugin) return plugin as any
let desc = plugin.getDescription()
return {
name: plugin.getName(),
version: desc.getVersion(),
authors: Java.from(desc.getAuthors() as string[]),
depends: Java.from(desc.getDepend() as string[]),
softDepends: Java.from(desc.getSoftDepend() as string[]),
enable: plugin.isEnabled(),
origin: plugin
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/plugin",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -19,16 +19,17 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@types/js-yaml": "^4.0.0",
"@types/js-yaml": "^4.0.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0",
"@ccms/i18n": "^0.14.1",
"js-yaml": "^4.0.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0",
"@ccms/i18n": "^0.16.0",
"js-yaml": "^4.1.0",
"yaml": "^1.10.2"
}
}

View File

@@ -47,8 +47,8 @@ export class PluginCommandManager {
let subcommand = args[0] || 'help'
let cmdKey = 'cmd' + subcommand
if (!pluginInstance[cmdKey]) {
console.sender(sender, '§4未知的子命令: §c' + subcommand)
pluginInstance['cmdhelp'] && console.sender(sender, `§6请执行 §b/${command} §ahelp §6查看帮助!`)
pluginInstance.logger.sender(sender, '§4未知的子命令: §c' + subcommand)
pluginInstance['cmdhelp'] && pluginInstance.logger.sender(sender, `§6请执行 §b/${command} §ahelp §6查看帮助!`)
return
}
args.shift()

View File

@@ -85,25 +85,27 @@ export class PluginConfigManager {
try {
metadata.file = fs.concat(fs.file(plugin.description.loadMetadata.file).parent, plugin.description.name, metadata.filename)
let configLoader = this.getConfigLoader(metadata.format)
let value = plugin[metadata.variable]
let defaultValue = metadata.default ?? plugin[metadata.variable]
let configValue = defaultValue
if (!fs.exists(metadata.file)) {
base.save(metadata.file, configLoader.dump(value))
base.save(metadata.file, configLoader.dump(defaultValue))
console.i18n("ms.plugin.manager.config.save.default", { plugin: plugin.description.name, name: metadata.name, format: metadata.format })
} else {
value = configLoader.load(base.read(metadata.file))
console.debug(`[${plugin.description.name}] Load Config ${metadata.variable} from file ${metadata.file} =>\n${JSON.stringify(value, undefined, 4)}`)
if (metadata.default) {
configValue = configLoader.load(base.read(metadata.file)) || {}
if (defaultValue) {
let needSave = false
for (const key of Object.keys(metadata.default)) {
if (value[key] == undefined) {
value[key] = metadata.default[key]
for (const key of Object.keys(defaultValue)) {
// 当配置文件不存在当前属性时才进行赋值
if (!Object.prototype.hasOwnProperty.call(configValue, key)) {
configValue[key] = defaultValue[key]
needSave = true
}
}
needSave && base.save(metadata.file, configLoader.dump(value))
needSave && base.save(metadata.file, configLoader.dump(configValue))
}
console.debug(`[${plugin.description.name}] Load Config ${metadata.variable} from file ${metadata.file} =>\n${JSON.stringify(configValue, undefined, 4)}`)
}
this.defienConfigProp(plugin, metadata, value)
this.defienConfigProp(plugin, metadata, configValue)
} catch (error) {
console.i18n("ms.plugin.manager.config.load.error", { plugin: plugin.description.name, name: metadata.name, format: metadata.format, error })
console.ex(error)

View File

@@ -8,9 +8,8 @@ import { getPluginMetadatas, getPluginCommandMetadata, getPluginListenerMetadata
* MiaoScript plugin
* @param metadata PluginMetadata
*/
export function plugin(metadata: pluginApi.PluginMetadata | any) {
export function plugin(metadata: pluginApi.PluginMetadata) {
return function (target: any) {
if (!metadata.source) metadata = { souece: metadata }
metadata = { name: target.name, version: '1.0.0', author: 'Unknow', target, type: 'ioc', ...metadata }
decorate(injectable(), target)
Reflect.defineMetadata(METADATA_KEY.plugin, metadata, target)

View File

@@ -41,6 +41,10 @@ export class PluginManagerImpl implements plugin.PluginManager {
private instanceMap: Map<string, plugin.Plugin>
private metadataMap: Map<string, plugin.PluginMetadata>
/**
* 延时加载插件
*/
private lazyMetadataMap: Map<string, plugin.PluginMetadata>
constructor() {
this.sacnnerMap = new Map()
@@ -48,6 +52,7 @@ export class PluginManagerImpl implements plugin.PluginManager {
this.instanceMap = new Map()
this.metadataMap = new Map()
this.lazyMetadataMap = new Map()
// ignore unused
this.taskManager
@@ -239,14 +244,14 @@ export class PluginManagerImpl implements plugin.PluginManager {
}
private buildPlugins() {
this.metadataMap.forEach((metadata) => {
try {
this.metadataMap.forEach((metadata, key) => {
if (metadata?.depends?.length) {
this.lazyMetadataMap.set(key, metadata)
} else {
this.buildPlugin(metadata)
} catch (error) {
console.console(`§4无法加载插件 §b${metadata.name} §4构建插件失败!`)
console.ex(error)
}
})
this.lazyMetadataMap.forEach((metadata, key) => this.buildPlugin(metadata))
}
private checkDepends(depends: string | string[]) {
@@ -262,16 +267,21 @@ export class PluginManagerImpl implements plugin.PluginManager {
return loseDepends
}
private buildPlugin(metadata: plugin.PluginMetadata) {
if (this.instanceMap.has(metadata.name)) { throw new Error(`Plugin ${metadata.name} is already load from ${metadata.source}...`) }
if (!this.loaderMap.has(metadata.type)) { throw new Error(`§4无法加载插件 §b${metadata.name} §4请检查 §c${metadata.type} §4加载器是否正常启用!`) }
if (!this.serverChecker.check(metadata.servers)) { throw new Error(`§6插件 §b${metadata.name} §c服务器类型不兼容(${metadata.servers.join(',')}) §6忽略加载...`) }
let loseDepends = this.checkDepends(metadata.depends) || []
if (loseDepends.length) { throw new Error(`§4无法加载插件 §b${metadata.name} §4请检查依赖 §3[${loseDepends.join(',')}] §4是否安装完整!`) }
let loseNativeDepends = this.checkNativeDepends(metadata.nativeDepends) || []
if (loseNativeDepends.length) { throw new Error(`§4无法加载插件 §b${metadata.name} §4请检查插件依赖 §3[${loseNativeDepends.join(',')}] §4是否安装完整!`) }
let pluginInstance = this.loaderMap.get(metadata.type).build(metadata)
if (!pluginInstance) { throw new Error(`§4加载器 §c${metadata.type} §4加载插件 §c${metadata.name} §4失败!`) }
this.instanceMap.set(metadata.name, pluginInstance)
return pluginInstance
try {
if (this.instanceMap.has(metadata.name)) { throw new Error(`Plugin ${metadata.name} is already load from ${metadata.source}...`) }
if (!this.loaderMap.has(metadata.type)) { throw new Error(`§4无法加载插件 §b${metadata.name} §4请检查 §c${metadata.type} §4加载器是否正常启用!`) }
if (!this.serverChecker.check(metadata.servers)) { throw new Error(`§6插件 §b${metadata.name} §c服务器类型不兼容(${metadata.servers.join(',')}) §6忽略加载...`) }
let loseDepends = this.checkDepends(metadata.depends) || []
if (loseDepends.length) { throw new Error(`§4无法加载插件 §b${metadata.name} §4请检查脚本依赖 §3[${loseDepends.join(',')}] §4是否安装完整!`) }
let loseNativeDepends = this.checkNativeDepends(metadata.nativeDepends) || []
if (loseNativeDepends.length) { throw new Error(`§4无法加载插件 §b${metadata.name} §4请检查插件依赖 §3[${loseNativeDepends.join(',')}] §4是否安装完整!`) }
let pluginInstance = this.loaderMap.get(metadata.type).build(metadata)
if (!pluginInstance) { throw new Error(`§4加载器 §c${metadata.type} §4加载插件 §c${metadata.name} §4失败!`) }
this.instanceMap.set(metadata.name, pluginInstance)
return pluginInstance
} catch (error) {
console.console(`§4无法加载插件 §b${metadata.name} §4构建插件失败!`)
console.ex(error)
}
}
}

View File

@@ -19,7 +19,7 @@ export class JSFileScanner implements plugin.PluginScanner {
load(metadata: plugin.PluginLoadMetadata): plugin.PluginLoadMetadata {
if (metadata.type !== this.type) { return }
this.updatePlugin(metadata.file)
//@ts-ignore
//@ts-ignore load plugin not use cache
metadata.instance = require(metadata.file.toString(), { cache: false })
return metadata
}

View File

@@ -0,0 +1,76 @@
# MiaoLobby
## 插件简介
- 用户进入服务器/用户登录后 自动选择大厅进行传送 防止堆积在登录服
- 支持配置 是否登录后自动分配 或 玩家手动执行随机分配
- 支持通过 ActionBar 展示传送状态
### 插件截图
- ![1.png](https://i.loli.net/2021/04/01/3JrtjupKHzAegwm.png)
- ![2.png](https://i.loli.net/2021/04/01/CgTRIHaUbX7uKJ1.png)
### 插件配置
```yaml
#配置文件版本 请勿修改
Version: 1.5
#服务器列表
Servers:
- lobby1
- lobby2
#传送超时时间(单位: Tick)
WaitTime: 35
#自带传送(如果开启 则Login自动传送失效)
AutoTP: false
#登录自动传送(暂时支持AuthMe)
LoginAutoTP: true
#传送延时(单位: 秒)
AutoTPDelay: 10
#尝试完毕后是否继续重试
ReTry: true
#传送提示
Message: '&a请稍候 正在传送至服务器 %s ...'
TimeOut: '&c传送超时 正在切换到服务器 %s ...'
TPDelay: '&a登陆成功 正在为您匹配服务器 剩余 %s 秒...'
Unavailable: '&4已尝试所有可用服务器 传送失败!'
```
### 插件命令
```
插件注册命令:
- MiaoLobby
别名: ml
描述: MiaoLobby - Minecraft 服务器插件父项目
权限: MiaoLobby.reload
用法: 使用/MiaoLobby help 查看帮助!
```
### 插件权限
```
插件注册权限:
- MiaoLobby.default - MiaoLobby 默认权限!
- MiaoLobby.admin - MiaoLobby 管理员权限!
- MiaoLobby.reload - 重新载入插件!
```
### 插件下载
[attach]1802025[/attach]
### Miao系列插件
- [[经济]MiaoReward —— 喵式奖励 让玩家看广告为服务器提供收入吧[1.7.10+全版本]](https://www.mcbbs.net/thread-1121423-1-1.html)
- [[编程]MiaoBlockly —— 喵式积木 用简单的积木来写插件吧[1.12.2+全版本]](https://www.mcbbs.net/thread-1129411-1-1.html)
- [[编程]MiaoConsole —— 喵式终端 通过MC端口直接控制服务器 调试插件[1.12.2+全版本]](https://www.mcbbs.net/thread-1129227-1-1.html)
- [[管理]MiaoBind —— 喵式绑定 兼容SoulBound的绑定插件 支持自定义关键词[1.7+全版本]](https://www.mcbbs.net/thread-922072-1-1.html)
- [[信息]MiaoBoard —— 喵式记分板 自定义动态记分板[1.7+全版本]](https://www.mcbbs.net/thread-631482-1-1.html)
- [[聊天]MiaoChat —— 喵式聊天 多功能自定义聊天格式 新增支持跨服[1.7.10+全版本]](https://www.mcbbs.net/thread-631240-1-1.html)
- [[菜单]MiaoMenu —— 喵式菜单 强大的自定义菜单 支持多种自定义操作[1.7+全版本]](https://www.mcbbs.net/thread-860047-1-1.html)
- [[管理]YUM —— 全能的服务器插件管理工具 全自动安装插件 更新插件[1.7.2+全版本]](https://www.mcbbs.net/thread-701333-1-1.html)
#### 本插件所用所有代码均为原创,不存在借用/抄袭等行为

View File

@@ -0,0 +1,22 @@
[综合|前置]MiaoNashorn —— 喵式犀牛引擎 用于 Java14+ 自动安装脚本引擎[全版本]
# MiaoNashorn
## 插件介绍
> 在Java14+环境下缺少Nashorn 本插件用于自动下载并且加载Nashorn依赖
> 自动从云端下载依赖 插件仅 8kb 大小
## 可用于下列插件 在 Java14+ 环境运行
- PlaceholderAPI 的 Javascript 扩展
- MiaoMenu
- MiaoScript
- TrMenu
- AttributePlus
- 等其他任何需要 Nashorn 引擎的插件
## 下载地址
[attach]1834431[/attach]
#### 本插件所用所有代码均为原创,不存在借用/抄袭等行为

View File

@@ -1,14 +1,57 @@
# MiaoPay
## 安装方式
先按照帖子
先按照帖子
### 网关
- https://pay.yumc.pw/api
### 请求规范
- 除业务参数外 每个请求必须包含下列系统参数
- 系统级参数
- 应用 ID `appid`
- 时间戳 `timestamp` 单位: 秒
- 随机字符串 `nonce` 32 位以内的随机字符串
#### 签名生成
- 对参数按照字典升序排列
- 拼接成查询字符串后追加 key=secret
- 获得字符串的 MD5 值 并且转换成大写
```php
ksort($data);
$signStr = urldecode(http_build_query($data)).'&key='.\getAppSecret();
return strtoupper(md5($signStr));
```
### 相关接口
#### 创建订单
- METHOD: /create
- PARAM:
-
- PARAM:
- 订单标题 `subject` 必填
- 订单金额 `amount` 必填 单位: 元
- 用户名 `username` 选填
- 用户唯一 ID `unionId` 选填
- 外部订单 ID `outOrderId` 选填 用于三方系统
- 通知地址 `notifyUrl` 选填 用于三方系统回调
- RETURN:
- 订单ID `order_id`
- 订单金额 `amount`
- 订单支付地址 `url`
#### 查询订单
- METHOD: /query
- PARAM:
- 订单ID `subject` 必填
- 订单金额 `amount` 必填 单位: 元
- 用户名 `username` 选填
- 用户唯一 ID `uuid` 选填
- RETURN:
- 订单数据

View File

@@ -0,0 +1,37 @@
[综合|信息]MiaoRGBSupport —— 喵式RGB 支持任意插件的RGB展示[1.16+]
# MiaoRGBSupport
## 插件简介
- 支持1.16+彩色字体展示
- 兼容原版聊天以及任意聊天插件
- 兼容任意记分板插件
- 仅需安装插件配置权限即可生效
## 插件权限
- `MiaoRGBSupport.color` 默认玩家没权限 需要手动添加
## 安装方式
- 本插件基于 MiaoScript 开发
- 请先安装 MiaoScript [坛内地址](https://www.mcbbs.net/thread-774401-1-1.html)
- 然后安装 ProtocolLib 自己解决
- 执行 `/mspm install MiaoChatRGBSupport`
## 使用方式
- 颜色格式 `#FFFFFF` 标准 HTML 的色彩格式
### 聊天插件配置
- 例如 配置 `MiaoChat``format.yml`
- ![image.png](https://i.loli.net/2021/06/24/1DiupnoPjaI7tZr.png)
- 配置完成后 重载插件
### 聊天时使用
- 聊天时 直接输入颜色代码即可
- ![image.png](https://i.loli.net/2021/06/24/2dnTlyv6oZrcBzg.png)
## 星球特供版本
- ![image.png](https://i.loli.net/2021/06/24/DOmhJRflxS3qwju.png)
- 此版本为知识星球特供
- 支持 聊天和记分板
- 支持 彩虹字
- 详情加群 650545561

View File

@@ -0,0 +1,114 @@
# MiaoRebate
## 插件简介
- 还在为服务器收入不足而倒闭烦恼嘛
- 还在为肝帝不氪金而烦恼嘛
- 快来接入 喵式返利
- 饿了么美团战略合作 玩家点外卖 腐竹拿返利 增加服务器收入
### 先来一张 1 块钱吃一餐的图
![image.png](https://i.loli.net/2021/06/08/PpZVWLwTBNuravY.png)
### 再来一张红包兑换的图
![image.png](https://i.loli.net/2021/06/07/GmCNXl2pyVnxYt5.png)
### 限时活动
> 即日起 至 7 月 15 日 额外奖励活动
> 每满 50 人关注公众号 额外奖励 50 元 上不封顶 (当月取消关注不算)
> 请绑定后加 QQ 群 1055983539 参加活动
## 插件展示
> 多图预警 折叠了
`[spoiler]`
- 命令帮助![image.png](https://i.loli.net/2021/06/07/PGse8CSFxnIR3D5.png)
- 扫码绑定![image.png](https://i.loli.net/2021/06/07/dwNXloU62zcTVkQ.png)
- 个人信息![image.jpg](https://i.loli.net/2021/06/07/8r9xQTnFqjV7ahZ.jpg)
- 兑换列表![image.jpg](https://i.loli.net/2021/06/07/FcjC1qdWMrB3vsR.jpg)
`[/spoiler]`
## 插件命令
```
>mre help
[外卖系统]====== [外卖系统] 帮助菜单 ======
[外卖系统]/mre bind 绑定账号
[外卖系统]/mre draw <兑换数量> 兑换点券
[外卖系统]由于您是管理员 以为您展示额外命令
[外卖系统]/mrd bind server 绑定服务器
```
## 接下来就是赚钱的操作
### 服务器准备工作
- 本插件依赖于 `MiaoReward` 请前往 [站内帖子](https://www.mcbbs.net/thread-1121423-1-1.html) 完成安装
- 执行 `/mspm install MiaoRebate` 安装 MiaoRebate 脚本插件
- 完成安装
### 绑定服务器
- 执行 `/mre bind server`
- 使用绿色儿的那个 APP 扫码 完成绑定
### 玩家绑定账号
- 执行 `/mre bind`
- 使用绿色儿的那个 APP 扫码 完成绑定
## 使用说明
- 玩家可以通过下列方式获取圈币
- 进入公众号 领取红包
- 小程序直接点餐或到饿了么/美团 APP 点餐
- 点餐后 发送订单号 兑换奖励
- 返利额度约为实付金额的 `1%-3%` 左右
- 获得的圈币 在服务器使用 `/mre draw 兑换金额`
- 腐竹获得圈币后 在公众号兑换成红包即可
## PAPI 兼容
- 目前暂不支持 PAPI 变量 后续会支持
## 配置文件
```yml
# 提示前缀
prefix: §6[§b外卖系统§6]§r
# 用于检查货币的变量
check: "%playerpoints_points%"
# 用于充值货币的命令
command: points give %player_name% %amount%
# 兑换比例 圈币 对应多少 货币
ratio: 1
# 货币名称
coinName: 点券
# 进服提示
joinTip: true
# 绑定数据(请勿手动修改 绑定后会自动填写数据)
owner:
userid:
ccid:
openid:
```
## 插件源码
- [MiaoScript 包管理中心](https://git.yumc.pw/circlecloud/ms/src/branch/master/packages/plugins/src/MiaoRebate.ts)
## 更新日志
- 暂无
## Roadmap
- 绑定服务器(已完成)
- 绑定玩家(已完成)
- 兑换圈币(已完成)

View File

@@ -1,7 +1,7 @@
{
"private": true,
"name": "@ccms/plugins",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript plugins package",
"keywords": [
"miaoscript",
@@ -26,16 +26,16 @@
"@javatypes/spring-data-redis": "^0.0.3",
"@javatypes/spring-web": "^0.0.3",
"@javatypes/tomcat": "^0.0.3",
"@types/crypto-js": "^4.0.1",
"@types/crypto-js": "^4.0.2",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@babel/standalone": "^7.13.12",
"@ccms/api": "^0.14.1",
"@ccms/container": "^0.14.0",
"@ccms/plugin": "^0.14.1",
"@babel/standalone": "^7.14.7",
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0",
"@ccms/plugin": "^0.16.2",
"crypto-js": "^4.0.0"
}
}

View File

@@ -50,7 +50,7 @@ class MiaoMessage {
}
}
@plugin({ version: '1.0.1', author: 'MiaoWoo', source: __filename })
@plugin({ version: '1.1.0', author: 'MiaoWoo', nativeDepends: ['PlaceholderAPI'], source: __filename })
export class MiaoChat extends interfaces.Plugin {
@Autowired()
private Server: server.Server

View File

@@ -0,0 +1,109 @@
import { task, server, constants } from "@ccms/api"
import { Autowired, JSClass } from "@ccms/container"
import { interfaces, JSPlugin } from "@ccms/plugin"
let createPacketAdapterFunction = eval(`
function(cls, plugin, type, onPacketSending){
return new cls(plugin, type) {
onPacketSending: onPacketSending
}
}
`)
const ChatColor = Java.type('net.md_5.bungee.api.ChatColor')
const Pattern = Java.type('java.util.regex.Pattern')
@JSPlugin({ prefix: 'MCRS', version: '1.0.0', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], nativeDepends: ['ProtocolLib'], source: __filename })
export class MiaoChatRGBSupport extends interfaces.Plugin {
private supportRGB: boolean = false
// 用于匹配 '#FFFFFF' 颜色格式
private RGBCOLOR_PATTERN = Pattern.compile("(#[a-fA-F0-9]{6}?)([^#?]*)");
@JSClass('com.comphenix.protocol.events.PacketAdapter')
private PacketAdapter: any
@JSClass('com.comphenix.protocol.PacketType')
private PacketType: any
@JSClass('com.comphenix.protocol.ProtocolLibrary')
private ProtocolLibrary: any
private adapter: any
load() {
try {
ChatColor.of('#FFFFFF').toString()
this.supportRGB = true
this.logger.console('§a检测到兼容RGB的服务端 已启动相关支持...')
} catch (error) {
this.logger.console('§c当前服务端不支持RGB色彩 Error: ' + error)
}
}
enable() {
if (this.supportRGB) {
this.initPacketAdapter()
}
}
disable() {
if (this.supportRGB) {
this.ProtocolLibrary.getProtocolManager().removePacketListener(this.adapter)
}
}
createPacketAdapter(onPacketSending: (event) => void) {
return createPacketAdapterFunction(this.PacketAdapter, base.getInstance(), [this.PacketType.Play.Server.CHAT], onPacketSending)
}
colorJson(jsonObj) {
if (jsonObj.extra && jsonObj.extra.length) {
for (const extra of jsonObj.extra) {
this.colorJson(extra)
}
}
let text: string = jsonObj.text
var matcher = this.RGBCOLOR_PATTERN.matcher(text)
let colors = []
let texts = []
let lastStart = 0
while (matcher.find()) {
if (lastStart == 0) {
texts.push(text.substr(lastStart, matcher.start()))
lastStart = matcher.end()
}
colors.push(matcher.group(1))
texts.push(matcher.group(2))
}
if (colors.length) {
jsonObj.text = ''
let extras = []
let firstText = texts.shift()
if (firstText) { extras.push({ text: firstText }) }
texts.forEach((value, index) => {
extras.push({
text: value,
color: colors[index]
})
})
if (jsonObj.extra) {
jsonObj.extra = extras.concat(jsonObj.extra)
} else {
jsonObj.extra = extras
}
}
return jsonObj
}
initPacketAdapter() {
this.adapter = this.createPacketAdapter((event) => {
try {
if (!event.getPlayer().hasPermission('MiaoChatRGBSupport.color')) { return }
let wcc = event.getPacket().getChatComponents().read(0)
if (wcc == null) { return }
wcc.setJson(JSON.stringify(this.colorJson(JSON.parse(wcc.getJson()))))
event.getPacket().getChatComponents().writeSafely(0, wcc)
} catch (error) {
console.ex(error)
}
})
this.ProtocolLibrary.getProtocolManager().addPacketListener(this.adapter)
}
}

View File

@@ -3,10 +3,12 @@
import { plugin as pluginApi, server, task, constants, command } from '@ccms/api'
import { plugin, interfaces, cmd, tab, enable, config, disable, PluginConfig } from '@ccms/plugin'
import { ContainerInstance, Container, Autowired } from '@ccms/container'
import io, { Server as SocketIOServer, Socket as SocketIOSocket } from '@ccms/websocket'
import io from '@ccms/websocket'
import * as fs from '@ccms/common/dist/fs'
import * as reflect from '@ccms/common/dist/reflect'
import type { Namespace, Server as SocketIOServer, Socket as SocketIOSocket } from '@ccms/websocket'
const suffixMap = {
ts: 'typescript',
js: 'javascript',
@@ -58,7 +60,7 @@ export class MiaoConsole extends interfaces.Plugin {
this.token = Java.type('java.util.UUID').randomUUID().toString()
this.logger.console(`§6已生成随机Token: §3${this.token} §c重启后或重新生成后失效!`)
}
process.on('message', (msg) => {
process.on('message', (msg: string) => {
this.logCache.push(msg)
if (this.logCache.length > 100) {
this.logCache = this.logCache.slice(this.logCache.length - 100, this.logCache.length)
@@ -191,6 +193,7 @@ export class MiaoConsole extends interfaces.Plugin {
disable() {
if (this.socketIOServer) {
this.socketIOServer.close()
process.removeAllListeners('websocket.create')
process.removeAllListeners('message')
}
if (this.container.isBound(io.Instance)) {
@@ -230,30 +233,43 @@ export class MiaoConsole extends interfaces.Plugin {
this.socketIOServer = io(this.instance, {
path: '/ws',
root: fs.concat(root, 'wwwroot')
})
} as any)
this.container.bind(io.Instance).toConstantValue(this.socketIOServer)
process.emit('websocket.create', this.socketIOServer)
}
registryWebSocketNamespace(namespace: string, initialization: (namespace: Namespace) => void) {
if (this.socketIOServer) {
initialization(this.socketIOServer.of(namespace))
} else {
process.once('websocket.create', (server) => initialization(server.of(namespace)))
}
}
checkWebSocketClient(client: SocketIOSocket) {
if (!this.token) {
this.logger.console(`§6客户端 §b${client.id} §a请求连接 §4服务器尚未设置 Token 无法连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return false
}
if (this.token != client.handshake.query.token) {
this.logger.console(`§6客户端 §b${client.id} §c无效请求 §4请提供正确Token后再次连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return false
}
return true
}
startSocketIOServer() {
let namespace = this.socketIOServer.of('/MiaoConsole')
process.on('message', (msg) => namespace.emit('log', msg))
namespace.on('connection', (client: SocketIOSocket) => {
if (!this.token) {
this.logger.console(`§6客户端 §b${client.id} §a请求连接 §4服务器尚未设置 Token 无法连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return
if (this.checkWebSocketClient(client)) {
this.initWebSocketClient(client)
this.logCache.forEach(msg => client.emit('log', msg))
this.logger.console(`§6客户端 §b${client.id} §a新建连接 ${this.rootLogger ? '启动日志转发' : '§4转发日志启动失败'}...`)
}
if (this.token != client.handshake.query.token) {
this.logger.console(`§6客户端 §b${client.id} §c无效请求 §4请提供正确Token后再次连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return
}
this.initWebSocketClient(client)
this.logCache.forEach(msg => client.emit('log', msg))
this.logger.console(`§6客户端 §b${client.id} §a新建连接 ${this.rootLogger ? '启动日志转发' : '§4转发日志启动失败'}...`)
})
process.emit('websocket.start', this.socketIOServer)
}
private initWebSocketClient(client: SocketIOSocket) {

View File

@@ -0,0 +1,303 @@
/// <reference types="@javatypes/bungee-api" />
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { plugin, server, task } from '@ccms/api'
import { Autowired } from '@ccms/container'
import { Config, interfaces, JSPlugin, PluginConfig } from '@ccms/plugin'
import http from '@ccms/common/dist/http'
import type { Socket as SocketIOSocket, Namespace } from '@ccms/websocket'
import type { MiaoConsole } from './MiaoConsole'
const defaultConfig = {
statistics: {
max: 300
}
}
const defaultDataConfig = {
server_online: "%server_online%",
server_tps: "%server_tps_1%",
server_ram_used: "%server_ram_used%",
server_total_chunks: "%server_total_chunks%",
server_total_living_entities: "%server_total_living_entities%",
server_total_entities: "%server_total_entities%",
}
@JSPlugin({ prefix: 'Dashboard', version: '1.0.0', author: 'MiaoWoo', source: __filename, depends: ['MiaoConsole'] })
export class MiaoDashboard extends interfaces.Plugin {
@Autowired()
private server: server.Server
@Autowired()
private nativePluginManager: server.NativePluginManager
@Autowired()
private pluginManager: plugin.PluginManager
@Autowired()
private taskManager: task.TaskManager
private namespace: Namespace
@Config()
private config: PluginConfig & typeof defaultConfig = defaultConfig
@Config()
private dataConfig: PluginConfig & typeof defaultDataConfig = defaultDataConfig
@Config({ autosave: true })
private dataCache: { [key: string]: { time: string, value: Number }[] } = {}
private statisticTimer: task.Task
private PlaceholderAPI: { setPlaceholders: (player: any, str: string) => string }
load() {
for (const key of Object.keys(this.dataConfig)) {
this.dataCache[key] = this.dataCache[key] ?? []
}
}
enable() {
let consolePlugin: MiaoConsole = this.pluginManager.getPlugin('MiaoConsole') as MiaoConsole
consolePlugin.registryWebSocketNamespace('/MiaoDashboard', (namespace: Namespace) => {
this.namespace = namespace
this.namespace.on('connection', (client: SocketIOSocket) => {
if (consolePlugin.checkWebSocketClient(client)) {
this.initWebSocketClient(client)
this.logger.console(`§6客户端 §b${client.id} §a新建连接...`)
}
})
})
this.PlaceholderAPI = base.getClass("me.clip.placeholderapi.PlaceholderAPI").static
this.statisticTimer = this.taskManager.create(() => {
for (const key of Object.keys(this.dataConfig)) {
let dataArray = this.dataCache[key]
dataArray.push({
time: this.dateFormat('HH:MM:SS'),
value: parseFloat(this.taskManager.callSyncMethod(() => this.PlaceholderAPI['setPlaceholders(Player,String)'](null, this.dataConfig[key]))) ?? 0
})
if (dataArray.length > this.config.statistics.max) {
this.dataCache[key] = dataArray.slice(dataArray.length - this.config.statistics.max, dataArray.length)
}
}
}, this).async().timer(20 * 10).submit()
}
private dateFormat(fmt: string, date = new Date()) {
let ret: RegExpExecArray
const opt = {
"Y+": date.getFullYear().toString(), // 年
"m+": (date.getMonth() + 1).toString(), // 月
"d+": date.getDate().toString(), // 日
"H+": date.getHours().toString(), // 时
"M+": date.getMinutes().toString(), // 分
"S+": date.getSeconds().toString() // 秒
// 有其他格式化字符需求可以继续添加,必须转化成字符串
}
for (let k in opt) {
ret = new RegExp("(" + k + ")").exec(fmt)
if (ret) {
fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0")))
};
};
return fmt
}
disable() {
this.namespace?.close()
this.statisticTimer.cancel()
}
private wrapper(fn, data) {
fn({ status: 0, data })
}
private initWebSocketClient(client: SocketIOSocket) {
client.on('message', (...args) => {
console.log(args)
})
this.initStatistics(client)
this.initPlayers(client)
this.initPlugins(client)
this.initServices(client)
client.on('error', (error) => {
this.logger.console(`§6客户端 §b${client.id} §c触发异常: ${error}`)
this.logger.error(error)
})
client.on('disconnect', () => {
this.logger.console(`§6客户端 §b${client.id} §c断开连接...`)
})
}
private initStatistics(client: SocketIOSocket) {
client.on('statistics', (data, callback) => {
let result = {}
data.keys.map((key: string) => result[key] = this.dataCache[key])
this.wrapper(callback, { time: Date.now(), data: result })
})
}
private initPlayers(client: SocketIOSocket) {
client.on('players', (callback) => {
let players = Java.from(this.server.getOnlinePlayers()).map((player: org.bukkit.entity.Player) => {
let loc = player.getLocation()
return {
name: player.getName(),
location: {
world: loc.getWorld().getName(),
x: loc.getX().toFixed(0),
y: loc.getY().toFixed(0),
z: loc.getZ().toFixed(0)
},
health: player.getHealth().toFixed(0),
foodlevel: player.getFoodLevel().toFixed(0),
gamemode: player.getGameMode().name(),
ip: player.getAddress().getAddress().getHostAddress()
}
})
this.wrapper(callback, {
items: players
})
})
this.initPlayerModify(client)
client.on('players.heal', ({ name }, callback) => {
let player: org.bukkit.entity.Player = this.server.getPlayer(name)
if (!player) {
return callback({
status: 1,
msg: '玩家 ' + name + ' 不在线!'
})
}
player.setHealth(player.getMaxHealth())
player.setFoodLevel(20)
player.sendMessage('§a您已被治疗 §7- 来自MiaoDashboard')
return callback({
status: 0,
msg: '操作成功!'
})
})
client.on('players.kick', ({ name, reason }, callback) => {
let player: org.bukkit.entity.Player = this.server.getPlayer(name)
if (!player) {
return callback({
status: 1,
msg: '玩家 ' + name + ' 不在线!'
})
}
player.kickPlayer(reason)
return callback({
status: 0,
msg: '操作成功!'
})
})
}
private initPlayerModify(client: SocketIOSocket) {
client.on('players.modify.gamemode', ({ name, gamemode }, callback) => {
let player: org.bukkit.entity.Player = this.server.getPlayer(name)
if (!player) {
return callback({
status: 1,
msg: '玩家 ' + name + ' 不在线!'
})
}
let GameMode = Java.type('org.bukkit.GameMode')
let mode = GameMode.valueOf(gamemode)
this.taskManager.callSyncMethod(() => player.setGameMode(mode))
player.sendMessage('§a您的游戏模式已被修改为 ' + gamemode + ' §7- 来自MiaoDashboard')
return callback({
status: 0,
msg: '操作成功!'
})
})
}
private initPlugins(client: SocketIOSocket) {
this.initNativePlugins(client)
this.initScriptPlugins(client)
}
private initServices(client: SocketIOSocket) {
client.on('services.plugins', (callback) => {
this.wrapper(callback, http.get('http://w.yumc.pw/api/free_plugin/find').data.map(plugin => {
let installed = this.nativePluginManager.get(plugin.name)
if (installed) {
plugin.installed = true
plugin.installedVersion = installed.version
}
plugin.installed = this.nativePluginManager.has(plugin.name)
return plugin
}))
})
client.on('services.plugins.install', ({ name }, callback) => {
return callback({
status: 0,
msg: `插件 ${name} 安装成功!`
})
})
client.on('services.plugins.update', ({ name }, callback) => {
return callback({
status: 0,
msg: `插件 ${name} 安装成功!`
})
})
}
private initNativePlugins(client: SocketIOSocket) {
client.on('plugins.natives', callback => {
this.wrapper(callback, this.nativePluginManager.list().map(plugin => {
plugin.status = plugin.enable ? 1 : 0
return plugin
}))
})
client.on('plugins.natives.reload', ({ name }, callback) => {
return callback({
status: 0,
msg: `插件 ${name} 不存在`
})
return callback({
status: 0,
msg: `插件 ${name} 安装完成`
})
})
client.on('plugins.natives.install', ({ name }, callback) => {
return callback({
status: 0,
msg: `插件 ${name} 不存在`
})
return callback({
status: 0,
msg: `插件 ${name} 安装完成`
})
})
}
private initScriptPlugins(client: SocketIOSocket) {
client.on('plugins.scripts', (callback) => {
let plugins = []
this.pluginManager.getPlugins().forEach((plugin) => {
plugins.push({
name: plugin.description.name,
version: plugin.description.version,
author: plugin.description.author,
source: plugin.description.source.toString().replace(root, ''),
type: plugin.description.type,
scanner: plugin.description.loadMetadata.scanner.type,
loader: plugin.description.loadMetadata.loader.type,
status: 1
})
})
this.wrapper(callback, {
items: plugins,
total: plugins.length
})
})
client.on('plugins.scripts.reload', ({ name }, callback) => {
let plugin: any = this.pluginManager.getPlugin(name)
if (!plugin) {
return callback({
status: 404,
msg: `插件 ${name} 不存在`
})
}
this.pluginManager.reload(plugin)
return callback({
status: 0,
msg: `插件 ${name} 重载完成`
})
})
}
}

View File

@@ -9,6 +9,8 @@ import { Server as SocketIOServer, Socket as SocketIOSocket, Namespace } from '@
import * as fs from '@ccms/common/dist/fs'
import type { MiaoConsole } from './MiaoConsole'
const FileFilter = Java.type('java.io.FileFilter')
const ByteArrayInputStream = java.io.ByteArrayInputStream
const ByteArrayOutputStream = java.io.ByteArrayOutputStream
@@ -16,14 +18,13 @@ const StandardCharsets = Java.type("java.nio.charset.StandardCharsets")
const GZIPInputStream = Java.type('java.util.zip.GZIPInputStream')
const ByteArray = Java.type("byte[]")
@JSPlugin({ prefix: 'Explorer', version: '1.0.0', author: 'MiaoWoo', source: __filename })
@JSPlugin({ prefix: 'Explorer', version: '1.0.0', author: 'MiaoWoo', source: __filename, depends: ['MiaoConsole'] })
export class MiaoExplorer extends interfaces.Plugin {
@Autowired()
private Server: server.Server
@Autowired()
private pluginManager: plugin.PluginManager
private token: string
private namespace: any
private chunkCacheMap: Map<string, Array<string>>
@@ -32,44 +33,25 @@ export class MiaoExplorer extends interfaces.Plugin {
}
enable() {
let consolePlugin: any = this.pluginManager.getPlugin('MiaoConsole')
if (consolePlugin.socketIOServer) {
this.startWebSocketServer(consolePlugin.socketIOServer)
} else {
process.on('websocket.create', (server: SocketIOServer) => {
this.startWebSocketServer(server)
let consolePlugin: MiaoConsole = this.pluginManager.getPlugin('MiaoConsole') as MiaoConsole
consolePlugin.registryWebSocketNamespace('/MiaoExplorer', (namespace: Namespace) => {
this.namespace = namespace
this.namespace.on('connection', (client: SocketIOSocket) => {
if (consolePlugin.checkWebSocketClient(client)) {
this.initWebSocketClient(client)
this.logger.console(`§6客户端 §b${client.id} §a新建连接...`)
}
})
}
}
private startWebSocketServer(server: SocketIOServer) {
let consolePlugin: any = this.pluginManager.getPlugin('MiaoConsole')
this.token = consolePlugin.token
this.namespace = server.of('/MiaoExplorer')
this.namespace.on('connection', (client: SocketIOSocket) => {
if (!this.token) {
this.logger.console(`§6客户端 §b${client.id} §a请求连接 §4服务器尚未设置 Token 无法连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return
}
if (this.token != client.handshake.query.token) {
this.logger.console(`§6客户端 §b${client.id} §c无效请求 §4请提供正确Token后再次连接!`)
client.emit('unauthorized', () => client.disconnect(true))
return
}
this.initWebSocketClient(client)
this.logger.console(`§6客户端 §b${client.id} §a新建连接...`)
})
}
disable() {
this.namespace.removeAllListeners('connect')
this.namespace.close()
this.namespace?.close()
}
private readDir(dir) {
let children = Java.from(dir.listFiles(new FileFilter({
accept: file => file.getName().endsWith('.yml') || file.isDirectory()
accept: file => file.getName().endsWith('.yml') || file.getName().endsWith('.js') || file.isDirectory()
}))).sort().map(file => {
if (file.isDirectory()) {
let children = this.readDir(file)
@@ -161,15 +143,4 @@ export class MiaoExplorer extends interfaces.Plugin {
this.logger.console(`§6客户端 §b${client.id} §c断开连接...`)
})
}
@Cmd()
msme(sender: any, command: string, args: string[]) {
this.logger.log(sender, command, args)
sender.sendMessage(JSON.stringify({ command, ...args }))
}
@Tab()
tabmsme(_sender: any, _command: string, _args: string[]) {
return ['world']
}
}

View File

@@ -2,7 +2,7 @@
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { plugin, server, task } from '@ccms/api'
import { constants, plugin, server, task } from '@ccms/api'
import { Autowired, JSClass } from '@ccms/container'
import { Cmd, Config, interfaces, JSPlugin, Listener, PluginConfig, Tab } from '@ccms/plugin'
@@ -17,7 +17,12 @@ interface PlayerPointsAPI {
give(name: string, amount: number)
take(name: string, amount: number)
}
interface App {
appid: string
appname: string
ratio: number
coin_name: string
}
interface Order {
order_id: string
amount: number
@@ -73,7 +78,7 @@ const defaultConfig = {
}
}
@JSPlugin({ version: '1.3.5', author: 'MiaoWoo', source: __filename, depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI'] })
@JSPlugin({ version: '1.5.0', author: 'MiaoWoo', source: __filename, servers: [constants.ServerType.Bukkit], depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI', 'ProtocolLib'] })
export class MiaoPay extends interfaces.Plugin {
@Autowired()
private server: server.Server
@@ -87,28 +92,32 @@ export class MiaoPay extends interfaces.Plugin {
private apiGateWay = "https://pay.yumc.pw"
private MiaoReward: MiaoReward
private appInfo: App
private cacheMap = new Map<string, Order>();
private cacheSyncMap = new Map<string, Sync>();
private checkSet = new Set<string>();
@Config()
private config: PluginConfig & typeof defaultConfig = defaultConfig
load() {
let needSave = false
for (const key of Object.keys(defaultConfig)) {
if (!this.config[key]) {
this.config[key] = defaultConfig[key]
needSave = true
}
}
needSave && this.config.save()
this.MiaoReward = this.pluginManager.getPlugin('MiaoReward') as MiaoReward
}
enable() {
this.MiaoReward = this.pluginManager.getPlugin('MiaoReward') as MiaoReward
if (!this.MiaoReward) { return this.logger.error('当前脚本插件需要 MiaoReward 作为前置脚本插件!') }
if (!this.config.id || !this.config.secret) { return this.logger.console('§4尚未配置商户信息 将无法正常收款!') }
let info = this.httpPost('/apps', { id: this.config.id })
if (info.code == 200) {
this.appInfo = info.data
this.config.ratio = this.appInfo.ratio
this.config.coinName = this.appInfo.coin_name
} else {
this.logger.console('§4初始化支付系统失败 请检查配置是否正确!')
this.logger.console('§c服务器返回异常: §4' + info.msg)
}
}
disable() {
@@ -123,7 +132,9 @@ export class MiaoPay extends interfaces.Plugin {
cmdpay(sender: org.bukkit.entity.Player, amount: number = 0) {
if (!sender.getItemInHand) { return this.logger.sender(sender, '§4控制台无法执行此命令!') }
if (!this.MiaoReward.serverInfo) { return this.logger.sender(sender, '§4当前服务器尚未配置 请联系管理员先配置MiaoReward!') }
if (!this.appInfo) {
return this.logger.sender(sender, '§4当前服务器尚未配置 请联系管理员配置MiaoPay!')
}
if (!this.config.id || !this.config.secret) { return this.logger.sender(sender, '§c当前服务器尚未配置 请联系管理员配置支付密钥!') }
if (this.cacheMap.has(sender.getName())) {
this.logger.sender(sender, '§c您有一笔订单尚未完成 请完成支付或等待订单超时!')
@@ -136,7 +147,7 @@ export class MiaoPay extends interfaces.Plugin {
return
}
if (amount < 1) { return this.logger.sender(sender, `§c充值异常 §4充值金额不得小于 1 ${this.config.coinName}!`) }
if (amount / this.config.ratio > 1000) { return this.logger.sender(sender, `§c充值异常 §4充值金额不得大于 ${this.config.ratio * 1000} ${this.config.coinName}!`) }
if (amount / this.config.ratio > 5000) { return this.logger.sender(sender, `§c充值异常 §4充值金额不得大于 ${this.config.ratio * 5000} ${this.config.coinName}!`) }
if (amount != Math.round(amount)) { return this.logger.sender(sender, `§c充值异常 §4充值金额必须为整数!`) }
try {
this.getPlayerAmount(sender)
@@ -204,17 +215,25 @@ export class MiaoPay extends interfaces.Plugin {
}
cmdcheck(sender: org.bukkit.entity.Player, force = 1) {
if (this.checkSet.has(sender.getName())) {
return this.logger.sender(sender, '§c检查任务执行中 请稍候...')
}
this.checkSet.add(sender.getName())
this.logger.sender(sender, `§3正在检查需要补单充值的订单 请稍候...`)
this.taskManager.create(() => {
let result = this.queryUnconverted(sender.getName(), force)
if (result.code != 200) { return this.logger.sender(sender, `§c订单查询失败: ${result.msg}`) }
let unconverteds = result.data
if (!unconverteds.length) { return this.logger.sender(sender, `§c未发现需要进行补单充值的订单!`) }
this.logger.sender(sender, `§3发现 §a${unconverteds.length}笔 §3未充值订单 §c正在充值 请稍候...`)
for (const unconverted of unconverteds) {
this.logger.sender(sender, `§3正在处理订单 §a${unconverted.order_id} §3请稍候...`)
this.recharge(sender, unconverted)
Thread.sleep(300)
try {
let result = this.queryUnconverted(sender.getName(), force)
if (result.code != 200) { return this.logger.sender(sender, `§c订单查询失败: ${result.msg}`) }
let unconverteds = result.data
if (!unconverteds.length) { return this.logger.sender(sender, `§c未发现需要进行补单充值订单!`) }
this.logger.sender(sender, `§3发现 §a${unconverteds.length}笔 §3未充值订单 §c正在充值 请稍候...`)
for (const unconverted of unconverteds) {
this.logger.sender(sender, `§3正在处理订单 §a${unconverted.order_id} §3请稍候...`)
this.recharge(sender, unconverted)
Thread.sleep(300)
}
} finally {
this.checkSet.delete(sender.getName())
}
}).async().submit()
}
@@ -287,6 +306,7 @@ export class MiaoPay extends interfaces.Plugin {
}
private rewardOrder(sender, order_id, point) {
if (!this.config.reward) { return }
this.taskManager.callSyncMethod(() => {
try {
if (this.config.reward['*']) {
@@ -355,11 +375,11 @@ export class MiaoPay extends interfaces.Plugin {
}
private createOrder(sender: org.bukkit.entity.Player, amount: number): Order {
let serverName = this.MiaoReward.serverInfo.name
let serverName = this.appInfo?.appname
if (this.config.name) { serverName = `${serverName}(${this.config.name})` }
let result = this.httpPost('/create', {
subject: `${serverName} 充值 ${amount} ${this.config.coinName}`,
amount: amount / this.config.ratio,
amount: amount / this.appInfo.ratio,
username: sender.getName(),
unionId: sender.getUniqueId().toString()
})
@@ -381,6 +401,7 @@ export class MiaoPay extends interfaces.Plugin {
let startTime = Date.now()
data.appid = this.config.id
data.timestamp = Math.round(Date.now() / 1000)
data.nonce = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () => (Math.random() * 16 | 0).toString(16))
data.sign = this.sign(data)
let url = `${this.apiGateWay}/api${method}`
let result = http.post(url, data)

View File

@@ -1,6 +1,6 @@
import { task, server, constants } from "@ccms/api"
import { Autowired, JSClass } from "@ccms/container"
import { plugin, interfaces } from "@ccms/plugin"
import { plugin, interfaces, Cmd } from "@ccms/plugin"
let createPacketAdapterFunction = eval(`
function(cls, plugin, type, onPacketSending){
@@ -10,6 +10,13 @@ function(cls, plugin, type, onPacketSending){
}
`)
interface PlaceholderAPI {
registerPlaceholderHook: (key: string, onPlaceholderRequest: (player, s) => string) => void
unregisterPlaceholderHook: (key: string) => void
setPlaceholders: (player: any, str: string) => string
}
const Pattern = Java.type('java.util.regex.Pattern')
@plugin({ prefix: 'MPTL', version: '1.0.0', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], source: __filename })
export class MiaoProtocol extends interfaces.Plugin {
@Autowired()
@@ -24,9 +31,24 @@ export class MiaoProtocol extends interfaces.Plugin {
@JSClass('com.comphenix.protocol.ProtocolLibrary')
private ProtocolLibrary
@JSClass('com.comphenix.protocol.wrappers.nbt.NbtFactory')
private NbtFactory
@JSClass('me.clip.placeholderapi.PlaceholderAPI')
private PlaceholderAPI: PlaceholderAPI
private pipeline: any
private adapter: any
@Cmd({ autoMain: true })
mptl() {
}
cmdnbt(sender: org.bukkit.entity.Player) {
let nbt = this.NbtFactory.fromItemOptional(sender.getItemInHand())
console.log(nbt)
}
enable() {
let count = 0
let wait = this.taskManager.create(() => {
@@ -46,20 +68,49 @@ export class MiaoProtocol extends interfaces.Plugin {
}
createPacketAdapter(onPacketSending: (event) => void) {
return createPacketAdapterFunction(this.PacketAdapter, base.getInstance(), [this.PacketType.Play.Server.MAP], onPacketSending)
return createPacketAdapterFunction(this.PacketAdapter, base.getInstance(), [this.PacketType.Play.Server.CHAT], onPacketSending)
}
initPacketAdapter() {
this.adapter = this.createPacketAdapter((event) => {
let integers = event.getPacket().getIntegers().getValues()
// console.log(`ProtocolLib onPacketSending filter Map
// Player: ${event.getPlayer()}
// MapId: ${integers.get(0)}
// Short: ${event.getPacket().getShorts().read(0)}
// Bytes: ${event.getPacket().getByteArrays().read(0).length}
// `)
//Size: ${integers.get(3)}x${integers.get(4)}
// org.bukkit.map.MapPalette.imageToBytes()
try {
// let wcc = event.getPacket().getChatComponents().read(0)
// if (wcc == null) { return }
// let json = wcc.getJson()
// console.log(json)
// let jsonObj = JSON.parse(json)
// let result = JSON.stringify(this.colorJson(jsonObj))
// console.log(result)
// wcc.setJson(result)
// event.getPacket().getChatComponents().writeSafely(0, wcc)
// let packet = event.getPacket()
// let modify = packet.getEntityModifier(event)
// console.log(modify.getValues().get(0).getType().getName())
// let modify = event.getPacket().getNbtModifier()
// let nbt = modify.read(0)
// let lines = ["Text1", "Text2", "Text3", "Text4"]
// console.log("Before Replace", event.getPlayer().getName(), nbt)
// lines.forEach((s: string) => {
// let origin = nbt.getString(s)
// let replaced = this.PlaceholderAPI.setPlaceholders(event.getPlayer(), origin)
// nbt.put(s, replaced)
// console.log(event.getPlayer(), origin, replaced)
// })
// // Arrays.asList("Text1", "Text2", "Text3", "Text4").forEach(s -> nbt.put(s, replace(event.getPlayer(), nbt.getString(s))))
// console.log("After Replace", event.getPlayer().getName(), nbt)
// modify.write(0, nbt)
// let integers = event.getPacket().getIntegers().getValues()
// console.log(`ProtocolLib onPacketSending filter Map
// Player: ${event.getPlayer()}
// MapId: ${integers.get(0)}
// Short: ${event.getPacket().getShorts().read(0)}
// Bytes: ${event.getPacket().getByteArrays().read(0).length}
// `)
//Size: ${integers.get(3)}x${integers.get(4)}
// org.bukkit.map.MapPalette.imageToBytes()
} catch (error) {
console.ex(error)
}
})
this.ProtocolLibrary.getProtocolManager().addPacketListener(this.adapter)
}

View File

@@ -0,0 +1,157 @@
import { constants } from "@ccms/api"
import { JSClass } from "@ccms/container"
import { interfaces, JSPlugin } from "@ccms/plugin"
let createPacketAdapterFunction = eval(`
function(cls, plugin, type, onPacketSending){
return new cls(plugin, type) {
onPacketSending: onPacketSending
}
}
`)
const Color = Java.type('java.awt.Color')
const Pattern = Java.type('java.util.regex.Pattern')
const ChatColor = Java.type('net.md_5.bungee.api.ChatColor')
@JSPlugin({ prefix: 'MRS', version: '1.0.0', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], nativeDepends: ['ProtocolLib'], source: __filename })
export class MiaoRGBSupport extends interfaces.Plugin {
private supportRGB: boolean = false
// 用于匹配 '#FFFFFF' 颜色格式
private RGBCOLOR_PATTERN = Pattern.compile("(#[a-fA-F0-9]{6}?)([^#?]*)")
// 用于匹配彩虹格式
private RAINBOW_PATTERN = Pattern.compile("#RAINBOW([0-9]{1,3})([^#?]*)")
@JSClass('com.comphenix.protocol.events.PacketAdapter')
private PacketAdapter: any
@JSClass('com.comphenix.protocol.PacketType')
private PacketType: any
@JSClass('com.comphenix.protocol.ProtocolLibrary')
private ProtocolLibrary: any
private adapter: any
load() {
try {
ChatColor.of('#FFFFFF').toString()
this.supportRGB = true
this.logger.console('§a检测到兼容RGB的服务端 已启动相关支持...')
} catch (error) {
this.logger.console('§c当前服务端不支持RGB色彩 Error: ' + error)
}
}
enable() {
if (this.supportRGB) {
this.initPacketAdapter()
}
}
disable() {
if (this.supportRGB) {
this.ProtocolLibrary.getProtocolManager().removePacketListener(this.adapter)
}
}
createPacketAdapter(onPacketSending: (event) => void) {
return createPacketAdapterFunction(this.PacketAdapter, base.getInstance(), [
this.PacketType.Play.Server.CHAT,
this.PacketType.Play.Server.SCOREBOARD_OBJECTIVE,
this.PacketType.Play.Server.SCOREBOARD_SCORE,
this.PacketType.Play.Server.SCOREBOARD_TEAM
], onPacketSending)
}
colorJson(jsonObj) {
return this.processJson(jsonObj, this.RGBCOLOR_PATTERN, (extras, colors) => {
return (value, index) => {
extras.push({
text: value,
color: colors[index]
})
}
})
}
rainbowJson(jsonObj) {
return this.processJson(jsonObj, this.RAINBOW_PATTERN, (extras, colors) => {
return (value: string, index) => {
let textArr = value.split("")
let rainbowColors = this.createRainbow(textArr.length, colors[index])
textArr.forEach((value, index) => {
extras.push({
text: value,
color: rainbowColors[index]
})
})
}
})
}
private processJson(jsonObj, PATTERN, process: (extras, colors) => (value, index) => void) {
let text: string = jsonObj.text
if (jsonObj.extra && jsonObj.extra.length) {
for (const extra of jsonObj.extra) {
this.processJson(extra, PATTERN, process)
}
}
if (!text) { return jsonObj }
var matcher = PATTERN.matcher(text)
let colors = []
let texts = []
let lastStart = 0
while (matcher.find()) {
if (lastStart == 0) {
texts.push(text.substr(lastStart, matcher.start()))
lastStart = matcher.end()
}
colors.push(matcher.group(1))
texts.push(matcher.group(2))
}
if (colors.length) {
jsonObj.text = ''
let extras = []
let firstText = texts.shift()
if (firstText) { extras.push({ text: firstText }) }
texts.forEach(process(extras, colors))
if (jsonObj.extra) {
jsonObj.extra = extras.concat(jsonObj.extra)
} else {
jsonObj.extra = extras
}
}
return jsonObj
}
private createRainbow(step, saturation) {
var colors = []
var colorStep = (1.00 / step)
for (var i = 0; i < step; i++) {
colors.push("#" + java.lang.String.format("%08x", Color.getHSBColor((colorStep * i), saturation, saturation).getRGB()).substring(2))
}
return colors
}
initPacketAdapter() {
this.adapter = this.createPacketAdapter((event) => {
try {
if (!event.getPlayer().hasPermission('MiaoRGBSupport.color')) { return }
let ccs = event.getPacket().getChatComponents()
let size = ccs.size()
for (let i = 0; i < size; i++) {
let wcc = ccs.read(i)
if (wcc == null) { continue }
let json = JSON.parse(wcc.getJson())
json = this.colorJson(json)
if (event.getPlayer().hasPermission('MiaoRGBSupport.rainbow')) {
json = this.rainbowJson(json)
}
wcc.setJson(JSON.stringify(json))
ccs.write(i, wcc)
}
} catch (error) {
console.ex(error)
}
})
this.ProtocolLibrary.getProtocolManager().addPacketListener(this.adapter)
}
}

View File

@@ -0,0 +1,253 @@
/// <reference types="@javatypes/bungee-api" />
/// <reference types="@javatypes/bukkit-api" />
/// <reference types="@javatypes/sponge-api" />
import { plugin, server, task } from '@ccms/api'
import { Autowired, JSClass } from '@ccms/container'
import { Cmd, Config, interfaces, JSPlugin, Listener, PluginConfig, Tab } from '@ccms/plugin'
import type { MiaoReward } from './MiaoReward'
import http from '@ccms/common/dist/http'
interface Sync {
scaned: boolean
start?: number
left?: number
cancelled?: boolean
paying?: boolean
}
interface PlaceholderAPI {
registerPlaceholderHook: (key: string, onPlaceholderRequest: (player, s) => string) => void
unregisterPlaceholderHook: (key: string) => void
setPlaceholders: (player: any, str: string) => string
}
const defaultConfig = {
prefix: '§6[§b外卖系统§6]§r',
check: '%playerpoints_points%',
command: 'points give %player_name% %amount%',
ratio: 1,
coinName: '点券',
joinTip: true,
owner: {
userid: '',
ccid: '',
openid: ''
}
}
@JSPlugin({ version: '1.0.1', author: 'MiaoWoo', source: __filename, depends: ['MiaoReward'], nativeDepends: ['PlaceholderAPI'] })
export class MiaoRebate extends interfaces.Plugin {
@Autowired()
private server: server.Server
@Autowired()
private taskManager: task.TaskManager
@Autowired()
private pluginManager: plugin.PluginManager
@JSClass('me.clip.placeholderapi.PlaceholderAPI')
private PlaceholderAPI: PlaceholderAPI
private apiGateWay = "https://rebate.yumc.pw"
private MiaoReward: MiaoReward
@Config({ default: defaultConfig })
private config: PluginConfig & typeof defaultConfig = defaultConfig
load() {
this.logger.prefix = this.config.prefix
}
enable() {
this.MiaoReward = this.pluginManager.getPlugin('MiaoReward') as MiaoReward
if (!this.MiaoReward) { return this.logger.error(`当前脚本插件需要 MiaoReward 作为前置脚本插件!`) }
}
disable() {
}
@Cmd({ autoMain: true })
mre() { }
cmdbind(sender: org.bukkit.entity.Player, server: boolean) {
if (!sender.getItemInHand) { return this.logger.sender(sender, `§c手持物品检测异常 请检查是否在客户端执行命令!`) }
if (server) { return this.bindServer(sender) }
if (!sender.getItemInHand) { return this.logger.sender(sender, `§c手持物品检测异常 请检查是否在客户端执行命令!`) }
if (!this.config.owner.openid || !this.config.owner.userid) { return this.logger.sender(sender, `§4当前服务器尚未绑定管理员账号 请联系管理员完成绑定!`) }
this.MiaoReward.sendTitle(sender, `§a获取二维码中`, `§6请稍候...`)
let scan = this.qrCreate(sender, `绑定成功 请返回游戏查看!`, {
v: 1,
type: "invite",
user: this.config.owner.openid,
userid: this.config.owner.userid,
})
this.createScanTask(sender, scan.url, `微信扫码绑定账号`, `微信扫码 点击关注 绑定账号`, (sender) => {
this.MiaoReward.sendTitle(sender, `§a绑定成功!`, `§6已绑定用户: §b${this.qrGet(scan.token).user.username}`)
})
}
cmddraw(sender: org.bukkit.entity.Player, amount: number) {
if (!sender.getItemInHand) { return this.logger.sender(sender, `§c手持物品检测异常 请检查是否在客户端执行命令!`) }
if (!this.config.owner.openid || !this.config.owner.userid) { return this.logger.sender(sender, `§4当前服务器尚未绑定管理员账号 请联系管理员完成绑定!`) }
amount = Number(amount)
if (!Number.isInteger(amount)) {
return this.logger.sender(sender, `§4兑换金额必须是数字!`)
}
if (amount < 1) {
return this.logger.sender(sender, `§4兑换金额必须大于1!`)
}
this.MiaoReward.sendTitle(sender, `§a获取二维码中`, `§6请稍候...`)
let scan = this.qrCreate(sender, ``, {
v: 1,
type: "draw",
ccid: this.config.owner.ccid,
userid: this.config.owner.userid,
amount
})
this.createScanTask(sender, scan.url, `微信扫码兑换奖励`, `微信扫码兑换奖励`, (sender) => {
let result = this.qrGet(scan.token)
this.MiaoReward.sendTitle(sender, `§a扫码成功`, `§a兑换奖励中 §b具体结果请查看公众号消息...`)
if (!(result = result.result)) {
return this.sendError(sender, amount, `§4服务器返回数据异常!`)
}
if (!result.success) {
return this.sendError(sender, amount, `§c` + result.message)
}
this.logger.sender(sender, `§a` + result.message)
this.taskManager.callSyncMethod(() => {
let point = this.safeMultiply(amount, this.config.ratio)
let command = this.config.command.replace(`%player_name%`, sender.getName()).replace(`%amount%`, `${point}`)
if (!this.server.dispatchConsoleCommand(command)) {
return this.sendError(sender, amount, `§4充值命令执行异常!`)
}
let nowPoint = this.getPlayerAmount(sender)
this.logger.sender(sender, [
`§6充值 §a${point} §6${this.config.coinName} §a成功 §6当前账户余额: §3${nowPoint} §6${this.config.coinName}`,
`§c如出现未到账的情况 请联系管理员!`
])
})
})
this.MiaoReward.clearTitle(sender)
}
private safeMultiply(a: number, b: number) {
return parseFloat((a * b).toFixed(0))
}
sendError(sender: org.bukkit.entity.Player, amount: number, error: string) {
return this.logger.sender(sender, [
`§c========== ${this.config.prefix}§4兑换异常 §c==========`,
`§6兑换圈币: §3${amount}`,
`§6异常原因: §4${error}`,
`§6异常账号: §b${sender.getName()}`,
`§6异常时间: §a${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
`§c如果已扣除圈币但${this.config.coinName}未到账 请截图发给腐竹!`,
`§c========== ${this.config.prefix}§4兑换异常 §c==========`,
])
}
private getPlayerAmount(sender: any): number {
let result = this.PlaceholderAPI.setPlaceholders(sender, this.config.check)
let amount = parseFloat(result)
if (isNaN(amount)) {
throw new Error(`§c读取玩家 §3${this.config.coinName} §c异常 §6请检查 §3check §6配置是否正确!
§6数据解析链路: §3${this.config.check} §6=> §3${result} §6=> §3${amount}`)
}
return amount
}
private bindServer(sender: org.bukkit.entity.Player) {
if (!sender.isOp()) { return this.logger.sender(sender, `§4您没有配置服务器的权限!`) }
if (this.config.owner.openid || this.config.owner.userid) {
this.logger.sender(sender, `§c更换管理员账号 历史绑定数据将不会迁移!`)
}
let scan = this.qrCreate(sender, `绑定成功 请返回游戏查看!`)
this.createScanTask(sender, scan.url, `微信扫码绑定账号`, `微信扫码 点击关注 绑定账号`, (sender) => {
let result = this.qrGet(scan.token)
let user = result.user
this.config.owner.userid = user.id
this.config.owner.ccid = user.ccid
this.config.owner.openid = result.openid
this.config.save()
this.MiaoReward.sendTitle(sender, `§a绑定成功!`, `§6已绑定用户: §b${user.username}`)
})
this.MiaoReward.clearTitle(sender)
}
private createScanTask(sender: org.bukkit.entity.Player, qrcode: string, name: string, tip: string, task: (sender: org.bukkit.entity.Player) => void) {
let sync: any = { scaned: false, start: Math.round(Date.now() / 1000) }
this.MiaoReward.setItemAndTp(sender, qrcode, sync, name, tip)
this.taskManager.create(() => {
try {
task(sender)
} catch (error) {
if (!sync.cancelled) {
this.logger.sender(sender, `§c` + error)
}
} finally {
sync.scaned = true
sender.updateInventory()
}
}).async().submit()
this.MiaoReward.clearTitle(sender)
}
@Listener()
private PlayerJoinEvent(event: org.bukkit.event.player.PlayerJoinEvent) {
if (this.config.joinTip) {
this.taskManager.create(() => {
this.logger.sender(event.getPlayer(), [
`§b本服已和§a饿了么§6美团§c达成战略合作!`,
`§3/mre bind §a扫码§e免费赠送§a外卖红包!`
])
}).later(30).submit()
}
}
@Tab()
tabmre(sender: any, _command: any, args: string | any[]) {
if (args.length === 2 && args[0] === "bind" && sender.isOp()) return [`server`]
}
private qrCreate(sender: org.bukkit.entity.Player, message: string, data: any = {}) {
let create = this.httpPost(`/qr/create/type/login/message/${encodeURIComponent(message)}`, data)
if (create.code != 200) {
return this.logger.sender(sender, `§c获取链接异常: ` + create.msg)
}
return create.data
}
private qrGet(token: string) {
let get = this.httpPost('/qr/get', { token })
if (get.code != 200) {
throw new Error(get.msg)
}
return get.data
}
private httpPost(method: string, data: any = {}) {
let startTime = Date.now()
let url = `${this.apiGateWay}${method}`
let result = http.post(url, data)
console.debug(`
====== HTTP POST ======
REQUEST URL : ${url}
REQUEST DATA: ${JSON.stringify(data)}
RESPONSE : ${JSON.stringify(result)}
CAST TIME : ${Date.now() - startTime}`)
return result
}
private cmdhelp(sender: any) {
let help = [
`§6====== ${this.config.prefix} §a帮助菜单 §6======`,
`§6/mre bind §a绑定账号`,
`§6/mre draw §e<兑换数量> §a兑换${this.config.coinName}`
]
if (sender.isOp()) {
help = help.concat([
`§c由于您是管理员 以为您展示额外命令`,
`§6/mre bind server §a绑定服务器`,
])
}
this.logger.sender(sender, help)
}
}

View File

@@ -68,7 +68,7 @@ const defaultConfig = {
joinTip: true
}
@JSPlugin({ prefix: 'MRD', version: '1.5.9', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], nativeDepends: ['ProtocolLib', 'PlaceholderAPI'], source: __filename })
@JSPlugin({ prefix: 'MRD', version: '1.6.0', author: 'MiaoWoo', servers: [constants.ServerType.Bukkit], nativeDepends: ['ProtocolLib', 'PlaceholderAPI'], source: __filename })
export class MiaoReward extends interfaces.Plugin {
public serverInfo: ServerInfo
private notifyError = true
@@ -79,6 +79,7 @@ export class MiaoReward extends interfaces.Plugin {
private playerInfoCache = new Map<string, UserInfo>()
private downgrade = false
private subversion = 0
@Autowired()
private chat: chat.Chat
@@ -132,6 +133,7 @@ export class MiaoReward extends interfaces.Plugin {
//@ts-ignore
this.logger.prefix = this.config.prefix
this.downgrade = this.Bukkit.server.class.name.split('.')[3] == "v1_7_R4"
this.subversion = parseInt(this.Bukkit.server.class.name.split('.')[3].split('_')[1])
this.updateServerInfo(null, () => this.updateOnlinePlayersInfo())
}
@@ -248,29 +250,50 @@ export class MiaoReward extends interfaces.Plugin {
if (!this.ProtocolLibrary) {
return this.logger.console(`§4服务器未安装 ProtocolLib 无法扫码功能 请安装后重试!`)
}
this.adapter = this.createPacketAdapter((event) => {
let writer = undefined
if (this.downgrade) {
writer = (packet, bytes) => {
// let xbytes = new Bytes(131)
let origin = packet.getByteArrays().read(0)
// xbytes[1] = origin[1]
// xbytes[2] = origin[2]
for (let y = 0; y < 128; ++y) {
origin[y + 3] = bytes[y * 128 + origin[1]]
}
packet.getByteArrays().write(0, origin)
}
} else if (this.subversion < 17) {
writer = (packet, bytes) => {
packet.getByteArrays().write(0, bytes)
packet.getIntegers().write(3, 128)
packet.getIntegers().write(4, 128)
}
} else if (this.subversion > 16) {
writer = (packet, bytes) => {
let b = packet.getModifier().read(4)
if (b) {
let bi = Java.type(b.class.name)
packet.getModifier().write(4, new bi(b.a, b.b, 128, 128, bytes))
}
}
}
if (writer) {
this.adapter = this.createPacketAdapter(this.getPacketAdapter(writer))
this.ProtocolLibrary.getProtocolManager().addPacketListener(this.adapter)
} else {
console.console('§4当前服务器不支持虚拟地图发包 将无法使用扫码功能!')
}
}
private getPacketAdapter(writer: (packet: any, bytes: number[]) => void) {
return (event) => {
let integers = event.getPacket().getIntegers().getValues()
let mapId = integers.get(0)
let player = event.getPlayer()
if (mapId == this.zeroMapView.getId() && this.playerImageCache.has(player.getName())) {
let bytes = this.playerImageCache.get(player.getName())
if (!this.downgrade) {
event.getPacket().getByteArrays().write(0, bytes)
event.getPacket().getIntegers().write(3, 128)
event.getPacket().getIntegers().write(4, 128)
} else {
// let xbytes = new Bytes(131)
let origin = event.getPacket().getByteArrays().read(0)
// xbytes[1] = origin[1]
// xbytes[2] = origin[2]
for (let y = 0; y < 128; ++y) {
origin[y + 3] = bytes[y * 128 + origin[1]]
}
event.getPacket().getByteArrays().write(0, origin)
}
writer(event.getPacket(), this.playerImageCache.get(player.getName()))
}
})
this.ProtocolLibrary.getProtocolManager().addPacketListener(this.adapter)
}
}
private sendWindowItems(player: org.bukkit.entity.Player, mapItem: any) {

View File

@@ -46,12 +46,14 @@ let langMap = {
'cloud.update.finish': '§6成功从 §aMiaoScriptPackageCenter §6获取到 §a{length} §6个插件!',
'cloud.not.exists': '§6当前 §aMiaoScriptPackageCenter §c不存在 §a{name} §c插件!',
'cloud.update.exists': '§6插件 §b{name} §6版本 §3{old_version} §a发现更新 §3{new_version} §r{changelog}§6!',
'cloud.update.tip': `§6发现存在 §b{count}个 §6需要更新的插件 请使用 §aupdate §6或 §cupgrade §6命令更新!`,
'download.start': '§6开始下载插件: §b{name} §6版本 §3{version}',
'download.url': '§6插件下载地址: §b{url}',
'download.finish': '§6插件 §b{name} §6版本 §3{version} §a下载完毕 开始加载 ...',
'install.already': '§6插件 §b{name} §6版本 §3{version} §c已安装在服务器 §3更新请用 update 命令!',
'install.finish': '§6插件 §b{name} §6版本 §3{version} §a安装成功!',
'update.finish': '§6插件 §b{name} §6版本 §3{version} §a更新成!',
'update.finish': '§6插件 §b{name} §6版本 §3{version} §a更新成!',
'update.tip': '§6插件 §b{name} §a更新完成 §6请使用 §areload §6命令重载生效!',
'upgrade.confirm': '§6您正在尝试更新 §bMiaoScript §c核心 §6请执行 §b/mpm §aupgrade §cconfirm §6确认执行!',
'upgrade.start': '§6开始§a更新 §bMiaoScript §6核心 §c正在清理 node_modules 请稍候...',
'upgrade.failed': '§6尝试热更新 §bMiaoScript §c核心 §4失败! §6请重启服务器完成更新...',
@@ -107,7 +109,7 @@ class SpongeFakeSender extends FakeSender {
}
}
@JSPlugin({ prefix: 'PM', version: '1.4.0', author: 'MiaoWoo', source: __filename })
@JSPlugin({ prefix: 'PM', version: '1.5.1', author: 'MiaoWoo', source: __filename })
export class MiaoScriptPackageManager extends interfaces.Plugin {
@Autowired()
private pluginManager: pluginApi.PluginManager
@@ -134,7 +136,6 @@ export class MiaoScriptPackageManager extends interfaces.Plugin {
public serverName: string
private translate: Translate
private channelOff: { off: () => void }
private subCommandCache = []
load() {
this.translate = new Translate({
@@ -142,7 +143,6 @@ export class MiaoScriptPackageManager extends interfaces.Plugin {
fallbackMap
})
this.updateRepo(this.server.getConsoleSender())
this.subCommandCache = Object.keys(this).filter(c => c.startsWith('cmd') && typeof this[c] == "function")
}
@enable({ servers: [constants.ServerType.Bukkit, constants.ServerType.Sponge] })
@@ -468,7 +468,7 @@ return eval(${JSON.stringify(code)});`)
return tfunc.apply(_this, params)
}
cmddeploy(sender: any, name: any) {
cmddeploy(sender: string, name: string, changelog: string) {
if (!process.env.AccessToken) { return this.i18n(sender, 'deploy.token.not.exists') }
this.taskManager.create(() => {
if (this.checkPlugin(sender, name)) {
@@ -477,7 +477,8 @@ return eval(${JSON.stringify(code)});`)
name,
author: plugin.description.author,
version: plugin.description.version,
source: base.read((plugin.description.source || plugin.description.loadMetadata.file).toString())
source: base.read((plugin.description.source || plugin.description.loadMetadata.file).toString()),
changelog: changelog.replace('&', '§')
})
this.i18n(sender, result.code == 200 ? 'deploy.success' : 'deploy.fail', { name, version: plugin.description.version, msg: result.msg })
}
@@ -488,7 +489,11 @@ return eval(${JSON.stringify(code)});`)
if (this.checkCloudPlugin(sender, name)) {
this.download(sender, name, true, () => {
this.i18n(sender, 'update.finish', { name, version: this.packageCache[name].version })
callback?.()
if (callback) {
callback()
} else {
this.i18n(sender, 'update.tip', { name, version: this.packageCache[name].version })
}
})
}
}
@@ -530,6 +535,7 @@ return eval(${JSON.stringify(code)});`)
this.i18n(sender, 'cloud.update.finish', { length: this.packageNameCache.length })
this.pluginManager.getPlugins().forEach(p => {
let cloudPlugin = this.packageCache[p.description.name]
let updateCount = 0
//§6插件名称: §b{name}\n§6版本: §a{version}\n§6作者: §3{author}\§6更新时间: §9{updated_at}
if (cloudPlugin && cloudPlugin.version != p.description.version) {
this.i18n(sender, 'cloud.update.exists', {
@@ -538,6 +544,10 @@ return eval(${JSON.stringify(code)});`)
old_version: p.description.version,
changelog: cloudPlugin.changelog || ''
})
updateCount++
}
if (updateCount) {
this.i18n(sender, 'cloud.update.tip', { count: updateCount })
}
})
}).async().submit()

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/polyfill",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript polyfill package",
"author": "MiaoWoo <admin@yumc.pw>",
"homepage": "https://github.com/circlecloud/ms.git",
@@ -14,14 +14,14 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"dependencies": {
"@ccms/i18n": "^0.14.1",
"@ccms/nodejs": "^0.14.0",
"core-js": "^3.9.1"
"@ccms/i18n": "^0.16.0",
"@ccms/nodejs": "^0.16.0",
"core-js": "^3.15.2"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -4,6 +4,7 @@ import i18n from '@ccms/i18n'
let polyfillStartTime = new Date().getTime()
i18n.initialize()
console.i18n("ms.polyfill.initialize")
import './openjdk-nashorn-shim'
import './es5-ext'
import './node-shim'
import 'core-js'

View File

@@ -39,17 +39,29 @@ class Process extends EventEmitter {
return super.on(event, (...args) => {
try {
listener(...args)
} catch (error) {
} catch (origin) {
try {
super.emit('error', error)
super.emit('error', origin)
} catch (error) {
console.ex(origin)
console.ex(error)
}
}
})
}
nextTick(func: Function) {
microTaskPool.execute(func)
nextTick(func: Function, ...args: any[]) {
microTaskPool.execute(() => {
try {
func(args)
} catch (origin) {
try {
super.emit('error', origin)
} catch (error) {
console.ex(origin)
console.ex(error)
}
}
})
}
exit(code: number) {
console.log(`process exit by code ${code}!`)
@@ -198,7 +210,7 @@ Object.defineProperty(process, require('core-js/es/symbol/to-string-tag'), { val
const eventLoop = new EventLoop()
Object.defineProperty(process, 'eventLoop', { value: eventLoop })
eventLoop.startEventLoop()
global.setGlobal('queueMicrotask', (func: any) => microTaskPool.execute(func), {})
global.setGlobal('queueMicrotask', (func: any, ...args: any[]) => process.nextTick(func, args), {})
global.setGlobal('setTimeout', eventLoop.setTimeout.bind(eventLoop), {})
global.setGlobal('clearTimeout', eventLoop.clearTimeout.bind(eventLoop), {})
global.setGlobal('setInterval', eventLoop.setInterval.bind(eventLoop), {})

View File

@@ -0,0 +1,7 @@
// fix OpenJDK Nahsorn setPrototypeOf Bug
try {
Java.type('org.openjdk.nashorn.api.scripting.NashornScriptEngine')
Object.setPrototypeOf = ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b })
} catch (error) {
}
export { }

View File

@@ -1,8 +1,6 @@
import '@ccms/nashorn'
const URL = Java.type("java.net.URL")
const Files = Java.type("java.nio.file.Files")
const StandardCopyOption = Java.type("java.nio.file.StandardCopyOption")
const JavaString = Java.type("java.lang.String")
const SecureRandom = Java.type("java.security.SecureRandom")
const SSLContext = Java.type("javax.net.ssl.SSLContext")
@@ -13,8 +11,12 @@ const X509TrustManager = Java.type("javax.net.ssl.X509TrustManager")
const SocketTimeoutException = Java.type('java.net.SocketTimeoutException')
const Callable = Java.type('java.util.concurrent.Callable')
const TimeUnit = Java.type('java.util.concurrent.TimeUnit')
const Executors = Java.type('java.util.concurrent.Executors')
const ByteArrayOutputStream = Java.type("java.io.ByteArrayOutputStream")
const ByteArray = Java.type("byte[]")
const UTF_8 = "UTF-8"
const TrustAnyHostnameVerifier = new HostnameVerifier({ verify: () => true })
@@ -72,7 +74,7 @@ type HttpHeader = { [key: string]: string }
const executor = Executors.newCachedThreadPool()
export class XMLHttpRequest {
private _timeout: number
private _timeout: number = 120000;
private _responseType: ResponseType = 'text';
private _withCredentials: boolean
@@ -170,6 +172,7 @@ export class XMLHttpRequest {
this._connection.setConnectTimeout(this._timeout)
this._connection.setReadTimeout(this._timeout)
this.setRequestHeader('X-Requested-With', 'XMLHttpRequest')
this.setReadyState(ReadyState.OPENED)
}
send(body?: string | object): Future<string> {
@@ -178,7 +181,7 @@ export class XMLHttpRequest {
}
if (this._readyState !== ReadyState.OPENED) { throw new Error(`Error Status ${this._readyState}!`) }
let future = executor.submit(new Callable({ call: () => this._send(body) }))
if (!this._async) { future.get() }
if (!this._async) { future.get(this._timeout, TimeUnit.MILLISECONDS) }
return future
}
get() {
@@ -248,8 +251,16 @@ export class XMLHttpRequest {
}
private readOutput(input: any) {
var tempFile = Files.createTempFile('xhr', '.response')
Files.copy(input, tempFile, StandardCopyOption['REPLACE_EXISTING']); tempFile.toFile().deleteOnExit()
return new JavaString(Files.readAllBytes(tempFile), 'UTF-8')
let output = new ByteArrayOutputStream()
let buffer = new ByteArray(1024)
try {
let n: number
while ((n = input.read(buffer)) != -1) {
output.write(buffer, 0, n)
}
return output.toString(UTF_8)
} finally {
output.close()
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/protocol",
"version": "0.14.0",
"version": "0.16.0",
"description": "MiaoScript protocol package",
"keywords": [
"miaoscript",
@@ -22,6 +22,6 @@
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/sponge",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript api package",
"keywords": [
"miaoscript",
@@ -22,11 +22,11 @@
"@javatypes/sponge-api": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0"
}
}

View File

@@ -1,12 +1,35 @@
import { server } from '@ccms/api'
const Sponge = org.spongepowered.api.Sponge
const Sponge: typeof org.spongepowered.api.Sponge = Java.type('org.spongepowered.api.Sponge')
export class SpongeNativePluginManager extends server.NativePluginManager {
private spongePluginManager: org.spongepowered.api.plugin.PluginManager
constructor() {
super()
this.spongePluginManager = Sponge.getPluginManager()
}
list(): server.NativePlugin[] {
return Java.from(this.spongePluginManager.getPlugins()).map(plugin => this.convert(plugin))
}
has(name: string) {
return !!this.get(name)
return !!this.spongePluginManager.getPlugin(name).orElse(null)
}
get(name: string) {
return Sponge.getPluginManager().getPlugin(name).orElse(null)
return this.convert(this.spongePluginManager.getPlugin(name).orElse(null))
}
private convert(plugin: org.spongepowered.api.plugin.PluginContainer): server.NativePlugin {
if (!plugin) return plugin as any
return {
name: plugin.getName(),
version: plugin.getVersion().get() as string,
authors: Java.from(plugin.getAuthors() as string[]),
depends: Java.from(plugin.getDependencies()),
softDepends: [],
enable: true,
origin: plugin
}
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/spring",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript spring package",
"keywords": [
"miaoscript",
@@ -21,12 +21,12 @@
"devDependencies": {
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/common": "^0.14.1",
"@ccms/container": "^0.14.0",
"@ccms/database": "^0.14.1"
"@ccms/api": "^0.16.2",
"@ccms/common": "^0.16.0",
"@ccms/container": "^0.16.0",
"@ccms/database": "^0.16.2"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/web",
"version": "0.14.1",
"version": "0.16.2",
"description": "MiaoScript web package",
"keywords": [
"miaoscript",
@@ -26,10 +26,10 @@
"@javatypes/tomcat": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
},
"dependencies": {
"@ccms/api": "^0.14.1",
"@ccms/container": "^0.14.0"
"@ccms/api": "^0.16.2",
"@ccms/container": "^0.16.0"
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@ccms/websocket",
"version": "0.14.0",
"version": "0.16.3",
"description": "MiaoScript websocket package",
"keywords": [
"miaoscript",
@@ -19,10 +19,10 @@
"test": "echo \"Error: run tests from root\" && exit 1"
},
"devDependencies": {
"@ccms/nashorn": "^0.14.0",
"@ccms/nashorn": "^0.16.0",
"@javatypes/tomcat-websocket-api": "^0.0.3",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"typescript": "^4.2.3"
"typescript": "^4.3.5"
}
}

View File

@@ -23,7 +23,7 @@ export class WebSocketManager {
}
}
export const managers = new WebSocketManager()
export const manager = new WebSocketManager()
export class WebSocket extends EventEmitter {
public static CONNECTING = 0
@@ -31,6 +31,7 @@ export class WebSocket extends EventEmitter {
public static CLOSING = 2
public static CLOSED = 3
public binaryType: 'blob' | 'arraybuffer'
protected manager: WebSocketManager
protected _url: string
protected _headers: WebSocketHeader = {}
@@ -39,6 +40,7 @@ export class WebSocket extends EventEmitter {
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
super()
this.manager = manager
this._url = url
this._headers = headers
try {
@@ -51,15 +53,15 @@ export class WebSocket extends EventEmitter {
}
this.client.on('open', (event) => {
this.onopen?.(event)
managers.add(this)
manager.add(this)
})
this.client.on('message', (event) => this.onmessage?.(event))
this.client.on('close', (event) => {
this.onclose?.(event)
managers.del(this)
manager.del(this)
})
this.client.on('error', (event) => this.onerror?.(event))
setTimeout(() => this.client.connect(), 20)
this.client.connect()
}
get id() {
return this.client.id

View File

@@ -20,15 +20,15 @@ export class WebSocketClientHandler extends WebSocketClientHandlerAdapter {
return true
}
handlerAdded(ctx: any) {
console.trace(`${ctx} handlerAdded`)
console.debug(`${ctx} handlerAdded`)
this.handshakeFuture = ctx.newPromise()
}
channelActive(ctx: any) {
console.trace(`${ctx} channelActive`)
console.debug(`${ctx} channelActive`)
this.handshaker.handshake(ctx.channel())
}
channelInactive(ctx: any) {
console.trace(`${ctx} channelInactive`)
console.debug(`${ctx} channelInactive`)
this.client.onclose({ code: 0, reason: 'server connection channel inactive!' })
}
channelRead0(ctx: any, msg: any) {
@@ -54,7 +54,7 @@ export class WebSocketClientHandler extends WebSocketClientHandlerAdapter {
}
}
exceptionCaught(ctx: any, cause: Error) {
console.trace(`${ctx} exceptionCaught ${cause}`)
console.debug(`${ctx} exceptionCaught ${cause}`)
this.client.onerror({ error: cause })
if (!this.handshakeFuture.isDone()) {
this.handshakeFuture.setFailure(cause)

View File

@@ -25,22 +25,51 @@ const CloseWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.Cl
const ChannelInitializer = Java.type('io.netty.channel.ChannelInitializer')
const DefaultHttpHeaders = Java.type('io.netty.handler.codec.http.DefaultHttpHeaders')
const SslContextBuilder = Java.type('io.netty.handler.ssl.SslContextBuilder')
const InsecureTrustManagerFactory = Java.type('io.netty.handler.ssl.util.InsecureTrustManagerFactory')
const epull = Epoll.isAvailable()
const group = epull ? new EpollEventLoopGroup() : new NioEventLoopGroup()
const socketChannelClass = epull ? EpollSocketChannel.class : NioSocketChannel.class
process.on('exit', () => group.shutdownGracefully())
export class NettyWebSocket extends Transport {
private _uri: any
private _schema: string
private _host: string
private _port: number
private channel: any
private b = new Bootstrap();
constructor(url: string, subProtocol: string = '', headers: WebSocketHeader = {}) {
super(url, subProtocol, headers)
if (!url) {
throw new Error("Failed to construct 'WebSocket': The URL '" + url + "' is invalid.")
}
this._uri = URI.create(this._url)
this._schema = this._uri.getScheme() ?? 'ws'
if (["wss", "ws"].indexOf(this._schema) == -1) {
throw new Error("Failed to construct 'WebSocket': The URL's scheme must be either 'ws' or 'wss'. '" + this._schema + "' is not allowed.")
}
this._host = this._uri.getHost()
if (!this._host) {
throw new Error("Failed to construct 'WebSocket': The Host '" + this._host + "' is invalid.")
}
this._port = this._uri.getPort()
if (this._port == -1) {
if (this._schema == "wss") {
this._port = 443
} else if (this._schema == "ws") {
this._port = 80
}
}
console.debug(`constructor NettyWebSocket url: ${url} scheme: ${this._schema} host: ${this._host} port: ${this._port}`)
}
getId() {
return this.channel?.id() + ''
}
doConnect() {
console.debug('client NettyWebSocket doConnect', this._url)
let uri = URI.create(this._url)
let headers = new DefaultHttpHeaders()
for (const key of Object.getOwnPropertyNames(this._headers || {})) {
@@ -56,12 +85,16 @@ export class NettyWebSocket extends Transport {
.handler(new ChannelInitializer({
initChannel: (ch: any) => {
let pipeline = ch.pipeline()
if (this._schema == "wss") {
let sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build()
pipeline.addLast(sslCtx.newHandler(ch.alloc(), this._host, this._port))
}
pipeline.addLast("http-codec", new HttpClientCodec())
pipeline.addLast("aggregator", new HttpObjectAggregator(65536))
pipeline.addLast("websocket", handler.getHandler())
}
}))
this.b.connect(uri.getHost(), uri.getPort()).addListener(new ChannelFutureListener((future: any) => {
this.b.connect(this._host, this._port).addListener(new ChannelFutureListener((future: any) => {
this.channel = future.sync().channel()
this.onconnection({})
handler.handshakeFuture.addListener(new ChannelFutureListener((future: any) => {
@@ -69,7 +102,7 @@ export class NettyWebSocket extends Transport {
future.sync()
this.onconnect({})
} catch (error) {
console.debug(error)
this.onerror({ error })
}
}))
}))

View File

@@ -0,0 +1,21 @@
const PACKET_TYPES = Object.create(null) // no Map = no polyfill
PACKET_TYPES["open"] = "0"
PACKET_TYPES["close"] = "1"
PACKET_TYPES["ping"] = "2"
PACKET_TYPES["pong"] = "3"
PACKET_TYPES["message"] = "4"
PACKET_TYPES["upgrade"] = "5"
PACKET_TYPES["noop"] = "6"
const PACKET_TYPES_REVERSE = Object.create(null)
Object.keys(PACKET_TYPES).forEach(key => {
PACKET_TYPES_REVERSE[PACKET_TYPES[key]] = key
})
const ERROR_PACKET = { type: "error", data: "parser error" }
export = {
PACKET_TYPES,
PACKET_TYPES_REVERSE,
ERROR_PACKET
}

View File

@@ -0,0 +1,48 @@
const { PACKET_TYPES_REVERSE, ERROR_PACKET } = require("./commons")
export const decodePacket = (encodedPacket, binaryType) => {
if (typeof encodedPacket !== "string") {
return {
type: "message",
data: mapBinary(encodedPacket, binaryType)
}
}
const type = encodedPacket.charAt(0)
if (type === "b") {
const buffer = Buffer.from(encodedPacket.substring(1), "base64")
return {
type: "message",
data: mapBinary(buffer, binaryType)
}
}
if (!PACKET_TYPES_REVERSE[type]) {
return ERROR_PACKET
}
return encodedPacket.length > 1
? {
type: PACKET_TYPES_REVERSE[type],
data: encodedPacket.substring(1)
}
: {
type: PACKET_TYPES_REVERSE[type]
}
}
const mapBinary = (data, binaryType) => {
switch (binaryType) {
case "arraybuffer":
return Buffer.isBuffer(data) ? toArrayBuffer(data) : data
case "nodebuffer":
default:
return data // assuming the data is already a Buffer
}
}
const toArrayBuffer = buffer => {
const arrayBuffer = new ArrayBuffer(buffer.length)
const view = new Uint8Array(arrayBuffer)
for (let i = 0; i < buffer.length; i++) {
view[i] = buffer[i]
}
return arrayBuffer
}

View File

@@ -0,0 +1,26 @@
const { PACKET_TYPES } = require("./commons")
export const encodePacket = ({ type, data }, supportsBinary, callback) => {
console.trace('encodePacket', type, JSON.stringify(data))
if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) {
const buffer = toBuffer(data)
return callback(encodeBuffer(buffer, supportsBinary))
}
// plain string
return callback(PACKET_TYPES[type] + (data || ""))
}
const toBuffer = data => {
if (Buffer.isBuffer(data)) {
return data
} else if (data instanceof ArrayBuffer) {
return Buffer.from(data)
} else {
return Buffer.from(data.buffer, data.byteOffset, data.byteLength)
}
}
// only 'message' packets can contain binary, so the type prefix is not needed
const encodeBuffer = (data, supportsBinary) => {
return supportsBinary ? data : "b" + data.toString("base64")
}

View File

@@ -0,0 +1,42 @@
import { encodePacket } from "./encodePacket"
import { decodePacket } from "./decodePacket"
const SEPARATOR = String.fromCharCode(30) // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
const encodePayload = (packets, callback) => {
// some packets may be added to the array while encoding, so the initial length must be saved
const length = packets.length
const encodedPackets = new Array(length)
let count = 0
packets.forEach((packet, i) => {
// force base64 encoding for binary packets
encodePacket(packet, false, encodedPacket => {
encodedPackets[i] = encodedPacket
if (++count === length) {
callback(encodedPackets.join(SEPARATOR))
}
})
})
}
const decodePayload = (encodedPayload, binaryType) => {
const encodedPackets = encodedPayload.split(SEPARATOR)
const packets = []
for (let i = 0; i < encodedPackets.length; i++) {
const decodedPacket = decodePacket(encodedPackets[i], binaryType)
packets.push(decodedPacket)
if (decodedPacket.type === "error") {
break
}
}
return packets
}
export default {
protocol: 4,
encodePacket,
encodePayload,
decodePacket,
decodePayload
}

View File

@@ -0,0 +1,27 @@
/**
* Module dependencies.
*/
import * as server from "../server"
// const http = require("http")
// const Server = require("./server")
import { Server } from './server'
/**
* Captures upgrade requests for a http.Server.
*
* @param {http.Server} server
* @param {Object} options
* @return {Server} engine server
* @api public
*/
function attach(srv, options) {
const engine = new Server(options)
engine.attach(server.attach(srv, options), options)
return engine
}
export = {
attach
}

View File

@@ -0,0 +1,690 @@
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
// }

View File

@@ -0,0 +1,530 @@
import { EventEmitter } from "events"
import { Server } from "./server"
import { Transport } from "./transport"
import type { Request } from "../server/request"
// const debug = require("debug")("engine:socket")
export class Socket extends EventEmitter {
public id: string
private server: Server
private upgrading = false
private upgraded = false
public readyState = "opening"
private writeBuffer = []
private packetsFn = []
private sentCallbackFn = []
private cleanupFn = []
public request: Request
public protocol: number
public remoteAddress: any
public transport: Transport
private checkIntervalTimer: NodeJS.Timeout
private upgradeTimeoutTimer: NodeJS.Timeout
private pingTimeoutTimer: NodeJS.Timeout
private pingIntervalTimer: NodeJS.Timeout
/**
* Client class (abstract).
*
* @api private
*/
constructor(id: string, server: Server, transport: Transport, req: Request, protocol: number) {
super()
this.id = id
this.server = server
this.request = req
this.protocol = protocol
// Cache IP since it might not be in the req later
if (req.websocket && req.websocket._socket) {
this.remoteAddress = req.websocket._socket.remoteAddress
} else {
this.remoteAddress = req.connection.remoteAddress
}
this.checkIntervalTimer = null
this.upgradeTimeoutTimer = null
this.pingTimeoutTimer = null
this.pingIntervalTimer = null
this.setTransport(transport)
this.onOpen()
}
/**
* Called upon transport considered open.
*
* @api private
*/
onOpen() {
this.readyState = "open"
// sends an `open` packet
this.transport.sid = this.id
this.sendPacket(
"open",
JSON.stringify({
sid: this.id,
upgrades: this.getAvailableUpgrades(),
pingInterval: this.server.opts.pingInterval,
pingTimeout: this.server.opts.pingTimeout
})
)
if (this.server.opts.initialPacket) {
this.sendPacket("message", this.server.opts.initialPacket)
}
this.emit("open")
if (this.protocol === 3) {
// in protocol v3, the client sends a ping, and the server answers with a pong
this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout
)
} else {
// in protocol v4, the server sends a ping, and the client answers with a pong
this.schedulePing()
}
}
/**
* Called upon transport packet.
*
* @param {Object} packet
* @api private
*/
onPacket(packet: { type: any; data: any }) {
if ("open" !== this.readyState) {
console.debug("packet received with closed socket")
return
}
// export packet event
// debug(`received packet ${packet.type}`)
this.emit("packet", packet)
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
this.resetPingTimeout(
this.server.opts.pingInterval + this.server.opts.pingTimeout
)
switch (packet.type) {
case "ping":
if (this.transport.protocol !== 3) {
this.onError("invalid heartbeat direction")
return
}
// debug("got ping")
this.sendPacket("pong")
this.emit("heartbeat")
break
case "pong":
if (this.transport.protocol === 3) {
this.onError("invalid heartbeat direction")
return
}
// debug("got pong")
this.schedulePing()
this.emit("heartbeat")
break
case "error":
this.onClose("parse error")
break
case "message":
this.emit("data", packet.data)
this.emit("message", packet.data)
break
}
}
/**
* Called upon transport error.
*
* @param {Error} error object
* @api private
*/
onError(err: string) {
// debug("transport error")
this.onClose("transport error", err)
}
/**
* Pings client every `this.pingInterval` and expects response
* within `this.pingTimeout` or closes connection.
*
* @api private
*/
schedulePing() {
clearTimeout(this.pingIntervalTimer)
this.pingIntervalTimer = setTimeout(() => {
// debug(
// "writing ping packet - expecting pong within %sms",
// this.server.opts.pingTimeout
// )
this.sendPacket("ping")
this.resetPingTimeout(this.server.opts.pingTimeout)
}, this.server.opts.pingInterval)
}
/**
* Resets ping timeout.
*
* @api private
*/
resetPingTimeout(timeout: number) {
clearTimeout(this.pingTimeoutTimer)
this.pingTimeoutTimer = setTimeout(() => {
if (this.readyState === "closed") return
this.onClose("ping timeout")
}, timeout)
}
/**
* Attaches handlers for the given transport.
*
* @param {Transport} transport
* @api private
*/
setTransport(transport: Transport) {
console.debug(`engine.io socket ${this.id} set transport ${transport.name}`)
const onError = this.onError.bind(this)
const onPacket = this.onPacket.bind(this)
const flush = this.flush.bind(this)
const onClose = this.onClose.bind(this, "transport close")
this.transport = transport
this.transport.once("error", onError)
this.transport.on("packet", onPacket)
this.transport.on("drain", flush)
this.transport.once("close", onClose)
// this function will manage packet events (also message callbacks)
this.setupSendCallback()
this.cleanupFn.push(function () {
transport.removeListener("error", onError)
transport.removeListener("packet", onPacket)
transport.removeListener("drain", flush)
transport.removeListener("close", onClose)
})
}
/**
* Upgrades socket to the given transport
*
* @param {Transport} transport
* @api private
*/
maybeUpgrade(transport: Transport) {
console.debug(
'might upgrade socket transport from "', this.transport.name, '" to "', transport.name, '"'
)
this.upgrading = true
// set transport upgrade timer
this.upgradeTimeoutTimer = setTimeout(() => {
console.debug("client did not complete upgrade - closing transport")
cleanup()
if ("open" === transport.readyState) {
transport.close()
}
}, this.server.opts.upgradeTimeout)
const onPacket = (packet: { type: string; data: string }) => {
if ("ping" === packet.type && "probe" === packet.data) {
transport.send([{ type: "pong", data: "probe" }])
this.emit("upgrading", transport)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = setInterval(check, 100)
} else if ("upgrade" === packet.type && this.readyState !== "closed") {
// debug("got upgrade packet - upgrading")
cleanup()
this.transport.discard()
this.upgraded = true
this.clearTransport()
this.setTransport(transport)
this.emit("upgrade", transport)
this.flush()
if (this.readyState === "closing") {
transport.close(() => {
this.onClose("forced close")
})
}
} else {
cleanup()
transport.close()
}
}
// we force a polling cycle to ensure a fast upgrade
const check = () => {
if ("polling" === this.transport.name && this.transport.writable) {
// debug("writing a noop packet to polling for fast upgrade")
this.transport.send([{ type: "noop" }])
}
}
const cleanup = () => {
this.upgrading = false
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
this.upgradeTimeoutTimer = null
transport.removeListener("packet", onPacket)
transport.removeListener("close", onTransportClose)
transport.removeListener("error", onError)
this.removeListener("close", onClose)
}
const onError = (err: string) => {
// debug("client did not complete upgrade - %s", err)
cleanup()
transport.close()
transport = null
}
const onTransportClose = () => {
onError("transport closed")
}
const onClose = () => {
onError("socket closed")
}
transport.on("packet", onPacket)
transport.once("close", onTransportClose)
transport.once("error", onError)
this.once("close", onClose)
}
/**
* Clears listeners and timers associated with current transport.
*
* @api private
*/
clearTransport() {
let cleanup: () => void
const toCleanUp = this.cleanupFn.length
for (let i = 0; i < toCleanUp; i++) {
cleanup = this.cleanupFn.shift()
cleanup()
}
// silence further transport errors and prevent uncaught exceptions
this.transport.on("error", function () {
// debug("error triggered by discarded transport")
})
// ensure transport won't stay open
this.transport.close()
clearTimeout(this.pingTimeoutTimer)
}
/**
* Called upon transport considered closed.
* Possible reasons: `ping timeout`, `client error`, `parse error`,
* `transport error`, `server close`, `transport close`
*/
onClose(reason: string, description?: string) {
if ("closed" !== this.readyState) {
this.readyState = "closed"
// clear timers
clearTimeout(this.pingIntervalTimer)
clearTimeout(this.pingTimeoutTimer)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
// clean writeBuffer in next tick, so developers can still
// grab the writeBuffer on 'close' event
process.nextTick(() => {
this.writeBuffer = []
})
this.packetsFn = []
this.sentCallbackFn = []
this.clearTransport()
this.emit("close", reason, description)
}
}
/**
* Setup and manage send callback
*
* @api private
*/
setupSendCallback() {
// the message was sent successfully, execute the callback
const onDrain = () => {
if (this.sentCallbackFn.length > 0) {
const seqFn = this.sentCallbackFn.splice(0, 1)[0]
if ("function" === typeof seqFn) {
// debug("executing send callback")
seqFn(this.transport)
} else if (Array.isArray(seqFn)) {
// debug("executing batch send callback")
const l = seqFn.length
let i = 0
for (; i < l; i++) {
if ("function" === typeof seqFn[i]) {
seqFn[i](this.transport)
}
}
}
}
}
this.transport.on("drain", onDrain)
this.cleanupFn.push(() => {
this.transport.removeListener("drain", onDrain)
})
}
/**
* Sends a message packet.
*
* @param {String} message
* @param {Object} options
* @param {Function} callback
* @return {Socket} for chaining
* @api public
*/
send(data: any, options: any, callback: any) {
this.sendPacket("message", data, options, callback)
return this
}
write(data: any, options: any, callback?: any) {
this.sendPacket("message", data, options, callback)
return this
}
/**
* Sends a packet.
*
* @param {String} packet type
* @param {String} optional, data
* @param {Object} options
* @api private
*/
sendPacket(type: string, data?: string, options?: { compress?: any }, callback?: undefined) {
if ("function" === typeof options) {
callback = options
options = null
}
options = options || {}
options.compress = false !== options.compress
if ("closing" !== this.readyState && "closed" !== this.readyState) {
// console.debug('sending packet "%s" (%s)', type, data)
const packet: any = {
type: type,
options: options
}
if (data) packet.data = data
// exports packetCreate event
this.emit("packetCreate", packet)
this.writeBuffer.push(packet)
// add send callback to object, if defined
if (callback) this.packetsFn.push(callback)
this.flush()
}
}
/**
* Attempts to flush the packets buffer.
*
* @api private
*/
flush() {
if (
"closed" !== this.readyState &&
this.transport.writable &&
this.writeBuffer.length
) {
console.trace("flushing buffer to transport")
this.emit("flush", this.writeBuffer)
this.server.emit("flush", this, this.writeBuffer)
const wbuf = this.writeBuffer
this.writeBuffer = []
if (!this.transport.supportsFraming) {
this.sentCallbackFn.push(this.packetsFn)
} else {
this.sentCallbackFn.push.apply(this.sentCallbackFn, this.packetsFn)
}
this.packetsFn = []
this.transport.send(wbuf)
this.emit("drain")
this.server.emit("drain", this)
}
}
/**
* Get available upgrades for this socket.
*
* @api private
*/
getAvailableUpgrades() {
const availableUpgrades = []
const allUpgrades = this.server.upgrades(this.transport.name)
let i = 0
const l = allUpgrades.length
for (; i < l; ++i) {
const upg = allUpgrades[i]
if (this.server.opts.transports.indexOf(upg) !== -1) {
availableUpgrades.push(upg)
}
}
return availableUpgrades
}
/**
* Closes the socket and underlying transport.
*
* @param {Boolean} optional, discard
* @return {Socket} for chaining
* @api public
*/
close(discard?: any) {
if ("open" !== this.readyState) return
this.readyState = "closing"
if (this.writeBuffer.length) {
this.once("drain", this.closeTransport.bind(this, discard))
return
}
this.closeTransport(discard)
}
/**
* Closes the underlying transport.
*
* @param {Boolean} discard
* @api private
*/
closeTransport(discard: any) {
if (discard) this.transport.discard()
this.transport.close(this.onClose.bind(this, "forced close"))
}
}

View File

@@ -0,0 +1,121 @@
import { EventEmitter } from 'events'
import parser_v4 from "../engine.io-parser"
import type { WebSocketClient } from '../server/client'
/**
* Noop function.
*
* @api private
*/
function noop() { }
export abstract class Transport extends EventEmitter {
public sid: string
public req /**http.IncomingMessage */
public socket: WebSocketClient
public writable: boolean
public readyState: string
public discarded: boolean
public protocol: Number
public parser: any
public perMessageDeflate: any
public supportsBinary: boolean = false
/**
* Transport constructor.
*
* @param {http.IncomingMessage} request
* @api public
*/
constructor(req) {
super()
this.readyState = "open"
this.discarded = false
this.protocol = req._query.EIO === "4" ? 4 : 3 // 3rd revision by default
this.parser = parser_v4//= this.protocol === 4 ? parser_v4 : parser_v3
}
/**
* Flags the transport as discarded.
*
* @api private
*/
discard() {
this.discarded = true
}
/**
* Called with an incoming HTTP request.
*
* @param {http.IncomingMessage} request
* @api private
*/
onRequest(req) {
console.debug(`engine.io transport ${this.socket.id} setting request`, JSON.stringify(req))
this.req = req
}
/**
* Closes the transport.
*
* @api private
*/
close(fn?) {
if ("closed" === this.readyState || "closing" === this.readyState) return
this.readyState = "closing"
this.doClose(fn || noop)
}
/**
* Called with a transport error.
*
* @param {String} message error
* @param {Object} error description
* @api private
*/
onError(msg: string, desc?: string) {
if (this.listeners("error").length) {
const err: any = new Error(msg)
err.type = "TransportError"
err.description = desc
this.emit("error", err)
} else {
console.debug(`ignored transport error ${msg} (${desc})`)
}
}
/**
* Called with parsed out a packets from the data stream.
*
* @param {Object} packet
* @api private
*/
onPacket(packet) {
this.emit("packet", packet)
}
/**
* Called with the encoded packet data.
*
* @param {String} data
* @api private
*/
onData(data) {
this.onPacket(this.parser.decodePacket(data))
}
/**
* Called upon transport close.
*
* @api private
*/
onClose() {
this.readyState = "closed"
this.emit("close")
}
abstract get supportsFraming()
abstract get name()
abstract send(...args: any[])
abstract doClose(d: Function)
}

View File

@@ -0,0 +1,3 @@
export default {
websocket: require("./websocket").WebSocket
}

View File

@@ -0,0 +1,116 @@
import { Transport } from '../transport'
// const debug = require("debug")("engine:ws")
export class WebSocket extends Transport {
public perMessageDeflate: any
/**
* WebSocket transport
*
* @param {http.IncomingMessage}
* @api public
*/
constructor(req) {
super(req)
this.socket = req.websocket
this.socket.on("message", this.onData.bind(this))
this.socket.once("close", this.onClose.bind(this))
this.socket.on("error", this.onError.bind(this))
this.writable = true
this.perMessageDeflate = null
}
/**
* Transport name
*
* @api public
*/
get name() {
return "websocket"
}
/**
* Advertise upgrade support.
*
* @api public
*/
get handlesUpgrades() {
return true
}
/**
* Advertise framing support.
*
* @api public
*/
get supportsFraming() {
return true
}
/**
* Processes the incoming data.
*
* @param {String} encoded packet
* @api private
*/
onData(data) {
// debug('received "%s"', data)
super.onData(data)
}
/**
* Writes a packet payload.
*
* @param {Array} packets
* @api private
*/
send(packets) {
// console.log('WebSocket send packets', JSON.stringify(packets))
const packet = packets.shift()
if (typeof packet === "undefined") {
this.writable = true
this.emit("drain")
return
}
// always creates a new object since ws modifies it
const opts: any = {}
if (packet.options) {
opts.compress = packet.options.compress
}
const send = data => {
if (this.perMessageDeflate) {
const len =
"string" === typeof data ? Buffer.byteLength(data) : data.length
if (len < this.perMessageDeflate.threshold) {
opts.compress = false
}
}
console.trace('writing', data)
this.writable = false
this.socket.send(data, opts, err => {
if (err) return this.onError("write error", err.stack)
this.send(packets)
})
}
if (packet.options && typeof packet.options.wsPreEncoded === "string") {
send(packet.options.wsPreEncoded)
} else {
this.parser.encodePacket(packet, this.supportsBinary, send)
}
}
/**
* Closes the transport.
*
* @api private
*/
doClose(fn) {
// debug("closing")
this.socket.close()
fn && fn()
}
}

View File

@@ -1,7 +1,7 @@
/// <reference types="@ccms/nashorn" />
/// <reference types="@javatypes/tomcat-websocket-api" />
import { Server, ServerOptions } from './socket-io'
import { Server, ServerOptions } from './socket.io'
interface SocketIOStatic {
/**
@@ -44,7 +44,7 @@ let io: SocketStatic = function (pipeline: any, options: Partial<ServerOptions>)
}
io.Instance = Symbol("@ccms/websocket")
export default io
export * from './socket-io'
export * from './socket.io'
export * from './client'
export * from './server'
export * from './transport'
export * from './engine.io/transport'

View File

@@ -1,24 +1,26 @@
import { Transport } from '../transport'
import { AttributeKeys } from './constants'
import { WebSocketClient } from '../server/client'
const TextWebSocketFrame = Java.type('io.netty.handler.codec.http.websocketx.TextWebSocketFrame')
export class NettyClient extends Transport {
export class NettyClient extends WebSocketClient {
private channel: any
constructor(server: any, channel: any) {
super(server)
this.remoteAddress = channel.remoteAddress() + ''
this.request = channel.attr(AttributeKeys.Request).get()
this._id = channel.id() + ''
constructor(channel: any) {
super()
this.id = channel.id() + ''
this.channel = channel
}
doSend(text: string) {
this.channel.writeAndFlush(new TextWebSocketFrame(text))
send(text: string, opts?: any, callback?: (err?: Error) => void) {
try {
this.channel.writeAndFlush(new TextWebSocketFrame(text))
callback?.()
} catch (error) {
callback?.(error)
}
}
doClose() {
close() {
this.channel.close()
}
}

View File

@@ -1,6 +1,7 @@
import { JavaServerOptions } from '../server'
import { HttpRequestHandlerAdapter } from './adapter'
import { AttributeKeys } from './constants'
import { ServerOptions } from 'socket-io'
const DefaultHttpResponse = Java.type('io.netty.handler.codec.http.DefaultHttpResponse')
const DefaultFullHttpResponse = Java.type('io.netty.handler.codec.http.DefaultFullHttpResponse')
@@ -18,7 +19,7 @@ const ChannelFutureListener = Java.type('io.netty.channel.ChannelFutureListener'
export class HttpRequestHandler extends HttpRequestHandlerAdapter {
private ws: string
private root: string
constructor(options: ServerOptions) {
constructor(options: JavaServerOptions) {
super()
this.root = options.root
this.ws = options.path

View File

@@ -1,65 +1,70 @@
import { EventEmitter } from 'events'
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { JavaServerOptions, ServerEvent, WebSocketServer } from '../server'
import { Request } from '../server/request'
import { NettyClient } from './client'
import { Keys } from './constants'
import { AttributeKeys, Keys } from './constants'
import { WebSocketDetect } from './websocket_detect'
import { WebSocketHandler } from './websocket_handler'
class NettyWebSocketServer extends EventEmitter {
private pipeline: any
private clients: Map<string, NettyClient>
class NettyWebSocketServer extends WebSocketServer {
constructor(pipeline: any, options: JavaServerOptions) {
super(pipeline, options)
}
constructor(pipeline: any, options: ServerOptions) {
super()
this.clients = new Map()
this.pipeline = pipeline
let connectEvent = options.event
try { this.pipeline.remove(Keys.Detect) } catch (error) { }
this.pipeline.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
protected initialize() {
let connectEvent = this.options.event
try { this.instance.remove(Keys.Detect) } catch (error) { }
this.instance.addFirst(Keys.Detect, new WebSocketDetect(connectEvent).getHandler())
connectEvent.on(ServerEvent.detect, (ctx, channel) => {
channel.pipeline().addFirst(Keys.Handler, new WebSocketHandler(options).getHandler())
channel.pipeline().addFirst(Keys.Handler, new WebSocketHandler(this.options).getHandler())
ctx.fireChannelRead(channel)
})
connectEvent.on(ServerEvent.connect, (ctx) => {
let cid = ctx?.channel().id() + ''
let nettyClient = new NettyClient(this, ctx.channel())
this.clients.set(cid, nettyClient)
this.emit(ServerEvent.connect, nettyClient)
this.onconnect(ctx)
})
connectEvent.on(ServerEvent.message, (ctx, msg) => {
let cid = ctx?.channel().id() + ''
if (this.clients.has(cid)) {
this.emit(ServerEvent.message, this.clients.get(cid), msg.text())
} else {
console.error(`unknow client ${ctx} reciver message ${msg.text()}`)
}
this.onmessage(ctx, msg.text())
})
connectEvent.on(ServerEvent.disconnect, (ctx, cause) => {
let cid = ctx?.channel().id() + ''
if (this.clients.has(cid)) {
this.emit(ServerEvent.disconnect, this.clients.get(cid), cause)
} else {
console.error(`unknow client ${ctx} disconnect cause ${cause}`)
}
this.ondisconnect(ctx, cause)
})
connectEvent.on(ServerEvent.error, (ctx, cause) => {
let cid = ctx?.channel().id() + ''
if (this.clients.has(cid)) {
this.emit(ServerEvent.error, this.clients.get(cid), cause)
} else {
console.error(`unknow client ${ctx} cause error ${cause}`)
console.ex(cause)
}
connectEvent.on(ServerEvent.error, (ctx, error) => {
this.onerror(ctx, error)
})
}
close() {
if (this.pipeline.names().contains(Keys.Detect)) {
this.pipeline.remove(Keys.Detect)
protected getId(ctx: any) {
try {
return ctx.channel().id() + ''
} catch (error) {
console.log(Object.toString.apply(ctx))
console.ex(error)
}
}
protected getRequest(ctx) {
let channel = ctx.channel()
let req = channel.attr(AttributeKeys.Request).get()
let headers = {}
let nativeHeaders = req.headers()
nativeHeaders.forEach(function (header) {
headers[header.getKey()] = header.getValue()
})
let request = new Request(req.uri(), req.method().name(), headers)
request.connection = {
remoteAddress: channel.remoteAddress() + ''
}
return request
}
protected getSocket(ctx) {
return new NettyClient(ctx.channel())
}
protected doClose() {
if (this.instance.names().contains(Keys.Detect)) {
this.instance.remove(Keys.Detect)
}
this.clients.forEach(client => client.close())
}
}

View File

@@ -1,11 +1,11 @@
import { EventEmitter } from 'events'
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { JavaServerOptions, ServerEvent } from '../server'
import { TextWebSocketFrameHandlerAdapter } from './adapter'
export class TextWebSocketFrameHandler extends TextWebSocketFrameHandlerAdapter {
private event: EventEmitter
constructor(options: ServerOptions) {
constructor(options: JavaServerOptions) {
super()
this.event = options.event
}

View File

@@ -1,6 +1,7 @@
import { EventEmitter } from 'events'
import { WebSocketHandlerAdapter } from "./adapter"
import { ServerEvent } from '../socket-io/constants'
import { ServerEvent } from '../server'
export class WebSocketDetect extends WebSocketHandlerAdapter {
private event: EventEmitter

View File

@@ -1,5 +1,4 @@
import { ServerOptions } from '../socket-io'
import { ServerEvent } from '../socket-io/constants'
import { JavaServerOptions, ServerEvent } from '../server'
import { Keys } from './constants'
import { HttpRequestHandler } from './httprequest'
@@ -13,8 +12,8 @@ const HttpObjectAggregator = Java.type('io.netty.handler.codec.http.HttpObjectAg
const WebSocketServerProtocolHandler = Java.type('io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler')
export class WebSocketHandler extends WebSocketHandlerAdapter {
private options: ServerOptions
constructor(options: ServerOptions) {
private options: JavaServerOptions
constructor(options: JavaServerOptions) {
super()
this.options = options
}
@@ -42,13 +41,11 @@ export class WebSocketHandler extends WebSocketHandlerAdapter {
}
channelInactive(ctx: any) {
console.debug('WebSocketHandler channelInactive ' + ctx)
this.options.event.emit(ServerEvent.disconnect, ctx, 'netty channelInactive')
super.channelInactive(ctx)
}
channelUnregistered(ctx: any) {
console.debug('WebSocketHandler channelUnregistered ' + ctx)
this.options.event.emit(ServerEvent.disconnect, ctx, 'netty channelUnregistered')
super.channelUnregistered(ctx)
}

View File

@@ -0,0 +1,7 @@
import { EventEmitter } from 'events'
export abstract class WebSocketClient extends EventEmitter {
public id: string
public _socket: any
abstract send(text: string, opts?: any, callback?: (err?: Error) => void)
abstract close()
}

View File

@@ -1,52 +1,90 @@
import { EventEmitter } from 'events'
import { ServerOptions } from '../socket.io'
import { WebSocketClient } from './client'
import { Transport } from '../transport'
import type { Request } from './request'
interface ServerOptions {
export enum ServerEvent {
detect = 'detect',
request = 'request',
upgrade = 'upgrade',
connect = 'connect',
connection = 'connection',
message = 'message',
error = 'error',
disconnecting = 'disconnecting',
disconnect = 'disconnect',
}
export interface JavaServerOptions extends ServerOptions {
event?: EventEmitter
root?: string
/**
* name of the path to capture
* @default "/socket.io"
*/
path: string
}
interface WebSocketServerImpl extends EventEmitter {
close(): void
}
export class WebSocketServer extends EventEmitter {
options: Partial<ServerOptions>
private websocketServer: WebSocketServerImpl
constructor(instance: any, options: Partial<ServerOptions>) {
export abstract class WebSocketServer extends EventEmitter {
protected instance: any
protected options: JavaServerOptions
private clients: Map<string, WebSocketClient>
constructor(instance: any, options: JavaServerOptions) {
super()
if (!instance) { throw new Error('instance can\'t be undefiend!') }
this.options = Object.assign({
event: new EventEmitter(),
path: '/ws',
root: root + '/wwwroot',
}, options)
this.selectServerImpl(instance)
this.instance = instance
this.options = options
this.clients = new Map()
console.debug('create websocket server from ' + this.constructor.name)
this.initialize()
}
on(event: "connect", cb: (transport: Transport) => void): this
on(event: "message", cb: (transport: Transport, text: string) => void): this
on(event: "disconnect", cb: (transport: Transport, reason: string) => void): this
on(event: "error", cb: (transport: Transport, cause: Error) => void): this
on(event: string, cb: (transport: Transport, extra?: any) => void): this {
this.websocketServer.on(event, cb)
return this
protected onconnect(handler: any) {
let id = this.getId(handler)
console.log('client', id, 'connect')
let request = this.getRequest(handler)
request.id = id
let websocket = this.getSocket(handler)
this.clients.set(this.getId(handler), websocket)
this.emit(ServerEvent.connect, request, websocket)
}
private selectServerImpl(instance: any) {
let WebSocketServerImpl = undefined
if (instance.class.name.startsWith('io.netty.channel')) {
WebSocketServerImpl = require("../netty").NettyWebSocketServer
} else {
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
protected onmessage(handler: any, message: string) {
this.execute(handler, (websocket) => websocket.emit(ServerEvent.message, message))
}
protected ondisconnect(handler: any, cause: string) {
this.execute(handler, (websocket) => websocket.emit(ServerEvent.disconnect, cause))
}
protected onerror(handler: any, error: Error) {
if (global.debug) {
console.ex(error)
}
this.websocketServer = new WebSocketServerImpl(instance, this.options)
this.execute(handler, (websocket) => websocket.emit(ServerEvent.error, error))
}
protected execute(handler: any, callback: (websocket: WebSocketClient) => void) {
let id = this.getId(handler)
if (this.clients.has(id)) {
this.clients.has(id) && callback(this.clients.get(id))
} else {
console.debug('ignore execute', handler, 'callback', callback)
}
}
public close() {
this.clients.forEach(websocket => websocket.close())
this.doClose()
}
protected abstract initialize(): void
protected abstract getId(handler: any): string
protected abstract getRequest(handler: any): Request
protected abstract getSocket(handler: any): WebSocketClient
protected abstract doClose(): void
}
export const attach = (instance, options) => {
if (!instance) { throw new Error('instance can\'t be undefiend!') }
options = Object.assign({
event: new EventEmitter(),
path: '/ws',
root: root + '/wwwroot',
}, options)
let WebSocketServerImpl = undefined
if (instance.class.name.startsWith('io.netty.channel')) {
WebSocketServerImpl = require("../netty").NettyWebSocketServer
} else {
WebSocketServerImpl = require("../tomcat").TomcatWebSocketServer
}
return new WebSocketServerImpl(instance, options)
}

View File

@@ -0,0 +1,23 @@
import { WebSocketClient } from "./client"
interface HttpHeaders {
[name: string]: string
}
interface Connection {
remoteAddress: string
}
export class Request {
public id: string
public url: string
public method: string
public headers: HttpHeaders
public connection: Connection
public websocket: WebSocketClient
public _query: any
constructor(url: string, method = "GET", headers = {}) {
this.url = url
this.method = method
this.headers = headers
}
}

View File

@@ -1,164 +0,0 @@
import { EventEmitter } from 'events'
import { Namespace } from './namespace'
import { Parser } from './parser'
import { Socket } from './socket'
export type SocketId = string
export type Room = string
export interface BroadcastFlags {
volatile?: boolean
compress?: boolean
local?: boolean
broadcast?: boolean
binary?: boolean
}
export interface BroadcastOptions {
rooms: Set<Room>
except?: Set<SocketId>
flags?: BroadcastFlags
}
export class Adapter extends EventEmitter implements Adapter {
rooms: Map<Room, Set<SocketId>>
sids: Map<SocketId, Set<Room>>
private readonly encoder: Parser
parser: Parser
constructor(readonly nsp: Namespace) {
super()
this.rooms = new Map()
this.sids = new Map()
this.parser = nsp.server._parser
this.encoder = this.parser
}
/**
* Adds a socket to a list of room.
*
* @param {String} socket id
* @param {String} rooms
* @param {Function} callback
* @api public
*/
addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
for (const room of rooms) {
if (!this.sids.has(id)) {
this.sids.set(id, new Set())
}
this.sids.get(id).add(room)
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
}
this.rooms.get(room).add(id)
}
}
del(id: string, room: string, callback?: (err?: any) => void): void {
if (this.sids.has(id)) {
this.sids.get(id).delete(room)
}
if (this.rooms.has(room)) {
this.rooms.get(room).delete(id)
if (this.rooms.get(room).size === 0) this.rooms.delete(room)
}
callback && callback.bind(null, null)
}
delAll(id: string): void {
if (!this.sids.has(id)) {
return
}
for (const room of this.sids.get(id)) {
if (this.rooms.has(room)) {
this.rooms.get(room).delete(id)
if (this.rooms.get(room).size === 0) this.rooms.delete(room)
}
}
this.sids.delete(id)
}
/**
* Broadcasts a packet.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @public
*/
public broadcast(packet: any, opts: BroadcastOptions): void {
const rooms = opts.rooms
const except = opts.except || new Set()
const flags = opts.flags || {}
const packetOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
}
const ids = new Set()
packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet)
if (rooms.size) {
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) {
socket.packet(encodedPackets as any, packetOpts)
ids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) socket.packet(encodedPackets as any, packetOpts)
}
}
}
/**
* Gets a list of sockets by sid.
*
* @param {Set<Room>} rooms the explicit set of rooms to check.
*/
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>()
if (rooms.size) {
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (this.nsp.sockets.has(id)) {
sids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (this.nsp.sockets.has(id)) sids.add(id)
}
}
return Promise.resolve(sids)
}
/**
* Gets the list of rooms a given socket has joined.
*
* @param {SocketId} id the socket id
*/
public socketRooms(id: SocketId): Set<Room> | undefined {
return this.sids.get(id)
}
}

View File

@@ -1,360 +0,0 @@
import { EventEmitter } from 'events'
import { Parser } from './parser'
import { Packet } from './packet'
import { Namespace, Server, Socket } from './index'
import { PacketTypes, SubPacketTypes } from './types'
import { ServerEvent } from './constants'
import { SocketId } from './adapter'
import { Transport } from '../transport'
const parser = new Parser()
export class Client extends EventEmitter {
public readonly conn: Transport
/**
* @private
*/
readonly id: string
private readonly server: Server
// private readonly encoder: Encoder
private readonly decoder: any
private sockets: Map<SocketId, Socket>
private nsps: Map<string, Socket>
private connectTimeout: NodeJS.Timeout
private checkIntervalTimer: NodeJS.Timeout
private upgradeTimeoutTimer: NodeJS.Timeout
private pingTimeoutTimer: NodeJS.Timeout
private pingIntervalTimer: NodeJS.Timeout
constructor(server: Server, conn) {
super()
this.server = server
this.conn = conn
// this.encoder = server.encoder
this.decoder = server._parser
this.id = this.conn.id + ''
this.setup()
// =============================
this.sockets = new Map()
this.nsps = new Map()
// ================== engine.io
this.onOpen()
// ================== Transport
this.conn.on(ServerEvent.disconnect, (reason) => {
this.onclose(reason)
})
}
/**
* @return the reference to the request that originated the Engine.IO connection
*
* @public
*/
public get request(): any /**IncomingMessage */ {
return this.conn.request
}
/**
* Sets up event listeners.
*
* @private
*/
private setup() {
// @ts-ignore
// this.decoder.on("decoded", this.ondecoded)
this.conn.on("data", this.ondata.bind(this))
this.conn.on("error", this.onerror.bind(this))
this.conn.on("close", this.onclose.bind(this))
console.debug(`setup client ${this.id}`)
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
console.debug("no namespace joined yet, close the client")
this.close()
} else {
console.debug("the client has already joined a namespace, nothing to do")
}
}, this.server._connectTimeout)
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}) {
console.debug(`client ${this.id} connecting to namespace ${name} has: ${this.server._nsps.has(name)}`)
if (this.server._nsps.has(name)) {
return this.doConnect(name, auth)
}
this.server._checkNamespace(name, auth, (dynamicNsp: Namespace) => {
if (dynamicNsp) {
console.debug(`dynamic namespace ${dynamicNsp.name} was created`)
this.doConnect(name, auth)
} else {
console.debug(`creation of namespace ${name} was denied`)
this._packet({
type: PacketTypes.MESSAGE,
sub_type: SubPacketTypes.ERROR,
nsp: name,
data: {
message: "Invalid namespace"
}
})
}
})
}
doConnect(name, auth: object) {
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = null
}
const nsp = this.server.of(name)
nsp._add(this, auth, (socket: Socket) => {
this.sockets.set(socket.id, socket)
this.nsps.set(nsp.name, socket)
})
}
/**
* Disconnects from all namespaces and closes transport.
*
* @private
*/
_disconnect() {
for (const socket of this.sockets.values()) {
socket.disconnect()
}
this.sockets.clear()
this.close()
}
/**
* Removes a socket. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket) {
if (this.sockets.has(socket.id)) {
this.sockets.delete(socket.id)
this.nsps.delete(socket.nsp.name)
} else {
console.debug(`ignoring remove for ${socket.id}`,)
}
process.nextTick(() => {
if (this.sockets.size == 0) {
this.onclose('no live socket')
}
})
}
/**
* Closes the underlying connection.
*
* @private
*/
private close() {
console.debug(`client ${this.id} close`)
if ("open" == this.conn.readyState) {
console.debug("forcing transport close")
this.onclose("forced server close")
this.conn.close()
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} opts
* @private
*/
_packet(packet: Packet, opts = { preEncoded: false }) {
// opts = opts || {}
// const self = this
// // this writes to the actual connection
// function writeToEngine(encodedPackets) {
// if (opts.volatile && !self.conn.transport.writable) return
// for (let i = 0; i < encodedPackets.length; i++) {
// self.conn.write(encodedPackets[i], { compress: opts.compress })
// }
// }
// if ("open" == this.conn.readyState) {
// debug("writing packet %j", packet)
// if (!opts.preEncoded) {
// // not broadcasting, need to encode
// writeToEngine(this.encoder.encode(packet)) // encode, then write results to engine
// } else {
// // a broadcast pre-encodes a packet
// writeToEngine(packet)
// }
// } else {
// debug("ignoring packet write %j", packet)
// }
if ("open" == this.conn.readyState) {
this.conn.send(opts.preEncoded ? packet as unknown as string : parser.encode(packet))
} else {
console.debug(`ignoring write packet ${JSON.stringify(packet)} to client ${this.id} is already close!`)
}
}
/**
* Called with incoming transport data.
*
* @private
*/
private ondata(data) {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data)
} catch (e) {
this.onerror(e)
}
}
/**
* Called when parser fully decodes a packet.
*
* @private
*/
ondecoded(packet: Packet) {
if (SubPacketTypes.CONNECT == packet.sub_type) {
this.connect(packet.nsp, packet.data)
} else {
process.nextTick(() => {
const socket = this.nsps.get(packet.nsp)
if (socket) {
socket._onpacket(packet)
} else {
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
}
})
}
}
/**
* Handles an error.
*
* @param {Object} err object
* @private
*/
private onerror(err) {
for (const socket of this.sockets.values()) {
socket._onerror(err)
}
this.conn.close()
}
onclose(reason?: string) {
this.conn.readyState = "closing"
// ======= engine.io
this.onClose(reason)
// cleanup connectTimeout
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = null
}
console.debug(`client ${this.id} close with reason ${reason}`)
// ignore a potential subsequent `close` event
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket._onclose(reason)
}
this.sockets.clear()
// this.decoder.destroy(); // clean up decoder
}
destroy() {
// this.conn.removeListener('data', this.ondata);
// this.conn.removeListener('error', this.onerror);
// this.conn.removeListener('close', this.onclose);
// this.decoder.removeListener('decoded', this.ondecoded);
}
//================== engine.io
onOpen() {
this.conn.readyState = "open"
this._packet({
type: PacketTypes.OPEN,
data: {
sid: this.id,
upgrades: [],
pingInterval: this.server.options.pingInterval,
pingTimeout: this.server.options.pingTimeout
}
})
this.schedulePing()
}
onPacket(packet: Packet) {
if ("open" === this.conn.readyState) {
// export packet event
// debug("packet")
// this.emit("packet", packet)
// Reset ping timeout on any packet, incoming data is a good sign of
// other side's liveness
this.resetPingTimeout(this.server.options.pingInterval + this.server.options.pingTimeout * 2)
switch (packet.type) {
case PacketTypes.PING:
this._packet({
type: PacketTypes.PONG,
data: packet.data
})
break
case PacketTypes.PONG:
this.schedulePing()
break
case PacketTypes.UPGRADE:
break
case PacketTypes.MESSAGE:
this.ondecoded(packet)
break
case PacketTypes.CLOSE:
this.onclose()
break
default:
console.log(`client ${this.id} reciver unknow packet type: ${packet.type}`)
}
} else {
console.debug(`packet received with closed client ${this.id}`)
}
}
/**
* Called upon transport considered closed.
* Possible reasons: `ping timeout`, `client error`, `parse error`,
* `transport error`, `server close`, `transport close`
*/
onClose(reason, description?: string) {
// if ("closed" !== this.conn.readyState) {
clearTimeout(this.pingIntervalTimer)
clearTimeout(this.pingTimeoutTimer)
clearInterval(this.checkIntervalTimer)
this.checkIntervalTimer = null
clearTimeout(this.upgradeTimeoutTimer)
// this.emit("close", reason, description)
// }
}
/**
* Pings client every `this.pingInterval` and expects response
* within `this.pingTimeout` or closes connection.
*
* @api private
*/
schedulePing() {
clearTimeout(this.pingIntervalTimer)
this.pingIntervalTimer = setTimeout(() => {
this.resetPingTimeout(this.server.options.pingTimeout)
process.nextTick(() => this._packet({ type: PacketTypes.PING }))
}, this.server.options.pingInterval)
}
/**
* Resets ping timeout.
*
* @api private
*/
resetPingTimeout(timeout: number) {
clearTimeout(this.pingTimeoutTimer)
this.pingTimeoutTimer = setTimeout(() => {
if (this.conn.readyState === "closed") return
this.onclose("ping timeout")
}, timeout)
}
}

View File

@@ -1,9 +0,0 @@
export enum ServerEvent {
detect = 'detect',
connect = 'connect',
connection = 'connection',
message = 'message',
error = 'error',
disconnecting = 'disconnecting',
disconnect = 'disconnect',
}

View File

@@ -1,677 +0,0 @@
import { EventEmitter } from 'events'
import { ServerEvent } from './constants'
import { Namespace } from './namespace'
import { Client } from './client'
import { Parser } from './parser'
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
}

View File

@@ -1,242 +0,0 @@
import { EventEmitter } from 'events'
import { Client } from './client'
import { ServerEvent } from './constants'
import { RESERVED_EVENTS, Socket } from './socket'
import { Adapter, Room, SocketId } from './adapter'
import { Server } from './index'
import { Packet } from './packet'
import { PacketTypes, SubPacketTypes } from './types'
export interface ExtendedError extends Error {
data?: any
}
export class Namespace extends EventEmitter {
public readonly name: string
public readonly sockets: Map<SocketId, Socket>
public adapter: Adapter
/** @private */
readonly server: Server
json: Namespace
/** @private */
_fns: Array<
(socket: Socket, next: (err: ExtendedError) => void) => void
> = [];
/** @private */
_rooms: Set<Room>
/** @private */
_flags: any = {}
/** @private */
_ids: number = 0
constructor(server: Server, name: string) {
super()
this.server = server
this.name = name + ''
this._initAdapter()
// =======================
this.sockets = new Map()
this._rooms = new Set()
}
_initAdapter() {
// @ts-ignore
this.adapter = new (this.server.adapter())(this)
}
/**
* Sets up namespace middleware.
*
* @return {Namespace} self
* @public
*/
public use(
fn: (socket: Socket, next: (err?: ExtendedError) => void) => void
): Namespace {
this._fns.push(fn)
return this
}
/**
* Executes the middleware for an incoming client.
*
* @param {Socket} socket - the socket that will get added
* @param {Function} fn - last fn call in the middleware
* @private
*/
private run(socket: Socket, fn: (err: ExtendedError) => void) {
const fns = this._fns.slice(0)
if (!fns.length) return fn(null)
function run(i) {
fns[i](socket, function (err) {
// upon error, short-circuit
if (err) return fn(err)
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null)
// go on to next
run(i + 1)
})
}
run(0)
}
to(name: string): Namespace {
this._rooms.add(name)
return this
}
in(name: string): Namespace {
return this.to(name)
}
_add(client: Client, query?: any, fn?: (socket: Socket) => void) {
const socket = new Socket(this, client, query || {})
console.debug(`client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
this.run(socket, err => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err)
return socket._error({
message: err.message,
data: err.data
})
// track socket
this.sockets.set(socket.id, socket)
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// !!! at java multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events
super.emit(ServerEvent.connect, socket)
super.emit(ServerEvent.connection, socket)
} else {
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
}
})
})
return socket
}
/**
* Removes a client. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket): void {
if (this.sockets.has(socket.id)) {
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
this.sockets.delete(socket.id)
} else {
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`)
}
}
emit(event: string, ...args: any[]): boolean {
if (RESERVED_EVENTS.has(event)) {
throw new Error(`"${event}" is a reserved event name`)
}
// set up packet object
var packet = {
type: PacketTypes.MESSAGE,
sub_type: (this._flags.binary !== undefined ? this._flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
name: event,
data: args
}
if ('function' == typeof args[args.length - 1]) {
throw new Error('Callbacks are not supported when broadcasting')
}
var rooms = new Set(this._rooms)
var flags = Object.assign({}, this._flags)
// reset flags
this._rooms.clear()
this._flags = {}
this.adapter.broadcast(packet, {
rooms: new Set(rooms),
flags: flags
})
// @ts-ignore
return this
}
send(...args: any[]): Namespace {
this.emit('message', ...args)
return this
}
write(...args: any[]): Namespace {
return this.send(...args)
}
/**
* Gets a list of clients.
*
* @return {Namespace} self
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error("No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?")
}
const rooms = new Set(this._rooms)
this._rooms.clear()
return this.adapter.sockets(rooms)
}
/**
* Sets the compress flag.
*
* @param {Boolean} compress - if `true`, compresses the sending data
* @return {Namespace} self
* @public
*/
public compress(compress: boolean): Namespace {
this._flags.compress = compress
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return {Namespace} self
* @public
*/
public get volatile(): Namespace {
this._flags.volatile = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Namespace} self
* @public
*/
public get local(): Namespace {
this._flags.local = true
return this
}
hasBin(args: any[]) {
return false
}
clients(fn: (sockets: Socket[]) => Namespace): Namespace {
return fn(Object.values(this.sockets))
}
close() {
this.removeAllListeners(ServerEvent.connect)
this.removeAllListeners(ServerEvent.connection)
Object.values(this.sockets).forEach(socket => socket.disconnect(false))
}
}

View File

@@ -1,11 +0,0 @@
import { PacketTypes, SubPacketTypes } from './types'
export interface Packet {
type: PacketTypes;
sub_type?: SubPacketTypes;
nsp?: string;
id?: number;
name?: string;
data?: any;
attachments?: any;
}

View File

@@ -1,40 +0,0 @@
import { Namespace } from "./namespace"
export class ParentNamespace extends Namespace {
private static count: number = 0;
private children: Set<Namespace> = new Set();
constructor(server) {
super(server, "/_" + ParentNamespace.count++)
}
_initAdapter() { }
public emit(...args: any[]): boolean {
this.children.forEach(nsp => {
nsp._rooms = this._rooms
nsp._flags = this._flags
nsp.emit.apply(nsp, args as any)
})
this._rooms.clear()
this._flags = {}
return true
}
createChild(name) {
const namespace = new Namespace(this.server, name)
namespace._fns = this._fns.slice(0)
this.listeners("connect").forEach(listener =>
// @ts-ignore
namespace.on("connect", listener)
)
this.listeners("connection").forEach(listener =>
// @ts-ignore
namespace.on("connection", listener)
)
this.children.add(namespace)
this.server._nsps.set(name, namespace)
return namespace
}
}

View File

@@ -1,164 +0,0 @@
import { EventEmitter } from 'events'
import { Packet } from "./packet"
import { PacketTypes, SubPacketTypes } from "./types"
export class Parser extends EventEmitter {
encode(packet: Packet): string {
let origin = JSON.stringify(packet)
// first is type
let str = '' + packet.type
if (packet.type == PacketTypes.PONG) {
if (packet.data) { str += packet.data };
return str
}
if (packet.sub_type != undefined) {
str += packet.sub_type
}
// attachments if we have them
if ([SubPacketTypes.BINARY_EVENT, SubPacketTypes.BINARY_ACK].includes(packet.sub_type)) {
str += packet.attachments + '-'
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (packet.nsp && '/' !== packet.nsp) {
str += packet.nsp + ','
}
// immediately followed by the id
if (null != packet.id) {
str += packet.id
}
if (packet.sub_type == SubPacketTypes.EVENT) {
if (packet.name == undefined) { throw new Error(`SubPacketTypes.EVENT name can't be empty!`) }
packet.data = [packet.name, ...packet.data]
}
// json data
if (null != packet.data) {
let payload = this.tryStringify(packet.data)
if (payload !== false) {
str += payload
} else {
return '4"encode error"'
}
}
console.trace(`encoded ${origin} as ${str}`)
return str
}
tryStringify(str: any) {
try {
return JSON.stringify(str)
} catch (e) {
return false
}
}
decode(str: string): Packet {
let i = 0
// ignore parse binary
// if ((frame.getByte(0) == 'b' && frame.getByte(1) == '4')
// || frame.getByte(0) == 4 || frame.getByte(0) == 1) {
// return parseBinary(head, frame);
// }
// look up type
let p: Packet = {
type: Number(str.charAt(i))
}
if (null == PacketTypes[p.type]) {
return this.error('unknown packet type ' + p.type)
}
// if str empty return
if (str.length == i + 1) {
return p
}
// if is ping packet read data and return
if (PacketTypes.PING == p.type) {
p.data = str.substr(++i)
return p
}
// look up sub type
p.sub_type = Number(str.charAt(++i))
if (null == PacketTypes[p.sub_type]) {
return this.error('unknown sub packet type ' + p.type)
}
// look up attachments if type binary
if ([SubPacketTypes.BINARY_ACK, SubPacketTypes.BINARY_EVENT].includes(p.sub_type)) {
let buf = ''
while (str.charAt(++i) !== '-') {
buf += str.charAt(i)
if (i == str.length) break
}
if (buf != `${Number(buf)}` || str.charAt(i) !== '-') {
return this.error('Illegal attachments')
}
p.attachments = Number(buf)
}
// look up namespace (if any)
if ('/' === str.charAt(i + 1)) {
p.nsp = ''
while (++i) {
let c = str.charAt(i)
if (',' === c) break
p.nsp += c
if (i === str.length) break
}
} else {
p.nsp = '/'
}
// handle namespace query
if (p.nsp.indexOf('?') !== -1) {
p.nsp = p.nsp.split('?')[0]
}
// look up id
let next = str.charAt(i + 1)
if ('' !== next && !isNaN(Number(next))) {
let id = ''
while (++i) {
let c = str.charAt(i)
if (null == c || isNaN(Number(c))) {
--i
break
}
id += str.charAt(i)
if (i === str.length) break
}
p.id = Number(id)
}
// ignore binary packet
if (p.sub_type == SubPacketTypes.BINARY_EVENT) {
return this.error('not support binary parse...')
}
// look up json data
if (str.charAt(++i)) {
let payload = this.tryParse(str.substr(i))
let isPayloadValid = payload !== false && (p.sub_type == SubPacketTypes.ERROR || Array.isArray(payload))
if (isPayloadValid) {
p.name = payload[0]
p.data = payload.slice(1)
} else {
return this.error('invalid payload ' + str.substr(i))
}
}
console.trace(`decoded ${str} as ${JSON.stringify(p)}`)
return p
}
tryParse(str: string) {
try {
return JSON.parse(str)
} catch (e) {
return false
}
}
error(error: string): Packet {
return {
type: PacketTypes.MESSAGE,
sub_type: SubPacketTypes.ERROR,
data: 'parser error: ' + error
}
}
}

View File

@@ -1,491 +0,0 @@
import { EventEmitter } from 'events'
import { Packet } from './packet'
import { PacketTypes, SubPacketTypes } from './types'
import { Client } from './client'
import { Namespace } from './namespace'
import * as querystring from 'querystring'
import { ServerEvent } from './constants'
import { Adapter, BroadcastFlags, Room, SocketId } from './adapter'
import { Server } from 'index'
export const RESERVED_EVENTS = new Set([
"connect",
"connect_error",
"disconnect",
"disconnecting",
// EventEmitter reserved events: https://nodejs.org/api/events.html#events_event_newlistener
"newListener",
"removeListener"
])
/**
* The handshake details
*/
export interface Handshake {
/**
* The headers sent as part of the handshake
*/
headers: object
/**
* The date of creation (as string)
*/
time: string
/**
* The ip of the client
*/
address: string
/**
* Whether the connection is cross-domain
*/
xdomain: boolean
/**
* Whether the connection is secure
*/
secure: boolean
/**
* The date of creation (as unix timestamp)
*/
issued: number
/**
* The request URL string
*/
url: string
/**
* The query object
*/
query: any
/**
* The auth object
*/
auth: any
}
export class Socket extends EventEmitter {
nsp: Namespace
public readonly id: SocketId
public readonly handshake: Handshake
public connected: boolean
public disconnected: boolean
private readonly server: Server
private readonly adapter: Adapter
client: Client
private acks: Map<number, () => void>
fns: any[]
private flags: BroadcastFlags = {};
private _rooms: Set<Room> = new Set();
private _anyListeners: Array<(...args: any[]) => void>
constructor(nsp: Namespace, client: Client, auth = {}) {
super()
this.nsp = nsp
this.server = nsp.server
this.adapter = this.nsp.adapter
this.id = nsp.name !== '/' ? nsp.name + '#' + client.id : client.id
this.client = client
this.acks = new Map()
this.connected = true
this.disconnected = false
this.handshake = this.buildHandshake(auth)
this.fns = []
this.flags = {}
this._rooms = new Set()
}
emit(event: string, ...args: any[]): boolean {
let packet: Packet = {
type: PacketTypes.MESSAGE,
sub_type: (this.flags.binary !== undefined ? this.flags.binary : this.hasBin(args)) ? SubPacketTypes.BINARY_EVENT : SubPacketTypes.EVENT,
name: event,
data: args
}
// access last argument to see if it's an ACK callback
if (typeof args[args.length - 1] === "function") {
if (this._rooms.size || this.flags.broadcast) {
throw new Error("Callbacks are not supported when broadcasting")
}
// console.debug("emitting packet with ack id %d", this.nsp._ids)
this.acks.set(this.nsp._ids, args.pop())
packet.id = this.nsp._ids++
}
const rooms = new Set(this._rooms)
const flags = Object.assign({}, this.flags)
// reset flags
this._rooms.clear()
this.flags = {}
if (rooms.size || flags.broadcast) {
this.adapter.broadcast(packet, {
except: new Set([this.id]),
rooms: rooms,
flags: flags
})
} else {
// dispatch packet
this.packet(packet, flags)
}
return true
}
to(name: Room): Socket {
this._rooms.add(name)
return this
}
in(room: string): Socket {
return this.to(room)
}
use(fn: (packet: Packet, next: (err?: any) => void) => void): Socket {
throw new Error("Method not implemented.")
}
send(...args: any[]): Socket {
this.emit("message", ...args)
return this
}
write(...args: any[]): Socket {
return this.send(...args)
}
public join(rooms: Room | Array<Room>): Promise<void> | void {
console.debug(`join room ${rooms}`)
return this.adapter.addAll(
this.id,
new Set(Array.isArray(rooms) ? rooms : [rooms])
)
}
/**
* Leaves a room.
*
* @param {String} room
* @return a Promise or nothing, depending on the adapter
* @public
*/
public leave(room: string): Promise<void> | void {
console.debug(`leave room ${room}`)
return this.adapter.del(this.id, room)
}
/**
* Leave all rooms.
*
* @private
*/
private leaveAll(): void {
this.adapter.delAll(this.id)
}
/**
* Called by `Namespace` upon successful
* middleware execution (ie: authorization).
* Socket is added to namespace array before
* call to join, so adapters can access it.
*
* @private
*/
_onconnect(): void {
console.debug(`socket ${this.id} connected - writing packet`)
this.join(this.id)
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.CONNECT, data: { sid: this.id } })
}
_onpacket(packet: Packet) {
switch (packet.sub_type) {
// 2
case SubPacketTypes.EVENT:
this.onevent(packet)
break
// 5
case SubPacketTypes.BINARY_EVENT:
this.onevent(packet)
break
// 3
case SubPacketTypes.ACK:
this.onack(packet)
break
// 6
case SubPacketTypes.BINARY_ACK:
this.onack(packet)
break
// 1
case SubPacketTypes.DISCONNECT:
this.ondisconnect()
break
// 4
case SubPacketTypes.ERROR:
this._onerror(new Error(packet.data))
}
}
onevent(packet: Packet) {
if (null != packet.id) {
console.trace(`attaching ack ${packet.id} callback to client ${this.id} event`)
this.dispatch(packet, this.ack(packet.id))
} else {
this.dispatch(packet)
}
}
ack(id: number) {
let sent = false
return (...args: any[]) => {
if (sent) return
this.packet({
id: id,
type: PacketTypes.MESSAGE,
sub_type: this.hasBin(args) ? SubPacketTypes.BINARY_ACK : SubPacketTypes.ACK,
data: args
})
sent = true
}
}
onack(packet: Packet) {
let ack = this.acks.get(packet.id)
if ('function' == typeof ack) {
console.trace(`calling ack ${packet.id} on socket ${this.id} with ${packet.data}`)
ack.apply(this, packet.data)
this.acks.delete(packet.id)
} else {
console.trace(`bad ack ${packet.id} on socket ${this.id}`)
}
}
/**
* Called upon client disconnect packet.
*
* @private
*/
private ondisconnect(): void {
console.debug(`socket ${this.id} got disconnect packet`)
this._onclose("client namespace disconnect")
}
/**
* Handles a client error.
*
* @private
*/
_onerror(err): void {
if (this.listeners("error").length) {
super.emit("error", err)
} else {
console.error(`Missing error handler on 'socket(${this.id})'.`)
console.error(err.stack)
}
}
/**
* Called upon closing. Called by `Client`.
*
* @param {String} reason
* @throw {Error} optional error object
*
* @private
*/
_onclose(reason: string) {
if (!this.connected) return this
console.debug(`closing socket ${this.id} - reason: ${reason} connected: ${this.connected}`)
super.emit(ServerEvent.disconnecting, reason)
this.leaveAll()
this.nsp._remove(this)
this.client._remove(this)
this.connected = false
this.disconnected = true
super.emit(ServerEvent.disconnect, reason)
}
/**
* Produces an `error` packet.
*
* @param {Object} err - error object
*
* @private
*/
_error(err) {
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.ERROR, data: err })
}
disconnect(close?: boolean): Socket {
if (!this.connected) return this
if (close) {
this.client._disconnect()
} else {
this.packet({ type: PacketTypes.MESSAGE, sub_type: SubPacketTypes.DISCONNECT })
this._onclose('server namespace disconnect')
}
return this
}
compress(compress: boolean): Socket {
throw new Error("Method not implemented.")
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return {Socket} self
* @public
*/
public get volatile(): Socket {
this.flags.volatile = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to every sockets but the
* sender.
*
* @return {Socket} self
* @public
*/
public get broadcast(): Socket {
this.flags.broadcast = true
return this
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return {Socket} self
* @public
*/
public get local(): Socket {
this.flags.local = true
return this
}
/**
* A reference to the request that originated the underlying Engine.IO Socket.
*
* @public
*/
public get request(): any {
return this.client.request
}
/**
* A reference to the underlying Client transport connection (Engine.IO Socket object).
*
* @public
*/
public get conn() {
return this.client.conn
}
/**
* @public
*/
public get rooms(): Set<Room> {
return this.adapter.socketRooms(this.id) || new Set()
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback.
*
* @param listener
* @public
*/
public onAny(listener: (...args: any[]) => void): Socket {
this._anyListeners = this._anyListeners || []
this._anyListeners.push(listener)
return this
}
/**
* Adds a listener that will be fired when any event is emitted. The event name is passed as the first argument to the
* callback. The listener is added to the beginning of the listeners array.
*
* @param listener
* @public
*/
public prependAny(listener: (...args: any[]) => void): Socket {
this._anyListeners = this._anyListeners || []
this._anyListeners.unshift(listener)
return this
}
/**
* Removes the listener that will be fired when any event is emitted.
*
* @param listener
* @public
*/
public offAny(listener?: (...args: any[]) => void): Socket {
if (!this._anyListeners) {
return this
}
if (listener) {
const listeners = this._anyListeners
for (let i = 0; i < listeners.length; i++) {
if (listener === listeners[i]) {
listeners.splice(i, 1)
return this
}
}
} else {
this._anyListeners = []
}
return this
}
/**
* Returns an array of listeners that are listening for any event that is specified. This array can be manipulated,
* e.g. to remove listeners.
*
* @public
*/
public listenersAny() {
return this._anyListeners || []
}
// ==========================================
buildHandshake(auth): Handshake {
let requestUri = this.request.uri()
let headers = {}
let nativeHeaders = this.request.headers()
nativeHeaders.forEach(function (header) {
headers[header.getKey()] = header.getValue()
})
return {
headers: headers,
time: new Date() + '',
address: this.conn.remoteAddress + '',
xdomain: !!headers['origin'],
secure: false,
issued: +new Date(),
url: requestUri,
query: querystring.parse(requestUri.indexOf('?') != -1 ? requestUri.split('?')[1] : ''),
auth
}
}
packet(packet: Packet, opts: any = { preEncoded: false }) {
if (!opts.preEncoded) {
packet.nsp = this.nsp.name
opts.compress = false !== opts.compress
}
try {
this.client._packet(packet, opts)
} catch (error) {
this._onerror(error)
}
}
dispatch(packet: Packet, ack?: () => void) {
if (ack) { this.acks.set(packet.id, ack) }
super.emit(packet.name, ...packet.data, ack)
}
private hasBin(obj: any) {
return false
}
}

View File

@@ -1,18 +0,0 @@
export enum PacketTypes {
OPEN,
CLOSE,
PING,
PONG,
MESSAGE,
UPGRADE,
NOOP,
}
export enum SubPacketTypes {
CONNECT,
DISCONNECT,
EVENT,
ACK,
ERROR,
BINARY_EVENT,
BINARY_ACK
}

View File

@@ -0,0 +1,279 @@
import { EventEmitter } from "events"
export type SocketId = string
export type Room = string
export interface BroadcastFlags {
volatile?: boolean
compress?: boolean
local?: boolean
broadcast?: boolean
binary?: boolean
}
export interface BroadcastOptions {
rooms: Set<Room>
except?: Set<SocketId>
flags?: BroadcastFlags
}
export class Adapter extends EventEmitter {
public rooms: Map<Room, Set<SocketId>> = new Map();
public sids: Map<SocketId, Set<Room>> = new Map();
private readonly encoder
/**
* In-memory adapter constructor.
*
* @param {Namespace} nsp
*/
constructor(readonly nsp: any) {
super()
this.encoder = nsp.server.encoder
}
/**
* To be overridden
*/
public init(): Promise<void> | void { }
/**
* To be overridden
*/
public close(): Promise<void> | void { }
/**
* Adds a socket to a list of room.
*
* @param {SocketId} id the socket id
* @param {Set<Room>} rooms a set of rooms
* @public
*/
public addAll(id: SocketId, rooms: Set<Room>): Promise<void> | void {
if (!this.sids.has(id)) {
this.sids.set(id, new Set())
}
for (const room of rooms) {
this.sids.get(id).add(room)
if (!this.rooms.has(room)) {
this.rooms.set(room, new Set())
this.emit("create-room", room)
}
if (!this.rooms.get(room).has(id)) {
this.rooms.get(room).add(id)
this.emit("join-room", room, id)
}
}
}
/**
* Removes a socket from a room.
*
* @param {SocketId} id the socket id
* @param {Room} room the room name
*/
public del(id: SocketId, room: Room): Promise<void> | void {
if (this.sids.has(id)) {
this.sids.get(id).delete(room)
}
this._del(room, id)
}
private _del(room, id) {
if (this.rooms.has(room)) {
const deleted = this.rooms.get(room).delete(id)
if (deleted) {
this.emit("leave-room", room, id)
}
if (this.rooms.get(room).size === 0) {
this.rooms.delete(room)
this.emit("delete-room", room)
}
}
}
/**
* Removes a socket from all rooms it's joined.
*
* @param {SocketId} id the socket id
*/
public delAll(id: SocketId): void {
if (!this.sids.has(id)) {
return
}
for (const room of this.sids.get(id)) {
this._del(room, id)
}
this.sids.delete(id)
}
/**
* Broadcasts a packet.
*
* Options:
* - `flags` {Object} flags for this packet
* - `except` {Array} sids that should be excluded
* - `rooms` {Array} list of rooms to broadcast to
*
* @param {Object} packet the packet object
* @param {Object} opts the options
* @public
*/
public broadcast(packet: any, opts: BroadcastOptions): void {
const flags = opts.flags || {}
const basePacketOpts = {
preEncoded: true,
volatile: flags.volatile,
compress: flags.compress
}
packet.nsp = this.nsp.name
const encodedPackets = this.encoder.encode(packet)
const packetOpts = encodedPackets.map(encodedPacket => {
if (typeof encodedPacket === "string") {
return {
...basePacketOpts,
wsPreEncoded: "4" + encodedPacket // "4" being the "message" packet type in Engine.IO
}
} else {
return basePacketOpts
}
})
this.apply(opts, socket => {
for (let i = 0; i < encodedPackets.length; i++) {
socket.client.writeToEngine(encodedPackets[i], packetOpts[i])
}
})
}
/**
* Gets a list of sockets by sid.
*
* @param {Set<Room>} rooms the explicit set of rooms to check.
*/
public sockets(rooms: Set<Room>): Promise<Set<SocketId>> {
const sids = new Set<SocketId>()
this.apply({ rooms }, socket => {
sids.add(socket.id)
})
return Promise.resolve(sids)
}
/**
* Gets the list of rooms a given socket has joined.
*
* @param {SocketId} id the socket id
*/
public socketRooms(id: SocketId): Set<Room> | undefined {
return this.sids.get(id)
}
/**
* Returns the matching socket instances
*
* @param opts - the filters to apply
*/
public fetchSockets(opts: BroadcastOptions): Promise<any[]> {
const sockets = []
this.apply(opts, socket => {
sockets.push(socket)
})
return Promise.resolve(sockets)
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param opts - the filters to apply
* @param rooms - the rooms to join
*/
public addSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => {
socket.join(rooms)
})
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param opts - the filters to apply
* @param rooms - the rooms to leave
*/
public delSockets(opts: BroadcastOptions, rooms: Room[]): void {
this.apply(opts, socket => {
rooms.forEach(room => socket.leave(room))
})
}
/**
* Makes the matching socket instances disconnect
*
* @param opts - the filters to apply
* @param close - whether to close the underlying connection
*/
public disconnectSockets(opts: BroadcastOptions, close: boolean): void {
this.apply(opts, socket => {
socket.disconnect(close)
})
}
private apply(opts: BroadcastOptions, callback: (socket) => void): void {
const rooms = opts.rooms
const except = this.computeExceptSids(opts.except)
if (rooms.size) {
const ids = new Set()
for (const room of rooms) {
if (!this.rooms.has(room)) continue
for (const id of this.rooms.get(room)) {
if (ids.has(id) || except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) {
callback(socket)
ids.add(id)
}
}
}
} else {
for (const [id] of this.sids) {
if (except.has(id)) continue
const socket = this.nsp.sockets.get(id)
if (socket) callback(socket)
}
}
}
private computeExceptSids(exceptRooms?: Set<Room>) {
const exceptSids = new Set()
if (exceptRooms && exceptRooms.size > 0) {
for (const room of exceptRooms) {
if (this.rooms.has(room)) {
this.rooms.get(room).forEach(sid => exceptSids.add(sid))
}
}
}
return exceptSids
}
/**
* Send a packet to the other Socket.IO servers in the cluster
* @param packet - an array of arguments, which may include an acknowledgement callback at the end
*/
public serverSideEmit(packet: any[]): void {
throw new Error(
"this adapter does not support the serverSideEmit() functionality"
)
}
}

View File

@@ -0,0 +1,78 @@
import { isBinary } from "./is-binary"
/**
* Replaces every Buffer | ArrayBuffer | Blob | File in packet with a numbered placeholder.
*
* @param {Object} packet - socket.io event packet
* @return {Object} with deconstructed packet and list of buffers
* @public
*/
export function deconstructPacket(packet) {
const buffers = []
const packetData = packet.data
const pack = packet
pack.data = _deconstructPacket(packetData, buffers)
pack.attachments = buffers.length // number of binary 'attachments'
return { packet: pack, buffers: buffers }
}
function _deconstructPacket(data, buffers) {
if (!data) return data
if (isBinary(data)) {
const placeholder = { _placeholder: true, num: buffers.length }
buffers.push(data)
return placeholder
} else if (Array.isArray(data)) {
const newData = new Array(data.length)
for (let i = 0; i < data.length; i++) {
newData[i] = _deconstructPacket(data[i], buffers)
}
return newData
} else if (typeof data === "object" && !(data instanceof Date)) {
const newData = {}
for (const key in data) {
if (data.hasOwnProperty(key)) {
newData[key] = _deconstructPacket(data[key], buffers)
}
}
return newData
}
return data
}
/**
* Reconstructs a binary packet from its placeholder packet and buffers
*
* @param {Object} packet - event packet with placeholders
* @param {Array} buffers - binary buffers to put in placeholder positions
* @return {Object} reconstructed packet
* @public
*/
export function reconstructPacket(packet, buffers) {
packet.data = _reconstructPacket(packet.data, buffers)
packet.attachments = undefined // no longer useful
return packet
}
function _reconstructPacket(data, buffers) {
if (!data) return data
if (data && data._placeholder) {
return buffers[data.num] // appropriate buffer (should be natural order anyway)
} else if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
data[i] = _reconstructPacket(data[i], buffers)
}
} else if (typeof data === "object") {
for (const key in data) {
if (data.hasOwnProperty(key)) {
data[key] = _reconstructPacket(data[key], buffers)
}
}
}
return data
}

View File

@@ -0,0 +1,316 @@
import EventEmitter = require("events")
import { deconstructPacket, reconstructPacket } from "./binary"
import { isBinary, hasBinary } from "./is-binary"
// const debug = require("debug")("socket.io-parser")
/**
* Protocol version.
*
* @public
*/
export const protocol: number = 5
export enum PacketType {
CONNECT,
DISCONNECT,
EVENT,
ACK,
CONNECT_ERROR,
BINARY_EVENT,
BINARY_ACK,
}
export interface Packet {
type: PacketType
nsp: string
data?: any
id?: number
attachments?: number
}
/**
* A socket.io Encoder instance
*/
export class Encoder {
/**
* Encode a packet as a single string if non-binary, or as a
* buffer sequence, depending on packet type.
*
* @param {Object} obj - packet object
*/
public encode(obj: Packet) {
console.trace("encoding packet", JSON.stringify(obj))
if (obj.type === PacketType.EVENT || obj.type === PacketType.ACK) {
if (hasBinary(obj)) {
obj.type =
obj.type === PacketType.EVENT
? PacketType.BINARY_EVENT
: PacketType.BINARY_ACK
return this.encodeAsBinary(obj)
}
}
return [this.encodeAsString(obj)]
}
/**
* Encode packet as string.
*/
private encodeAsString(obj: Packet) {
// first is type
let str = "" + obj.type
// attachments if we have them
if (
obj.type === PacketType.BINARY_EVENT ||
obj.type === PacketType.BINARY_ACK
) {
str += obj.attachments + "-"
}
// if we have a namespace other than `/`
// we append it followed by a comma `,`
if (obj.nsp && "/" !== obj.nsp) {
str += obj.nsp + ","
}
// immediately followed by the id
if (null != obj.id) {
str += obj.id
}
// json data
if (null != obj.data) {
str += JSON.stringify(obj.data)
}
console.trace("encoded", JSON.stringify(obj), "as", str)
return str
}
/**
* Encode packet as 'buffer sequence' by removing blobs, and
* deconstructing packet into object with placeholders and
* a list of buffers.
*/
private encodeAsBinary(obj: Packet) {
const deconstruction = deconstructPacket(obj)
const pack = this.encodeAsString(deconstruction.packet)
const buffers = deconstruction.buffers
buffers.unshift(pack) // add packet info to beginning of data list
return buffers // write all the buffers
}
}
/**
* A socket.io Decoder instance
*
* @return {Object} decoder
*/
export class Decoder extends EventEmitter {
private reconstructor: BinaryReconstructor
constructor() {
super()
}
/**
* Decodes an encoded packet string into packet JSON.
*
* @param {String} obj - encoded packet
*/
public add(obj: any) {
let packet
if (typeof obj === "string") {
packet = this.decodeString(obj)
if (
packet.type === PacketType.BINARY_EVENT ||
packet.type === PacketType.BINARY_ACK
) {
// binary packet's json
this.reconstructor = new BinaryReconstructor(packet)
// no attachments, labeled binary but no binary data to follow
if (packet.attachments === 0) {
super.emit("decoded", packet)
}
} else {
// non-binary full packet
super.emit("decoded", packet)
}
} else if (isBinary(obj) || obj.base64) {
// raw binary data
if (!this.reconstructor) {
throw new Error("got binary data when not reconstructing a packet")
} else {
packet = this.reconstructor.takeBinaryData(obj)
if (packet) {
// received final buffer
this.reconstructor = null
super.emit("decoded", packet)
}
}
} else {
throw new Error("Unknown type: " + obj)
}
}
/**
* Decode a packet String (JSON data)
*
* @param {String} str
* @return {Object} packet
*/
private decodeString(str): Packet {
let i = 0
// look up type
const p: any = {
type: Number(str.charAt(0)),
}
if (PacketType[p.type] === undefined) {
throw new Error("unknown packet type " + p.type)
}
// look up attachments if type binary
if (
p.type === PacketType.BINARY_EVENT ||
p.type === PacketType.BINARY_ACK
) {
const start = i + 1
while (str.charAt(++i) !== "-" && i != str.length) { }
const buf = str.substring(start, i)
if (buf != Number(buf) || str.charAt(i) !== "-") {
throw new Error("Illegal attachments")
}
p.attachments = Number(buf)
}
// look up namespace (if any)
if ("/" === str.charAt(i + 1)) {
const start = i + 1
while (++i) {
const c = str.charAt(i)
if ("," === c) break
if (i === str.length) break
}
p.nsp = str.substring(start, i)
} else {
p.nsp = "/"
}
// look up id
const next = str.charAt(i + 1)
if ("" !== next && Number(next) == next) {
const start = i + 1
while (++i) {
const c = str.charAt(i)
if (null == c || Number(c) != c) {
--i
break
}
if (i === str.length) break
}
p.id = Number(str.substring(start, i + 1))
}
// look up json data
if (str.charAt(++i)) {
const payload = tryParse(str.substr(i))
if (Decoder.isPayloadValid(p.type, payload)) {
p.data = payload
} else {
throw new Error("invalid payload")
}
}
console.trace("decoded", str, "as", p)
return p
}
private static isPayloadValid(type: PacketType, payload: any): boolean {
switch (type) {
case PacketType.CONNECT:
return typeof payload === "object"
case PacketType.DISCONNECT:
return payload === undefined
case PacketType.CONNECT_ERROR:
return typeof payload === "string" || typeof payload === "object"
case PacketType.EVENT:
case PacketType.BINARY_EVENT:
return Array.isArray(payload) && payload.length > 0
case PacketType.ACK:
case PacketType.BINARY_ACK:
return Array.isArray(payload)
}
}
/**
* Deallocates a parser's resources
*/
public destroy() {
if (this.reconstructor) {
this.reconstructor.finishedReconstruction()
}
}
}
function tryParse(str) {
try {
return JSON.parse(str)
} catch (e) {
return false
}
}
/**
* A manager of a binary event's 'buffer sequence'. Should
* be constructed whenever a packet of type BINARY_EVENT is
* decoded.
*
* @param {Object} packet
* @return {BinaryReconstructor} initialized reconstructor
*/
class BinaryReconstructor {
private reconPack
private buffers: Array<Buffer | ArrayBuffer> = [];
constructor(readonly packet: Packet) {
this.reconPack = packet
}
/**
* Method to be called when binary data received from connection
* after a BINARY_EVENT packet.
*
* @param {Buffer | ArrayBuffer} binData - the raw binary data received
* @return {null | Object} returns null if more binary data is expected or
* a reconstructed packet object if all buffers have been received.
*/
public takeBinaryData(binData) {
this.buffers.push(binData)
if (this.buffers.length === this.reconPack.attachments) {
// done with buffer list
const packet = reconstructPacket(this.reconPack, this.buffers)
this.finishedReconstruction()
return packet
}
return null
}
/**
* Cleans up binary packet reconstruction variables.
*/
public finishedReconstruction() {
this.reconPack = null
this.buffers = []
}
}

View File

@@ -0,0 +1,65 @@
const withNativeArrayBuffer: boolean = typeof ArrayBuffer === "function"
const isView = (obj: any) => {
return typeof ArrayBuffer.isView === "function"
? ArrayBuffer.isView(obj)
: obj.buffer instanceof ArrayBuffer
}
const toString = Object.prototype.toString
const withNativeBlob = false
// typeof Blob === "function" ||
// (typeof Blob !== "undefined" &&
// toString.call(Blob) === "[object BlobConstructor]")
const withNativeFile = false
// typeof File === "function" ||
// (typeof File !== "undefined" &&
// toString.call(File) === "[object FileConstructor]")
/**
* Returns true if obj is a Buffer, an ArrayBuffer, a Blob or a File.
*
* @private
*/
export function isBinary(obj: any) {
return (
(withNativeArrayBuffer && (obj instanceof ArrayBuffer || isView(obj)))
// || (withNativeBlob && obj instanceof Blob) || (withNativeFile && obj instanceof File)
)
}
export function hasBinary(obj: any, toJSON?: boolean) {
if (!obj || typeof obj !== "object") {
return false
}
if (Array.isArray(obj)) {
for (let i = 0, l = obj.length; i < l; i++) {
if (hasBinary(obj[i])) {
return true
}
}
return false
}
if (isBinary(obj)) {
return true
}
if (
obj.toJSON &&
typeof obj.toJSON === "function" &&
arguments.length === 1
) {
return hasBinary(obj.toJSON(), true)
}
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key) && hasBinary(obj[key])) {
return true
}
}
return false
}

View File

@@ -0,0 +1,320 @@
// import type { BroadcastFlags, Room, SocketId } from "socket.io-adapter"
import type { BroadcastFlags, Room, SocketId } from "../socket.io-adapter"
import { Handshake, RESERVED_EVENTS, Socket } from "./socket"
// import { PacketType } from "socket.io-parser"
import { PacketType } from "../socket.io-parser"
// import type { Adapter } from "socket.io-adapter"
import type { Adapter } from "../socket.io-adapter"
import type {
EventParams,
EventNames,
EventsMap,
TypedEventBroadcaster,
} from "./typed-events"
export class BroadcastOperator<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents>
{
constructor(
private readonly adapter: Adapter,
private readonly rooms: Set<Room> = new Set<Room>(),
private readonly exceptRooms: Set<Room> = new Set<Room>(),
private readonly flags: BroadcastFlags = {}
) { }
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
const rooms = new Set(this.rooms)
if (Array.isArray(room)) {
room.forEach((r) => rooms.add(r))
} else {
rooms.add(room)
}
return new BroadcastOperator(
this.adapter,
rooms,
this.exceptRooms,
this.flags
)
}
/**
* Targets a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.to(room)
}
/**
* Excludes a room when emitting.
*
* @param room
* @return a new BroadcastOperator instance
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
const exceptRooms = new Set(this.exceptRooms)
if (Array.isArray(room)) {
room.forEach((r) => exceptRooms.add(r))
} else {
exceptRooms.add(room)
}
return new BroadcastOperator(
this.adapter,
this.rooms,
exceptRooms,
this.flags
)
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return a new BroadcastOperator instance
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { compress })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return a new BroadcastOperator instance
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { volatile: true })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return a new BroadcastOperator instance
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
const flags = Object.assign({}, this.flags, { local: true })
return new BroadcastOperator(
this.adapter,
this.rooms,
this.exceptRooms,
flags
)
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`)
}
// set up packet object
const data = [ev, ...args]
const packet = {
type: PacketType.EVENT,
data: data,
}
if ("function" == typeof data[data.length - 1]) {
throw new Error("Callbacks are not supported when broadcasting")
}
this.adapter.broadcast(packet, {
rooms: this.rooms,
except: this.exceptRooms,
flags: this.flags,
})
return true
}
/**
* Gets a list of clients.
*
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
if (!this.adapter) {
throw new Error(
"No adapter for this namespace, are you trying to get the list of clients of a dynamic namespace?"
)
}
return this.adapter.sockets(this.rooms)
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.adapter
.fetchSockets({
rooms: this.rooms,
except: this.exceptRooms,
})
.then((sockets) => {
return sockets.map((socket) => {
if (socket instanceof Socket) {
// FIXME the TypeScript compiler complains about missing private properties
return socket as unknown as RemoteSocket<EmitEvents>
} else {
return new RemoteSocket(this.adapter, socket as SocketDetails)
}
})
})
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
this.adapter.addSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
this.adapter.delSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
Array.isArray(room) ? room : [room]
)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
this.adapter.disconnectSockets(
{
rooms: this.rooms,
except: this.exceptRooms,
},
close
)
}
}
/**
* Format of the data when the Socket instance exists on another Socket.IO server
*/
interface SocketDetails {
id: SocketId
handshake: Handshake
rooms: Room[]
data: any
}
/**
* Expose of subset of the attributes and methods of the Socket class
*/
export class RemoteSocket<EmitEvents extends EventsMap>
implements TypedEventBroadcaster<EmitEvents>
{
public readonly id: SocketId
public readonly handshake: Handshake
public readonly rooms: Set<Room>
public readonly data: any
private readonly operator: BroadcastOperator<EmitEvents>
constructor(adapter: Adapter, details: SocketDetails) {
this.id = details.id
this.handshake = details.handshake
this.rooms = new Set(details.rooms)
this.data = details.data
this.operator = new BroadcastOperator(adapter, new Set([this.id]))
}
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return this.operator.emit(ev, ...args)
}
/**
* Joins a room.
*
* @param {String|Array} room - room or array of rooms
* @public
*/
public join(room: Room | Room[]): void {
return this.operator.socketsJoin(room)
}
/**
* Leaves a room.
*
* @param {String} room
* @public
*/
public leave(room: Room): void {
return this.operator.socketsLeave(room)
}
/**
* Disconnects this client.
*
* @param {Boolean} close - if `true`, closes the underlying connection
* @return {Socket} self
*
* @public
*/
public disconnect(close = false): this {
this.operator.disconnectSockets(close)
return this
}
}

View File

@@ -0,0 +1,331 @@
// import { Decoder, Encoder, Packet, PacketType } from "socket.io-parser"
import { Decoder, Encoder, Packet, PacketType } from "../socket.io-parser"
// import debugModule = require("debug")
import url = require("url")
// import type { IncomingMessage } from "http"
import type { Server } from "./index"
import type { Namespace } from "./namespace"
import type { EventsMap } from "./typed-events"
import type { Socket } from "./socket"
// import type { SocketId } from "socket.io-adapter"
import type { SocketId } from "../socket.io-adapter"
import type { Socket as EngineIOSocket } from '../engine.io/socket'
// const debug = debugModule("socket.io:client");
interface WriteOptions {
compress?: boolean
volatile?: boolean
preEncoded?: boolean
wsPreEncoded?: string
}
export class Client<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap
> {
public readonly conn: EngineIOSocket
/**
* @private
*/
readonly id: string
private readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
private readonly encoder: Encoder
private readonly decoder: any
private sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map()
private nsps: Map<
string,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map()
private connectTimeout: NodeJS.Timeout
/**
* Client constructor.
*
* @param server instance
* @param conn
* @package
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
conn: EngineIOSocket
) {
this.server = server
this.conn = conn
this.encoder = server.encoder
this.decoder = new server._parser.Decoder()
this.id = conn.id
this.setup()
}
/**
* @return the reference to the request that originated the Engine.IO connection
*
* @public
*/
public get request(): any/** IncomingMessage */ {
return this.conn.request
}
/**
* Sets up event listeners.
*
* @private
*/
private setup() {
console.debug(`socket.io client setup conn ${this.conn.id}`)
this.onclose = this.onclose.bind(this)
this.ondata = this.ondata.bind(this)
this.onerror = this.onerror.bind(this)
this.ondecoded = this.ondecoded.bind(this)
// @ts-ignore
this.decoder.on("decoded", this.ondecoded)
this.conn.on("data", this.ondata)
this.conn.on("error", this.onerror)
this.conn.on("close", this.onclose)
this.connectTimeout = setTimeout(() => {
if (this.nsps.size === 0) {
console.debug(`no namespace joined yet, close the client ${this.id}`)
this.close()
} else {
console.debug(`the client ${this.id} has already joined a namespace, nothing to do`)
}
}, this.server._connectTimeout)
}
/**
* Connects a client to a namespace.
*
* @param {String} name - the namespace
* @param {Object} auth - the auth parameters
* @private
*/
private connect(name: string, auth: object = {}): void {
if (this.server._nsps.has(name)) {
console.debug(`socket.io client ${this.id} connecting to namespace ${name}`)
return this.doConnect(name, auth)
}
this.server._checkNamespace(
name,
auth,
(
dynamicNspName:
| Namespace<ListenEvents, EmitEvents, ServerSideEvents>
| false
) => {
if (dynamicNspName) {
console.debug(`dynamic namespace ${dynamicNspName} was created`)
this.doConnect(name, auth)
} else {
console.debug(`creation of namespace ${name} was denied`)
this._packet({
type: PacketType.CONNECT_ERROR,
nsp: name,
data: {
message: "Invalid namespace",
},
})
}
}
)
}
/**
* Connects a client to a namespace.
*
* @param name - the namespace
* @param {Object} auth - the auth parameters
*
* @private
*/
private doConnect(name: string, auth: object): void {
const nsp = this.server.of(name)
// @java-patch multi thread need direct callback socket
const socket = nsp._add(this, auth, (socket) => {
this.sockets.set(socket.id, socket)
this.nsps.set(nsp.name, socket)
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
}
})
}
/**
* Disconnects from all namespaces and closes transport.
*
* @private
*/
_disconnect(): void {
for (const socket of this.sockets.values()) {
socket.disconnect()
}
this.sockets.clear()
this.close()
}
/**
* Removes a socket. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
if (this.sockets.has(socket.id)) {
const nsp = this.sockets.get(socket.id)!.nsp.name
this.sockets.delete(socket.id)
this.nsps.delete(nsp)
} else {
console.debug("ignoring remove for", socket.id)
}
// @java-patch disconnect client when no live socket
process.nextTick(() => {
if (this.sockets.size == 0) {
this.onclose('no live socket')
}
})
}
/**
* Closes the underlying connection.
*
* @private
*/
private close(): void {
console.debug(`client ${this.id} clise - reason: forcing transport close`)
if ("open" === this.conn.readyState) {
console.debug("forcing transport close")
this.conn.close()
this.onclose("forced server close")
}
}
/**
* Writes a packet to the transport.
*
* @param {Object} packet object
* @param {Object} opts
* @private
*/
_packet(packet: Packet | any[], opts: WriteOptions = {}): void {
if (this.conn.readyState !== "open") {
console.debug(`client ${this.id} ignoring packet write ${JSON.stringify(packet)}`)
return
}
const encodedPackets = opts.preEncoded
? (packet as any[]) // previous versions of the adapter incorrectly used socket.packet() instead of writeToEngine()
: this.encoder.encode(packet as Packet)
for (const encodedPacket of encodedPackets) {
this.writeToEngine(encodedPacket, opts)
}
}
private writeToEngine(
encodedPacket: String | Buffer,
opts: WriteOptions
): void {
if (opts.volatile && !this.conn.transport.writable) {
console.debug(`client ${this.id} volatile packet is discarded since the transport is not currently writable`)
return
}
this.conn.write(encodedPacket, opts)
}
/**
* Called with incoming transport data.
*
* @private
*/
private ondata(data): void {
// try/catch is needed for protocol violations (GH-1880)
try {
this.decoder.add(data)
} catch (e) {
this.onerror(e)
}
}
/**
* Called when parser fully decodes a packet.
*
* @private
*/
private ondecoded(packet: Packet): void {
if (PacketType.CONNECT === packet.type) {
if (this.conn.protocol === 3) {
const parsed = url.parse(packet.nsp, true)
this.connect(parsed.pathname!, parsed.query)
} else {
this.connect(packet.nsp, packet.data)
}
} else {
const socket = this.nsps.get(packet.nsp)
if (socket) {
process.nextTick(function () {
socket._onpacket(packet)
})
} else {
console.debug(`client ${this.id} no socket for namespace ${packet.nsp}.`)
}
}
}
/**
* Handles an error.
*
* @param {Object} err object
* @private
*/
private onerror(err): void {
for (const socket of this.sockets.values()) {
socket._onerror(err)
}
this.conn.close()
}
/**
* Called upon transport close.
*
* @param reason
* @private
*/
private onclose(reason: string): void {
console.debug(`client ${this.id} close with reason ${reason}`)
// ignore a potential subsequent `close` event
this.destroy()
// `nsps` and `sockets` are cleaned up seamlessly
for (const socket of this.sockets.values()) {
socket._onclose(reason)
}
this.sockets.clear()
this.decoder.destroy() // clean up decoder
}
/**
* Cleans up event listeners.
* @private
*/
private destroy(): void {
this.conn.removeListener("data", this.ondata)
this.conn.removeListener("error", this.onerror)
this.conn.removeListener("close", this.onclose)
// @ts-ignore
this.decoder.removeListener("decoded", this.ondecoded)
if (this.connectTimeout) {
clearTimeout(this.connectTimeout)
this.connectTimeout = undefined
}
}
}

View File

@@ -0,0 +1,825 @@
// import http = require("http");
// import { createReadStream } from "fs";
// import { createDeflate, createGzip, createBrotliCompress } from "zlib";
// import accepts = require("accepts");
// import { pipeline } from "stream";
// import path = require("path");
import engine = require("../engine.io")
import { Client } from './client'
import { EventEmitter } from 'events'
import { ExtendedError, Namespace, ServerReservedEventsMap } from "./namespace"
import { ParentNamespace } from './parent-namespace'
// import { Adapter, Room, SocketId } from "socket.io-adapter"
import { Adapter, Room, SocketId } from "../socket.io-adapter"
// import * as parser from "socket.io-parser";
import * as parser from "../socket.io-parser"
// import type { Encoder } from "socket.io-parser";
import type { Encoder } from "../socket.io-parser"
// import debugModule from "debug";
import { Socket } from './socket'
// import type { CookieSerializeOptions } from "cookie";
// import type { CorsOptions } from "cors";
import type { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
import {
EventsMap,
DefaultEventsMap,
EventParams,
StrictEventEmitter,
EventNames,
} from "./typed-events"
import type { Socket as EngineIOSocket } from '../engine.io/socket'
// const clientVersion = require("../package.json").version
// const dotMapRegex = /\.map/
// type Transport = "polling" | "websocket";
type ParentNspNameMatchFn = (
name: string,
auth: { [key: string]: any },
fn: (err: Error | null, success: boolean) => void
) => void
type AdapterConstructor = typeof Adapter | ((nsp: Namespace) => Adapter)
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
/**
* whether to enable compatibility with Socket.IO v2 clients
* @default false
*/
allowEIO3: boolean
}
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 {
/**
* 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
}
export class Server<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
ServerReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
> {
public readonly sockets: Namespace<
ListenEvents,
EmitEvents,
ServerSideEvents
>
/**
* A reference to the underlying Engine.IO server.
*
* Example:
*
* <code>
* const clientsCount = io.engine.clientsCount;
* </code>
*
*/
public engine: any
/** @private */
readonly _parser: typeof parser
/** @private */
readonly encoder: Encoder
/**
* @private
*/
_nsps: Map<string, Namespace<ListenEvents, EmitEvents, ServerSideEvents>> =
new Map();
private parentNsps: Map<
ParentNspNameMatchFn,
ParentNamespace<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map();
private _adapter?: AdapterConstructor
private _serveClient: boolean
private opts: Partial<EngineOptions>
private eio
private _path: string
private clientPathRegex: RegExp
/**
* @private
*/
_connectTimeout: number
// private httpServer: http.Server
constructor(srv: any, opts: Partial<ServerOptions> = {}) {
super()
if (!srv) { throw new Error('srv can\'t be undefiend!') }
// if (
// "object" === typeof srv &&
// srv instanceof Object &&
// !(srv as Partial<http.Server>).listen
// ) {
// opts = srv as Partial<ServerOptions>
// srv = undefined
// }
this.path(opts.path || "/socket.io")
this.connectTimeout(opts.connectTimeout || 45000)
this.serveClient(false !== opts.serveClient)
this._parser = opts.parser || parser
this.encoder = new this._parser.Encoder()
this.adapter(opts.adapter || Adapter)
this.sockets = this.of('/')
// if (srv) this.attach(srv as http.Server);
this.attach(srv, this.opts)
}
/**
* Sets/gets whether client code is being served.
*
* @param v - whether to serve client code
* @return self when setting or value when getting
* @public
*/
public serveClient(v: boolean): this
public serveClient(): boolean
public serveClient(v?: boolean): this | boolean
public serveClient(v?: boolean): this | boolean {
if (!arguments.length) return this._serveClient
this._serveClient = v!
return this
}
/**
* Executes the middleware for an incoming namespace not already created on the server.
*
* @param name - name of incoming namespace
* @param auth - the auth parameters
* @param fn - callback
*
* @private
*/
_checkNamespace(
name: string,
auth: { [key: string]: any },
fn: (
nsp: Namespace<ListenEvents, EmitEvents, ServerSideEvents> | false
) => void
): void {
if (this.parentNsps.size === 0) return fn(false)
const keysIterator = this.parentNsps.keys()
const run = () => {
const nextFn = keysIterator.next()
if (nextFn.done) {
return fn(false)
}
nextFn.value(name, auth, (err, allow) => {
if (err || !allow) {
run()
} else {
const namespace = this.parentNsps
.get(nextFn.value)!
.createChild(name)
// @ts-ignore
this.sockets.emitReserved("new_namespace", namespace)
fn(namespace)
}
})
}
run()
}
/**
* Sets the client serving path.
*
* @param {String} v pathname
* @return {Server|String} self when setting or value when getting
* @public
*/
public path(v: string): this
public path(): string
public path(v?: string): this | string
public path(v?: string): this | string {
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): this
public connectTimeout(): number
public connectTimeout(v?: number): this | number
public connectTimeout(v?: number): this | number {
if (v === undefined) return this._connectTimeout
this._connectTimeout = v
return this
}
/**
* Sets the adapter for rooms.
*
* @param v pathname
* @return self when setting or value when getting
* @public
*/
public adapter(): AdapterConstructor | undefined
public adapter(v: AdapterConstructor): this
public adapter(
v?: AdapterConstructor
): AdapterConstructor | undefined | this {
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 srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public listen(
srv: any,//http.Server | number,
opts: Partial<ServerOptions> = {}
): this {
throw Error('Unsupport listen at MiaoScript Engine!')
//return this.attach(srv, opts)
}
/**
* Attaches socket.io to a server or port.
*
* @param srv - server or port
* @param opts - options passed to engine.io
* @return self
* @public
*/
public attach(
srv: any,//http.Server | number,
opts: Partial<ServerOptions> = {}
): this {
// 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)
// }
// merge the options passed to the Socket.IO server
Object.assign(opts, this.opts)
// 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: any, opts: Partial<EngineAttachOptions>) {
// // initialize engine
console.debug("creating engine.io instance with opts", JSON.stringify(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 srv http server
// * @private
// */
// private attachServe(srv: http.Server): void {
// 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 req
// * @param res
// * @private
// */
// private serve(req: http.IncomingMessage, res: http.ServerResponse): void {
// 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 weakEtag = "W/" + expectedEtag
// const etag = req.headers["if-none-match"]
// if (etag) {
// if (expectedEtag === etag || weakEtag === 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)
// Server.sendFile(filename, req, res)
// }
// /**
// * @param filename
// * @param req
// * @param res
// * @private
// */
// private static sendFile(
// filename: string,
// req: http.IncomingMessage,
// res: http.ServerResponse
// ): void {
// const readStream = createReadStream(
// path.join(__dirname, "../client-dist/", filename)
// )
// const encoding = accepts(req).encodings(["br", "gzip", "deflate"])
// const onError = (err: NodeJS.ErrnoException | null) => {
// 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 {
console.debug('engine.io', engine.constructor.name, 'bind to socket.io')
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: EngineIOSocket): Server {
console.debug(`socket.io index incoming connection with id ${conn.id}`)
let client = new Client(this, conn)
if (conn.protocol === 3) {
// @ts-ignore
client.connect("/")
}
return this
}
/**
* Looks up a namespace.
*
* @param {String|RegExp|Function} name nsp name
* @param fn optional, nsp `connection` ev handler
* @public
*/
public of(
name: string | RegExp | ParentNspNameMatchFn,
fn?: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
): Namespace<ListenEvents, EmitEvents, ServerSideEvents> {
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 (name !== "/") {
// @ts-ignore
this.sockets.emitReserved("new_namespace", nsp)
}
}
if (fn) nsp.on("connect", fn)
return nsp
}
/**
* Closes server connection
*
* @param [fn] optional, called as `fn([err])` on error OR all conns closed
* @public
*/
public close(fn?: (err?: Error) => void): void {
for (const socket of this.sockets.sockets.values()) {
socket._onclose("server shutting down")
}
this.engine.close()
// if (this.httpServer) {
// this.httpServer.close(fn)
// } else {
fn && fn()
// }
}
/**
* Sets up namespace middleware.
*
* @return self
* @public
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
next: (err?: ExtendedError) => void
) => void
): this {
this.sockets.use(fn)
return this
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.to(room)
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.in(room)
}
/**
* Excludes a room when emitting.
*
* @param name
* @return self
* @public
*/
public except(name: Room | Room[]): BroadcastOperator<EmitEvents> {
return this.sockets.except(name)
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args)
return this
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.sockets.emit("message", ...args)
return this
}
/**
* Emit a packet to other Socket.IO servers
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
return this.sockets.serverSideEmit(ev, ...args)
}
/**
* Gets a list of socket ids.
*
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
return this.sockets.allSockets()
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return this.sockets.compress(compress)
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return self
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
return this.sockets.volatile
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return this.sockets.local
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return this.sockets.fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return this.sockets.socketsJoin(room)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return this.sockets.socketsLeave(room)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return this.sockets.disconnectSockets(close)
}
}
/**
* 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 { Socket, ServerOptions, Namespace, BroadcastOperator, RemoteSocket }

View File

@@ -0,0 +1,407 @@
import { Socket } from "./socket"
import type { Server } from "./index"
import {
EventParams,
EventNames,
EventsMap,
StrictEventEmitter,
DefaultEventsMap,
} from "./typed-events"
import type { Client } from "./client"
// import debugModule from "debug"
// import type { Adapter, Room, SocketId } from "socket.io-adapter"
import type { Adapter, Room, SocketId } from "../socket.io-adapter"
import { BroadcastOperator, RemoteSocket } from "./broadcast-operator"
// const debug = debugModule("socket.io:namespace");
export interface ExtendedError extends Error {
data?: any
}
export interface NamespaceReservedEventsMap<
ListenEvents extends EventsMap,
EmitEvents extends EventsMap,
ServerSideEvents extends EventsMap
> {
connect: (socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>) => void
connection: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>
) => void
}
export interface ServerReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents
> extends NamespaceReservedEventsMap<
ListenEvents,
EmitEvents,
ServerSideEvents
> {
new_namespace: (
namespace: Namespace<ListenEvents, EmitEvents, ServerSideEvents>
) => void
}
export const RESERVED_EVENTS: ReadonlySet<string | Symbol> = new Set<
keyof ServerReservedEventsMap<never, never, never>
>(<const>["connect", "connection", "new_namespace"])
export class Namespace<
ListenEvents extends EventsMap = DefaultEventsMap,
EmitEvents extends EventsMap = ListenEvents,
ServerSideEvents extends EventsMap = DefaultEventsMap
> extends StrictEventEmitter<
ServerSideEvents,
EmitEvents,
NamespaceReservedEventsMap<ListenEvents, EmitEvents, ServerSideEvents>
> {
public readonly name: string
public readonly sockets: Map<
SocketId,
Socket<ListenEvents, EmitEvents, ServerSideEvents>
> = new Map();
public adapter: Adapter
/** @private */
readonly server: Server<ListenEvents, EmitEvents, ServerSideEvents>
/** @private */
_fns: Array<
(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
next: (err?: ExtendedError) => void
) => void
> = [];
/** @private */
_ids: number = 0;
/**
* Namespace constructor.
*
* @param server instance
* @param name
*/
constructor(
server: Server<ListenEvents, EmitEvents, ServerSideEvents>,
name: string
) {
super()
this.server = server
this.name = name
this._initAdapter()
}
/**
* Initializes the `Adapter` for this nsp.
* Run upon changing adapter by `Server#adapter`
* in addition to the constructor.
*
* @private
*/
_initAdapter() {
// @ts-ignore
this.adapter = new (this.server.adapter()!)(this)
}
/**
* Sets up namespace middleware.
*
* @return self
* @public
*/
public use(
fn: (
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
next: (err?: ExtendedError) => void
) => void
): this {
this._fns.push(fn)
return this
}
/**
* Executes the middleware for an incoming client.
*
* @param socket - the socket that will get added
* @param fn - last fn call in the middleware
* @private
*/
private run(
socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>,
fn: (err: ExtendedError | null) => void
) {
const fns = this._fns.slice(0)
if (!fns.length) return fn(null)
function run(i: number) {
fns[i](socket, function (err) {
// upon error, short-circuit
if (err) return fn(err)
// if no middleware left, summon callback
if (!fns[i + 1]) return fn(null)
// go on to next
run(i + 1)
})
}
run(0)
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public to(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).to(room)
}
/**
* Targets a room when emitting.
*
* @param room
* @return self
* @public
*/
public in(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).in(room)
}
/**
* Excludes a room when emitting.
*
* @param room
* @return self
* @public
*/
public except(room: Room | Room[]): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).except(room)
}
/**
* Adds a new client.
*
* @return {Socket}
* @private
*/
_add(
client: Client<ListenEvents, EmitEvents, ServerSideEvents>,
query,
fn?: (socket: Socket) => void
): Socket<ListenEvents, EmitEvents, ServerSideEvents> {
const socket = new Socket(this, client, query || {})
console.debug(`socket.io namespace client ${client.id} adding socket ${socket.id} to nsp ${this.name}`)
this.run(socket, err => {
process.nextTick(() => {
if ("open" == client.conn.readyState) {
if (err) {
if (client.conn.protocol === 3) {
return socket._error(err.data || err.message)
} else {
return socket._error({
message: err.message,
data: err.data,
})
}
}
// track socket
this.sockets.set(socket.id, socket)
console.debug(`socket.io namespace ${this.name} track client ${client.id} socket ${socket.id}`)
// it's paramount that the internal `onconnect` logic
// fires before user-set events to prevent state order
// violations (such as a disconnection before the connection
// logic is complete)
socket._onconnect()
// @java-patch multi thread need direct callback socket
if (fn) fn(socket)
// fire user-set events
this.emitReserved("connect", socket)
this.emitReserved("connection", socket)
} else {
console.debug(`next called after client ${client.id} was closed - ignoring socket`)
}
})
})
return socket
}
/**
* Removes a client. Called by each `Socket`.
*
* @private
*/
_remove(socket: Socket<ListenEvents, EmitEvents, ServerSideEvents>): void {
if (this.sockets.has(socket.id)) {
console.debug(`namespace ${this.name} remove socket ${socket.id}`)
this.sockets.delete(socket.id)
} else {
console.debug(`namespace ${this.name} ignoring remove for ${socket.id}`)
}
}
/**
* Emits to all clients.
*
* @return Always true
* @public
*/
public emit<Ev extends EventNames<EmitEvents>>(
ev: Ev,
...args: EventParams<EmitEvents, Ev>
): boolean {
return new BroadcastOperator<EmitEvents>(this.adapter).emit(ev, ...args)
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public send(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
}
/**
* Sends a `message` event to all clients.
*
* @return self
* @public
*/
public write(...args: EventParams<EmitEvents, "message">): this {
this.emit("message", ...args)
return this
}
/**
* Emit a packet to other Socket.IO servers
*
* @param ev - the event name
* @param args - an array of arguments, which may include an acknowledgement callback at the end
* @public
*/
public serverSideEmit<Ev extends EventNames<ServerSideEvents>>(
ev: Ev,
...args: EventParams<ServerSideEvents, Ev>
): boolean {
if (RESERVED_EVENTS.has(ev)) {
throw new Error(`"${ev}" is a reserved event name`)
}
args.unshift(ev)
this.adapter.serverSideEmit(args)
return true
}
/**
* Called when a packet is received from another Socket.IO server
*
* @param args - an array of arguments, which may include an acknowledgement callback at the end
*
* @private
*/
_onServerSideEmit(args: [string, ...any[]]) {
super.emitUntyped.apply(this, args)
}
/**
* Gets a list of clients.
*
* @return self
* @public
*/
public allSockets(): Promise<Set<SocketId>> {
return new BroadcastOperator(this.adapter).allSockets()
}
/**
* Sets the compress flag.
*
* @param compress - if `true`, compresses the sending data
* @return self
* @public
*/
public compress(compress: boolean): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).compress(compress)
}
/**
* Sets a modifier for a subsequent event emission that the event data may be lost if the client is not ready to
* receive messages (because of network slowness or other issues, or because theyre connected through long polling
* and is in the middle of a request-response cycle).
*
* @return self
* @public
*/
public get volatile(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).volatile
}
/**
* Sets a modifier for a subsequent event emission that the event data will only be broadcast to the current node.
*
* @return self
* @public
*/
public get local(): BroadcastOperator<EmitEvents> {
return new BroadcastOperator(this.adapter).local
}
/**
* Returns the matching socket instances
*
* @public
*/
public fetchSockets(): Promise<RemoteSocket<EmitEvents>[]> {
return new BroadcastOperator(this.adapter).fetchSockets()
}
/**
* Makes the matching socket instances join the specified rooms
*
* @param room
* @public
*/
public socketsJoin(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsJoin(room)
}
/**
* Makes the matching socket instances leave the specified rooms
*
* @param room
* @public
*/
public socketsLeave(room: Room | Room[]): void {
return new BroadcastOperator(this.adapter).socketsLeave(room)
}
/**
* Makes the matching socket instances disconnect
*
* @param close - whether to close the underlying connection
* @public
*/
public disconnectSockets(close: boolean = false): void {
return new BroadcastOperator(this.adapter).disconnectSockets(close)
}
public close() {
RESERVED_EVENTS.forEach(event => this.removeAllListeners(event as any))
this.server._nsps.delete(this.name)
// @java-patch close all socket when namespace close
this.sockets.forEach(socket => socket._onclose(`namepsace ${this.name} close`))
}
}

Some files were not shown because too many files have changed in this diff Show More