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 |
|
||||
|------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `--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. |
|
||||
| `--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. |
|
||||
|
@ -6,7 +6,8 @@
|
||||
"@id": "urn:solid-server:default:ServerInitializer",
|
||||
"@type": "ServerInitializer",
|
||||
"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."
|
||||
}
|
||||
},
|
||||
{
|
||||
"@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",
|
||||
"name": "rootFilePath",
|
||||
|
@ -28,6 +28,14 @@
|
||||
"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_value": {
|
||||
|
@ -7,6 +7,11 @@
|
||||
"@id": "urn:solid-server:default:variable:port",
|
||||
"@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.",
|
||||
"@id": "urn:solid-server:default:variable:baseUrl",
|
||||
|
@ -9,19 +9,28 @@ import { Initializer } from './Initializer';
|
||||
*/
|
||||
export class ServerInitializer extends Initializer implements Finalizable {
|
||||
private readonly serverFactory: HttpServerFactory;
|
||||
private readonly port: number;
|
||||
private readonly port?: number;
|
||||
private readonly socketPath?: string;
|
||||
|
||||
private server?: Server;
|
||||
|
||||
public constructor(serverFactory: HttpServerFactory, port: number) {
|
||||
public constructor(serverFactory: HttpServerFactory, port?: number, socketPath?: string) {
|
||||
super();
|
||||
this.serverFactory = serverFactory;
|
||||
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> {
|
||||
if (this.socketPath) {
|
||||
this.server = this.serverFactory.startServer(this.socketPath);
|
||||
} else if (this.port) {
|
||||
this.server = this.serverFactory.startServer(this.port);
|
||||
}
|
||||
}
|
||||
|
||||
public async finalize(): Promise<void> {
|
||||
if (this.server) {
|
||||
|
@ -18,6 +18,9 @@ export class BaseUrlExtractor extends ShorthandExtractor {
|
||||
if (typeof args.baseUrl === 'string') {
|
||||
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;
|
||||
return `http://localhost:${port}/`;
|
||||
}
|
||||
|
@ -50,12 +50,12 @@ export class BaseHttpServerFactory implements HttpServerFactory {
|
||||
|
||||
/**
|
||||
* 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 url = new URL(`${protocol}://localhost:${port}/`).href;
|
||||
this.logger.info(`Listening to server at ${url}`);
|
||||
|
||||
const createServer = this.options.https ? createHttpsServer : createHttpServer;
|
||||
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 {
|
||||
|
@ -4,5 +4,8 @@ import type { Server } from 'http';
|
||||
* A factory for HTTP servers
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
public startServer(port: number): Server {
|
||||
public startServer(port: number): Server;
|
||||
public startServer(socket: string): Server;
|
||||
public startServer(portOrSocket: number | string): Server {
|
||||
// Create WebSocket server
|
||||
const webSocketServer = new WebSocketServer({ noServer: true });
|
||||
webSocketServer.on('connection', async(webSocket: WebSocket, upgradeRequest: HttpRequest): Promise<void> => {
|
||||
@ -26,7 +28,7 @@ export class WebSocketServerFactory implements HttpServerFactory {
|
||||
});
|
||||
|
||||
// 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 => {
|
||||
webSocketServer.handleUpgrade(upgradeRequest, socket, head, (webSocket: WebSocket): void => {
|
||||
webSocketServer.emit('connection', webSocket, upgradeRequest);
|
||||
|
@ -51,6 +51,7 @@ export function getDefaultVariables(port: number, baseUrl?: string): Record<stri
|
||||
return {
|
||||
'urn:solid-server:default:variable:baseUrl': baseUrl ?? `http://localhost:${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:showStackTrace': true,
|
||||
'urn:solid-server:default:variable:seededPodConfigJson': null,
|
||||
|
@ -22,6 +22,18 @@ describe('ServerInitializer', (): void => {
|
||||
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> => {
|
||||
await initializer.handle();
|
||||
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/');
|
||||
});
|
||||
|
||||
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> => {
|
||||
await expect(computer.handle({})).resolves.toBe('http://localhost:3000/');
|
||||
});
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { mkdirSync } from 'fs';
|
||||
import type { Server } from 'http';
|
||||
import request from 'supertest';
|
||||
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 { HttpResponse } from '../../../src/server/HttpResponse';
|
||||
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');
|
||||
|
||||
@ -125,4 +127,91 @@ describe('A BaseHttpServerFactory', (): void => {
|
||||
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',
|
||||
] as const;
|
||||
|
||||
const socketNames = [
|
||||
// Unit
|
||||
'BaseHttpServerFactory',
|
||||
];
|
||||
|
||||
export function getPort(name: typeof portNames[number]): number {
|
||||
const idx = portNames.indexOf(name);
|
||||
// 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;
|
||||
}
|
||||
|
||||
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 {
|
||||
const flag = `TEST_${envFlag.toUpperCase()}`;
|
||||
const enabled = !/^(|0|false)$/iu.test(process.env[flag] ?? '');
|
||||
|
Loading…
x
Reference in New Issue
Block a user