@directus/api 11.0.1 → 12.0.0
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/dist/app.js +3 -4
- package/dist/auth/auth.d.ts +4 -4
- package/dist/auth/auth.js +2 -2
- package/dist/auth/drivers/ldap.js +20 -17
- package/dist/auth/drivers/local.js +5 -5
- package/dist/auth/drivers/oauth2.js +16 -16
- package/dist/auth/drivers/openid.js +18 -17
- package/dist/auth/drivers/saml.js +6 -7
- package/dist/auth.js +3 -2
- package/dist/cache.js +3 -13
- package/dist/cli/utils/create-env/env-stub.liquid +5 -7
- package/dist/controllers/activity.js +7 -6
- package/dist/controllers/assets.js +25 -12
- package/dist/controllers/auth.js +8 -7
- package/dist/controllers/collections.js +4 -3
- package/dist/controllers/dashboards.js +5 -4
- package/dist/controllers/extensions.js +3 -3
- package/dist/controllers/fields.js +9 -8
- package/dist/controllers/files.js +11 -11
- package/dist/controllers/flows.js +5 -4
- package/dist/controllers/folders.js +5 -4
- package/dist/controllers/items.js +14 -13
- package/dist/controllers/not-found.js +2 -2
- package/dist/controllers/notifications.js +5 -4
- package/dist/controllers/operations.js +5 -4
- package/dist/controllers/panels.js +5 -4
- package/dist/controllers/permissions.js +5 -4
- package/dist/controllers/presets.js +5 -4
- package/dist/controllers/relations.js +6 -5
- package/dist/controllers/roles.js +5 -4
- package/dist/controllers/schema.js +8 -8
- package/dist/controllers/server.js +2 -2
- package/dist/controllers/settings.js +3 -2
- package/dist/controllers/shares.js +7 -6
- package/dist/controllers/translations.js +6 -5
- package/dist/controllers/users.js +22 -21
- package/dist/controllers/utils.js +10 -10
- package/dist/controllers/webhooks.js +5 -4
- package/dist/{exceptions/database → database/errors}/dialects/mssql.js +8 -18
- package/dist/{exceptions/database → database/errors}/dialects/mysql.js +9 -19
- package/dist/{exceptions/database → database/errors}/dialects/oracle.js +2 -2
- package/dist/{exceptions/database → database/errors}/dialects/postgres.js +7 -18
- package/dist/{exceptions/database → database/errors}/dialects/sqlite.js +7 -10
- package/dist/{exceptions/database → database/errors}/translate.js +1 -1
- package/dist/database/migrations/run.js +10 -1
- package/dist/emitter.d.ts +3 -2
- package/dist/emitter.js +12 -4
- package/dist/env.js +21 -17
- package/dist/errors/codes.d.ts +29 -0
- package/dist/errors/codes.js +30 -0
- package/dist/errors/contains-null-values.d.ts +7 -0
- package/dist/errors/contains-null-values.js +4 -0
- package/dist/errors/content-too-large.d.ts +1 -0
- package/dist/errors/content-too-large.js +3 -0
- package/dist/errors/forbidden.d.ts +1 -0
- package/dist/errors/forbidden.js +3 -0
- package/dist/errors/hit-rate-limit.d.ts +6 -0
- package/dist/errors/hit-rate-limit.js +8 -0
- package/dist/errors/illegal-asset-transformation.d.ts +4 -0
- package/dist/errors/illegal-asset-transformation.js +3 -0
- package/dist/errors/index.d.ts +28 -0
- package/dist/errors/index.js +28 -0
- package/dist/errors/invalid-credentials.d.ts +1 -0
- package/dist/errors/invalid-credentials.js +3 -0
- package/dist/errors/invalid-foreign-key.d.ts +6 -0
- package/dist/errors/invalid-foreign-key.js +14 -0
- package/dist/errors/invalid-ip.d.ts +1 -0
- package/dist/errors/invalid-ip.js +3 -0
- package/dist/errors/invalid-otp.d.ts +1 -0
- package/dist/errors/invalid-otp.js +3 -0
- package/dist/errors/invalid-payload.d.ts +5 -0
- package/dist/errors/invalid-payload.js +4 -0
- package/dist/errors/invalid-provider-config.d.ts +5 -0
- package/dist/errors/invalid-provider-config.js +3 -0
- package/dist/errors/invalid-provider.d.ts +1 -0
- package/dist/errors/invalid-provider.js +3 -0
- package/dist/errors/invalid-query.d.ts +5 -0
- package/dist/errors/invalid-query.js +4 -0
- package/dist/errors/invalid-token.d.ts +1 -0
- package/dist/errors/invalid-token.js +3 -0
- package/dist/errors/method-not-allowed.d.ts +6 -0
- package/dist/errors/method-not-allowed.js +6 -0
- package/dist/errors/not-null-violation.d.ts +6 -0
- package/dist/errors/not-null-violation.js +14 -0
- package/dist/errors/range-not-satisfiable.d.ts +7 -0
- package/dist/errors/range-not-satisfiable.js +7 -0
- package/dist/errors/record-not-unique.d.ts +6 -0
- package/dist/errors/record-not-unique.js +14 -0
- package/dist/errors/route-not-found.d.ts +5 -0
- package/dist/errors/route-not-found.js +4 -0
- package/dist/errors/service-unavailable.d.ts +7 -0
- package/dist/errors/service-unavailable.js +4 -0
- package/dist/errors/token-expired.d.ts +1 -0
- package/dist/errors/token-expired.js +3 -0
- package/dist/errors/unexpected-response.d.ts +1 -0
- package/dist/errors/unexpected-response.js +3 -0
- package/dist/errors/unprocessable-content.d.ts +5 -0
- package/dist/errors/unprocessable-content.js +4 -0
- package/dist/errors/unsupported-media-type.d.ts +6 -0
- package/dist/errors/unsupported-media-type.js +4 -0
- package/dist/errors/user-suspended.d.ts +1 -0
- package/dist/errors/user-suspended.js +3 -0
- package/dist/errors/value-out-of-range.d.ts +6 -0
- package/dist/errors/value-out-of-range.js +14 -0
- package/dist/errors/value-too-long.d.ts +6 -0
- package/dist/errors/value-too-long.js +14 -0
- package/dist/extensions.js +0 -4
- package/dist/flows.js +6 -8
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -2
- package/dist/messenger.d.ts +3 -3
- package/dist/messenger.js +18 -9
- package/dist/middleware/authenticate.js +2 -38
- package/dist/middleware/check-ip.js +2 -2
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/middleware/error-handler.js +7 -7
- package/dist/middleware/graphql.js +11 -9
- package/dist/middleware/rate-limiter-global.d.ts +2 -2
- package/dist/middleware/rate-limiter-global.js +2 -3
- package/dist/middleware/rate-limiter-ip.d.ts +2 -2
- package/dist/middleware/rate-limiter-ip.js +2 -3
- package/dist/middleware/validate-batch.js +3 -4
- package/dist/rate-limiter.js +2 -9
- package/dist/server.js +10 -0
- package/dist/services/activity.js +3 -2
- package/dist/services/assets.js +9 -10
- package/dist/services/authentication.js +12 -11
- package/dist/services/authorization.d.ts +1 -1
- package/dist/services/authorization.js +16 -16
- package/dist/services/collections.js +17 -16
- package/dist/services/fields.js +16 -14
- package/dist/services/files.js +7 -6
- package/dist/services/graphql/errors/execution.d.ts +6 -0
- package/dist/services/graphql/errors/execution.js +2 -0
- package/dist/services/graphql/errors/index.d.ts +2 -0
- package/dist/services/graphql/errors/index.js +2 -0
- package/dist/services/graphql/errors/validation.d.ts +6 -0
- package/dist/services/graphql/errors/validation.js +2 -0
- package/dist/services/graphql/index.d.ts +2 -8
- package/dist/services/graphql/index.js +125 -66
- package/dist/services/graphql/subscription.d.ts +16 -0
- package/dist/services/graphql/subscription.js +77 -0
- package/dist/services/graphql/utils/process-error.js +3 -3
- package/dist/services/import-export.js +7 -7
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +14 -13
- package/dist/services/mail/index.js +3 -3
- package/dist/services/meta.js +3 -3
- package/dist/services/payload.js +11 -7
- package/dist/services/relations.js +32 -22
- package/dist/services/revisions.js +3 -3
- package/dist/services/roles.js +10 -9
- package/dist/services/schema.js +5 -5
- package/dist/services/server.js +24 -0
- package/dist/services/shares.js +4 -4
- package/dist/services/tfa.js +6 -6
- package/dist/services/translations.d.ts +2 -2
- package/dist/services/translations.js +4 -4
- package/dist/services/users.js +26 -29
- package/dist/services/utils.js +4 -4
- package/dist/services/websocket.d.ts +14 -0
- package/dist/services/websocket.js +26 -0
- package/dist/synchronization.js +3 -3
- package/dist/types/items.d.ts +2 -2
- package/dist/utils/apply-diff.js +13 -4
- package/dist/utils/apply-query.js +22 -13
- package/dist/utils/get-accountability-for-role.js +1 -2
- package/dist/utils/get-accountability-for-token.d.ts +2 -0
- package/dist/utils/get-accountability-for-token.js +50 -0
- package/dist/utils/get-column-path.js +5 -3
- package/dist/utils/get-column.js +3 -3
- package/dist/utils/get-service.d.ts +7 -0
- package/dist/utils/get-service.js +49 -0
- package/dist/utils/jwt.js +5 -5
- package/dist/utils/redact.d.ts +4 -0
- package/dist/utils/redact.js +15 -1
- package/dist/utils/to-boolean.d.ts +4 -0
- package/dist/utils/to-boolean.js +6 -0
- package/dist/utils/validate-diff.js +23 -9
- package/dist/utils/validate-keys.js +3 -3
- package/dist/utils/validate-query.d.ts +2 -0
- package/dist/utils/validate-query.js +27 -21
- package/dist/utils/validate-snapshot.js +11 -5
- package/dist/websocket/authenticate.d.ts +6 -0
- package/dist/websocket/authenticate.js +59 -0
- package/dist/websocket/controllers/base.d.ts +42 -0
- package/dist/websocket/controllers/base.js +279 -0
- package/dist/websocket/controllers/graphql.d.ts +12 -0
- package/dist/websocket/controllers/graphql.js +102 -0
- package/dist/websocket/controllers/hooks.d.ts +1 -0
- package/dist/websocket/controllers/hooks.js +122 -0
- package/dist/websocket/controllers/index.d.ts +10 -0
- package/dist/websocket/controllers/index.js +31 -0
- package/dist/websocket/controllers/rest.d.ts +9 -0
- package/dist/websocket/controllers/rest.js +47 -0
- package/dist/websocket/errors.d.ts +16 -0
- package/dist/websocket/errors.js +55 -0
- package/dist/websocket/handlers/heartbeat.d.ts +11 -0
- package/dist/websocket/handlers/heartbeat.js +72 -0
- package/dist/websocket/handlers/index.d.ts +4 -0
- package/dist/websocket/handlers/index.js +11 -0
- package/dist/websocket/handlers/items.d.ts +6 -0
- package/dist/websocket/handlers/items.js +103 -0
- package/dist/websocket/handlers/subscribe.d.ts +43 -0
- package/dist/websocket/handlers/subscribe.js +278 -0
- package/dist/websocket/messages.d.ts +311 -0
- package/dist/websocket/messages.js +96 -0
- package/dist/websocket/types.d.ts +34 -0
- package/dist/websocket/types.js +1 -0
- package/dist/websocket/utils/get-expires-at-for-token.d.ts +1 -0
- package/dist/websocket/utils/get-expires-at-for-token.js +8 -0
- package/dist/websocket/utils/message.d.ts +4 -0
- package/dist/websocket/utils/message.js +27 -0
- package/dist/websocket/utils/wait-for-message.d.ts +4 -0
- package/dist/websocket/utils/wait-for-message.js +45 -0
- package/package.json +21 -16
- package/dist/exceptions/content-too-large.d.ts +0 -4
- package/dist/exceptions/content-too-large.js +0 -6
- package/dist/exceptions/database/contains-null-values.d.ts +0 -9
- package/dist/exceptions/database/contains-null-values.js +0 -6
- package/dist/exceptions/database/invalid-foreign-key.d.ts +0 -10
- package/dist/exceptions/database/invalid-foreign-key.js +0 -11
- package/dist/exceptions/database/not-null-violation.d.ts +0 -9
- package/dist/exceptions/database/not-null-violation.js +0 -6
- package/dist/exceptions/database/record-not-unique.d.ts +0 -10
- package/dist/exceptions/database/record-not-unique.js +0 -11
- package/dist/exceptions/database/value-out-of-range.d.ts +0 -10
- package/dist/exceptions/database/value-out-of-range.js +0 -11
- package/dist/exceptions/database/value-too-long.d.ts +0 -9
- package/dist/exceptions/database/value-too-long.js +0 -11
- package/dist/exceptions/forbidden.d.ts +0 -6
- package/dist/exceptions/forbidden.js +0 -13
- package/dist/exceptions/graphql-validation.d.ts +0 -4
- package/dist/exceptions/graphql-validation.js +0 -6
- package/dist/exceptions/hit-rate-limit.d.ts +0 -9
- package/dist/exceptions/hit-rate-limit.js +0 -6
- package/dist/exceptions/illegal-asset-transformation.d.ts +0 -4
- package/dist/exceptions/illegal-asset-transformation.js +0 -6
- package/dist/exceptions/index.d.ts +0 -21
- package/dist/exceptions/index.js +0 -21
- package/dist/exceptions/invalid-config.d.ts +0 -4
- package/dist/exceptions/invalid-config.js +0 -6
- package/dist/exceptions/invalid-credentials.d.ts +0 -4
- package/dist/exceptions/invalid-credentials.js +0 -6
- package/dist/exceptions/invalid-ip.d.ts +0 -4
- package/dist/exceptions/invalid-ip.js +0 -6
- package/dist/exceptions/invalid-otp.d.ts +0 -4
- package/dist/exceptions/invalid-otp.js +0 -6
- package/dist/exceptions/invalid-payload.d.ts +0 -4
- package/dist/exceptions/invalid-payload.js +0 -6
- package/dist/exceptions/invalid-provider.d.ts +0 -4
- package/dist/exceptions/invalid-provider.js +0 -6
- package/dist/exceptions/invalid-query.d.ts +0 -4
- package/dist/exceptions/invalid-query.js +0 -6
- package/dist/exceptions/invalid-token.d.ts +0 -4
- package/dist/exceptions/invalid-token.js +0 -6
- package/dist/exceptions/method-not-allowed.d.ts +0 -8
- package/dist/exceptions/method-not-allowed.js +0 -6
- package/dist/exceptions/range-not-satisfiable.d.ts +0 -5
- package/dist/exceptions/range-not-satisfiable.js +0 -9
- package/dist/exceptions/route-not-found.d.ts +0 -4
- package/dist/exceptions/route-not-found.js +0 -6
- package/dist/exceptions/service-unavailable.d.ts +0 -9
- package/dist/exceptions/service-unavailable.js +0 -6
- package/dist/exceptions/token-expired.d.ts +0 -4
- package/dist/exceptions/token-expired.js +0 -6
- package/dist/exceptions/unexpected-response.d.ts +0 -4
- package/dist/exceptions/unexpected-response.js +0 -6
- package/dist/exceptions/unprocessable-entity.d.ts +0 -4
- package/dist/exceptions/unprocessable-entity.js +0 -6
- package/dist/exceptions/unsupported-media-type.d.ts +0 -4
- package/dist/exceptions/unsupported-media-type.js +0 -6
- package/dist/exceptions/user-suspended.d.ts +0 -4
- package/dist/exceptions/user-suspended.js +0 -6
- /package/dist/{exceptions/database → database/errors}/dialects/mssql.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/mysql.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/oracle.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/postgres.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/sqlite.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/types.d.ts +0 -0
- /package/dist/{exceptions/database → database/errors}/dialects/types.js +0 -0
- /package/dist/{exceptions/database → database/errors}/translate.d.ts +0 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { parseJSON } from '@directus/utils';
|
|
2
|
+
import { parse } from 'url';
|
|
3
|
+
import { v4 as uuid } from 'uuid';
|
|
4
|
+
import WebSocket, { WebSocketServer } from 'ws';
|
|
5
|
+
import { fromZodError } from 'zod-validation-error';
|
|
6
|
+
import emitter from '../../emitter.js';
|
|
7
|
+
import env from '../../env.js';
|
|
8
|
+
import { InvalidProviderConfigError, TokenExpiredError } from '../../errors/index.js';
|
|
9
|
+
import logger from '../../logger.js';
|
|
10
|
+
import { createRateLimiter } from '../../rate-limiter.js';
|
|
11
|
+
import { getAccountabilityForToken } from '../../utils/get-accountability-for-token.js';
|
|
12
|
+
import { toBoolean } from '../../utils/to-boolean.js';
|
|
13
|
+
import { authenticateConnection, authenticationSuccess } from '../authenticate.js';
|
|
14
|
+
import { WebSocketError, handleWebSocketError } from '../errors.js';
|
|
15
|
+
import { AuthMode, WebSocketAuthMessage, WebSocketMessage } from '../messages.js';
|
|
16
|
+
import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js';
|
|
17
|
+
import { getMessageType } from '../utils/message.js';
|
|
18
|
+
import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js';
|
|
19
|
+
import { registerWebSocketEvents } from './hooks.js';
|
|
20
|
+
const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes
|
|
21
|
+
export default class SocketController {
|
|
22
|
+
server;
|
|
23
|
+
clients;
|
|
24
|
+
authentication;
|
|
25
|
+
endpoint;
|
|
26
|
+
maxConnections;
|
|
27
|
+
rateLimiter;
|
|
28
|
+
authInterval;
|
|
29
|
+
constructor(httpServer, configPrefix) {
|
|
30
|
+
this.server = new WebSocketServer({ noServer: true });
|
|
31
|
+
this.clients = new Set();
|
|
32
|
+
this.authInterval = null;
|
|
33
|
+
const { endpoint, authentication, maxConnections } = this.getEnvironmentConfig(configPrefix);
|
|
34
|
+
this.endpoint = endpoint;
|
|
35
|
+
this.authentication = authentication;
|
|
36
|
+
this.maxConnections = maxConnections;
|
|
37
|
+
this.rateLimiter = this.getRateLimiter();
|
|
38
|
+
httpServer.on('upgrade', this.handleUpgrade.bind(this));
|
|
39
|
+
this.checkClientTokens();
|
|
40
|
+
registerWebSocketEvents();
|
|
41
|
+
}
|
|
42
|
+
getEnvironmentConfig(configPrefix) {
|
|
43
|
+
const endpoint = String(env[`${configPrefix}_PATH`]);
|
|
44
|
+
const authMode = AuthMode.safeParse(String(env[`${configPrefix}_AUTH`]).toLowerCase());
|
|
45
|
+
const authTimeout = Number(env[`${configPrefix}_AUTH_TIMEOUT`]) * 1000;
|
|
46
|
+
const maxConnections = `${configPrefix}_CONN_LIMIT` in env ? Number(env[`${configPrefix}_CONN_LIMIT`]) : Number.POSITIVE_INFINITY;
|
|
47
|
+
if (!authMode.success) {
|
|
48
|
+
throw new InvalidProviderConfigError({
|
|
49
|
+
provider: 'ws',
|
|
50
|
+
reason: fromZodError(authMode.error, { prefix: `${configPrefix}_AUTH` }).message,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
endpoint,
|
|
55
|
+
maxConnections,
|
|
56
|
+
authentication: {
|
|
57
|
+
mode: authMode.data,
|
|
58
|
+
timeout: authTimeout,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
getRateLimiter() {
|
|
63
|
+
if (toBoolean(env['RATE_LIMITER_ENABLED']) === true) {
|
|
64
|
+
return createRateLimiter('RATE_LIMITER', {
|
|
65
|
+
keyPrefix: 'websocket',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
async handleUpgrade(request, socket, head) {
|
|
71
|
+
const { pathname, query } = parse(request.url, true);
|
|
72
|
+
if (pathname !== this.endpoint)
|
|
73
|
+
return;
|
|
74
|
+
if (this.clients.size >= this.maxConnections) {
|
|
75
|
+
logger.debug('WebSocket upgrade denied - max connections reached');
|
|
76
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
77
|
+
socket.destroy();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const context = { request, socket, head };
|
|
81
|
+
if (this.authentication.mode === 'strict') {
|
|
82
|
+
await this.handleStrictUpgrade(context, query);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (this.authentication.mode === 'handshake') {
|
|
86
|
+
await this.handleHandshakeUpgrade(context);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
90
|
+
const state = { accountability: null, expires_at: null };
|
|
91
|
+
this.server.emit('connection', ws, state);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async handleStrictUpgrade({ request, socket, head }, query) {
|
|
95
|
+
let accountability, expires_at;
|
|
96
|
+
try {
|
|
97
|
+
const token = query['access_token'];
|
|
98
|
+
accountability = await getAccountabilityForToken(token);
|
|
99
|
+
expires_at = getExpiresAtForToken(token);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
accountability = null;
|
|
103
|
+
expires_at = null;
|
|
104
|
+
}
|
|
105
|
+
if (!accountability || !accountability.user) {
|
|
106
|
+
logger.debug('WebSocket upgrade denied - ' + JSON.stringify(accountability || 'invalid'));
|
|
107
|
+
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
|
108
|
+
socket.destroy();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
112
|
+
const state = { accountability, expires_at };
|
|
113
|
+
this.server.emit('connection', ws, state);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
async handleHandshakeUpgrade({ request, socket, head }) {
|
|
117
|
+
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
118
|
+
try {
|
|
119
|
+
const payload = await waitForAnyMessage(ws, this.authentication.timeout);
|
|
120
|
+
if (getMessageType(payload) !== 'auth')
|
|
121
|
+
throw new Error();
|
|
122
|
+
const state = await authenticateConnection(WebSocketAuthMessage.parse(payload));
|
|
123
|
+
ws.send(authenticationSuccess(payload['uid'], state.refresh_token));
|
|
124
|
+
this.server.emit('connection', ws, state);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
logger.debug('WebSocket authentication handshake failed');
|
|
128
|
+
const error = new WebSocketError('auth', 'AUTH_FAILED', 'Authentication handshake failed.');
|
|
129
|
+
handleWebSocketError(ws, error, 'auth');
|
|
130
|
+
ws.close();
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
createClient(ws, { accountability, expires_at }) {
|
|
135
|
+
const client = ws;
|
|
136
|
+
client.accountability = accountability;
|
|
137
|
+
client.expires_at = expires_at;
|
|
138
|
+
client.uid = uuid();
|
|
139
|
+
client.auth_timer = null;
|
|
140
|
+
ws.on('message', async (data) => {
|
|
141
|
+
if (this.rateLimiter !== null) {
|
|
142
|
+
try {
|
|
143
|
+
await this.rateLimiter.consume(client.uid);
|
|
144
|
+
}
|
|
145
|
+
catch (limit) {
|
|
146
|
+
const timeout = limit?.msBeforeNext ?? this.rateLimiter.msDuration;
|
|
147
|
+
const error = new WebSocketError('server', 'REQUESTS_EXCEEDED', `Too many messages, retry after ${timeout}ms.`);
|
|
148
|
+
handleWebSocketError(client, error, 'server');
|
|
149
|
+
logger.debug(`WebSocket#${client.uid} is rate limited`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
let message;
|
|
154
|
+
try {
|
|
155
|
+
message = this.parseMessage(data.toString());
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
handleWebSocketError(client, err, 'server');
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (getMessageType(message) === 'auth') {
|
|
162
|
+
try {
|
|
163
|
+
await this.handleAuthRequest(client, WebSocketAuthMessage.parse(message));
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// ignore errors
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
// this log cannot be higher in the function or it will leak credentials
|
|
171
|
+
logger.trace(`WebSocket#${client.uid} - ${JSON.stringify(message)}`);
|
|
172
|
+
ws.emit('parsed-message', message);
|
|
173
|
+
});
|
|
174
|
+
ws.on('error', () => {
|
|
175
|
+
logger.debug(`WebSocket#${client.uid} connection errored`);
|
|
176
|
+
if (client.auth_timer) {
|
|
177
|
+
clearTimeout(client.auth_timer);
|
|
178
|
+
client.auth_timer = null;
|
|
179
|
+
}
|
|
180
|
+
this.clients.delete(client);
|
|
181
|
+
});
|
|
182
|
+
ws.on('close', () => {
|
|
183
|
+
logger.debug(`WebSocket#${client.uid} connection closed`);
|
|
184
|
+
if (client.auth_timer) {
|
|
185
|
+
clearTimeout(client.auth_timer);
|
|
186
|
+
client.auth_timer = null;
|
|
187
|
+
}
|
|
188
|
+
this.clients.delete(client);
|
|
189
|
+
});
|
|
190
|
+
logger.debug(`WebSocket#${client.uid} connected`);
|
|
191
|
+
if (accountability) {
|
|
192
|
+
logger.trace(`WebSocket#${client.uid} authenticated as ${JSON.stringify(accountability)}`);
|
|
193
|
+
}
|
|
194
|
+
this.setTokenExpireTimer(client);
|
|
195
|
+
this.clients.add(client);
|
|
196
|
+
return client;
|
|
197
|
+
}
|
|
198
|
+
parseMessage(data) {
|
|
199
|
+
let message;
|
|
200
|
+
try {
|
|
201
|
+
message = WebSocketMessage.parse(parseJSON(data));
|
|
202
|
+
}
|
|
203
|
+
catch (err) {
|
|
204
|
+
throw new WebSocketError('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
|
|
205
|
+
}
|
|
206
|
+
return message;
|
|
207
|
+
}
|
|
208
|
+
async handleAuthRequest(client, message) {
|
|
209
|
+
try {
|
|
210
|
+
const { accountability, expires_at, refresh_token } = await authenticateConnection(message);
|
|
211
|
+
client.accountability = accountability;
|
|
212
|
+
client.expires_at = expires_at;
|
|
213
|
+
this.setTokenExpireTimer(client);
|
|
214
|
+
emitter.emitAction('websocket.auth.success', { client });
|
|
215
|
+
client.send(authenticationSuccess(message.uid, refresh_token));
|
|
216
|
+
logger.trace(`WebSocket#${client.uid} authenticated as ${JSON.stringify(client.accountability)}`);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
logger.trace(`WebSocket#${client.uid} failed authentication`);
|
|
220
|
+
emitter.emitAction('websocket.auth.failure', { client });
|
|
221
|
+
client.accountability = null;
|
|
222
|
+
client.expires_at = null;
|
|
223
|
+
const _error = error instanceof WebSocketError
|
|
224
|
+
? error
|
|
225
|
+
: new WebSocketError('auth', 'AUTH_FAILED', 'Authentication failed.', message.uid);
|
|
226
|
+
handleWebSocketError(client, _error, 'auth');
|
|
227
|
+
if (this.authentication.mode !== 'public') {
|
|
228
|
+
client.close();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
setTokenExpireTimer(client) {
|
|
233
|
+
if (client.auth_timer !== null) {
|
|
234
|
+
// clear up old timeouts if needed
|
|
235
|
+
clearTimeout(client.auth_timer);
|
|
236
|
+
client.auth_timer = null;
|
|
237
|
+
}
|
|
238
|
+
if (!client.expires_at)
|
|
239
|
+
return;
|
|
240
|
+
const expiresIn = client.expires_at * 1000 - Date.now();
|
|
241
|
+
if (expiresIn > TOKEN_CHECK_INTERVAL)
|
|
242
|
+
return;
|
|
243
|
+
client.auth_timer = setTimeout(() => {
|
|
244
|
+
client.accountability = null;
|
|
245
|
+
client.expires_at = null;
|
|
246
|
+
handleWebSocketError(client, new TokenExpiredError(), 'auth');
|
|
247
|
+
waitForMessageType(client, 'auth', this.authentication.timeout).catch((msg) => {
|
|
248
|
+
const error = new WebSocketError('auth', 'AUTH_TIMEOUT', 'Authentication timed out.', msg?.uid);
|
|
249
|
+
handleWebSocketError(client, error, 'auth');
|
|
250
|
+
if (this.authentication.mode !== 'public') {
|
|
251
|
+
client.close();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}, expiresIn);
|
|
255
|
+
}
|
|
256
|
+
checkClientTokens() {
|
|
257
|
+
this.authInterval = setInterval(() => {
|
|
258
|
+
if (this.clients.size === 0)
|
|
259
|
+
return;
|
|
260
|
+
// check the clients and set shorter timeouts if needed
|
|
261
|
+
for (const client of this.clients) {
|
|
262
|
+
if (client.expires_at === null || client.auth_timer !== null)
|
|
263
|
+
continue;
|
|
264
|
+
this.setTokenExpireTimer(client);
|
|
265
|
+
}
|
|
266
|
+
}, TOKEN_CHECK_INTERVAL);
|
|
267
|
+
}
|
|
268
|
+
terminate() {
|
|
269
|
+
if (this.authInterval)
|
|
270
|
+
clearInterval(this.authInterval);
|
|
271
|
+
this.clients.forEach((client) => {
|
|
272
|
+
if (client.auth_timer)
|
|
273
|
+
clearTimeout(client.auth_timer);
|
|
274
|
+
});
|
|
275
|
+
this.server.clients.forEach((ws) => {
|
|
276
|
+
ws.terminate();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { Server } from 'graphql-ws';
|
|
3
|
+
import type { Server as httpServer } from 'http';
|
|
4
|
+
import type { GraphQLSocket, UpgradeContext, WebSocketClient } from '../types.js';
|
|
5
|
+
import SocketController from './base.js';
|
|
6
|
+
export declare class GraphQLSubscriptionController extends SocketController {
|
|
7
|
+
gql: Server<GraphQLSocket>;
|
|
8
|
+
constructor(httpServer: httpServer);
|
|
9
|
+
private bindEvents;
|
|
10
|
+
setTokenExpireTimer(client: WebSocketClient): void;
|
|
11
|
+
protected handleHandshakeUpgrade({ request, socket, head }: UpgradeContext): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { CloseCode, MessageType, makeServer } from 'graphql-ws';
|
|
2
|
+
import env from '../../env.js';
|
|
3
|
+
import logger from '../../logger.js';
|
|
4
|
+
import { bindPubSub } from '../../services/graphql/subscription.js';
|
|
5
|
+
import { GraphQLService } from '../../services/index.js';
|
|
6
|
+
import { getSchema } from '../../utils/get-schema.js';
|
|
7
|
+
import { authenticateConnection, refreshAccountability } from '../authenticate.js';
|
|
8
|
+
import { handleWebSocketError } from '../errors.js';
|
|
9
|
+
import { ConnectionParams, WebSocketMessage } from '../messages.js';
|
|
10
|
+
import { getMessageType } from '../utils/message.js';
|
|
11
|
+
import SocketController from './base.js';
|
|
12
|
+
export class GraphQLSubscriptionController extends SocketController {
|
|
13
|
+
gql;
|
|
14
|
+
constructor(httpServer) {
|
|
15
|
+
super(httpServer, 'WEBSOCKETS_GRAPHQL');
|
|
16
|
+
this.server.on('connection', (ws, auth) => {
|
|
17
|
+
this.bindEvents(this.createClient(ws, auth));
|
|
18
|
+
});
|
|
19
|
+
this.gql = makeServer({
|
|
20
|
+
schema: async (ctx) => {
|
|
21
|
+
const accountability = ctx.extra.client.accountability;
|
|
22
|
+
// for now only the items will be watched, system events tbd
|
|
23
|
+
const service = new GraphQLService({
|
|
24
|
+
schema: await getSchema(),
|
|
25
|
+
scope: 'items',
|
|
26
|
+
accountability,
|
|
27
|
+
});
|
|
28
|
+
return service.getSchema();
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
bindPubSub();
|
|
32
|
+
logger.info(`GraphQL Subscriptions started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
|
|
33
|
+
}
|
|
34
|
+
bindEvents(client) {
|
|
35
|
+
const closedHandler = this.gql.opened({
|
|
36
|
+
protocol: client.protocol,
|
|
37
|
+
send: (data) => new Promise((resolve, reject) => {
|
|
38
|
+
client.send(data, (err) => (err ? reject(err) : resolve()));
|
|
39
|
+
}),
|
|
40
|
+
close: (code, reason) => client.close(code, reason),
|
|
41
|
+
onMessage: (cb) => {
|
|
42
|
+
client.on('parsed-message', async (message) => {
|
|
43
|
+
try {
|
|
44
|
+
if (getMessageType(message) === 'connection_init' && this.authentication.mode !== 'strict') {
|
|
45
|
+
const params = ConnectionParams.parse(message['payload'] ?? {});
|
|
46
|
+
if (this.authentication.mode === 'handshake') {
|
|
47
|
+
if (typeof params.access_token === 'string') {
|
|
48
|
+
const { accountability, expires_at } = await authenticateConnection({
|
|
49
|
+
access_token: params.access_token,
|
|
50
|
+
});
|
|
51
|
+
client.accountability = accountability;
|
|
52
|
+
client.expires_at = expires_at;
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
client.close(CloseCode.Forbidden, 'Forbidden');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if (this.authentication.mode === 'handshake' && !client.accountability?.user) {
|
|
61
|
+
// the first message should authenticate successfully in this mode
|
|
62
|
+
client.close(CloseCode.Forbidden, 'Forbidden');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
client.accountability = await refreshAccountability(client.accountability);
|
|
67
|
+
}
|
|
68
|
+
await cb(JSON.stringify(message));
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
handleWebSocketError(client, error, MessageType.Error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
}, { client });
|
|
76
|
+
// notify server that the socket closed
|
|
77
|
+
client.once('close', (code, reason) => closedHandler(code, reason.toString()));
|
|
78
|
+
// check strict authentication status
|
|
79
|
+
if (this.authentication.mode === 'strict' && !client.accountability?.user) {
|
|
80
|
+
client.close(CloseCode.Forbidden, 'Forbidden');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
setTokenExpireTimer(client) {
|
|
84
|
+
if (client.auth_timer !== null) {
|
|
85
|
+
clearTimeout(client.auth_timer);
|
|
86
|
+
client.auth_timer = null;
|
|
87
|
+
}
|
|
88
|
+
if (this.authentication.mode !== 'handshake')
|
|
89
|
+
return;
|
|
90
|
+
client.auth_timer = setTimeout(() => {
|
|
91
|
+
if (!client.accountability?.user) {
|
|
92
|
+
client.close(CloseCode.Forbidden, 'Forbidden');
|
|
93
|
+
}
|
|
94
|
+
}, this.authentication.timeout);
|
|
95
|
+
}
|
|
96
|
+
async handleHandshakeUpgrade({ request, socket, head }) {
|
|
97
|
+
this.server.handleUpgrade(request, socket, head, async (ws) => {
|
|
98
|
+
this.server.emit('connection', ws, { accountability: null, expires_at: null });
|
|
99
|
+
// actual enforcement is handled by the setTokenExpireTimer function
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerWebSocketEvents(): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import emitter from '../../emitter.js';
|
|
2
|
+
import { getMessenger } from '../../messenger.js';
|
|
3
|
+
let actionsRegistered = false;
|
|
4
|
+
export function registerWebSocketEvents() {
|
|
5
|
+
if (actionsRegistered)
|
|
6
|
+
return;
|
|
7
|
+
actionsRegistered = true;
|
|
8
|
+
registerActionHooks([
|
|
9
|
+
'items',
|
|
10
|
+
'activity',
|
|
11
|
+
'collections',
|
|
12
|
+
'folders',
|
|
13
|
+
'permissions',
|
|
14
|
+
'presets',
|
|
15
|
+
'revisions',
|
|
16
|
+
'roles',
|
|
17
|
+
'settings',
|
|
18
|
+
'users',
|
|
19
|
+
'webhooks',
|
|
20
|
+
]);
|
|
21
|
+
registerFieldsHooks();
|
|
22
|
+
registerFilesHooks();
|
|
23
|
+
registerRelationsHooks();
|
|
24
|
+
}
|
|
25
|
+
function registerActionHooks(modules) {
|
|
26
|
+
// register event hooks that can be handled in an uniform manner
|
|
27
|
+
for (const module of modules) {
|
|
28
|
+
registerAction(module + '.create', ({ key, collection, payload = {} }) => ({
|
|
29
|
+
collection,
|
|
30
|
+
action: 'create',
|
|
31
|
+
key,
|
|
32
|
+
payload,
|
|
33
|
+
}));
|
|
34
|
+
registerAction(module + '.update', ({ keys, collection, payload = {} }) => ({
|
|
35
|
+
collection,
|
|
36
|
+
action: 'update',
|
|
37
|
+
keys,
|
|
38
|
+
payload,
|
|
39
|
+
}));
|
|
40
|
+
registerAction(module + '.delete', ({ keys, collection, payload = [] }) => ({
|
|
41
|
+
collection,
|
|
42
|
+
action: 'delete',
|
|
43
|
+
keys,
|
|
44
|
+
payload,
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function registerFieldsHooks() {
|
|
49
|
+
// exception for field hooks that don't report `directus_fields` as being the collection
|
|
50
|
+
registerAction('fields.create', ({ key, payload = {} }) => ({
|
|
51
|
+
collection: 'directus_fields',
|
|
52
|
+
action: 'create',
|
|
53
|
+
key,
|
|
54
|
+
payload,
|
|
55
|
+
}));
|
|
56
|
+
registerAction('fields.update', ({ keys, payload = {} }) => ({
|
|
57
|
+
collection: 'directus_fields',
|
|
58
|
+
action: 'update',
|
|
59
|
+
keys,
|
|
60
|
+
payload,
|
|
61
|
+
}));
|
|
62
|
+
registerAction('fields.delete', ({ keys, payload = [] }) => ({
|
|
63
|
+
collection: 'directus_fields',
|
|
64
|
+
action: 'delete',
|
|
65
|
+
keys,
|
|
66
|
+
payload,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
function registerFilesHooks() {
|
|
70
|
+
// extra event for file uploads that doubles as create event
|
|
71
|
+
registerAction('files.upload', ({ key, collection, payload = {} }) => ({
|
|
72
|
+
collection,
|
|
73
|
+
action: 'create',
|
|
74
|
+
key,
|
|
75
|
+
payload,
|
|
76
|
+
}));
|
|
77
|
+
registerAction('files.update', ({ keys, collection, payload = {} }) => ({
|
|
78
|
+
collection,
|
|
79
|
+
action: 'update',
|
|
80
|
+
keys,
|
|
81
|
+
payload,
|
|
82
|
+
}));
|
|
83
|
+
registerAction('files.delete', ({ keys, collection, payload = [] }) => ({
|
|
84
|
+
collection,
|
|
85
|
+
action: 'delete',
|
|
86
|
+
keys,
|
|
87
|
+
payload,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
function registerRelationsHooks() {
|
|
91
|
+
// exception for relation hooks that don't report `directus_relations` as being the collection
|
|
92
|
+
registerAction('relations.create', ({ key, payload = {} }) => ({
|
|
93
|
+
collection: 'directus_relations',
|
|
94
|
+
action: 'create',
|
|
95
|
+
key,
|
|
96
|
+
payload: { ...payload, key },
|
|
97
|
+
}));
|
|
98
|
+
registerAction('relations.update', ({ keys, payload = {} }) => ({
|
|
99
|
+
collection: 'directus_relations',
|
|
100
|
+
action: 'update',
|
|
101
|
+
keys,
|
|
102
|
+
payload,
|
|
103
|
+
}));
|
|
104
|
+
registerAction('relations.delete', ({ collection, payload = [] }) => ({
|
|
105
|
+
collection: 'directus_relations',
|
|
106
|
+
action: 'delete',
|
|
107
|
+
keys: payload,
|
|
108
|
+
payload: { collection, fields: payload },
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Wrapper for emitter.onAction to hook into system events
|
|
113
|
+
* @param event The action event to watch
|
|
114
|
+
* @param transform Transformer function
|
|
115
|
+
*/
|
|
116
|
+
function registerAction(event, transform) {
|
|
117
|
+
const messenger = getMessenger();
|
|
118
|
+
emitter.onAction(event, async (data) => {
|
|
119
|
+
// push the event through the Redis pub/sub
|
|
120
|
+
messenger.publish('websocket.event', transform(data));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { Server as httpServer } from 'http';
|
|
3
|
+
import { GraphQLSubscriptionController } from './graphql.js';
|
|
4
|
+
import { WebSocketController } from './rest.js';
|
|
5
|
+
export declare function createWebSocketController(server: httpServer): void;
|
|
6
|
+
export declare function getWebSocketController(): WebSocketController;
|
|
7
|
+
export declare function createSubscriptionController(server: httpServer): void;
|
|
8
|
+
export declare function getSubscriptionController(): GraphQLSubscriptionController | undefined;
|
|
9
|
+
export * from './graphql.js';
|
|
10
|
+
export * from './rest.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import env from '../../env.js';
|
|
2
|
+
import { ServiceUnavailableError } from '../../errors/index.js';
|
|
3
|
+
import { toBoolean } from '../../utils/to-boolean.js';
|
|
4
|
+
import { GraphQLSubscriptionController } from './graphql.js';
|
|
5
|
+
import { WebSocketController } from './rest.js';
|
|
6
|
+
let websocketController;
|
|
7
|
+
let subscriptionController;
|
|
8
|
+
export function createWebSocketController(server) {
|
|
9
|
+
if (toBoolean(env['WEBSOCKETS_REST_ENABLED'])) {
|
|
10
|
+
websocketController = new WebSocketController(server);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function getWebSocketController() {
|
|
14
|
+
if (!toBoolean(env['WEBSOCKETS_ENABLED']) || !toBoolean(env['WEBSOCKETS_REST_ENABLED'])) {
|
|
15
|
+
throw new ServiceUnavailableError({ service: 'ws', reason: 'WebSocket server is disabled' });
|
|
16
|
+
}
|
|
17
|
+
if (!websocketController) {
|
|
18
|
+
throw new ServiceUnavailableError({ service: 'ws', reason: 'WebSocket server is not initialized' });
|
|
19
|
+
}
|
|
20
|
+
return websocketController;
|
|
21
|
+
}
|
|
22
|
+
export function createSubscriptionController(server) {
|
|
23
|
+
if (toBoolean(env['WEBSOCKETS_GRAPHQL_ENABLED'])) {
|
|
24
|
+
subscriptionController = new GraphQLSubscriptionController(server);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function getSubscriptionController() {
|
|
28
|
+
return subscriptionController;
|
|
29
|
+
}
|
|
30
|
+
export * from './graphql.js';
|
|
31
|
+
export * from './rest.js';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
import type { Server as httpServer } from 'http';
|
|
3
|
+
import { WebSocketMessage } from '../messages.js';
|
|
4
|
+
import SocketController from './base.js';
|
|
5
|
+
export declare class WebSocketController extends SocketController {
|
|
6
|
+
constructor(httpServer: httpServer);
|
|
7
|
+
private bindEvents;
|
|
8
|
+
protected parseMessage(data: string): WebSocketMessage;
|
|
9
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parseJSON } from '@directus/utils';
|
|
2
|
+
import emitter from '../../emitter.js';
|
|
3
|
+
import env from '../../env.js';
|
|
4
|
+
import logger from '../../logger.js';
|
|
5
|
+
import { refreshAccountability } from '../authenticate.js';
|
|
6
|
+
import { WebSocketError, handleWebSocketError } from '../errors.js';
|
|
7
|
+
import { WebSocketMessage } from '../messages.js';
|
|
8
|
+
import SocketController from './base.js';
|
|
9
|
+
export class WebSocketController extends SocketController {
|
|
10
|
+
constructor(httpServer) {
|
|
11
|
+
super(httpServer, 'WEBSOCKETS_REST');
|
|
12
|
+
this.server.on('connection', (ws, auth) => {
|
|
13
|
+
this.bindEvents(this.createClient(ws, auth));
|
|
14
|
+
});
|
|
15
|
+
logger.info(`WebSocket Server started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
|
|
16
|
+
}
|
|
17
|
+
bindEvents(client) {
|
|
18
|
+
client.on('parsed-message', async (message) => {
|
|
19
|
+
try {
|
|
20
|
+
message = WebSocketMessage.parse(await emitter.emitFilter('websocket.message', message, { client }));
|
|
21
|
+
client.accountability = await refreshAccountability(client.accountability);
|
|
22
|
+
emitter.emitAction('websocket.message', { message, client });
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
handleWebSocketError(client, error, 'server');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
client.on('error', (event) => {
|
|
30
|
+
emitter.emitAction('websocket.error', { client, event });
|
|
31
|
+
});
|
|
32
|
+
client.on('close', (event) => {
|
|
33
|
+
emitter.emitAction('websocket.close', { client, event });
|
|
34
|
+
});
|
|
35
|
+
emitter.emitAction('websocket.connect', { client });
|
|
36
|
+
}
|
|
37
|
+
parseMessage(data) {
|
|
38
|
+
let message;
|
|
39
|
+
try {
|
|
40
|
+
message = parseJSON(data);
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
throw new WebSocketError('server', 'INVALID_PAYLOAD', 'Unable to parse the incoming message.');
|
|
44
|
+
}
|
|
45
|
+
return message;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DirectusError } from '@directus/errors';
|
|
2
|
+
import type { WebSocket } from 'ws';
|
|
3
|
+
import { ZodError } from 'zod';
|
|
4
|
+
import type { WebSocketResponse } from './messages.js';
|
|
5
|
+
import type { WebSocketClient } from './types.js';
|
|
6
|
+
export declare class WebSocketError extends Error {
|
|
7
|
+
type: string;
|
|
8
|
+
code: string;
|
|
9
|
+
uid: string | number | undefined;
|
|
10
|
+
constructor(type: string, code: string, message: string, uid?: string | number);
|
|
11
|
+
toJSON(): WebSocketResponse;
|
|
12
|
+
toMessage(): string;
|
|
13
|
+
static fromError(error: DirectusError<unknown>, type?: string): WebSocketError;
|
|
14
|
+
static fromZodError(error: ZodError, type?: string): WebSocketError;
|
|
15
|
+
}
|
|
16
|
+
export declare function handleWebSocketError(client: WebSocketClient | WebSocket, error: unknown, type?: string): void;
|