diff --git a/src/shadowbox/README.md b/src/shadowbox/README.md index 1d23ebd1..932a8573 100644 --- a/src/shadowbox/README.md +++ b/src/shadowbox/README.md @@ -121,9 +121,9 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve ### Enabling WebSocket Support -1. **Configure Listeners for New Access Keys:** +1. **Configure Listeners:** - Set up listener configuration including WebSocket paths: + Set defaults for new keys (add `"applyToExisting": true` to update all existing keys): ```sh curl --insecure -X PUT -H "Content-Type: application/json" \ @@ -133,7 +133,7 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve "websocketStream": {"path": "/tcp", "webServerPort": 8080}, "websocketPacket": {"path": "/udp", "webServerPort": 8080} }' \ - $API_URL/server/listeners-for-new-access-keys + $API_URL/server/listeners ``` 2. **Enable the Caddy Web Server (for automatic HTTPS):** @@ -149,23 +149,20 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve $API_URL/server/web-server ``` -3. **Create WebSocket-Enabled Access Keys:** +3. **Update a Specific Key's Listeners:** ```sh - curl --insecure -X POST -H "Content-Type: application/json" \ - -d '{ - "name": "WebSocket User", - "listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"] - }' \ - $API_URL/access-keys + curl --insecure -X PUT -H "Content-Type: application/json" \ + -d '{"listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"]}' \ + $API_URL/access-keys/0/listeners ``` -4. **Retrieve WebSocket Access Key Configuration:** +4. **Get Dynamic Config (YAML):** - For WebSocket-enabled keys, `GET /access-keys/{id}` returns YAML configuration (Outline Client v1.15.0+): + Use the dedicated endpoint to retrieve YAML transport configuration (Outline Client v1.15.0+): ```sh - curl --insecure $API_URL/access-keys/0 + curl --insecure $API_URL/access-keys/0/dynamic-config ``` Example response (`Content-Type: text/yaml`): @@ -189,6 +186,8 @@ The Outline Server supports Shadowsocks over WebSocket (SS over WSS) for improve secret: XxXxXx ``` +> [!NOTE] > `GET /access-keys/{id}` always returns JSON. Use `/access-keys/{id}/dynamic-config` for YAML transport configuration. + ### Listener Types - `tcp` - Traditional TCP Shadowsocks diff --git a/src/shadowbox/model/access_key.ts b/src/shadowbox/model/access_key.ts index 826d6051..baa1fbc8 100644 --- a/src/shadowbox/model/access_key.ts +++ b/src/shadowbox/model/access_key.ts @@ -90,4 +90,8 @@ export interface AccessKeyRepository { setAccessKeyDataLimit(id: AccessKeyId, limit: DataLimit): void; // Removes the custom data limit from access key `id`. removeAccessKeyDataLimit(id: AccessKeyId): void; + // Updates the listeners for access key `id`. + setAccessKeyListeners(id: AccessKeyId, listeners: ListenerType[]): void; + // Updates the listeners for all access keys. + setListenersForAllKeys(listeners: ListenerType[]): void; } diff --git a/src/shadowbox/server/api.yml b/src/shadowbox/server/api.yml index e20875af..2aed0721 100644 --- a/src/shadowbox/server/api.yml +++ b/src/shadowbox/server/api.yml @@ -99,24 +99,36 @@ paths: '409': description: The requested port was already in use by another service. - /server/listeners-for-new-access-keys: + /server/listeners: put: - description: Sets the listeners configuration for newly created access keys. Supports different ports for TCP and UDP, and WebSocket paths. + description: | + Sets the listeners configuration for access keys. + - By default, only affects newly created keys + - Set `applyToExisting: true` to also update all existing keys + + Use PUT /access-keys/{id}/listeners for per-key overrides. tags: - - Access Key + - Server requestBody: required: true content: application/json: schema: - $ref: "#/components/schemas/ListenersConfig" + allOf: + - $ref: "#/components/schemas/ListenersConfig" + - type: object + properties: + applyToExisting: + type: boolean + default: false + description: If true, also update all existing access keys examples: - 'Basic TCP/UDP': + 'Defaults only': value: '{"tcp": {"port": 443}, "udp": {"port": 443}}' 'With WebSocket': - value: '{"tcp": {"port": 443}, "udp": {"port": 443}, "websocketStream": {"path": "/tcp", "webServerPort": 8080}, "websocketPacket": {"path": "/udp", "webServerPort": 8080}}' - 'Different TCP/UDP ports': - value: '{"tcp": {"port": 443}, "udp": {"port": 8443}}' + value: '{"tcp": {"port": 443}, "websocketStream": {"path": "/tcp", "webServerPort": 8080}, "websocketPacket": {"path": "/udp", "webServerPort": 8080}}' + 'Update all existing keys': + value: '{"tcp": {"port": 443}, "websocketStream": {"path": "/tcp"}, "applyToExisting": true}' responses: '204': description: The listeners configuration was successfully updated. @@ -421,7 +433,7 @@ paths: value: >- {"id":"my-identifier","name":"First","password":"XxXxXx","port":9795,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:9795/?outline=1"} get: - description: Get an access key + description: Get an access key (always returns JSON) tags: - Access Key parameters: @@ -433,10 +445,7 @@ paths: type: string responses: '200': - description: | - The access key. Response format depends on key type: - - Traditional keys (TCP/UDP only): JSON with AccessKey schema - - WebSocket-enabled keys: YAML with dynamic transport configuration + description: The access key content: application/json: schema: @@ -444,28 +453,8 @@ paths: examples: 'Traditional key': value: '{"id":"0","name":"Admin","password":"XxXxXx","port":18162,"method":"chacha20-ietf-poly1305","accessUrl":"ss://SADFJSKADFJAKSD@0.0.0.0:18162/?outline=1"}' - text/yaml: - schema: - type: string - examples: - 'WebSocket-enabled key': - value: | - transport: - $type: tcpudp - tcp: - $type: shadowsocks - endpoint: - $type: websocket - url: wss://example.com/tcp - cipher: chacha20-ietf-poly1305 - secret: XxXxXx - udp: - $type: shadowsocks - endpoint: - $type: websocket - url: wss://example.com/udp - cipher: chacha20-ietf-poly1305 - secret: XxXxXx + 'WebSocket key': + value: '{"id":"1","name":"WSS User","listeners":["websocket-stream","websocket-packet"],"dynamicConfig":{"transport":{"$type":"tcpudp"}}}' '404': description: Access key inexistent content: @@ -540,6 +529,91 @@ paths: description: Access key renamed successfully '404': description: Access key inexistent + /access-keys/{id}/listeners: + put: + description: | + Updates the listeners for a specific access key. + This overrides the global /server/listeners setting for this key. + tags: + - Access Key + parameters: + - name: id + in: path + required: true + description: The id of the access key + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - listeners + properties: + listeners: + type: array + items: + type: string + enum: ['tcp', 'udp', 'websocket-stream', 'websocket-packet'] + examples: + 'All transports': + value: '{"listeners": ["tcp", "udp", "websocket-stream", "websocket-packet"]}' + 'WebSocket only': + value: '{"listeners": ["websocket-stream", "websocket-packet"]}' + 'Traditional only': + value: '{"listeners": ["tcp", "udp"]}' + responses: + '204': + description: Listeners updated successfully + '400': + description: Invalid listeners configuration + '404': + description: Access key not found + /access-keys/{id}/dynamic-config: + get: + description: | + Returns the dynamic transport configuration for WebSocket-enabled keys. + Returns YAML content compatible with Outline Client v1.15.0+. + Returns 404 if the key has no WebSocket listeners. + tags: + - Access Key + parameters: + - name: id + in: path + required: true + description: The id of the access key + schema: + type: string + responses: + '200': + description: Dynamic transport configuration in YAML format + content: + text/yaml: + schema: + type: string + examples: + 'WebSocket config': + value: | + transport: + $type: tcpudp + tcp: + $type: shadowsocks + endpoint: + $type: websocket + url: wss://example.com/tcp + cipher: chacha20-ietf-poly1305 + secret: XxXxXx + udp: + $type: shadowsocks + endpoint: + $type: websocket + url: wss://example.com/udp + cipher: chacha20-ietf-poly1305 + secret: XxXxXx + '404': + description: Access key not found or has no WebSocket listeners /access-keys/{id}/data-limit: put: description: Sets a data limit for the given access key diff --git a/src/shadowbox/server/main.ts b/src/shadowbox/server/main.ts index 5a77496e..9f65a3ab 100644 --- a/src/shadowbox/server/main.ts +++ b/src/shadowbox/server/main.ts @@ -170,7 +170,7 @@ async function main() { ); // Configure listener defaults (e.g., WebSocket paths/ports) based on server configuration. - const listenersConfig = serverConfig.data().listenersForNewAccessKeys; + const listenersConfig = serverConfig.data().listeners; shadowsocksServer.configureListeners( listenersConfig ? { @@ -239,7 +239,7 @@ async function main() { try { await caddyServer.applyConfig({ accessKeys: accessKeyRepository.listAccessKeys(), - listeners: serverConfig.data().listenersForNewAccessKeys, + listeners: serverConfig.data().listeners, caddyConfig: serverConfig.data().caddyWebServer, hostname: serverConfig.data().hostname, apiPort: apiProxyEnabled ? apiPortNumber : undefined, diff --git a/src/shadowbox/server/manager_service.spec.ts b/src/shadowbox/server/manager_service.spec.ts index 5401470f..34618541 100644 --- a/src/shadowbox/server/manager_service.spec.ts +++ b/src/shadowbox/server/manager_service.spec.ts @@ -758,7 +758,7 @@ describe('ShadowsocksManagerService', () => { }); }); - describe('setListenersForNewAccessKeys', () => { + describe('setListeners', () => { it('persists configuration and updates the Shadowsocks server', async () => { const repo = getAccessKeyRepository(); const serverConfig = new InMemoryConfig({} as ServerConfigJson); @@ -785,9 +785,9 @@ describe('ShadowsocksManagerService', () => { }, }; - await service.setListenersForNewAccessKeys({params: listeners}, res, () => {}); + await service.setListeners({params: listeners}, res, () => {}); - expect(serverConfig.data().listenersForNewAccessKeys).toEqual(listeners); + expect(serverConfig.data().listeners).toEqual(listeners); expect(fakeServer.getListenerSettings()).toEqual({ websocketStream: listeners.websocketStream, websocketPacket: listeners.websocketPacket, @@ -814,11 +814,7 @@ describe('ShadowsocksManagerService', () => { websocketStream: {path: '/tcp', webServerPort: 8080}, websocketPacket: {path: '/udp', webServerPort: 8080}, }; - await service.setListenersForNewAccessKeys( - {params: listenersWithWebsocket}, - {send: () => {}}, - () => {} - ); + await service.setListeners({params: listenersWithWebsocket}, {send: () => {}}, () => {}); expect(fakeServer.getListenerSettings()).toEqual({ websocketStream: listenersWithWebsocket.websocketStream, websocketPacket: listenersWithWebsocket.websocketPacket, @@ -834,13 +830,9 @@ describe('ShadowsocksManagerService', () => { responseProcessed = true; }, }; - await service.setListenersForNewAccessKeys( - {params: listenersWithoutWebsocket}, - res, - () => {} - ); + await service.setListeners({params: listenersWithoutWebsocket}, res, () => {}); - expect(serverConfig.data().listenersForNewAccessKeys).toEqual(listenersWithoutWebsocket); + expect(serverConfig.data().listeners).toEqual(listenersWithoutWebsocket); expect(fakeServer.getListenerSettings()).toBeUndefined(); expect(fakeCaddy.applyCalls.length).toEqual(2); expect(fakeCaddy.applyCalls[1].listeners).toEqual(listenersWithoutWebsocket); diff --git a/src/shadowbox/server/manager_service.ts b/src/shadowbox/server/manager_service.ts index 28f032f1..f145b24f 100644 --- a/src/shadowbox/server/manager_service.ts +++ b/src/shadowbox/server/manager_service.ts @@ -25,7 +25,7 @@ import * as errors from '../model/errors'; import * as version from './version'; import {ManagerMetrics} from './manager_metrics'; -import {ServerConfigJson, ListenersForNewAccessKeys, WebServerConfig} from './server_config'; +import {ServerConfigJson, ListenersConfig, WebServerConfig} from './server_config'; import {SharedMetricsPublisher} from './shared_metrics'; import {ShadowsocksServer} from '../model/shadowsocks_server'; import type {OutlineCaddyController} from './outline_caddy_server'; @@ -135,10 +135,7 @@ export function bindService( `${apiPrefix}/server/port-for-new-access-keys`, service.setPortForNewAccessKeys.bind(service) ); - apiServer.put( - `${apiPrefix}/server/listeners-for-new-access-keys`, - service.setListenersForNewAccessKeys.bind(service) - ); + apiServer.put(`${apiPrefix}/server/listeners`, service.setListeners.bind(service)); apiServer.put(`${apiPrefix}/server/web-server`, service.configureCaddyWebServer.bind(service)); apiServer.post(`${apiPrefix}/access-keys`, service.createNewAccessKey.bind(service)); @@ -146,6 +143,14 @@ export function bindService( apiServer.get(`${apiPrefix}/access-keys`, service.listAccessKeys.bind(service)); apiServer.get(`${apiPrefix}/access-keys/:id`, service.getAccessKey.bind(service)); + apiServer.put( + `${apiPrefix}/access-keys/:id/listeners`, + service.setAccessKeyListeners.bind(service) + ); + apiServer.get( + `${apiPrefix}/access-keys/:id/dynamic-config`, + service.getAccessKeyDynamicConfig.bind(service) + ); apiServer.del(`${apiPrefix}/access-keys/:id`, service.removeAccessKey.bind(service)); apiServer.put(`${apiPrefix}/access-keys/:id/name`, service.renameAccessKey.bind(service)); apiServer.put( @@ -320,7 +325,7 @@ export class ShadowsocksManagerService { const domain = configData?.caddyWebServer?.domain || configData?.hostname; if (domain) { - const listenersConfig = configData?.listenersForNewAccessKeys; + const listenersConfig = configData?.listeners; // Cast to access generateDynamicAccessKeyConfig method const serverWithDynamicConfig = this.shadowsocksServer as ShadowsocksServer & { @@ -443,72 +448,14 @@ export class ShadowsocksManagerService { next(); } - // Get an access key + // Get an access key (always returns JSON) getAccessKey(req: RequestType, res: ResponseType, next: restify.Next): void { try { logging.debug(`getAccessKey request ${JSON.stringify(req.params)}`); const accessKeyId = validateAccessKeyId(req.params.id); const accessKey = this.accessKeys.getAccessKey(accessKeyId); - // Check if this key uses WebSocket listeners - const hasWebSocketListeners = - accessKey.listeners && - (accessKey.listeners.indexOf('websocket-stream') !== -1 || - accessKey.listeners.indexOf('websocket-packet') !== -1); - - if (hasWebSocketListeners) { - // Generate and return YAML for WebSocket keys - const configData = this.serverConfig.data(); - const domain = configData?.caddyWebServer?.domain || configData?.hostname; - const listenersConfig = configData?.listenersForNewAccessKeys; - - logging.debug( - `WebSocket key detected. Domain: ${domain}, Listeners config: ${JSON.stringify( - listenersConfig - )}` - ); - - // Generate YAML even if listenersConfig is not fully configured, using defaults - if (domain) { - const serverWithWebSocket = this.shadowsocksServer as ShadowsocksServer & { - generateDynamicAccessKeyYaml?: ( - proxyParams: {encryptionMethod: string; password: string}, - domain: string, - tcpPath: string, - udpPath: string, - tls: boolean, - listeners?: ListenerType[] - ) => string | null; - }; - - const yamlConfig = serverWithWebSocket.generateDynamicAccessKeyYaml?.( - accessKey.proxyParams, - domain, - listenersConfig?.websocketStream?.path || '/tcp', - listenersConfig?.websocketPacket?.path || '/udp', - this.serverConfig.data()?.caddyWebServer?.autoHttps !== false, - accessKey.listeners as ListenerType[] | undefined - ); - - if (yamlConfig) { - // Return raw YAML for WebSocket keys - const nodeResponse = res as unknown as { - setHeader: (name: string, value: string) => void; - statusCode: number; - write: (data: string) => void; - end: () => void; - }; - - nodeResponse.setHeader('Content-Type', 'text/yaml; charset=utf-8'); - nodeResponse.statusCode = HttpSuccess.OK; - nodeResponse.write(yamlConfig); - nodeResponse.end(); - return; - } - } - } - - // Return JSON for traditional (non-WebSocket) keys + // Always return JSON - use /access-keys/{id}/dynamic-config for YAML const accessKeyJson = this.accessKeyToApiJson(accessKey); logging.debug(`getAccessKey response ${JSON.stringify(accessKeyJson)}`); res.send(HttpSuccess.OK, accessKeyJson); @@ -562,7 +509,7 @@ export class ShadowsocksManagerService { } } else { // If no listeners specified, use default listeners based on server config - const serverListeners = this.serverConfig.data()?.listenersForNewAccessKeys; + const serverListeners = this.serverConfig.data()?.listeners; if (serverListeners) { listeners = []; if (serverListeners.tcp) listeners.push('tcp'); @@ -664,11 +611,11 @@ export class ShadowsocksManagerService { if (configData) { configData.portForNewAccessKeys = port; // Also update listeners config for backward compatibility - if (!configData.listenersForNewAccessKeys) { - configData.listenersForNewAccessKeys = {}; + if (!configData.listeners) { + configData.listeners = {}; } - configData.listenersForNewAccessKeys.tcp = {port}; - configData.listenersForNewAccessKeys.udp = {port}; + configData.listeners.tcp = {port}; + configData.listeners.udp = {port}; } this.serverConfig.write(); await this.updateCaddyConfig(); @@ -687,16 +634,14 @@ export class ShadowsocksManagerService { } } - // Sets the listeners for new access keys - async setListenersForNewAccessKeys( - req: RequestType, - res: ResponseType, - next: restify.Next - ): Promise { + // Sets the listeners configuration (defaults for new keys, optionally updates all keys) + async setListeners(req: RequestType, res: ResponseType, next: restify.Next): Promise { try { - logging.debug(`setListenersForNewAccessKeys request ${JSON.stringify(req.params)}`); + logging.debug(`setListeners request ${JSON.stringify(req.params)}`); - const listeners = req.params as unknown as ListenersForNewAccessKeys; + const {applyToExisting, ...listeners} = req.params as unknown as ListenersConfig & { + applyToExisting?: boolean; + }; if (!listeners || typeof listeners !== 'object') { return next( new restifyErrors.InvalidArgumentError( @@ -807,7 +752,7 @@ export class ShadowsocksManagerService { // Store the listeners configuration const configData = this.serverConfig.data(); if (configData) { - configData.listenersForNewAccessKeys = listeners; + configData.listeners = listeners; // Update legacy portForNewAccessKeys if TCP port is set if (listeners.tcp?.port) { @@ -816,6 +761,17 @@ export class ShadowsocksManagerService { } } + // If applyToExisting is true, update all existing keys + if (applyToExisting) { + const derivedListeners: ListenerType[] = []; + if (listeners.tcp) derivedListeners.push('tcp'); + if (listeners.udp) derivedListeners.push('udp'); + if (listeners.websocketStream) derivedListeners.push('websocket-stream'); + if (listeners.websocketPacket) derivedListeners.push('websocket-packet'); + + this.accessKeys.setListenersForAllKeys(derivedListeners); + } + // Update the underlying Shadowsocks server with the new listener defaults. const listenerSettings = listeners.websocketStream || listeners.websocketPacket @@ -914,6 +870,119 @@ export class ShadowsocksManagerService { } } + // Updates listeners for a specific access key + async setAccessKeyListeners( + req: RequestType, + res: ResponseType, + next: restify.Next + ): Promise { + try { + logging.debug(`setAccessKeyListeners request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + + const listenersParam = req.params.listeners as string[] | undefined; + if (!listenersParam || !Array.isArray(listenersParam)) { + return next( + new restifyErrors.InvalidArgumentError({statusCode: 400}, 'listeners must be an array') + ); + } + + const validListeners = ['tcp', 'udp', 'websocket-stream', 'websocket-packet']; + for (const listener of listenersParam) { + if (!validListeners.includes(listener)) { + return next( + new restifyErrors.InvalidArgumentError( + {statusCode: 400}, + `Invalid listener type: ${listener}` + ) + ); + } + } + + this.accessKeys.setAccessKeyListeners(accessKeyId, listenersParam as ListenerType[]); + await this.updateCaddyConfig(); + res.send(HttpSuccess.NO_CONTENT); + return next(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } + return next(new restifyErrors.InternalServerError()); + } + } + + // Returns dynamic config YAML for WebSocket-enabled keys + getAccessKeyDynamicConfig(req: RequestType, res: ResponseType, next: restify.Next): void { + try { + logging.debug(`getAccessKeyDynamicConfig request ${JSON.stringify(req.params)}`); + const accessKeyId = validateAccessKeyId(req.params.id); + const accessKey = this.accessKeys.getAccessKey(accessKeyId); + + const hasWebSocketListeners = + accessKey.listeners && + (accessKey.listeners.includes('websocket-stream') || + accessKey.listeners.includes('websocket-packet')); + + if (!hasWebSocketListeners) { + return next( + new restifyErrors.NotFoundError('Access key has no WebSocket listeners configured') + ); + } + + const configData = this.serverConfig.data(); + const domain = configData?.caddyWebServer?.domain || configData?.hostname; + + if (!domain) { + return next( + new restifyErrors.InternalServerError('No domain configured for WebSocket URLs') + ); + } + + const listenersConfig = configData?.listeners; + const serverWithWebSocket = this.shadowsocksServer as ShadowsocksServer & { + generateDynamicAccessKeyYaml?: ( + proxyParams: {encryptionMethod: string; password: string}, + domain: string, + tcpPath: string, + udpPath: string, + tls: boolean, + listeners?: ListenerType[] + ) => string | null; + }; + + const yamlConfig = serverWithWebSocket.generateDynamicAccessKeyYaml?.( + accessKey.proxyParams, + domain, + listenersConfig?.websocketStream?.path || '/tcp', + listenersConfig?.websocketPacket?.path || '/udp', + configData?.caddyWebServer?.autoHttps !== false, + accessKey.listeners + ); + + if (!yamlConfig) { + return next(new restifyErrors.InternalServerError('Failed to generate dynamic config')); + } + + const nodeResponse = res as unknown as { + setHeader: (name: string, value: string) => void; + statusCode: number; + write: (data: string) => void; + end: () => void; + }; + nodeResponse.setHeader('Content-Type', 'text/yaml; charset=utf-8'); + nodeResponse.statusCode = HttpSuccess.OK; + nodeResponse.write(yamlConfig); + nodeResponse.end(); + } catch (error) { + logging.error(error); + if (error instanceof errors.AccessKeyNotFound) { + return next(new restifyErrors.NotFoundError(error.message)); + } + return next(error); + } + } + // Removes an existing access key async removeAccessKey(req: RequestType, res: ResponseType, next: restify.Next): Promise { try { @@ -1134,7 +1203,7 @@ export class ShadowsocksManagerService { const apiPort = configData.caddyWebServer?.apiProxyPath ? apiPortNumber : undefined; await this.caddyServer.applyConfig({ accessKeys: this.accessKeys.listAccessKeys(), - listeners: configData.listenersForNewAccessKeys, + listeners: configData.listeners, caddyConfig: configData.caddyWebServer, hostname: configData.hostname, apiPort, diff --git a/src/shadowbox/server/outline_caddy_server.ts b/src/shadowbox/server/outline_caddy_server.ts index e6635f65..48008d4f 100644 --- a/src/shadowbox/server/outline_caddy_server.ts +++ b/src/shadowbox/server/outline_caddy_server.ts @@ -21,11 +21,11 @@ import * as yaml from 'js-yaml'; import * as file from '../infrastructure/file'; import * as logging from '../infrastructure/logging'; import {AccessKey, ListenerType} from '../model/access_key'; -import {WebServerConfig, ListenerConfig, ListenersForNewAccessKeys} from './server_config'; +import {WebServerConfig, ListenerConfig, ListenersConfig} from './server_config'; export interface OutlineCaddyConfigPayload { accessKeys: AccessKey[]; - listeners?: ListenersForNewAccessKeys; + listeners?: ListenersConfig; caddyConfig?: WebServerConfig; hostname?: string; apiPort?: number; // Internal API port for reverse proxy diff --git a/src/shadowbox/server/server_access_key.ts b/src/shadowbox/server/server_access_key.ts index 404ed946..235807b7 100644 --- a/src/shadowbox/server/server_access_key.ts +++ b/src/shadowbox/server/server_access_key.ts @@ -305,6 +305,20 @@ export class ServerAccessKeyRepository implements AccessKeyRepository { this.enforceAccessKeyDataLimits(); } + setAccessKeyListeners(id: AccessKeyId, listeners: ListenerType[]): void { + this.getAccessKey(id).listeners = listeners; + this.saveAccessKeys(); + this.updateServer(); + } + + setListenersForAllKeys(listeners: ListenerType[]): void { + for (const accessKey of this.accessKeys) { + accessKey.listeners = listeners; + } + this.saveAccessKeys(); + this.updateServer(); + } + // Compares access key usage with collected metrics, marking them as under or over limit. // Updates access key data usage. async enforceAccessKeyDataLimits() { diff --git a/src/shadowbox/server/server_config.ts b/src/shadowbox/server/server_config.ts index 4763fbd6..3c19c6fd 100644 --- a/src/shadowbox/server/server_config.ts +++ b/src/shadowbox/server/server_config.ts @@ -24,7 +24,7 @@ export interface ListenerConfig { webServerPort?: number; } -export interface ListenersForNewAccessKeys { +export interface ListenersConfig { tcp?: ListenerConfig; udp?: ListenerConfig; websocketStream?: ListenerConfig; @@ -53,8 +53,8 @@ export interface ServerConfigJson { createdTimestampMs?: number; // What port number should we use for new access keys? portForNewAccessKeys?: number; - // Listeners configuration for new access keys (supersedes portForNewAccessKeys) - listenersForNewAccessKeys?: ListenersForNewAccessKeys; + // Listeners configuration for access keys + listeners?: ListenersConfig; // Caddy web server configuration for automatic HTTPS caddyWebServer?: WebServerConfig; // Which staged rollouts we should force enabled or disabled.