From eabe6bc4ed7966677f62943597a88106d67684cd Mon Sep 17 00:00:00 2001 From: Ruben Verborgh Date: Tue, 1 Dec 2020 15:52:44 +0100 Subject: [PATCH] feat: Implement --baseUrl flag. Closes https://github.com/solid/community-server/issues/372 --- config/presets/cli-params.json | 2 +- config/presets/middleware.json | 4 +-- config/presets/setup.json | 2 +- .../storage/backend/storage-filesystem.json | 4 +-- .../storage/backend/storage-memory.json | 4 +-- .../backend/storage-sparql-endpoint.json | 4 +-- .../storage/routing/regex-routing.json | 2 +- src/init/CliRunner.ts | 15 ++++++---- src/server/middleware/WebSocketAdvertiser.ts | 16 ++-------- test/integration/Middleware.test.ts | 1 + test/integration/WebSocketsProtocol.test.ts | 27 +++++++++-------- test/unit/init/CliRunner.test.ts | 16 +++++----- .../middleware/WebSocketAdvertiser.test.ts | 29 ++++++++----------- 13 files changed, 60 insertions(+), 66 deletions(-) diff --git a/config/presets/cli-params.json b/config/presets/cli-params.json index af95638df..260b76994 100644 --- a/config/presets/cli-params.json +++ b/config/presets/cli-params.json @@ -6,7 +6,7 @@ "@type": "Variable" }, { - "@id": "urn:solid-server:default:variable:base", + "@id": "urn:solid-server:default:variable:baseUrl", "@type": "Variable" }, { diff --git a/config/presets/middleware.json b/config/presets/middleware.json index 32a989809..564319ad6 100644 --- a/config/presets/middleware.json +++ b/config/presets/middleware.json @@ -31,8 +31,8 @@ }, { "@type": "WebSocketAdvertiser", - "WebSocketAdvertiser:_settings_port": { - "@id": "urn:solid-server:default:variable:port" + "WebSocketAdvertiser:_baseUrl": { + "@id": "urn:solid-server:default:variable:baseUrl" } } ] diff --git a/config/presets/setup.json b/config/presets/setup.json index 5b849c784..a1d01bb12 100644 --- a/config/presets/setup.json +++ b/config/presets/setup.json @@ -17,7 +17,7 @@ "@id": "urn:solid-server:default:LoggerFactory" }, "Setup:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" }, "Setup:_port": { "@id": "urn:solid-server:default:variable:port" diff --git a/config/presets/storage/backend/storage-filesystem.json b/config/presets/storage/backend/storage-filesystem.json index 971d404c3..9e8c7bbb7 100644 --- a/config/presets/storage/backend/storage-filesystem.json +++ b/config/presets/storage/backend/storage-filesystem.json @@ -5,7 +5,7 @@ "@id": "urn:solid-server:default:FileIdentifierMapper", "@type": "ExtensionBasedMapper", "ExtensionBasedMapper:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" }, "ExtensionBasedMapper:_rootFilepath": { "@id": "urn:solid-server:default:variable:rootFilePath" @@ -27,7 +27,7 @@ "@id": "urn:solid-server:default:FileDataAccessor" }, "DataAccessorBasedStore:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" } } ] diff --git a/config/presets/storage/backend/storage-memory.json b/config/presets/storage/backend/storage-memory.json index d5d44d200..2ae772557 100644 --- a/config/presets/storage/backend/storage-memory.json +++ b/config/presets/storage/backend/storage-memory.json @@ -5,7 +5,7 @@ "@id": "urn:solid-server:default:MemoryDataAccessor", "@type": "InMemoryDataAccessor", "InMemoryDataAccessor:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" } }, { @@ -15,7 +15,7 @@ "@id": "urn:solid-server:default:MemoryDataAccessor" }, "DataAccessorBasedStore:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" } } ] diff --git a/config/presets/storage/backend/storage-sparql-endpoint.json b/config/presets/storage/backend/storage-sparql-endpoint.json index a2c9c0e68..0aa123bce 100644 --- a/config/presets/storage/backend/storage-sparql-endpoint.json +++ b/config/presets/storage/backend/storage-sparql-endpoint.json @@ -8,7 +8,7 @@ "@id": "urn:solid-server:default:variable:sparqlEndpoint" }, "SparqlDataAccessor:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" } }, @@ -19,7 +19,7 @@ "@id": "urn:solid-server:default:SparqlDataAccessor" }, "DataAccessorBasedStore:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" } }, diff --git a/config/presets/storage/routing/regex-routing.json b/config/presets/storage/routing/regex-routing.json index e5f5b9728..32be5c179 100644 --- a/config/presets/storage/routing/regex-routing.json +++ b/config/presets/storage/routing/regex-routing.json @@ -5,7 +5,7 @@ "@id": "urn:solid-server:default:RegexRouterRule", "@type": "RegexRouterRule", "RegexRouterRule:_base": { - "@id": "urn:solid-server:default:variable:base" + "@id": "urn:solid-server:default:variable:baseUrl" } }, { diff --git a/src/init/CliRunner.ts b/src/init/CliRunner.ts index be521536a..bd6fa4023 100644 --- a/src/init/CliRunner.ts +++ b/src/init/CliRunner.ts @@ -4,6 +4,7 @@ import type { LoaderProperties } from 'componentsjs'; import { Loader } from 'componentsjs'; import yargs from 'yargs'; import { getLoggerFor } from '../logging/LogUtil'; +import { ensureTrailingSlash } from '../util/PathUtil'; import type { Setup } from './Setup'; const logger = getLoggerFor('CliRunner'); @@ -30,11 +31,12 @@ export const runCli = function({ const { argv: params } = yargs(argv.slice(2)) .usage('node ./bin/server.js [args]') .options({ - port: { type: 'number', alias: 'p', default: 3000 }, + baseUrl: { type: 'string', alias: 'b' }, config: { type: 'string', alias: 'c' }, + loggingLevel: { type: 'string', alias: 'l', default: 'info' }, + port: { type: 'number', alias: 'p', default: 3000 }, rootFilePath: { type: 'string', alias: 'f' }, sparqlEndpoint: { type: 'string', alias: 's' }, - loggingLevel: { type: 'string', alias: 'l', default: 'info' }, }) .help(); @@ -50,16 +52,17 @@ export const runCli = function({ const setup: Setup = await loader .instantiateFromUrl('urn:solid-server:default', configPath, undefined, { variables: { + 'urn:solid-server:default:variable:baseUrl': + params.baseUrl ? ensureTrailingSlash(params.baseUrl) : `http://localhost:${params.port}/`, + 'urn:solid-server:default:variable:loggingLevel': params.loggingLevel, 'urn:solid-server:default:variable:port': params.port, - 'urn:solid-server:default:variable:base': `http://localhost:${params.port}/`, 'urn:solid-server:default:variable:rootFilePath': params.rootFilePath ?? process.cwd(), 'urn:solid-server:default:variable:sparqlEndpoint': params.sparqlEndpoint, - 'urn:solid-server:default:variable:loggingLevel': params.loggingLevel, }, }) as Setup; return await setup.setup(); - })().then((base: string): void => { - logger.info(`Running at ${base}`); + })().then((baseUrl: string): void => { + logger.info(`Running at ${baseUrl}`); }).catch((error): void => { // This is the only time we can *not* use the logger to print error messages, as dependency injection has failed. stderr.write(`${error}\n`); diff --git a/src/server/middleware/WebSocketAdvertiser.ts b/src/server/middleware/WebSocketAdvertiser.ts index bb121175c..c1fa4e1fb 100644 --- a/src/server/middleware/WebSocketAdvertiser.ts +++ b/src/server/middleware/WebSocketAdvertiser.ts @@ -2,26 +2,16 @@ import { addHeader } from '../../util/HeaderUtil'; import { HttpHandler } from '../HttpHandler'; import type { HttpResponse } from '../HttpResponse'; -interface WebSocketSettings { - hostname?: string; - port?: number; - protocol?: string; -} - /** * Handler that advertises a WebSocket through the Updates-Via header. */ export class WebSocketAdvertiser extends HttpHandler { private readonly socketUrl: string; - public constructor(settings: WebSocketSettings = {}) { + public constructor(baseUrl: string) { super(); - const { hostname = 'localhost', port = 80, protocol = 'ws:' } = settings; - const secure = /^(?:https|wss)/u.test(protocol); - const socketUrl = new URL(`${secure ? 'wss' : 'ws'}://${hostname}:${port}/`); - if (socketUrl.hostname !== hostname) { - throw new Error(`Invalid hostname: ${hostname}`); - } + const socketUrl = new URL(baseUrl); + socketUrl.protocol = /^(?:http|ws):/u.test(baseUrl) ? 'ws:' : 'wss:'; this.socketUrl = socketUrl.href; } diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts index 8d0710bb0..7e2b1888d 100644 --- a/test/integration/Middleware.test.ts +++ b/test/integration/Middleware.test.ts @@ -23,6 +23,7 @@ describe('An Express server with middleware', (): void => { 'urn:solid-server:default:ExpressHttpServerFactory', 'middleware.json', { 'urn:solid-server:default:LdpHandler': new SimpleHttpHandler(), 'urn:solid-server:default:variable:port': port, + 'urn:solid-server:default:variable:baseUrl': 'https://example.pod/', }, ) as ExpressHttpServerFactory; server = factory.startServer(port); diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/WebSocketsProtocol.test.ts index d3d64ef1b..c679df58c 100644 --- a/test/integration/WebSocketsProtocol.test.ts +++ b/test/integration/WebSocketsProtocol.test.ts @@ -5,7 +5,7 @@ import type { HttpServerFactory } from '../../src/server/HttpServerFactory'; import { instantiateFromConfig } from '../configs/Util'; const port = 6001; -const baseUrl = `http://localhost:${port}/`; +const serverUrl = `http://localhost:${port}/`; describe('A server with the Solid WebSockets API', (): void => { let server: Server; @@ -14,7 +14,7 @@ describe('A server with the Solid WebSockets API', (): void => { const factory = await instantiateFromConfig( 'urn:solid-server:default:ServerFactory', 'websockets.json', { 'urn:solid-server:default:variable:port': port, - 'urn:solid-server:default:variable:base': baseUrl, + 'urn:solid-server:default:variable:baseUrl': 'http://example.pod/', }, ) as HttpServerFactory; server = factory.startServer(port); @@ -27,17 +27,17 @@ describe('A server with the Solid WebSockets API', (): void => { }); it('returns a 200.', async(): Promise => { - const response = await fetch(baseUrl); + const response = await fetch(serverUrl, { headers: { host: 'example.pod' }}); expect(response.status).toBe(200); }); it('sets the Updates-Via header.', async(): Promise => { - const response = await fetch(baseUrl); - expect(response.headers.get('Updates-Via')).toBe(`ws://localhost:${port}/`); + const response = await fetch(serverUrl, { headers: { host: 'example.pod' }}); + expect(response.headers.get('Updates-Via')).toBe('ws://example.pod/'); }); it('exposes the Updates-Via header via CORS.', async(): Promise => { - const response = await fetch(baseUrl); + const response = await fetch(serverUrl, { headers: { host: 'example.pod' }}); expect(response.headers.get('Access-Control-Expose-Headers')!.split(',')) .toContain('Updates-Via'); }); @@ -47,7 +47,7 @@ describe('A server with the Solid WebSockets API', (): void => { const messages = new Array(); beforeAll(async(): Promise => { - client = new WebSocket(`ws://localhost:${port}`, [ 'solid/0.1.0-alpha' ]); + client = new WebSocket(`ws://localhost:${port}`, [ 'solid/0.1.0-alpha' ], { headers: { host: 'example.pod' }}); client.on('message', (message: string): any => messages.push(message)); await new Promise((resolve): any => client.on('open', resolve)); }); @@ -69,21 +69,24 @@ describe('A server with the Solid WebSockets API', (): void => { describe('when the client subscribes to a resource', (): void => { beforeAll(async(): Promise => { - client.send(`sub ${baseUrl}my-resource`); + client.send(`sub http://example.pod/my-resource`); await new Promise((resolve): any => client.once('message', resolve)); }); it('acknowledges the subscription.', async(): Promise => { - expect(messages).toEqual([ `ack ${baseUrl}my-resource` ]); + expect(messages).toEqual([ `ack http://example.pod/my-resource` ]); }); it('notifies the client of resource updates.', async(): Promise => { - await fetch(`${baseUrl}my-resource`, { + await fetch(`${serverUrl}my-resource`, { method: 'PUT', - headers: { 'content-type': 'application/json' }, + headers: { + host: 'example.pod', + 'content-type': 'application/json', + }, body: '{}', }); - expect(messages).toEqual([ `pub ${baseUrl}my-resource` ]); + expect(messages).toEqual([ `pub http://example.pod/my-resource` ]); }); }); }); diff --git a/test/unit/init/CliRunner.test.ts b/test/unit/init/CliRunner.test.ts index 0b55a3654..9a8376d08 100644 --- a/test/unit/init/CliRunner.test.ts +++ b/test/unit/init/CliRunner.test.ts @@ -5,13 +5,13 @@ import type { Setup } from '../../../src/init/Setup'; const mainModulePath = path.join(__dirname, '../../../'); -const mockSetup = { +const mockSetup: jest.Mocked = { setup: jest.fn(async(): Promise => null), -} as unknown as jest.Mocked; -const loader = { +} as any; +const loader: jest.Mocked = { instantiateFromUrl: jest.fn(async(): Promise => mockSetup), registerAvailableModuleResources: jest.fn(async(): Promise => mockSetup), -} as unknown as jest.Mocked; +} as any; // Mock the Loader class. jest.mock('componentsjs', (): any => ({ @@ -40,7 +40,7 @@ describe('CliRunner', (): void => { { variables: { 'urn:solid-server:default:variable:port': 3000, - 'urn:solid-server:default:variable:base': `http://localhost:3000/`, + 'urn:solid-server:default:variable:baseUrl': 'http://localhost:3000/', 'urn:solid-server:default:variable:rootFilePath': process.cwd(), 'urn:solid-server:default:variable:sparqlEndpoint': undefined, 'urn:solid-server:default:variable:loggingLevel': 'info', @@ -58,6 +58,7 @@ describe('CliRunner', (): void => { argv: [ 'node', 'script', '-p', '4000', + '-b', 'http://pod.example/', '-c', 'myconfig.json', '-f', '/root', '-s', 'http://localhost:5000/sparql', @@ -73,7 +74,7 @@ describe('CliRunner', (): void => { { variables: { 'urn:solid-server:default:variable:port': 4000, - 'urn:solid-server:default:variable:base': `http://localhost:4000/`, + 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', 'urn:solid-server:default:variable:rootFilePath': '/root', 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', 'urn:solid-server:default:variable:loggingLevel': 'debug', @@ -87,6 +88,7 @@ describe('CliRunner', (): void => { argv: [ 'node', 'script', '--port', '4000', + '--baseUrl', 'http://pod.example/', '--config', 'myconfig.json', '--rootFilePath', '/root', '--sparqlEndpoint', 'http://localhost:5000/sparql', @@ -102,7 +104,7 @@ describe('CliRunner', (): void => { { variables: { 'urn:solid-server:default:variable:port': 4000, - 'urn:solid-server:default:variable:base': `http://localhost:4000/`, + 'urn:solid-server:default:variable:baseUrl': 'http://pod.example/', 'urn:solid-server:default:variable:rootFilePath': '/root', 'urn:solid-server:default:variable:sparqlEndpoint': 'http://localhost:5000/sparql', 'urn:solid-server:default:variable:loggingLevel': 'debug', diff --git a/test/unit/server/middleware/WebSocketAdvertiser.test.ts b/test/unit/server/middleware/WebSocketAdvertiser.test.ts index 5421c5346..37695f968 100644 --- a/test/unit/server/middleware/WebSocketAdvertiser.test.ts +++ b/test/unit/server/middleware/WebSocketAdvertiser.test.ts @@ -2,36 +2,31 @@ import { createResponse } from 'node-mocks-http'; import { WebSocketAdvertiser } from '../../../../src/server/middleware/WebSocketAdvertiser'; describe('A WebSocketAdvertiser', (): void => { - it('writes a default HTTP WebSocket.', async(): Promise => { - const writer = new WebSocketAdvertiser(); - const response = createResponse(); - await writer.handle({ response } as any); - expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://localhost/' }); - }); - - it('writes an HTTP WebSocket with port 80.', async(): Promise => { - const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 80, protocol: 'http' }); + it('writes a ws: socket when given an http: URL.', async(): Promise => { + const writer = new WebSocketAdvertiser('http://test.example/'); const response = createResponse(); await writer.handle({ response } as any); expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example/' }); }); - it('writes an HTTP WebSocket with port 3000.', async(): Promise => { - const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 3000, protocol: 'http' }); + it('writes a ws: socket when given a ws: URL.', async(): Promise => { + const writer = new WebSocketAdvertiser('ws://test.example/'); const response = createResponse(); await writer.handle({ response } as any); - expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example:3000/' }); + expect(response.getHeaders()).toEqual({ 'updates-via': 'ws://test.example/' }); }); - it('writes an HTTPS WebSocket with port 443.', async(): Promise => { - const writer = new WebSocketAdvertiser({ hostname: 'test.example', port: 443, protocol: 'https' }); + it('writes a wss: socket when given an https: URL.', async(): Promise => { + const writer = new WebSocketAdvertiser('https://test.example/'); const response = createResponse(); await writer.handle({ response } as any); expect(response.getHeaders()).toEqual({ 'updates-via': 'wss://test.example/' }); }); - it('rejects an invalid hostname.', (): void => { - expect((): any => new WebSocketAdvertiser({ hostname: 'test.example/invalid' })) - .toThrow('Invalid hostname: test.example/invalid'); + it('writes a wss: socket when given a wss: URL.', async(): Promise => { + const writer = new WebSocketAdvertiser('wss://test.example/'); + const response = createResponse(); + await writer.handle({ response } as any); + expect(response.getHeaders()).toEqual({ 'updates-via': 'wss://test.example/' }); }); });