Skip to content

Commit

Permalink
Merge 2e95499 into 75e4f73
Browse files Browse the repository at this point in the history
  • Loading branch information
RubenVerborgh committed Nov 19, 2020
2 parents 75e4f73 + 2e95499 commit 1ba66e9
Show file tree
Hide file tree
Showing 11 changed files with 136 additions and 41 deletions.
2 changes: 0 additions & 2 deletions .eslintrc.js
Expand Up @@ -40,8 +40,6 @@ module.exports = {
'dot-location': ['error', 'property'],
'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }],
'max-len': ['error', { code: 120, ignoreUrls: true }],
'mocha/no-exports': 'off', // we are not using Mocha
'mocha/no-skipped-tests': 'off', // we are not using Mocha
'new-cap': 'off', // used for RDF constants
'no-param-reassign': 'off', // necessary in constructor overloading
'no-underscore-dangle': 'off', // conflicts with external libraries
Expand Down
5 changes: 4 additions & 1 deletion config/presets/http.json
Expand Up @@ -4,8 +4,11 @@
{
"@id": "urn:solid-server:default:HttpServer",
"@type": "ExpressHttpServer",
"ExpressHttpServer:_handler": {
"ExpressHttpServer:_httpHandler": {
"@id": "urn:solid-server:default:HttpHandler"
},
"ExpressHttpServer:_webSocketHandler": {
"@type": "RejectWebSocketHandler"
}
}
]
Expand Down
1 change: 1 addition & 0 deletions index.ts
Expand Up @@ -88,6 +88,7 @@ export * from './src/server/ExpressHttpServer';
export * from './src/server/HttpHandler';
export * from './src/server/HttpRequest';
export * from './src/server/HttpResponse';
export * from './src/server/RejectWebSocketHandler';

// Storage/Accessors
export * from './src/storage/accessors/DataAccessor';
Expand Down
55 changes: 33 additions & 22 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -83,6 +83,7 @@
"@types/sparqljs": "^3.1.0",
"@types/streamify-array": "^1.0.0",
"@types/uuid": "^8.3.0",
"@types/ws": "^7.4.0",
"@types/yargs": "^15.0.5",
"arrayify-stream": "^1.0.0",
"async-lock": "^1.2.4",
Expand All @@ -101,6 +102,7 @@
"uuid": "^8.3.0",
"winston": "^3.3.3",
"winston-transport": "^4.4.0",
"ws": "^7.4.0",
"yargs": "^16.0.0"
},
"devDependencies": {
Expand Down
35 changes: 28 additions & 7 deletions src/server/ExpressHttpServer.ts
@@ -1,26 +1,47 @@
import type { Server } from 'http';
import type { Socket } from 'net';
import cors from 'cors';
import type { Express } from 'express';
import express from 'express';
import type WebSocket from 'ws';
import { Server as WebSocketServer } from 'ws';
import { getLoggerFor } from '../logging/LogUtil';
import type { HttpHandler } from './HttpHandler';
import type { HttpRequest } from './HttpRequest';
import type { WebSocketHandler } from './WebSocketHandler';

export class ExpressHttpServer {
protected readonly logger = getLoggerFor(this);

private readonly handler: HttpHandler;
private readonly httpHandler: HttpHandler;
private readonly webSocketHandler: WebSocketHandler;

public constructor(handler: HttpHandler) {
this.handler = handler;
public constructor(httpHandler: HttpHandler, webSocketHandler: WebSocketHandler) {
this.httpHandler = httpHandler;
this.webSocketHandler = webSocketHandler;
}

public listen(port?: number): Server {
// Set up HTTP
const app = express();
this.setup(app);
return app.listen(port);
this.setupRoutes(app);
const httpServer = app.listen(port);

// Set up WebSockets
const webSocketServer = new WebSocketServer({ noServer: true });
webSocketServer.on('connection', (webSocket: WebSocket, upgradeRequest: HttpRequest): void => {
this.webSocketHandler.handle(webSocket, upgradeRequest);
});
httpServer.on('upgrade', (upgradeRequest: HttpRequest, socket: Socket, head: Buffer): void => {
webSocketServer.handleUpgrade(upgradeRequest, socket, head, (webSocket: WebSocket): void => {
webSocketServer.emit('connection', webSocket, upgradeRequest);
});
});

return httpServer;
}

protected setup(app: Express): void {
protected setupRoutes(app: Express): void {
// Set up server identification
app.use((request, response, done): void => {
response.setHeader('X-Powered-By', 'Community Solid Server');
Expand All @@ -39,7 +60,7 @@ export class ExpressHttpServer {
app.use(async(request, response, done): Promise<void> => {
try {
this.logger.info(`Received request for ${request.url}`);
await this.handler.handleSafe({ request, response });
await this.httpHandler.handleSafe({ request, response });
} catch (error: unknown) {
const errMsg = error instanceof Error ? `${error.name}: ${error.message}\n${error.stack}` : 'Unknown error.';
this.logger.error(errMsg);
Expand Down
11 changes: 11 additions & 0 deletions src/server/RejectWebSocketHandler.ts
@@ -0,0 +1,11 @@
import type WebSocket from 'ws';
import type { WebSocketHandler } from './WebSocketHandler';

/**
* WebSocket handler that closes socket connections
*/
export class RejectWebSocketHandler implements WebSocketHandler {
public handle(socket: WebSocket): void {
socket.close();
}
}
9 changes: 9 additions & 0 deletions src/server/WebSocketHandler.ts
@@ -0,0 +1,9 @@
import type WebSocket from 'ws';
import type { HttpRequest } from './HttpRequest';

/**
* A WebSocketHandler handles the communication with multiple WebSockets
*/
export interface WebSocketHandler {
handle: (socket: WebSocket, upgradeRequest: HttpRequest) => void;
}
5 changes: 5 additions & 0 deletions test/.eslintrc.js
Expand Up @@ -11,6 +11,11 @@ module.exports = {
'unicorn/no-useless-undefined': 'off',
'no-process-env': 'off',

// We are not using Mocha
'mocha/no-exports': 'off',
'mocha/no-skipped-tests': 'off',
'mocha/no-synchronous-tests': 'off',

// Need these 2 to run tests for throwing non-Error objects
'@typescript-eslint/no-throw-literal': 'off',
'no-throw-literal': 'off',
Expand Down
41 changes: 32 additions & 9 deletions test/unit/server/ExpressHttpServer.test.ts
@@ -1,9 +1,11 @@
import type { Server } from 'http';
import request from 'supertest';
import WebSocket from 'ws';
import { ExpressHttpServer } from '../../../src/server/ExpressHttpServer';
import { HttpHandler } from '../../../src/server/HttpHandler';
import type { HttpRequest } from '../../../src/server/HttpRequest';
import type { HttpResponse } from '../../../src/server/HttpResponse';
import type { WebSocketHandler } from '../../../src/server/WebSocketHandler';
import SpyInstance = jest.SpyInstance;

const handle = async(input: { request: HttpRequest; response: HttpResponse }): Promise<void> => {
Expand All @@ -17,11 +19,22 @@ class SimpleHttpHandler extends HttpHandler {
}
}

class SimpleWebSocketHandler implements WebSocketHandler {
public host: any;

public handle(socket: WebSocket, upgradeRequest: HttpRequest): void {
socket.send('SimpleWebSocketHandler');
socket.close();
this.host = upgradeRequest.headers.host;
}
}

describe('ExpressHttpServer', (): void => {
let server: Server;
let canHandleJest: jest.Mock<Promise<void>, []>;
let handleJest: jest.Mock<Promise<void>, [any]>;
let handler: SimpleHttpHandler;
let httpHandler: SimpleHttpHandler;
let webSocketHandler: SimpleWebSocketHandler;
let mock: SpyInstance;

beforeAll(async(): Promise<void> => {
Expand All @@ -30,15 +43,16 @@ describe('ExpressHttpServer', (): void => {
});

beforeEach(async(): Promise<void> => {
handler = new SimpleHttpHandler();
httpHandler = new SimpleHttpHandler();
webSocketHandler = new SimpleWebSocketHandler();
canHandleJest = jest.fn(async(): Promise<void> => undefined);
handleJest = jest.fn(async(input): Promise<void> => handle(input));

handler.canHandle = canHandleJest;
handler.handle = handleJest;
httpHandler.canHandle = canHandleJest;
httpHandler.handle = handleJest;

const expressServer = new ExpressHttpServer(handler);
server = expressServer.listen();
const expressServer = new ExpressHttpServer(httpHandler, webSocketHandler);
server = expressServer.listen(5555);
});

afterEach(async(): Promise<void> => {
Expand Down Expand Up @@ -93,14 +107,14 @@ describe('ExpressHttpServer', (): void => {
});

it('returns a 404 when the handler does not do anything.', async(): Promise<void> => {
handler.handle = async(input): Promise<void> => {
httpHandler.handle = async(input): Promise<void> => {
expect(input).toBeDefined();
};
await request(server).get('/').expect(404);
});

it('catches errors thrown by its handler.', async(): Promise<void> => {
handler.handle = async(): Promise<void> => {
httpHandler.handle = async(): Promise<void> => {
throw new Error('dummyError');
};

Expand All @@ -109,11 +123,20 @@ describe('ExpressHttpServer', (): void => {
});

it('throws unknown errors if its handler throw non-Error objects.', async(): Promise<void> => {
handler.handle = async(): Promise<void> => {
httpHandler.handle = async(): Promise<void> => {
throw 'apple';
};

const res = await request(server).get('/').expect(500);
expect(res.text).toContain('Unknown error.');
});

it('passes WebSocket requests to the WebSocket handler.', async(): Promise<void> => {
const client = new WebSocket('ws://localhost:5555');
const text = await new Promise((resolve): void => {
client.on('message', resolve);
});
expect(text).toBe('SimpleWebSocketHandler');
expect(webSocketHandler.host).toBe('localhost:5555');
});
});
11 changes: 11 additions & 0 deletions test/unit/server/RejectWebSocketHandler.test.ts
@@ -0,0 +1,11 @@
import { RejectWebSocketHandler } from '../../../src/server/RejectWebSocketHandler';

describe('RejectWebSocketHandler', (): void => {
const handler = new RejectWebSocketHandler();

it('closes the socket.', (): void => {
const close = jest.fn();
handler.handle({ close } as any);
expect(close).toHaveBeenCalledTimes(1);
});
});

0 comments on commit 1ba66e9

Please sign in to comment.