mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: allow server to bind to Unix Domain Sockets
This commit is contained in:
parent
0eb50891ec
commit
bf0e35be37
@ -125,7 +125,8 @@ to some commonly used settings:
|
|||||||
| parameter name | default value | description |
|
| parameter name | default value | description |
|
||||||
|------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
|
|------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `--port, -p` | `3000` | The TCP port on which the server should listen. |
|
| `--port, -p` | `3000` | The TCP port on which the server should listen. |
|
||||||
| `--baseUrl, -b` | `http://localhost:$PORT/` | The base URL used internally to generate URLs. Change this if your server does not run on `http://localhost:$PORT/`. |
|
| `--baseUrl, -b` | `http://localhost:$PORT/` | The base URL used internally to generate URLs. Change this if your server does not run on `http://localhost:$PORT/`.
|
||||||
|
| `--socket` | | The Unix Domain Socket on which the server should listen. `--baseUrl` must be set if this option is provided |
|
||||||
| `--loggingLevel, -l` | `info` | The detail level of logging; useful for debugging problems. Use `debug` for full information. |
|
| `--loggingLevel, -l` | `info` | The detail level of logging; useful for debugging problems. Use `debug` for full information. |
|
||||||
| `--config, -c` | `@css:config/default.json` | The configuration(s) for the server. The default only stores data in memory; to persist to your filesystem, use `@css:config/file.json` |
|
| `--config, -c` | `@css:config/default.json` | The configuration(s) for the server. The default only stores data in memory; to persist to your filesystem, use `@css:config/file.json` |
|
||||||
| `--rootFilePath, -f` | `./` | Root folder where the server stores data, when using a file-based configuration. |
|
| `--rootFilePath, -f` | `./` | Root folder where the server stores data, when using a file-based configuration. |
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
"@id": "urn:solid-server:default:ServerInitializer",
|
"@id": "urn:solid-server:default:ServerInitializer",
|
||||||
"@type": "ServerInitializer",
|
"@type": "ServerInitializer",
|
||||||
"serverFactory": { "@id": "urn:solid-server:default:ServerFactory" },
|
"serverFactory": { "@id": "urn:solid-server:default:ServerFactory" },
|
||||||
"port": { "@id": "urn:solid-server:default:variable:port" }
|
"port": { "@id": "urn:solid-server:default:variable:port" },
|
||||||
|
"socketPath": { "@id": "urn:solid-server:default:variable:socket" }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -56,6 +56,15 @@
|
|||||||
"describe": "The TCP port on which the server runs."
|
"describe": "The TCP port on which the server runs."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"@type": "YargsParameter",
|
||||||
|
"name": "socket",
|
||||||
|
"options": {
|
||||||
|
"requiresArg": true,
|
||||||
|
"type": "string",
|
||||||
|
"describe": "The path to the Unix Domain Socket on which the server runs. This overrides the port argument."
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"@type": "YargsParameter",
|
"@type": "YargsParameter",
|
||||||
"name": "rootFilePath",
|
"name": "rootFilePath",
|
||||||
|
@ -28,6 +28,14 @@
|
|||||||
"defaultValue": 3000
|
"defaultValue": 3000
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:socket",
|
||||||
|
"CombinedShorthandResolver:_resolvers_value": {
|
||||||
|
"@type": "KeyExtractor",
|
||||||
|
"key": "socket",
|
||||||
|
"defaultValue" : ""
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:rootFilePath",
|
"CombinedShorthandResolver:_resolvers_key": "urn:solid-server:default:variable:rootFilePath",
|
||||||
"CombinedShorthandResolver:_resolvers_value": {
|
"CombinedShorthandResolver:_resolvers_value": {
|
||||||
|
@ -7,6 +7,11 @@
|
|||||||
"@id": "urn:solid-server:default:variable:port",
|
"@id": "urn:solid-server:default:variable:port",
|
||||||
"@type": "Variable"
|
"@type": "Variable"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"comment": "Unix Domain Socket of the server.",
|
||||||
|
"@id": "urn:solid-server:default:variable:socket",
|
||||||
|
"@type": "Variable"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"comment": "Needs to be set to the base URL of the server for authentication and authorization to function.",
|
"comment": "Needs to be set to the base URL of the server for authentication and authorization to function.",
|
||||||
"@id": "urn:solid-server:default:variable:baseUrl",
|
"@id": "urn:solid-server:default:variable:baseUrl",
|
||||||
|
@ -9,19 +9,28 @@ import { Initializer } from './Initializer';
|
|||||||
*/
|
*/
|
||||||
export class ServerInitializer extends Initializer implements Finalizable {
|
export class ServerInitializer extends Initializer implements Finalizable {
|
||||||
private readonly serverFactory: HttpServerFactory;
|
private readonly serverFactory: HttpServerFactory;
|
||||||
private readonly port: number;
|
private readonly port?: number;
|
||||||
|
private readonly socketPath?: string;
|
||||||
|
|
||||||
private server?: Server;
|
private server?: Server;
|
||||||
|
|
||||||
public constructor(serverFactory: HttpServerFactory, port: number) {
|
public constructor(serverFactory: HttpServerFactory, port?: number, socketPath?: string) {
|
||||||
super();
|
super();
|
||||||
this.serverFactory = serverFactory;
|
this.serverFactory = serverFactory;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
|
this.socketPath = socketPath;
|
||||||
|
if (!port && !socketPath) {
|
||||||
|
throw new Error('Either Port or Socket arguments must be set');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handle(): Promise<void> {
|
public async handle(): Promise<void> {
|
||||||
|
if (this.socketPath) {
|
||||||
|
this.server = this.serverFactory.startServer(this.socketPath);
|
||||||
|
} else if (this.port) {
|
||||||
this.server = this.serverFactory.startServer(this.port);
|
this.server = this.serverFactory.startServer(this.port);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async finalize(): Promise<void> {
|
public async finalize(): Promise<void> {
|
||||||
if (this.server) {
|
if (this.server) {
|
||||||
|
@ -18,6 +18,9 @@ export class BaseUrlExtractor extends ShorthandExtractor {
|
|||||||
if (typeof args.baseUrl === 'string') {
|
if (typeof args.baseUrl === 'string') {
|
||||||
return ensureTrailingSlash(args.baseUrl);
|
return ensureTrailingSlash(args.baseUrl);
|
||||||
}
|
}
|
||||||
|
if (typeof args.socket === 'string') {
|
||||||
|
throw new Error('BaseUrl argument should be provided when using Unix Domain Sockets.');
|
||||||
|
}
|
||||||
const port = args.port ?? this.defaultPort;
|
const port = args.port ?? this.defaultPort;
|
||||||
return `http://localhost:${port}/`;
|
return `http://localhost:${port}/`;
|
||||||
}
|
}
|
||||||
|
@ -50,12 +50,12 @@ export class BaseHttpServerFactory implements HttpServerFactory {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates and starts an HTTP(S) server
|
* Creates and starts an HTTP(S) server
|
||||||
* @param port - Port on which the server listens
|
* @param portOrSocket - Port or Unix Domain Socket on which the server listens
|
||||||
*/
|
*/
|
||||||
public startServer(port: number): Server {
|
public startServer(port: number): Server;
|
||||||
|
public startServer(socket: string): Server;
|
||||||
|
public startServer(portOrSocket: number | string): Server {
|
||||||
const protocol = this.options.https ? 'https' : 'http';
|
const protocol = this.options.https ? 'https' : 'http';
|
||||||
const url = new URL(`${protocol}://localhost:${port}/`).href;
|
|
||||||
this.logger.info(`Listening to server at ${url}`);
|
|
||||||
|
|
||||||
const createServer = this.options.https ? createHttpsServer : createHttpServer;
|
const createServer = this.options.https ? createHttpsServer : createHttpServer;
|
||||||
const options = this.createServerOptions();
|
const options = this.createServerOptions();
|
||||||
@ -92,7 +92,17 @@ export class BaseHttpServerFactory implements HttpServerFactory {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return server.listen(port);
|
if (typeof portOrSocket === 'string') {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
throw new Error('Windows does not support Unix Domain Sockets');
|
||||||
|
}
|
||||||
|
const result = server.listen(portOrSocket);
|
||||||
|
this.logger.info(`Listening to server at ${server.address()}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
const url = new URL(`${protocol}://localhost:${portOrSocket}/`).href;
|
||||||
|
this.logger.info(`Listening to server at ${url}`);
|
||||||
|
return server.listen(portOrSocket);
|
||||||
}
|
}
|
||||||
|
|
||||||
private createServerOptions(): BaseHttpServerOptions {
|
private createServerOptions(): BaseHttpServerOptions {
|
||||||
|
@ -4,5 +4,8 @@ import type { Server } from 'http';
|
|||||||
* A factory for HTTP servers
|
* A factory for HTTP servers
|
||||||
*/
|
*/
|
||||||
export interface HttpServerFactory {
|
export interface HttpServerFactory {
|
||||||
startServer: (port: number) => Server;
|
/* eslint-disable @typescript-eslint/method-signature-style */
|
||||||
|
startServer(port: number): Server;
|
||||||
|
startServer(socket: string): Server;
|
||||||
|
startServer(portOrSocket: number | string): Server;
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,9 @@ export class WebSocketServerFactory implements HttpServerFactory {
|
|||||||
this.webSocketHandler = webSocketHandler;
|
this.webSocketHandler = webSocketHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
public startServer(port: number): Server {
|
public startServer(port: number): Server;
|
||||||
|
public startServer(socket: string): Server;
|
||||||
|
public startServer(portOrSocket: number | string): Server {
|
||||||
// Create WebSocket server
|
// Create WebSocket server
|
||||||
const webSocketServer = new WebSocketServer({ noServer: true });
|
const webSocketServer = new WebSocketServer({ noServer: true });
|
||||||
webSocketServer.on('connection', async(webSocket: WebSocket, upgradeRequest: HttpRequest): Promise<void> => {
|
webSocketServer.on('connection', async(webSocket: WebSocket, upgradeRequest: HttpRequest): Promise<void> => {
|
||||||
@ -26,7 +28,7 @@ export class WebSocketServerFactory implements HttpServerFactory {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Create base HTTP server
|
// Create base HTTP server
|
||||||
const httpServer = this.baseServerFactory.startServer(port);
|
const httpServer = this.baseServerFactory.startServer(portOrSocket);
|
||||||
httpServer.on('upgrade', (upgradeRequest: HttpRequest, socket: Socket, head: Buffer): void => {
|
httpServer.on('upgrade', (upgradeRequest: HttpRequest, socket: Socket, head: Buffer): void => {
|
||||||
webSocketServer.handleUpgrade(upgradeRequest, socket, head, (webSocket: WebSocket): void => {
|
webSocketServer.handleUpgrade(upgradeRequest, socket, head, (webSocket: WebSocket): void => {
|
||||||
webSocketServer.emit('connection', webSocket, upgradeRequest);
|
webSocketServer.emit('connection', webSocket, upgradeRequest);
|
||||||
|
@ -51,6 +51,7 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
|
|||||||
return {
|
return {
|
||||||
'urn:solid-server:default:variable:baseUrl': baseUrl ?? `http://localhost:${port}/`,
|
'urn:solid-server:default:variable:baseUrl': baseUrl ?? `http://localhost:${port}/`,
|
||||||
'urn:solid-server:default:variable:port': port,
|
'urn:solid-server:default:variable:port': port,
|
||||||
|
'urn:solid-server:default:variable:socket': null,
|
||||||
'urn:solid-server:default:variable:loggingLevel': 'off',
|
'urn:solid-server:default:variable:loggingLevel': 'off',
|
||||||
'urn:solid-server:default:variable:showStackTrace': true,
|
'urn:solid-server:default:variable:showStackTrace': true,
|
||||||
'urn:solid-server:default:variable:seededPodConfigJson': null,
|
'urn:solid-server:default:variable:seededPodConfigJson': null,
|
||||||
|
@ -22,6 +22,18 @@ describe('ServerInitializer', (): void => {
|
|||||||
expect(serverFactory.startServer).toHaveBeenCalledWith(3000);
|
expect(serverFactory.startServer).toHaveBeenCalledWith(3000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('starts a server on the specified Unix Domain Socket.', async(): Promise<void> => {
|
||||||
|
initializer = new ServerInitializer(serverFactory, undefined, '/tmp/css.sock');
|
||||||
|
await initializer.handle();
|
||||||
|
expect(serverFactory.startServer).toHaveBeenCalledWith('/tmp/css.sock');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when neither port or socket are set.', async(): Promise<void> => {
|
||||||
|
expect((): void => {
|
||||||
|
initializer = new ServerInitializer(serverFactory, undefined, undefined);
|
||||||
|
}).toThrow('Either Port or Socket arguments must be set');
|
||||||
|
});
|
||||||
|
|
||||||
it('can stop the server.', async(): Promise<void> => {
|
it('can stop the server.', async(): Promise<void> => {
|
||||||
await initializer.handle();
|
await initializer.handle();
|
||||||
await expect(initializer.finalize()).resolves.toBeUndefined();
|
await expect(initializer.finalize()).resolves.toBeUndefined();
|
||||||
|
@ -16,6 +16,11 @@ describe('A BaseUrlExtractor', (): void => {
|
|||||||
await expect(computer.handle({ port: 3333 })).resolves.toBe('http://localhost:3333/');
|
await expect(computer.handle({ port: 3333 })).resolves.toBe('http://localhost:3333/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('throws when a Unix Socket Path is provided without a baseUrl.', async(): Promise<void> => {
|
||||||
|
await expect(computer.handle({ socket: '/tmp/css.sock' })).rejects
|
||||||
|
.toThrow('BaseUrl argument should be provided when using Unix Domain Sockets.');
|
||||||
|
});
|
||||||
|
|
||||||
it('defaults to port 3000.', async(): Promise<void> => {
|
it('defaults to port 3000.', async(): Promise<void> => {
|
||||||
await expect(computer.handle({})).resolves.toBe('http://localhost:3000/');
|
await expect(computer.handle({})).resolves.toBe('http://localhost:3000/');
|
||||||
});
|
});
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { mkdirSync } from 'fs';
|
||||||
import type { Server } from 'http';
|
import type { Server } from 'http';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import type { BaseHttpServerOptions } from '../../../src/server/BaseHttpServerFactory';
|
import type { BaseHttpServerOptions } from '../../../src/server/BaseHttpServerFactory';
|
||||||
@ -5,7 +6,8 @@ import { BaseHttpServerFactory } from '../../../src/server/BaseHttpServerFactory
|
|||||||
import type { HttpHandler } from '../../../src/server/HttpHandler';
|
import type { HttpHandler } from '../../../src/server/HttpHandler';
|
||||||
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
import type { HttpResponse } from '../../../src/server/HttpResponse';
|
||||||
import { joinFilePath } from '../../../src/util/PathUtil';
|
import { joinFilePath } from '../../../src/util/PathUtil';
|
||||||
import { getPort } from '../../util/Util';
|
import { getTestFolder, removeFolder } from '../../integration/Config';
|
||||||
|
import { getPort, getSocket } from '../../util/Util';
|
||||||
|
|
||||||
const port = getPort('BaseHttpServerFactory');
|
const port = getPort('BaseHttpServerFactory');
|
||||||
|
|
||||||
@ -125,4 +127,91 @@ describe('A BaseHttpServerFactory', (): void => {
|
|||||||
expect(res.text).toBe(`${error.stack}\n`);
|
expect(res.text).toBe(`${error.stack}\n`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('A Base HttpServerFactory (With Unix Sockets)', (): void => {
|
||||||
|
const socketFolder = getTestFolder('sockets');
|
||||||
|
const socket = joinFilePath(socketFolder, getSocket('BaseHttpServerFactory'));
|
||||||
|
const httpOptions = {
|
||||||
|
http: true,
|
||||||
|
showStackTrace: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async(): Promise<void> => {
|
||||||
|
mkdirSync(socketFolder, { recursive: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async(): Promise<void> => {
|
||||||
|
server.close();
|
||||||
|
await removeFolder(socketFolder);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(): Promise<void> => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
describe('On linux', (): void => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it('sends incoming requests to the handler.', async(): Promise<void> => {
|
||||||
|
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||||
|
server = factory.startServer(socket);
|
||||||
|
await request(`http+unix://${socket.replace(/\//gui, '%2F')}`).get('/').set('Host', 'test.com').expect(200);
|
||||||
|
|
||||||
|
expect(handler.handleSafe).toHaveBeenCalledTimes(1);
|
||||||
|
expect(handler.handleSafe).toHaveBeenLastCalledWith({
|
||||||
|
request: expect.objectContaining({
|
||||||
|
headers: expect.objectContaining({ host: 'test.com' }),
|
||||||
|
}),
|
||||||
|
response: expect.objectContaining({}),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws an error on windows.', async(): Promise<void> => {
|
||||||
|
const prevPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||||
|
expect((): void => {
|
||||||
|
factory.startServer(socket);
|
||||||
|
}).toThrow();
|
||||||
|
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: prevPlatform,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('On Windows', (): void => {
|
||||||
|
if (process.platform !== 'win32') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
it('throws an error when trying to start the server on windows.', async(): Promise<void> => {
|
||||||
|
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||||
|
expect((): void => {
|
||||||
|
factory.startServer(socket);
|
||||||
|
}).toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('On any platform', (): void => {
|
||||||
|
it('throws an error when trying to start with an invalid socket Path.', async(): Promise<void> => {
|
||||||
|
const prevPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'linux',
|
||||||
|
});
|
||||||
|
|
||||||
|
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||||
|
factory.startServer('/fake/path')
|
||||||
|
.on('error', (error): void => {
|
||||||
|
expect(error).toHaveProperty('code', 'EACCES');
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: prevPlatform,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -33,6 +33,11 @@ const portNames = [
|
|||||||
'BaseHttpServerFactory',
|
'BaseHttpServerFactory',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const socketNames = [
|
||||||
|
// Unit
|
||||||
|
'BaseHttpServerFactory',
|
||||||
|
];
|
||||||
|
|
||||||
export function getPort(name: typeof portNames[number]): number {
|
export function getPort(name: typeof portNames[number]): number {
|
||||||
const idx = portNames.indexOf(name);
|
const idx = portNames.indexOf(name);
|
||||||
// Just in case something doesn't listen to the typings
|
// Just in case something doesn't listen to the typings
|
||||||
@ -42,6 +47,15 @@ export function getPort(name: typeof portNames[number]): number {
|
|||||||
return 6000 + idx;
|
return 6000 + idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSocket(name: typeof socketNames[number]): string {
|
||||||
|
const idx = socketNames.indexOf(name);
|
||||||
|
// Just in case something doesn't listen to the typings
|
||||||
|
if (idx < 0) {
|
||||||
|
throw new Error(`Unknown socket name ${name}`);
|
||||||
|
}
|
||||||
|
return `css${idx}.sock`;
|
||||||
|
}
|
||||||
|
|
||||||
export function describeIf(envFlag: string): Describe {
|
export function describeIf(envFlag: string): Describe {
|
||||||
const flag = `TEST_${envFlag.toUpperCase()}`;
|
const flag = `TEST_${envFlag.toUpperCase()}`;
|
||||||
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');
|
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');
|
||||||
|
Loading…
x
Reference in New Issue
Block a user