api-ape 0.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +261 -0
- package/client/README.md +69 -0
- package/client/browser.js +17 -0
- package/client/connectSocket.js +260 -0
- package/dist/ape.js +454 -0
- package/example/ExpressJs/README.md +97 -0
- package/example/ExpressJs/api/message.js +11 -0
- package/example/ExpressJs/backend.js +37 -0
- package/example/ExpressJs/index.html +88 -0
- package/example/ExpressJs/package-lock.json +834 -0
- package/example/ExpressJs/package.json +10 -0
- package/example/ExpressJs/styles.css +128 -0
- package/example/NextJs/.dockerignore +29 -0
- package/example/NextJs/Dockerfile +52 -0
- package/example/NextJs/Dockerfile.dev +27 -0
- package/example/NextJs/README.md +113 -0
- package/example/NextJs/ape/client.js +66 -0
- package/example/NextJs/ape/embed.js +12 -0
- package/example/NextJs/ape/index.js +23 -0
- package/example/NextJs/ape/logic/chat.js +62 -0
- package/example/NextJs/ape/onConnect.js +69 -0
- package/example/NextJs/ape/onDisconnect.js +13 -0
- package/example/NextJs/ape/onError.js +9 -0
- package/example/NextJs/ape/onReceive.js +15 -0
- package/example/NextJs/ape/onSend.js +15 -0
- package/example/NextJs/api/message.js +44 -0
- package/example/NextJs/docker-compose.yml +22 -0
- package/example/NextJs/next-env.d.ts +5 -0
- package/example/NextJs/next.config.js +8 -0
- package/example/NextJs/package-lock.json +5107 -0
- package/example/NextJs/package.json +25 -0
- package/example/NextJs/pages/_app.tsx +6 -0
- package/example/NextJs/pages/index.tsx +182 -0
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +4 -0
- package/example/NextJs/server.js +40 -0
- package/example/NextJs/styles/Chat.module.css +194 -0
- package/example/NextJs/styles/Home.module.css +129 -0
- package/example/NextJs/styles/globals.css +26 -0
- package/example/NextJs/tsconfig.json +20 -0
- package/example/README.md +66 -0
- package/index.d.ts +179 -0
- package/index.js +11 -0
- package/package.json +11 -4
- package/server/README.md +93 -0
- package/server/index.js +6 -0
- package/server/lib/broadcast.js +63 -0
- package/server/lib/loader.js +10 -0
- package/server/lib/main.js +23 -0
- package/server/lib/wiring.js +94 -0
- package/server/security/extractRootDomain.js +21 -0
- package/server/security/origin.js +13 -0
- package/server/security/reply.js +21 -0
- package/server/socket/open.js +10 -0
- package/server/socket/receive.js +66 -0
- package/server/socket/send.js +55 -0
- package/server/utils/deepRequire.js +45 -0
- package/server/utils/genId.js +24 -0
- package/todo.md +85 -0
- package/utils/jss.js +273 -0
- package/utils/jss.test.js +261 -0
- package/utils/messageHash.js +43 -0
- package/utils/messageHash.test.js +56 -0
package/index.d.ts
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// Type definitions for api-ape
|
|
2
|
+
// Project: https://github.com/codemeasandwich/api-ape
|
|
3
|
+
|
|
4
|
+
import { Application } from 'express'
|
|
5
|
+
import { WebSocket } from 'ws'
|
|
6
|
+
import { IncomingMessage } from 'http'
|
|
7
|
+
|
|
8
|
+
// =============================================================================
|
|
9
|
+
// SERVER TYPES
|
|
10
|
+
// =============================================================================
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Controller context available as `this` inside controller functions
|
|
14
|
+
*/
|
|
15
|
+
export interface ControllerContext {
|
|
16
|
+
/** Send to ALL connected clients */
|
|
17
|
+
broadcast(type: string, data: any): void
|
|
18
|
+
/** Send to all clients EXCEPT the caller */
|
|
19
|
+
broadcastOthers(type: string, data: any): void
|
|
20
|
+
/** Get count of connected clients */
|
|
21
|
+
online(): number
|
|
22
|
+
/** Get array of connected hostIds */
|
|
23
|
+
getClients(): string[]
|
|
24
|
+
/** Unique ID of the calling client */
|
|
25
|
+
hostId: string
|
|
26
|
+
/** Original HTTP request */
|
|
27
|
+
req: IncomingMessage
|
|
28
|
+
/** WebSocket instance */
|
|
29
|
+
socket: WebSocket
|
|
30
|
+
/** Parsed user-agent info */
|
|
31
|
+
agent: {
|
|
32
|
+
browser: { name?: string; version?: string }
|
|
33
|
+
os: { name?: string; version?: string }
|
|
34
|
+
device: { type?: string; vendor?: string; model?: string }
|
|
35
|
+
}
|
|
36
|
+
/** Custom embedded values from onConnent */
|
|
37
|
+
[key: string]: any
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Controller function type
|
|
42
|
+
*/
|
|
43
|
+
export type ControllerFunction<T = any, R = any> = (
|
|
44
|
+
this: ControllerContext,
|
|
45
|
+
data: T
|
|
46
|
+
) => R | Promise<R>
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Send function provided to onConnent
|
|
50
|
+
*/
|
|
51
|
+
export type SendFunction = {
|
|
52
|
+
(type: string, data: any): void
|
|
53
|
+
toString(): string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* After hook returned from onReceive/onSend
|
|
58
|
+
*/
|
|
59
|
+
export type AfterHook = (err: Error | null, result: any) => void
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Connection lifecycle hooks returned from onConnent
|
|
63
|
+
*/
|
|
64
|
+
export interface ConnectionHandlers {
|
|
65
|
+
/** Values to embed into controller context */
|
|
66
|
+
embed?: Record<string, any>
|
|
67
|
+
/** Called before processing incoming message, return after hook */
|
|
68
|
+
onReceive?: (queryId: string, data: any, type: string) => AfterHook | void
|
|
69
|
+
/** Called before sending message, return after hook */
|
|
70
|
+
onSend?: (data: any, type: string) => AfterHook | void
|
|
71
|
+
/** Called on error */
|
|
72
|
+
onError?: (errorString: string) => void
|
|
73
|
+
/** Called when client disconnects */
|
|
74
|
+
onDisconnent?: () => void
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* onConnent callback signature
|
|
79
|
+
*/
|
|
80
|
+
export type OnConnectCallback = (
|
|
81
|
+
socket: WebSocket,
|
|
82
|
+
req: IncomingMessage,
|
|
83
|
+
send: SendFunction
|
|
84
|
+
) => ConnectionHandlers | void
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Server options for ape()
|
|
88
|
+
*/
|
|
89
|
+
export interface ApeServerOptions {
|
|
90
|
+
/** Directory containing controller files */
|
|
91
|
+
where: string
|
|
92
|
+
/** Connection lifecycle hook */
|
|
93
|
+
onConnent?: OnConnectCallback
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Initialize api-ape on an Express app
|
|
98
|
+
*/
|
|
99
|
+
declare function ape(app: Application, options: ApeServerOptions): void
|
|
100
|
+
|
|
101
|
+
export default ape
|
|
102
|
+
|
|
103
|
+
// =============================================================================
|
|
104
|
+
// CLIENT TYPES
|
|
105
|
+
// =============================================================================
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Message received from server
|
|
109
|
+
*/
|
|
110
|
+
export interface ReceivedMessage<T = any> {
|
|
111
|
+
err?: Error | string
|
|
112
|
+
type: string
|
|
113
|
+
data: T
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Message handler callback
|
|
118
|
+
*/
|
|
119
|
+
export type MessageHandler<T = any> = (message: ReceivedMessage<T>) => void
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Proxy-based sender - any property access creates a callable path
|
|
123
|
+
* Example: sender.users.list() calls type="/users/list"
|
|
124
|
+
*/
|
|
125
|
+
export interface ApeSender {
|
|
126
|
+
[key: string]: ApeSender & (<T = any, R = any>(data?: T) => Promise<R>)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Set receiver for specific message type or all messages
|
|
131
|
+
*/
|
|
132
|
+
export type SetOnReceiver = {
|
|
133
|
+
(type: string, handler: MessageHandler): void
|
|
134
|
+
(handler: MessageHandler): void
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Connected client interface
|
|
139
|
+
*/
|
|
140
|
+
export interface ApeClient {
|
|
141
|
+
sender: ApeSender
|
|
142
|
+
setOnReciver: SetOnReceiver
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Configuration options for client
|
|
147
|
+
*/
|
|
148
|
+
export interface ApeClientConfig {
|
|
149
|
+
/** WebSocket port */
|
|
150
|
+
port?: number
|
|
151
|
+
/** WebSocket host */
|
|
152
|
+
host?: string
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Connect socket function with configuration methods
|
|
157
|
+
*/
|
|
158
|
+
export interface ConnectSocket {
|
|
159
|
+
(): ApeClient
|
|
160
|
+
/** Configure connection options */
|
|
161
|
+
configure(options: ApeClientConfig): void
|
|
162
|
+
/** Enable auto-reconnection on disconnect */
|
|
163
|
+
autoReconnect(): void
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Client module default export
|
|
168
|
+
*/
|
|
169
|
+
declare const connectSocket: ConnectSocket
|
|
170
|
+
|
|
171
|
+
export { connectSocket }
|
|
172
|
+
|
|
173
|
+
// =============================================================================
|
|
174
|
+
// BROADCAST MODULE
|
|
175
|
+
// =============================================================================
|
|
176
|
+
|
|
177
|
+
export declare const broadcast: (type: string, data: any) => void
|
|
178
|
+
export declare const online: () => number
|
|
179
|
+
export declare const getClients: () => string[]
|
package/index.js
ADDED
package/package.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-ape",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Remote procedure events",
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"scripts": {
|
|
7
8
|
"test": "jest --no-cache ",
|
|
8
9
|
"test:go": "npm test -- --watch --coverage",
|
|
9
10
|
"test:update": "npm test -- --updateSnapshot",
|
|
@@ -20,12 +21,18 @@
|
|
|
20
21
|
"ape"
|
|
21
22
|
],
|
|
22
23
|
"author": "brian shannon",
|
|
23
|
-
"license": "
|
|
24
|
+
"license": "MIT",
|
|
24
25
|
"bugs": {
|
|
25
26
|
"url": "https://github.com/codemeasandwich/api-ape/issues"
|
|
26
27
|
},
|
|
27
28
|
"homepage": "https://github.com/codemeasandwich/api-ape#readme",
|
|
28
29
|
"dependencies": {
|
|
29
|
-
"
|
|
30
|
+
"express-ws": "^5.0.2",
|
|
31
|
+
"jest": "^29.3.1",
|
|
32
|
+
"ua-parser-js": "^1.0.37",
|
|
33
|
+
"ws": "^8.14.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"esbuild": "^0.27.2"
|
|
30
37
|
}
|
|
31
38
|
}
|
package/server/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# 🦍 api-ape Server
|
|
2
|
+
|
|
3
|
+
Express.js integration for WebSocket-based Remote Procedure Events (RPE).
|
|
4
|
+
|
|
5
|
+
## Directory Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
server/
|
|
9
|
+
├── index.js # Entry point (exports lib/main)
|
|
10
|
+
├── lib/
|
|
11
|
+
│ ├── main.js # Express integration & setup
|
|
12
|
+
│ ├── loader.js # Auto-loads controller files from folder
|
|
13
|
+
│ ├── broadcast.js # Client tracking & broadcast utilities
|
|
14
|
+
│ └── wiring.js # WebSocket handler setup
|
|
15
|
+
├── socket/
|
|
16
|
+
│ ├── receive.js # Incoming message handler
|
|
17
|
+
│ └── send.js # Outgoing message handler
|
|
18
|
+
├── security/
|
|
19
|
+
│ └── reply.js # Duplicate request protection
|
|
20
|
+
└── utils/
|
|
21
|
+
└── ... # Server utilities
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm i api-ape
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```js
|
|
31
|
+
const express = require('express')
|
|
32
|
+
const ape = require('api-ape')
|
|
33
|
+
|
|
34
|
+
const app = express()
|
|
35
|
+
|
|
36
|
+
ape(app, {
|
|
37
|
+
where: 'api', // Controller directory
|
|
38
|
+
onConnent: (socket, req, send) => ({
|
|
39
|
+
embed: { userId: req.session?.userId },
|
|
40
|
+
onDisconnent: () => console.log('Client left')
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
app.listen(3000)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## API
|
|
48
|
+
|
|
49
|
+
### `ape(app, options)`
|
|
50
|
+
|
|
51
|
+
| Option | Type | Description |
|
|
52
|
+
|--------|------|-------------|
|
|
53
|
+
| `where` | `string` | Directory containing controller files |
|
|
54
|
+
| `onConnent` | `function` | Connection lifecycle hook |
|
|
55
|
+
|
|
56
|
+
### Controller Context (`this`)
|
|
57
|
+
|
|
58
|
+
| Property | Description |
|
|
59
|
+
|----------|-------------|
|
|
60
|
+
| `this.broadcast(type, data)` | Send to ALL connected clients |
|
|
61
|
+
| `this.broadcastOthers(type, data)` | Send to all EXCEPT the caller |
|
|
62
|
+
| `this.online()` | Get count of connected clients |
|
|
63
|
+
| `this.getClients()` | Get array of connected hostIds |
|
|
64
|
+
| `this.hostId` | Unique ID of the calling client |
|
|
65
|
+
| `this.req` | Original HTTP request |
|
|
66
|
+
| `this.socket` | WebSocket instance |
|
|
67
|
+
| `this.agent` | Parsed user-agent |
|
|
68
|
+
|
|
69
|
+
### Connection Lifecycle Hooks
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
onConnent(socket, req, send) {
|
|
73
|
+
return {
|
|
74
|
+
embed: { ... }, // Values available as this.* in controllers
|
|
75
|
+
onReceive: (queryId, data, type) => afterFn,
|
|
76
|
+
onSend: (data, type) => afterFn,
|
|
77
|
+
onError: (errStr) => { ... },
|
|
78
|
+
onDisconnent: () => { ... }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Auto-Routing
|
|
84
|
+
|
|
85
|
+
Drop JS files in your `where` directory:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
api/
|
|
89
|
+
├── hello.js → ape.hello(data)
|
|
90
|
+
├── users/
|
|
91
|
+
│ ├── list.js → ape.users.list(data)
|
|
92
|
+
│ └── create.js → ape.users.create(data)
|
|
93
|
+
```
|
package/server/index.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Broadcast utilities for api-ape
|
|
3
|
+
* Tracks connected clients and provides broadcast functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// Track all connected clients for broadcast
|
|
7
|
+
const connectedClients = new Set()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add a client to the connected set
|
|
11
|
+
*/
|
|
12
|
+
function addClient(clientInfo) {
|
|
13
|
+
connectedClients.add(clientInfo)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Remove a client from the connected set
|
|
18
|
+
*/
|
|
19
|
+
function removeClient(clientInfo) {
|
|
20
|
+
connectedClients.delete(clientInfo)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Broadcast to all connected clients
|
|
25
|
+
* @param {string} type - Message type
|
|
26
|
+
* @param {any} data - Data to send
|
|
27
|
+
* @param {string} [excludeHostId] - Optional hostId to exclude (e.g., sender)
|
|
28
|
+
*/
|
|
29
|
+
function broadcast(type, data, excludeHostId) {
|
|
30
|
+
console.log(`📢 Broadcasting "${type}" to ${connectedClients.size} clients`, excludeHostId ? `(excluding ${excludeHostId})` : '')
|
|
31
|
+
connectedClients.forEach(client => {
|
|
32
|
+
if (excludeHostId && client.hostId === excludeHostId) {
|
|
33
|
+
return // Skip excluded client
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
client.send(false, type, data, false)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
console.error(`📢 Broadcast failed to ${client.hostId}:`, e.message)
|
|
39
|
+
}
|
|
40
|
+
})
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Get count of online clients
|
|
45
|
+
*/
|
|
46
|
+
function online() {
|
|
47
|
+
return connectedClients.size
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get all connected client hostIds
|
|
52
|
+
*/
|
|
53
|
+
function getClients() {
|
|
54
|
+
return Array.from(connectedClients).map(c => c.hostId)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
addClient,
|
|
59
|
+
removeClient,
|
|
60
|
+
broadcast,
|
|
61
|
+
online,
|
|
62
|
+
getClients
|
|
63
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const deeprequire = require('../utils/deepRequire')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
|
|
4
|
+
// Use the current working directory (where node was started)
|
|
5
|
+
// This ensures 'where' folder is relative to the calling application
|
|
6
|
+
const currentDir = process.cwd()
|
|
7
|
+
|
|
8
|
+
module.exports = function (dirname, selector) {
|
|
9
|
+
return deeprequire(path.join(currentDir, dirname), selector)
|
|
10
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const loader = require('./loader')
|
|
2
|
+
const wiring = require('./wiring')
|
|
3
|
+
const expressWs = require('express-ws');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
let created = false
|
|
7
|
+
|
|
8
|
+
module.exports = function (app, { where, onConnent }) {
|
|
9
|
+
|
|
10
|
+
if (created) {
|
|
11
|
+
throw new Error("Api-Ape already started")
|
|
12
|
+
}
|
|
13
|
+
created = true;
|
|
14
|
+
expressWs(app)
|
|
15
|
+
const controllers = loader(where)
|
|
16
|
+
|
|
17
|
+
// Serve bundled client at /ape.js
|
|
18
|
+
app.get('/api/ape.js', (req, res) => {
|
|
19
|
+
res.sendFile(path.join(__dirname, '../../dist/ape.js'))
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
app.ws('/api/ape', wiring(controllers, onConnent))
|
|
23
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const replySecurity = require('../security/reply')
|
|
2
|
+
const socketOpen = require('../socket/open')
|
|
3
|
+
const socketReceive = require('../socket/receive')
|
|
4
|
+
const socketSend = require('../socket/send')
|
|
5
|
+
const makeid = require('../utils/genId')
|
|
6
|
+
const UAParser = require('ua-parser-js');
|
|
7
|
+
const { addClient, removeClient } = require('./broadcast')
|
|
8
|
+
|
|
9
|
+
const parser = new UAParser();
|
|
10
|
+
|
|
11
|
+
// connent, beforeSend, beforeReceive, error, afterSend, afterReceive, disconnent
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
function defaultEvents(events = {}) {
|
|
15
|
+
const fallBackEvents = {
|
|
16
|
+
embed: {},
|
|
17
|
+
onReceive: () => { },
|
|
18
|
+
onSend: () => { },
|
|
19
|
+
onError: (errSt) => console.error(errSt),
|
|
20
|
+
onDisconnent: () => { },
|
|
21
|
+
} // END fallBackEvents
|
|
22
|
+
return Object.assign({}, fallBackEvents, events)
|
|
23
|
+
} // END defaultEvents
|
|
24
|
+
|
|
25
|
+
//=====================================================
|
|
26
|
+
//============================================== wiring
|
|
27
|
+
//=====================================================
|
|
28
|
+
|
|
29
|
+
module.exports = function wiring(controllers, onConnent) {
|
|
30
|
+
onConnent = onConnent || (() => { });
|
|
31
|
+
return function webSocketHandler(socket, req) {
|
|
32
|
+
|
|
33
|
+
let send;
|
|
34
|
+
let sentBufferAr = []
|
|
35
|
+
const sentBufferFn = (...args) => {
|
|
36
|
+
if (send) {
|
|
37
|
+
send(...args)
|
|
38
|
+
} else {
|
|
39
|
+
sentBufferAr.push(args)
|
|
40
|
+
}
|
|
41
|
+
} // END sentBufferFn
|
|
42
|
+
|
|
43
|
+
const hostId = makeid(20)
|
|
44
|
+
const agent = parser.setUA(req.headers['user-agent']).getResult()
|
|
45
|
+
const sharedValues = {
|
|
46
|
+
socket, req, agent, send: (type, data, err) => sentBufferFn(false, type, data, err)
|
|
47
|
+
}
|
|
48
|
+
sharedValues.send.toString = () => hostId
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
let result = onConnent(socket, req, sharedValues.send)
|
|
52
|
+
if (!result || !result.then) {
|
|
53
|
+
result = Promise.resolve(result)
|
|
54
|
+
}
|
|
55
|
+
result.then(defaultEvents)
|
|
56
|
+
.then(({ embed, onReceive, onSend, onError, onDisconnent }) => {
|
|
57
|
+
const isOk = socketOpen(socket, req, onError)
|
|
58
|
+
|
|
59
|
+
if (!isOk) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
const checkReply = replySecurity()
|
|
65
|
+
const ape = {
|
|
66
|
+
socket,
|
|
67
|
+
req,
|
|
68
|
+
hostId,
|
|
69
|
+
checkReply,
|
|
70
|
+
events: { onReceive, onSend, onError, onDisconnent },
|
|
71
|
+
controllers,
|
|
72
|
+
sharedValues,
|
|
73
|
+
embedValues: embed
|
|
74
|
+
}// END ape
|
|
75
|
+
send = socketSend(ape)
|
|
76
|
+
ape.send = send
|
|
77
|
+
|
|
78
|
+
// Track this client for broadcast
|
|
79
|
+
const clientInfo = { hostId, send, embed }
|
|
80
|
+
addClient(clientInfo)
|
|
81
|
+
|
|
82
|
+
// Remove client on disconnect
|
|
83
|
+
socket.on('close', () => {
|
|
84
|
+
removeClient(clientInfo)
|
|
85
|
+
onDisconnent()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
sentBufferAr.forEach(args => send(...args))
|
|
89
|
+
sentBufferAr = []
|
|
90
|
+
socket.on('message', socketReceive(ape))
|
|
91
|
+
}) // END result.then
|
|
92
|
+
|
|
93
|
+
} // END webSocketHandler
|
|
94
|
+
} // END wiring
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract root domain from a URL
|
|
3
|
+
* e.g., "https://sub.example.com:3000/path" -> "example.com"
|
|
4
|
+
*/
|
|
5
|
+
module.exports = function extractRootDomain(url) {
|
|
6
|
+
if (!url) return ''
|
|
7
|
+
try {
|
|
8
|
+
// Handle full URLs
|
|
9
|
+
if (url.includes('://')) {
|
|
10
|
+
const hostname = new URL(url).hostname
|
|
11
|
+
const parts = hostname.split('.')
|
|
12
|
+
return parts.length > 2 ? parts.slice(-2).join('.') : hostname
|
|
13
|
+
}
|
|
14
|
+
// Handle hostname:port format
|
|
15
|
+
const hostname = url.split(':')[0]
|
|
16
|
+
const parts = hostname.split('.')
|
|
17
|
+
return parts.length > 2 ? parts.slice(-2).join('.') : hostname
|
|
18
|
+
} catch {
|
|
19
|
+
return url.split(':')[0]
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const extractRootDomain = require('./extractRootDomain')
|
|
2
|
+
|
|
3
|
+
module.exports = function (socket, req, onError) {
|
|
4
|
+
onError = onError || console.error
|
|
5
|
+
const origin = extractRootDomain(req.header('Origin') || "");
|
|
6
|
+
const host = extractRootDomain(req.header('Host'));
|
|
7
|
+
if (origin && origin !== host) {
|
|
8
|
+
onError("REJECTING socket from " + req.header('Origin') + " miss-match with " + req.header('Host'))
|
|
9
|
+
socket.destroy()
|
|
10
|
+
return false
|
|
11
|
+
}
|
|
12
|
+
return true
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = function(){
|
|
2
|
+
let requestCheck = []
|
|
3
|
+
return (queryId,createdAt)=>{
|
|
4
|
+
const startTime = Date.now();
|
|
5
|
+
if (createdAt > startTime) {
|
|
6
|
+
throw new Error("createdAt ahead of server by `${(createdAt - startTime) / 1000}secs. +${msg}`")
|
|
7
|
+
}
|
|
8
|
+
const tenSecAgo = startTime - 10000
|
|
9
|
+
if(createdAt < tenSecAgo) {
|
|
10
|
+
throw new Error("request is old by `${(startTime - createdAt) / 1000}secs. +${msg}`")
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
requestCheck = requestCheck.filter(([passQueryId,createdWhen])=>{
|
|
14
|
+
if (passQueryId === queryId) {
|
|
15
|
+
throw new Error(`Reply: ${queryId} ${msg}`)
|
|
16
|
+
}
|
|
17
|
+
return createdWhen > tenSecAgo
|
|
18
|
+
})
|
|
19
|
+
requestCheck.push([queryId,createdAt])
|
|
20
|
+
} // END checkReply
|
|
21
|
+
} // END replySecurity
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const messageHash = require('../../utils/messageHash')
|
|
2
|
+
const { broadcast, online, getClients } = require('../lib/broadcast')
|
|
3
|
+
const jss = require('../../utils/jss')
|
|
4
|
+
|
|
5
|
+
module.exports = function receiveHandler(ape) {
|
|
6
|
+
const { send, checkReply, events, controllers, sharedValues, hostId, embedValues } = ape
|
|
7
|
+
|
|
8
|
+
// Build `this` context for controllers
|
|
9
|
+
// Includes: client metadata + api-ape utilities
|
|
10
|
+
const that = {
|
|
11
|
+
...sharedValues,
|
|
12
|
+
...embedValues,
|
|
13
|
+
// api-ape utilities available via `this`
|
|
14
|
+
broadcast: (type, data) => broadcast(type, data),
|
|
15
|
+
broadcastOthers: (type, data) => broadcast(type, data, hostId), // exclude self
|
|
16
|
+
online,
|
|
17
|
+
getClients,
|
|
18
|
+
hostId
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return function onReceive(msg) {
|
|
22
|
+
// Convert Buffer to string - WebSocket messages may arrive as binary
|
|
23
|
+
const msgString = typeof msg === 'string' ? msg : msg.toString('utf8');
|
|
24
|
+
const queryId = messageHash(msgString);
|
|
25
|
+
try {
|
|
26
|
+
const { type: rawType, data, referer, createdAt, requestedAt } = jss.parse(msgString);
|
|
27
|
+
|
|
28
|
+
// Normalize type: strip leading slash, lowercase
|
|
29
|
+
const type = rawType.replace(/^\//, '').toLowerCase()
|
|
30
|
+
|
|
31
|
+
// Call onReceive hook - it should return a finish callback
|
|
32
|
+
const onFinish = events.onReceive(queryId, data, type) || (() => { })
|
|
33
|
+
|
|
34
|
+
const result = new Promise((resolve, reject) => {
|
|
35
|
+
try {
|
|
36
|
+
const controller = controllers[type]
|
|
37
|
+
if (!controller) {
|
|
38
|
+
throw `TypeError: "${type}" was not found`
|
|
39
|
+
}
|
|
40
|
+
checkReply(queryId, createdAt)
|
|
41
|
+
resolve(controller.call(that, data))
|
|
42
|
+
} catch (err) {
|
|
43
|
+
reject(err)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
result.then(val => {
|
|
47
|
+
if (undefined !== val) {
|
|
48
|
+
send(queryId, false, val, false)
|
|
49
|
+
}
|
|
50
|
+
if (typeof onFinish === 'function') {
|
|
51
|
+
onFinish(false, val)
|
|
52
|
+
}
|
|
53
|
+
}).catch(err => {
|
|
54
|
+
send(queryId, false, false, err)
|
|
55
|
+
if (typeof onFinish === 'function') {
|
|
56
|
+
onFinish(err, true)
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
} catch (err) {
|
|
61
|
+
const errMessage = err.message || err
|
|
62
|
+
events.onError(hostId, queryId, errMessage)
|
|
63
|
+
} // END catch
|
|
64
|
+
|
|
65
|
+
} // END onReceive
|
|
66
|
+
} // END receiveHandler
|