mirror of
https://github.com/CommunitySolidServer/CommunitySolidServer.git
synced 2024-10-03 14:55:10 +00:00
feat: Split up server creation and request handling
This allows us to decouple the WebSocket listening from the HTTP configs, making these features completely orthogonal.
This commit is contained in:
@@ -21,6 +21,7 @@
|
||||
"Promise",
|
||||
"Readonly",
|
||||
"RegExp",
|
||||
"Server",
|
||||
"Shorthand",
|
||||
"Template",
|
||||
"TemplateEngine",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/optional.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
@@ -30,7 +30,9 @@
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/file.json",
|
||||
"css:config/util/variables/default.json"
|
||||
"css:config/util/variables/default.json",
|
||||
|
||||
"css:config/http/server-factory/configurator/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
@@ -39,23 +41,15 @@
|
||||
"The http/server-factory import above has been omitted since that feature is set below."
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"comment": "The key/cert values should be replaces with paths to the correct files. The 'options' block can be removed if not needed.",
|
||||
"comment": "The key/cert values should be replaced with paths to the correct files.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "WebSocketServerFactory",
|
||||
"baseServerFactory": {
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"options_https": true,
|
||||
"options_key": "/path/to/server.key",
|
||||
"options_cert": "/path/to/server.cert"
|
||||
},
|
||||
"webSocketHandler": {
|
||||
"@type": "UnsecureWebSocketsProtocol",
|
||||
"source": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
}
|
||||
"@type": "BaseServerFactory",
|
||||
"configurator": { "@id": "urn:solid-server:default:ServerConfigurator" },
|
||||
"options_https": true,
|
||||
"options_key": "/path/to/server.key",
|
||||
"options_cert": "/path/to/server.cert"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Advertises the websocket connection.",
|
||||
"@id": "urn:solid-server:default:Middleware_WebSocket",
|
||||
"@type": "WebSocketAdvertiser",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/http/middleware/handlers/constant-headers.json",
|
||||
"css:config/http/middleware/handlers/cors.json",
|
||||
"css:config/http/middleware/handlers/updates-via.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "All of these will always be executed on any incoming request. These are mostly used for adding response headers.",
|
||||
"@id": "urn:solid-server:default:Middleware",
|
||||
"@type": "SequenceHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"comment": "These handlers can be executed in any order.",
|
||||
"@id": "urn:solid-server:default:ParallelMiddleware",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{ "@id": "urn:solid-server:default:Middleware_Header" },
|
||||
{ "@id": "urn:solid-server:default:Middleware_WebSocket" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"comment": "CORS has to be last since it can close the connection.",
|
||||
"@id": "urn:solid-server:default:Middleware_Cors"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
8
config/http/notifications/disabled.json
Normal file
8
config/http/notifications/disabled.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Disable notifications by not attaching a notification listener."
|
||||
}
|
||||
]
|
||||
}
|
||||
27
config/http/notifications/legacy-websockets.json
Normal file
27
config/http/notifications/legacy-websockets.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:ServerConfigurator",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"comment": "Catches the server upgrade events and handles the WebSocket connections.",
|
||||
"@type": "UnsecureWebSocketsProtocol",
|
||||
"source": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"@id": "urn:solid-server:default:ParallelMiddleware",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"comment": "Advertises the websocket connection.",
|
||||
"@type": "WebSocketAdvertiser",
|
||||
"baseUrl": { "@id": "urn:solid-server:default:variable:baseUrl" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
config/http/server-factory/configurator/default.json
Normal file
18
config/http/server-factory/configurator/default.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:ServerConfigurator",
|
||||
"@type": "ParallelHandler",
|
||||
"handlers": [
|
||||
{
|
||||
"comment": "Handles all request events from the server.",
|
||||
"@id": "urn:solid-server:default:HandlerServerConfigurator",
|
||||
"@type": "HandlerServerConfigurator",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
14
config/http/server-factory/http.json
Normal file
14
config/http/server-factory/http.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/http/server-factory/configurator/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Creates an empty HTTP server listening to the provided port.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "BaseServerFactory",
|
||||
"configurator": { "@id": "urn:solid-server:default:ServerConfigurator" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "An example of how to set up a server with HTTPS",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "WebSocketServerFactory",
|
||||
"baseServerFactory": {
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"options_https": true,
|
||||
"options_key": "/path/to/server.key",
|
||||
"options_cert": "/path/to/server.cert"
|
||||
},
|
||||
"webSocketHandler": {
|
||||
"@type": "UnsecureWebSocketsProtocol",
|
||||
"source": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
17
config/http/server-factory/https-no-cli-example.json
Normal file
17
config/http/server-factory/https-no-cli-example.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/http/server-factory/configurator/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "An example of how to set up an HTTPS server providing key/cert paths directly in the config.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "BaseServerFactory",
|
||||
"configurator": { "@id": "urn:solid-server:default:ServerConfigurator" },
|
||||
"options_https": true,
|
||||
"options_key": "/path/to/server.key",
|
||||
"options_cert": "/path/to/server.cert"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/http/server-factory/https/cli.json",
|
||||
"css:config/http/server-factory/https/resolver.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Creates an HTTPS server with the settings provided via the command line.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "WebSocketServerFactory",
|
||||
"baseServerFactory": {
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"options_https": true,
|
||||
"options_key": {
|
||||
"@id": "urn:solid-server:custom:variable:httpsKey",
|
||||
"@type": "Variable"
|
||||
},
|
||||
"options_cert": {
|
||||
"@id": "urn:solid-server:custom:variable:httpsCert",
|
||||
"@type": "Variable"
|
||||
}
|
||||
},
|
||||
"webSocketHandler": {
|
||||
"@type": "UnsecureWebSocketsProtocol",
|
||||
"source": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/http/server-factory/configurator/default.json",
|
||||
"css:config/http/server-factory/https/cli.json",
|
||||
"css:config/http/server-factory/https/resolver.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Creates a server that supports HTTPS requests.",
|
||||
"comment": "Creates an empty HTTPS server listening to the provided port using the key/cert paths provided through the CLI.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" },
|
||||
"@type": "BaseServerFactory",
|
||||
"configurator": { "@id": "urn:solid-server:default:ServerConfigurator" },
|
||||
"options_https": true,
|
||||
"options_key": {
|
||||
"@id": "urn:solid-server:custom:variable:httpsKey",
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Creates a server that supports HTTP requests.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"@graph": [
|
||||
{
|
||||
"comment": "Creates a server that supports both websocket and HTTP requests.",
|
||||
"@id": "urn:solid-server:default:ServerFactory",
|
||||
"@type": "WebSocketServerFactory",
|
||||
"baseServerFactory": {
|
||||
"@id": "urn:solid-server:default:HttpServerFactory",
|
||||
"@type": "BaseHttpServerFactory",
|
||||
"handler": { "@id": "urn:solid-server:default:HttpHandler" },
|
||||
"options_showStackTrace": { "@id": "urn:solid-server:default:variable:showStackTrace" }
|
||||
},
|
||||
"webSocketHandler": {
|
||||
"@type": "UnsecureWebSocketsProtocol",
|
||||
"source": { "@id": "urn:solid-server:default:ResourceStore" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/https-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/https.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/optional.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/restricted.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/app/variables/default.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { IncomingMessage } from 'http';
|
||||
import type { TLSSocket } from 'tls';
|
||||
import type { WebSocket } from 'ws';
|
||||
import type { SingleThreaded } from '../init/cluster/SingleThreaded';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpRequest } from '../server/HttpRequest';
|
||||
import { WebSocketHandler } from '../server/WebSocketHandler';
|
||||
import type { ActivityEmitter } from '../server/notifications/ActivityEmitter';
|
||||
import { WebSocketServerConfigurator } from '../server/WebSocketServerConfigurator';
|
||||
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||
import type { GenericEventEmitter } from '../util/GenericEventEmitter';
|
||||
import { createGenericEventEmitterClass } from '../util/GenericEventEmitter';
|
||||
import { parseForwarded } from '../util/HeaderUtil';
|
||||
import { splitCommaSeparated } from '../util/StringUtil';
|
||||
import type { ResourceIdentifier } from './representation/ResourceIdentifier';
|
||||
|
||||
const VERSION = 'solid-0.1';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const WebSocketListenerEmitter = createGenericEventEmitterClass<GenericEventEmitter<'closed', () => void>>();
|
||||
|
||||
/**
|
||||
* Implementation of Solid WebSockets API Spec solid-0.1
|
||||
* at https://github.com/solid/solid-spec/blob/master/api-websockets.md
|
||||
*/
|
||||
class WebSocketListener extends EventEmitter {
|
||||
class WebSocketListener extends WebSocketListenerEmitter {
|
||||
private host = '';
|
||||
private protocol = '';
|
||||
private readonly socket: WebSocket;
|
||||
@@ -30,7 +36,7 @@ class WebSocketListener extends EventEmitter {
|
||||
socket.addListener('message', (message: string): void => this.onMessage(message));
|
||||
}
|
||||
|
||||
public start({ headers, socket }: HttpRequest): void {
|
||||
public start({ headers, socket }: IncomingMessage): void {
|
||||
// Greet the client
|
||||
this.sendMessage('protocol', VERSION);
|
||||
|
||||
@@ -105,7 +111,7 @@ class WebSocketListener extends EventEmitter {
|
||||
this.logger.debug(`WebSocket subscribed to changes on ${url}`);
|
||||
} catch (error: unknown) {
|
||||
// Report errors to the socket
|
||||
const errorText: string = (error as any).message;
|
||||
const errorText: string = createErrorMessage(error);
|
||||
this.sendMessage('error', errorText);
|
||||
this.logger.warn(`WebSocket could not subscribe to ${path}: ${errorText}`);
|
||||
}
|
||||
@@ -120,11 +126,11 @@ class WebSocketListener extends EventEmitter {
|
||||
* Provides live update functionality following
|
||||
* the Solid WebSockets API Spec solid-0.1
|
||||
*/
|
||||
export class UnsecureWebSocketsProtocol extends WebSocketHandler implements SingleThreaded {
|
||||
private readonly logger = getLoggerFor(this);
|
||||
export class UnsecureWebSocketsProtocol extends WebSocketServerConfigurator implements SingleThreaded {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
private readonly listeners = new Set<WebSocketListener>();
|
||||
|
||||
public constructor(source: EventEmitter) {
|
||||
public constructor(source: ActivityEmitter) {
|
||||
super();
|
||||
|
||||
this.logger.warn('The chosen configuration includes Solid WebSockets API 0.1, which is unauthenticated.');
|
||||
@@ -133,8 +139,8 @@ export class UnsecureWebSocketsProtocol extends WebSocketHandler implements Sing
|
||||
source.on('changed', (changed: ResourceIdentifier): void => this.onResourceChanged(changed));
|
||||
}
|
||||
|
||||
public async handle(input: { webSocket: WebSocket; upgradeRequest: HttpRequest }): Promise<void> {
|
||||
const listener = new WebSocketListener(input.webSocket);
|
||||
protected async handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise<void> {
|
||||
const listener = new WebSocketListener(webSocket);
|
||||
this.listeners.add(listener);
|
||||
this.logger.info(`New WebSocket added, ${this.listeners.size} in total`);
|
||||
|
||||
@@ -142,7 +148,7 @@ export class UnsecureWebSocketsProtocol extends WebSocketHandler implements Sing
|
||||
this.listeners.delete(listener);
|
||||
this.logger.info(`WebSocket closed, ${this.listeners.size} remaining`);
|
||||
});
|
||||
listener.start(input.upgradeRequest);
|
||||
listener.start(upgradeRequest);
|
||||
}
|
||||
|
||||
private onResourceChanged(changed: ResourceIdentifier): void {
|
||||
|
||||
@@ -283,15 +283,16 @@ export * from './pods/PodManager';
|
||||
|
||||
// Server
|
||||
export * from './server/AuthorizingHttpHandler';
|
||||
export * from './server/BaseHttpServerFactory';
|
||||
export * from './server/BaseServerFactory';
|
||||
export * from './server/HandlerServerConfigurator';
|
||||
export * from './server/HttpHandler';
|
||||
export * from './server/HttpRequest';
|
||||
export * from './server/HttpResponse';
|
||||
export * from './server/HttpServerFactory';
|
||||
export * from './server/OperationHttpHandler';
|
||||
export * from './server/ParsingHttpHandler';
|
||||
export * from './server/WebSocketHandler';
|
||||
export * from './server/WebSocketServerFactory';
|
||||
export * from './server/ServerConfigurator';
|
||||
export * from './server/WebSocketServerConfigurator';
|
||||
|
||||
// Server/Middleware
|
||||
export * from './server/middleware/AcpHeaderHandler';
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Server } from 'http';
|
||||
import { URL } from 'url';
|
||||
import { promisify } from 'util';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { isHttpsServer } from '../server/HttpServerFactory';
|
||||
import type { HttpServerFactory } from '../server/HttpServerFactory';
|
||||
import type { Finalizable } from './final/Finalizable';
|
||||
import { Initializer } from './Initializer';
|
||||
@@ -8,6 +11,8 @@ import { Initializer } from './Initializer';
|
||||
* Creates and starts an HTTP server.
|
||||
*/
|
||||
export class ServerInitializer extends Initializer implements Finalizable {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly serverFactory: HttpServerFactory;
|
||||
private readonly port: number;
|
||||
|
||||
@@ -20,7 +25,11 @@ export class ServerInitializer extends Initializer implements Finalizable {
|
||||
}
|
||||
|
||||
public async handle(): Promise<void> {
|
||||
this.server = this.serverFactory.startServer(this.port);
|
||||
this.server = await this.serverFactory.createServer();
|
||||
|
||||
const url = new URL(`http${isHttpsServer(this.server) ? 's' : ''}://localhost:${this.port}/`).href;
|
||||
this.logger.info(`Listening to server at ${url}`);
|
||||
this.server.listen(this.port);
|
||||
}
|
||||
|
||||
public async finalize(): Promise<void> {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import type { Server, IncomingMessage, ServerResponse } from 'http';
|
||||
import { createServer as createHttpServer } from 'http';
|
||||
import { createServer as createHttpsServer } from 'https';
|
||||
import { URL } from 'url';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { isError } from '../util/errors/ErrorUtil';
|
||||
import { guardStream } from '../util/GuardedStream';
|
||||
import type { HttpHandler } from './HttpHandler';
|
||||
import type { HttpServerFactory } from './HttpServerFactory';
|
||||
|
||||
/**
|
||||
* Options to be used when creating the server.
|
||||
* Due to Components.js not supporting external types, this has been simplified (for now?).
|
||||
* The common https keys here (key/cert/pfx) will be interpreted as file paths that need to be read
|
||||
* before passing the options to the `createServer` function.
|
||||
*/
|
||||
export interface BaseHttpServerOptions {
|
||||
/**
|
||||
* If the server should start as an http or https server.
|
||||
*/
|
||||
https?: boolean;
|
||||
|
||||
/**
|
||||
* If the error stack traces should be shown in case the HttpHandler throws one.
|
||||
*/
|
||||
showStackTrace?: boolean;
|
||||
|
||||
key?: string;
|
||||
cert?: string;
|
||||
|
||||
pfx?: string;
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpServerFactory based on the native Node.js http module
|
||||
*/
|
||||
export class BaseHttpServerFactory implements HttpServerFactory {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
/** The main HttpHandler */
|
||||
private readonly handler: HttpHandler;
|
||||
private readonly options: BaseHttpServerOptions;
|
||||
|
||||
public constructor(handler: HttpHandler, options: BaseHttpServerOptions = { https: false }) {
|
||||
this.handler = handler;
|
||||
this.options = { ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and starts an HTTP(S) server
|
||||
* @param port - Port on which the server listens
|
||||
*/
|
||||
public startServer(port: number): 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();
|
||||
|
||||
const server = createServer(options,
|
||||
async(request: IncomingMessage, response: ServerResponse): Promise<void> => {
|
||||
try {
|
||||
this.logger.info(`Received ${request.method} request for ${request.url}`);
|
||||
const guardedRequest = guardStream(request);
|
||||
guardedRequest.on('error', (error): void => {
|
||||
this.logger.error(`Request error: ${error.message}`);
|
||||
});
|
||||
await this.handler.handleSafe({ request: guardedRequest, response });
|
||||
} catch (error: unknown) {
|
||||
let errMsg: string;
|
||||
if (!isError(error)) {
|
||||
errMsg = `Unknown error: ${error}.\n`;
|
||||
} else if (this.options.showStackTrace && error.stack) {
|
||||
errMsg = `${error.stack}\n`;
|
||||
} else {
|
||||
errMsg = `${error.name}: ${error.message}\n`;
|
||||
}
|
||||
this.logger.error(errMsg);
|
||||
if (response.headersSent) {
|
||||
response.end();
|
||||
} else {
|
||||
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
response.writeHead(500).end(errMsg);
|
||||
}
|
||||
} finally {
|
||||
if (!response.headersSent) {
|
||||
response.writeHead(404).end();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return server.listen(port);
|
||||
}
|
||||
|
||||
private createServerOptions(): BaseHttpServerOptions {
|
||||
const options = { ...this.options };
|
||||
for (const id of [ 'key', 'cert', 'pfx' ] as const) {
|
||||
const val = options[id];
|
||||
if (val) {
|
||||
options[id] = readFileSync(val, 'utf8');
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
69
src/server/BaseServerFactory.ts
Normal file
69
src/server/BaseServerFactory.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import type { Server } from 'http';
|
||||
import { createServer as createHttpServer } from 'http';
|
||||
import { createServer as createHttpsServer } from 'https';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import type { HttpServerFactory } from './HttpServerFactory';
|
||||
import type { ServerConfigurator } from './ServerConfigurator';
|
||||
|
||||
/**
|
||||
* Options to be used when creating the server.
|
||||
* Due to Components.js not supporting external types, this has been simplified (for now?).
|
||||
* The common https keys here (key/cert/pfx) will be interpreted as file paths that need to be read
|
||||
* before passing the options to the `createServer` function.
|
||||
*/
|
||||
export interface BaseServerFactoryOptions {
|
||||
/**
|
||||
* If the server should start as an HTTP or HTTPS server.
|
||||
*/
|
||||
https?: boolean;
|
||||
|
||||
key?: string;
|
||||
cert?: string;
|
||||
|
||||
pfx?: string;
|
||||
passphrase?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP(S) server native Node.js `http`/`https` modules.
|
||||
*
|
||||
* Will apply a {@link ServerConfigurator} to the server,
|
||||
* which should be used to attach listeners.
|
||||
*/
|
||||
export class BaseServerFactory implements HttpServerFactory {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
private readonly configurator: ServerConfigurator;
|
||||
private readonly options: BaseServerFactoryOptions;
|
||||
|
||||
public constructor(configurator: ServerConfigurator, options: BaseServerFactoryOptions = { https: false }) {
|
||||
this.configurator = configurator;
|
||||
this.options = { ...options };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an HTTP(S) server.
|
||||
*/
|
||||
public async createServer(): Promise<Server> {
|
||||
const createServer = this.options.https ? createHttpsServer : createHttpServer;
|
||||
const options = this.createServerOptions();
|
||||
|
||||
const server = createServer(options);
|
||||
|
||||
await this.configurator.handleSafe(server);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
private createServerOptions(): BaseServerFactoryOptions {
|
||||
const options = { ...this.options };
|
||||
for (const id of [ 'key', 'cert', 'pfx' ] as const) {
|
||||
const val = options[id];
|
||||
if (val) {
|
||||
options[id] = readFileSync(val, 'utf8');
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
}
|
||||
68
src/server/HandlerServerConfigurator.ts
Normal file
68
src/server/HandlerServerConfigurator.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import type { Server, IncomingMessage, ServerResponse } from 'http';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { isError } from '../util/errors/ErrorUtil';
|
||||
import { guardStream } from '../util/GuardedStream';
|
||||
import type { HttpHandler } from './HttpHandler';
|
||||
import { ServerConfigurator } from './ServerConfigurator';
|
||||
|
||||
/**
|
||||
* A {@link ServerConfigurator} that attaches an {@link HttpHandler} to the `request` event of a {@link Server}.
|
||||
* All incoming requests will be sent to the provided handler.
|
||||
* Failsafes are added to make sure a valid response is sent in case something goes wrong.
|
||||
*
|
||||
* The `showStackTrace` parameter can be used to add stack traces to error outputs.
|
||||
*/
|
||||
export class HandlerServerConfigurator extends ServerConfigurator {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
protected readonly errorLogger = (error: Error): void => {
|
||||
this.logger.error(`Request error: ${error.message}`);
|
||||
};
|
||||
|
||||
/** The main HttpHandler */
|
||||
private readonly handler: HttpHandler;
|
||||
private readonly showStackTrace: boolean;
|
||||
|
||||
public constructor(handler: HttpHandler, showStackTrace = false) {
|
||||
super();
|
||||
this.handler = handler;
|
||||
this.showStackTrace = showStackTrace;
|
||||
}
|
||||
|
||||
public async handle(server: Server): Promise<void> {
|
||||
server.on('request',
|
||||
async(request: IncomingMessage, response: ServerResponse): Promise<void> => {
|
||||
try {
|
||||
this.logger.info(`Received ${request.method} request for ${request.url}`);
|
||||
const guardedRequest = guardStream(request);
|
||||
guardedRequest.on('error', this.errorLogger);
|
||||
await this.handler.handleSafe({ request: guardedRequest, response });
|
||||
} catch (error: unknown) {
|
||||
const errMsg = this.createErrorMessage(error);
|
||||
this.logger.error(errMsg);
|
||||
if (response.headersSent) {
|
||||
response.end();
|
||||
} else {
|
||||
response.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
||||
response.writeHead(500).end(errMsg);
|
||||
}
|
||||
} finally {
|
||||
if (!response.headersSent) {
|
||||
response.writeHead(404).end();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a readable error message based on the error and the `showStackTrace` parameter.
|
||||
*/
|
||||
private createErrorMessage(error: unknown): string {
|
||||
if (!isError(error)) {
|
||||
return `Unknown error: ${error}.\n`;
|
||||
}
|
||||
if (this.showStackTrace && error.stack) {
|
||||
return `${error.stack}\n`;
|
||||
}
|
||||
return `${error.name}: ${error.message}\n`;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,16 @@
|
||||
import type { Server } from 'http';
|
||||
import { Server as HttpsServer } from 'https';
|
||||
|
||||
/**
|
||||
* A factory for HTTP servers
|
||||
* Returns `true` if the server is an HTTPS server.
|
||||
*/
|
||||
export function isHttpsServer(server: Server): server is HttpsServer {
|
||||
return server instanceof HttpsServer;
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory for HTTP servers.
|
||||
*/
|
||||
export interface HttpServerFactory {
|
||||
startServer: (port: number) => Server;
|
||||
createServer: () => Promise<Server>;
|
||||
}
|
||||
|
||||
7
src/server/ServerConfigurator.ts
Normal file
7
src/server/ServerConfigurator.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { Server } from 'http';
|
||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||
|
||||
/**
|
||||
* Configures a {@link Server} by attaching listeners for specific events.
|
||||
*/
|
||||
export abstract class ServerConfigurator extends AsyncHandler<Server> {}
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { WebSocket } from 'ws';
|
||||
import { AsyncHandler } from '../util/handlers/AsyncHandler';
|
||||
import type { HttpRequest } from './HttpRequest';
|
||||
|
||||
/**
|
||||
* A WebSocketHandler handles the communication with multiple WebSockets
|
||||
*/
|
||||
export abstract class WebSocketHandler
|
||||
extends AsyncHandler<{ webSocket: WebSocket; upgradeRequest: HttpRequest }> {}
|
||||
30
src/server/WebSocketServerConfigurator.ts
Normal file
30
src/server/WebSocketServerConfigurator.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { IncomingMessage, Server } from 'http';
|
||||
import type { Socket } from 'net';
|
||||
import type { WebSocket } from 'ws';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { getLoggerFor } from '../logging/LogUtil';
|
||||
import { createErrorMessage } from '../util/errors/ErrorUtil';
|
||||
import { ServerConfigurator } from './ServerConfigurator';
|
||||
|
||||
/**
|
||||
* {@link ServerConfigurator} that adds WebSocket functionality to an existing {@link Server}.
|
||||
*
|
||||
* Implementations need to implement the `handleConnection` function to receive the necessary information.
|
||||
*/
|
||||
export abstract class WebSocketServerConfigurator extends ServerConfigurator {
|
||||
protected readonly logger = getLoggerFor(this);
|
||||
|
||||
public async handle(server: Server): Promise<void> {
|
||||
// Create WebSocket server
|
||||
const webSocketServer = new WebSocketServer({ noServer: true });
|
||||
server.on('upgrade', (upgradeRequest: IncomingMessage, socket: Socket, head: Buffer): void => {
|
||||
webSocketServer.handleUpgrade(upgradeRequest, socket, head, (webSocket: WebSocket): void => {
|
||||
this.handleConnection(webSocket, upgradeRequest).catch((error: Error): void => {
|
||||
this.logger.error(`Something went wrong handling a WebSocket connection: ${createErrorMessage(error)}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise<void>;
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { Server } from 'http';
|
||||
import type { Socket } from 'net';
|
||||
import type { WebSocket } from 'ws';
|
||||
import { Server as WebSocketServer } from 'ws';
|
||||
import type { HttpRequest } from './HttpRequest';
|
||||
import type { HttpServerFactory } from './HttpServerFactory';
|
||||
import type { WebSocketHandler } from './WebSocketHandler';
|
||||
|
||||
/**
|
||||
* Factory that adds WebSocket functionality to an existing server
|
||||
*/
|
||||
export class WebSocketServerFactory implements HttpServerFactory {
|
||||
private readonly baseServerFactory: HttpServerFactory;
|
||||
private readonly webSocketHandler: WebSocketHandler;
|
||||
|
||||
public constructor(baseServerFactory: HttpServerFactory, webSocketHandler: WebSocketHandler) {
|
||||
this.baseServerFactory = baseServerFactory;
|
||||
this.webSocketHandler = webSocketHandler;
|
||||
}
|
||||
|
||||
public startServer(port: number): Server {
|
||||
// Create WebSocket server
|
||||
const webSocketServer = new WebSocketServer({ noServer: true });
|
||||
webSocketServer.on('connection', async(webSocket: WebSocket, upgradeRequest: HttpRequest): Promise<void> => {
|
||||
await this.webSocketHandler.handleSafe({ webSocket, upgradeRequest });
|
||||
});
|
||||
|
||||
// Create base HTTP server
|
||||
const httpServer = this.baseServerFactory.startServer(port);
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import type { App } from '../../src/init/App';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
|
||||
|
||||
const port = getPort('WebSocketsProtocol');
|
||||
const port = getPort('LegacyWebSocketsProtocol');
|
||||
const serverUrl = `http://localhost:${port}/`;
|
||||
const headers = { forwarded: 'host=example.pod;proto=https' };
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('A server with the Solid WebSockets API behind a proxy', (): void => {
|
||||
beforeAll(async(): Promise<void> => {
|
||||
app = await instantiateFromConfig(
|
||||
'urn:solid-server:default:App',
|
||||
getTestConfigPath('server-without-auth.json'),
|
||||
getTestConfigPath('legacy-websockets.json'),
|
||||
getDefaultVariables(port, 'https://example.pod/'),
|
||||
) as App;
|
||||
|
||||
@@ -1,39 +1,36 @@
|
||||
import type { Server } from 'http';
|
||||
import request from 'supertest';
|
||||
import type { BaseHttpServerFactory } from '../../src/server/BaseHttpServerFactory';
|
||||
import type { HttpHandlerInput } from '../../src/server/HttpHandler';
|
||||
import { HttpHandler } from '../../src/server/HttpHandler';
|
||||
import type { App } from '../../src/init/App';
|
||||
import type { HttpServerFactory } from '../../src/server/HttpServerFactory';
|
||||
import { splitCommaSeparated } from '../../src/util/StringUtil';
|
||||
import { getPort } from '../util/Util';
|
||||
import { getTestConfigPath, instantiateFromConfig } from './Config';
|
||||
import { getDefaultVariables, getTestConfigPath, instantiateFromConfig } from './Config';
|
||||
|
||||
const port = getPort('Middleware');
|
||||
|
||||
class SimpleHttpHandler extends HttpHandler {
|
||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
||||
input.response.writeHead(200, { location: '/' });
|
||||
input.response.end('Hello World');
|
||||
}
|
||||
}
|
||||
|
||||
describe('An http server with middleware', (): void => {
|
||||
let app: App;
|
||||
let server: Server;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const factory = await instantiateFromConfig(
|
||||
'urn:solid-server:default:HttpServerFactory',
|
||||
getTestConfigPath('server-middleware.json'),
|
||||
{
|
||||
'urn:solid-server:default:LdpHandler': new SimpleHttpHandler(),
|
||||
'urn:solid-server:default:variable:baseUrl': 'https://example.pod/',
|
||||
'urn:solid-server:default:variable:showStackTrace': true,
|
||||
},
|
||||
) as BaseHttpServerFactory;
|
||||
server = factory.startServer(port);
|
||||
const instances = await instantiateFromConfig(
|
||||
'urn:solid-server:test:Instances',
|
||||
[
|
||||
getTestConfigPath('server-middleware.json'),
|
||||
],
|
||||
getDefaultVariables(port),
|
||||
) as { app: App; factory: HttpServerFactory };
|
||||
|
||||
({ app } = instances);
|
||||
|
||||
server = await instances.factory.createServer();
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
server.close();
|
||||
// Even though the server was started separately, there might still be finalizers that need to be stopped
|
||||
await app.stop();
|
||||
});
|
||||
|
||||
it('sets a Vary header containing Accept.', async(): Promise<void> => {
|
||||
@@ -75,7 +72,6 @@ describe('An http server with middleware', (): void => {
|
||||
expect(res.header).toEqual(expect.objectContaining({
|
||||
'access-control-allow-origin': '*',
|
||||
'access-control-allow-headers': 'content-type',
|
||||
'updates-via': 'wss://example.pod/',
|
||||
'x-powered-by': 'Community Solid Server',
|
||||
}));
|
||||
const { vary } = res.header;
|
||||
@@ -90,12 +86,12 @@ describe('An http server with middleware', (): void => {
|
||||
});
|
||||
|
||||
it('specifies CORS origin header if an origin was supplied.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').set('origin', 'test.com').expect(200);
|
||||
const res = await request(server).options('/').set('origin', 'test.com').expect(204);
|
||||
expect(res.header).toEqual(expect.objectContaining({ 'access-control-allow-origin': 'test.com' }));
|
||||
});
|
||||
|
||||
it('exposes the Accept-[Method] header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('Accept-Patch');
|
||||
expect(splitCommaSeparated(exposed)).toContain('Accept-Post');
|
||||
@@ -103,45 +99,39 @@ describe('An http server with middleware', (): void => {
|
||||
});
|
||||
|
||||
it('exposes the Last-Modified and ETag headers via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('ETag');
|
||||
expect(splitCommaSeparated(exposed)).toContain('Last-Modified');
|
||||
});
|
||||
|
||||
it('exposes the Link header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('Link');
|
||||
});
|
||||
|
||||
it('exposes the Location header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('Location');
|
||||
});
|
||||
|
||||
it('exposes the WAC-Allow header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('WAC-Allow');
|
||||
});
|
||||
|
||||
it('exposes the Updates-Via header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('Updates-Via');
|
||||
});
|
||||
|
||||
it('exposes the Www-Authenticate header via CORS.', async(): Promise<void> => {
|
||||
const res = await request(server).get('/').expect(200);
|
||||
const res = await request(server).options('/').expect(204);
|
||||
const exposed = res.header['access-control-expose-headers'];
|
||||
expect(splitCommaSeparated(exposed)).toContain('Www-Authenticate');
|
||||
});
|
||||
|
||||
it('sends incoming requests to the handler.', async(): Promise<void> => {
|
||||
const response = request(server).get('/').set('Host', 'test.com');
|
||||
expect(response).toBeDefined();
|
||||
await response.expect(200).expect('Hello World');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
"css:config/app/setup/disabled.json",
|
||||
|
||||
"css:config/http/handler/simple.json",
|
||||
"css:config/http/middleware/no-websockets.json",
|
||||
"css:config/http/server-factory/no-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
|
||||
@@ -5,19 +5,23 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/simple.json",
|
||||
"css:config/http/middleware/no-websockets.json",
|
||||
"css:config/http/server-factory/no-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/disabled.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
"css:config/ldp/metadata-parser/default.json",
|
||||
"css:config/ldp/metadata-writer/default.json",
|
||||
"css:config/ldp/modes/default.json",
|
||||
|
||||
"css:config/storage/key-value/memory.json",
|
||||
"css:config/storage/middleware/default.json",
|
||||
"css:config/util/auxiliary/acl.json",
|
||||
|
||||
37
test/integration/config/legacy-websockets.json
Normal file
37
test/integration/config/legacy-websockets.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/app/main/default.json",
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
"css:config/ldp/metadata-parser/default.json",
|
||||
"css:config/ldp/metadata-writer/default.json",
|
||||
"css:config/ldp/modes/default.json",
|
||||
"css:config/storage/backend/memory.json",
|
||||
"css:config/storage/key-value/resource-store.json",
|
||||
"css:config/storage/middleware/default.json",
|
||||
"css:config/util/auxiliary/acl.json",
|
||||
"css:config/util/identifiers/suffix.json",
|
||||
"css:config/util/index/default.json",
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/memory.json",
|
||||
"css:config/util/variables/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
]
|
||||
}
|
||||
@@ -6,8 +6,8 @@
|
||||
"css:config/app/setup/disabled.json",
|
||||
|
||||
"css:config/http/handler/simple.json",
|
||||
"css:config/http/middleware/no-websockets.json",
|
||||
"css:config/http/server-factory/no-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"css:config/app/init/default.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/restricted.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/no-websockets.json",
|
||||
"css:config/http/server-factory/no-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/disabled.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
|
||||
@@ -1,16 +1,51 @@
|
||||
{
|
||||
"@context": "https://linkedsoftwaredependencies.org/bundles/npm/@solid/community-server/^5.0.0/components/context.jsonld",
|
||||
"import": [
|
||||
"css:config/app/main/default.json",
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/simple.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
"css:config/identity/handler/default.json",
|
||||
"css:config/identity/ownership/token.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/enabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/webacl.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
"css:config/ldp/metadata-parser/default.json",
|
||||
"css:config/ldp/metadata-writer/default.json",
|
||||
"css:config/ldp/modes/default.json",
|
||||
"css:config/storage/backend/memory.json",
|
||||
"css:config/storage/key-value/resource-store.json",
|
||||
"css:config/storage/middleware/default.json",
|
||||
"css:config/util/auxiliary/acl.json",
|
||||
"css:config/util/identifiers/suffix.json",
|
||||
"css:config/util/index/default.json",
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/memory.json",
|
||||
"css:config/util/variables/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
"@id": "urn:solid-server:default:LdpHandler",
|
||||
"@type": "Variable"
|
||||
"@id": "urn:solid-server:test:Instances",
|
||||
"@type": "RecordObject",
|
||||
"record": [
|
||||
{
|
||||
"RecordObject:_record_key": "app",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:App" }
|
||||
},
|
||||
{
|
||||
"RecordObject:_record_key": "factory",
|
||||
"RecordObject:_record_value": { "@id": "urn:solid-server:default:ServerFactory" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/simple.json",
|
||||
"css:config/http/middleware/no-websockets.json",
|
||||
"css:config/http/server-factory/no-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/disabled.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/handler/account-store/default.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json",
|
||||
|
||||
|
||||
"css:config/identity/pod/static.json",
|
||||
|
||||
|
||||
|
||||
"css:config/ldp/authentication/debug-auth-header.json",
|
||||
"css:config/ldp/authorization/allow-all.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
@@ -26,7 +30,10 @@
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/redis.json",
|
||||
"css:config/util/variables/default.json"
|
||||
"css:config/util/variables/default.json",
|
||||
|
||||
"css:config/identity/handler/account-store/default.json",
|
||||
"css:config/identity/ownership/unsafe-no-check.json"
|
||||
],
|
||||
"@graph": [
|
||||
{
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/no-websockets.json",
|
||||
"css:config/http/server-factory/no-websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/disabled.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
@@ -20,6 +21,7 @@
|
||||
"css:config/ldp/metadata-parser/default.json",
|
||||
"css:config/ldp/metadata-writer/default.json",
|
||||
"css:config/ldp/modes/default.json",
|
||||
|
||||
"css:config/storage/key-value/memory.json",
|
||||
"css:config/storage/middleware/default.json",
|
||||
"css:config/util/auxiliary/acl.json",
|
||||
|
||||
@@ -5,12 +5,16 @@
|
||||
"css:config/app/init/initialize-root.json",
|
||||
"css:config/app/setup/disabled.json",
|
||||
"css:config/http/handler/simple.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/handler/account-store/default.json",
|
||||
|
||||
|
||||
|
||||
"css:config/identity/ownership/unsafe-no-check.json",
|
||||
"css:config/identity/pod/static.json",
|
||||
"css:config/identity/registration/disabled.json",
|
||||
"css:config/ldp/authentication/dpop-bearer.json",
|
||||
"css:config/ldp/authorization/allow-all.json",
|
||||
"css:config/ldp/handler/default.json",
|
||||
@@ -26,7 +30,9 @@
|
||||
"css:config/util/logging/winston.json",
|
||||
"css:config/util/representation-conversion/default.json",
|
||||
"css:config/util/resource-locker/memory.json",
|
||||
"css:config/util/variables/default.json"
|
||||
"css:config/util/variables/default.json",
|
||||
|
||||
"css:config/identity/handler/account-store/default.json"
|
||||
],
|
||||
"@graph": [
|
||||
]
|
||||
|
||||
@@ -5,8 +5,9 @@
|
||||
"css:config/app/init/default.json",
|
||||
"css:config/app/setup/required.json",
|
||||
"css:config/http/handler/default.json",
|
||||
"css:config/http/middleware/websockets.json",
|
||||
"css:config/http/server-factory/websockets.json",
|
||||
"css:config/http/middleware/default.json",
|
||||
"css:config/http/notifications/legacy-websockets.json",
|
||||
"css:config/http/server-factory/http.json",
|
||||
"css:config/http/static/default.json",
|
||||
"css:config/identity/access/public.json",
|
||||
"css:config/identity/email/default.json",
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Server } from 'http';
|
||||
import { UnsecureWebSocketsProtocol } from '../../../src/http/UnsecureWebSocketsProtocol';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import { BaseActivityEmitter } from '../../../src/server/notifications/ActivityEmitter';
|
||||
import { AS } from '../../../src/util/Vocabularies';
|
||||
|
||||
jest.mock('ws', (): any => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
WebSocketServer: jest.fn().mockImplementation((): any => ({
|
||||
handleUpgrade(upgradeRequest: any, socket: any, head: any, callback: any): void {
|
||||
callback(socket, upgradeRequest);
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
class DummySocket extends EventEmitter {
|
||||
public readonly messages = new Array<string>();
|
||||
@@ -12,13 +24,18 @@ class DummySocket extends EventEmitter {
|
||||
}
|
||||
|
||||
describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
const source = new EventEmitter();
|
||||
const protocol = new UnsecureWebSocketsProtocol(source);
|
||||
let server: Server;
|
||||
let webSocket: DummySocket;
|
||||
const source = new BaseActivityEmitter();
|
||||
let protocol: UnsecureWebSocketsProtocol;
|
||||
|
||||
describe('after registering a socket', (): void => {
|
||||
const webSocket = new DummySocket();
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
server = new EventEmitter() as any;
|
||||
webSocket = new DummySocket();
|
||||
protocol = new UnsecureWebSocketsProtocol(source);
|
||||
await protocol.handle(server);
|
||||
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
host: 'mypod.example',
|
||||
@@ -28,11 +45,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
encrypted: true,
|
||||
},
|
||||
} as any as HttpRequest;
|
||||
await protocol.handle({ webSocket, upgradeRequest } as any);
|
||||
});
|
||||
|
||||
afterEach((): void => {
|
||||
webSocket.messages.length = 0;
|
||||
server.emit('upgrade', upgradeRequest, webSocket);
|
||||
});
|
||||
|
||||
it('sends a protocol message.', (): void => {
|
||||
@@ -54,7 +67,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
|
||||
describe('before subscribing to resources', (): void => {
|
||||
it('does not emit pub messages.', (): void => {
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' });
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update);
|
||||
expect(webSocket.messages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -70,7 +83,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
});
|
||||
|
||||
it('emits pub messages for that resource.', (): void => {
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' });
|
||||
source.emit('changed', { path: 'https://mypod.example/foo/bar' }, AS.terms.Update);
|
||||
expect(webSocket.messages).toHaveLength(1);
|
||||
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/foo/bar');
|
||||
});
|
||||
@@ -87,7 +100,7 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
});
|
||||
|
||||
it('emits pub messages for that resource.', (): void => {
|
||||
source.emit('changed', { path: 'https://mypod.example/relative/foo' });
|
||||
source.emit('changed', { path: 'https://mypod.example/relative/foo' }, AS.terms.Update);
|
||||
expect(webSocket.messages).toHaveLength(1);
|
||||
expect(webSocket.messages.shift()).toBe('pub https://mypod.example/relative/foo');
|
||||
});
|
||||
@@ -118,84 +131,83 @@ describe('An UnsecureWebSocketsProtocol', (): void => {
|
||||
});
|
||||
});
|
||||
|
||||
it('unsubscribes when a socket closes.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
await protocol.handle({ webSocket, upgradeRequest: { headers: {}, socket: {}}} as any);
|
||||
expect(webSocket.listenerCount('message')).toBe(1);
|
||||
webSocket.emit('close');
|
||||
expect(webSocket.listenerCount('message')).toBe(0);
|
||||
expect(webSocket.listenerCount('close')).toBe(0);
|
||||
expect(webSocket.listenerCount('error')).toBe(0);
|
||||
});
|
||||
describe('handling other situations', (): void => {
|
||||
beforeEach(async(): Promise<void> => {
|
||||
server = new EventEmitter() as any;
|
||||
webSocket = new DummySocket();
|
||||
protocol = new UnsecureWebSocketsProtocol(source);
|
||||
await protocol.handle(server);
|
||||
});
|
||||
|
||||
it('unsubscribes when a socket errors.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
await protocol.handle({ webSocket, upgradeRequest: { headers: {}, socket: {}}} as any);
|
||||
expect(webSocket.listenerCount('message')).toBe(1);
|
||||
webSocket.emit('error');
|
||||
expect(webSocket.listenerCount('message')).toBe(0);
|
||||
expect(webSocket.listenerCount('close')).toBe(0);
|
||||
expect(webSocket.listenerCount('error')).toBe(0);
|
||||
});
|
||||
it('unsubscribes when a socket closes.', async(): Promise<void> => {
|
||||
server.emit('upgrade', { headers: {}, socket: {}} as any, webSocket);
|
||||
expect(webSocket.listenerCount('message')).toBe(1);
|
||||
webSocket.emit('close');
|
||||
expect(webSocket.listenerCount('message')).toBe(0);
|
||||
expect(webSocket.listenerCount('close')).toBe(0);
|
||||
expect(webSocket.listenerCount('error')).toBe(0);
|
||||
});
|
||||
|
||||
it('emits a warning when no Sec-WebSocket-Protocol is supplied.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
const upgradeRequest = {
|
||||
headers: {},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
await protocol.handle({ webSocket, upgradeRequest } as any);
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop())
|
||||
.toBe('warning Missing Sec-WebSocket-Protocol header, expected value \'solid-0.1\'');
|
||||
expect(webSocket.close).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
it('unsubscribes when a socket errors.', async(): Promise<void> => {
|
||||
server.emit('upgrade', { headers: {}, socket: {}} as any, webSocket);
|
||||
expect(webSocket.listenerCount('message')).toBe(1);
|
||||
webSocket.emit('error');
|
||||
expect(webSocket.listenerCount('message')).toBe(0);
|
||||
expect(webSocket.listenerCount('close')).toBe(0);
|
||||
expect(webSocket.listenerCount('error')).toBe(0);
|
||||
});
|
||||
|
||||
it('emits an error and closes the connection with the wrong Sec-WebSocket-Protocol.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
'sec-websocket-protocol': 'solid/1.0.0, other',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
await protocol.handle({ webSocket, upgradeRequest } as any);
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('error Client does not support protocol solid-0.1');
|
||||
expect(webSocket.close).toHaveBeenCalledTimes(1);
|
||||
expect(webSocket.listenerCount('message')).toBe(0);
|
||||
expect(webSocket.listenerCount('close')).toBe(0);
|
||||
expect(webSocket.listenerCount('error')).toBe(0);
|
||||
});
|
||||
it('emits a warning when no Sec-WebSocket-Protocol is supplied.', async(): Promise<void> => {
|
||||
server.emit('upgrade', { headers: {}, socket: {}} as any, webSocket);
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop())
|
||||
.toBe('warning Missing Sec-WebSocket-Protocol header, expected value \'solid-0.1\'');
|
||||
expect(webSocket.close).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('respects the Forwarded header.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
forwarded: 'proto=https;host=other.example',
|
||||
'sec-websocket-protocol': 'solid-0.1',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
await protocol.handle({ webSocket, upgradeRequest } as any);
|
||||
webSocket.emit('message', 'sub https://other.example/protocol/foo');
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
|
||||
});
|
||||
it('emits an error and closes the connection with the wrong Sec-WebSocket-Protocol.', async(): Promise<void> => {
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
'sec-websocket-protocol': 'solid/1.0.0, other',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
server.emit('upgrade', upgradeRequest, webSocket);
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('error Client does not support protocol solid-0.1');
|
||||
expect(webSocket.close).toHaveBeenCalledTimes(1);
|
||||
expect(webSocket.listenerCount('message')).toBe(0);
|
||||
expect(webSocket.listenerCount('close')).toBe(0);
|
||||
expect(webSocket.listenerCount('error')).toBe(0);
|
||||
});
|
||||
|
||||
it('respects the X-Forwarded-* headers if Forwarded header is not present.', async(): Promise<void> => {
|
||||
const webSocket = new DummySocket();
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
'x-forwarded-host': 'other.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
'sec-websocket-protocol': 'solid-0.1',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
await protocol.handle({ webSocket, upgradeRequest } as any);
|
||||
webSocket.emit('message', 'sub https://other.example/protocol/foo');
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
|
||||
it('respects the Forwarded header.', async(): Promise<void> => {
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
forwarded: 'proto=https;host=other.example',
|
||||
'sec-websocket-protocol': 'solid-0.1',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
server.emit('upgrade', upgradeRequest, webSocket);
|
||||
webSocket.emit('message', 'sub https://other.example/protocol/foo');
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
|
||||
});
|
||||
|
||||
it('respects the X-Forwarded-* headers if Forwarded header is not present.', async(): Promise<void> => {
|
||||
const upgradeRequest = {
|
||||
headers: {
|
||||
'x-forwarded-host': 'other.example',
|
||||
'x-forwarded-proto': 'https',
|
||||
'sec-websocket-protocol': 'solid-0.1',
|
||||
},
|
||||
socket: {},
|
||||
} as any as HttpRequest;
|
||||
server.emit('upgrade', upgradeRequest, webSocket);
|
||||
webSocket.emit('message', 'sub https://other.example/protocol/foo');
|
||||
expect(webSocket.messages).toHaveLength(2);
|
||||
expect(webSocket.messages.pop()).toBe('ack https://other.example/protocol/foo');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,52 @@
|
||||
import type { Server } from 'http';
|
||||
import { Server as HttpsServer } from 'https';
|
||||
import { ServerInitializer } from '../../../src/init/ServerInitializer';
|
||||
import type { Logger } from '../../../src/logging/Logger';
|
||||
import { getLoggerFor } from '../../../src/logging/LogUtil';
|
||||
import type { HttpServerFactory } from '../../../src/server/HttpServerFactory';
|
||||
|
||||
// Mock so we don't create an actual HTTPS server in the test below
|
||||
jest.mock('https');
|
||||
jest.mock('../../../src/logging/LogUtil');
|
||||
|
||||
describe('ServerInitializer', (): void => {
|
||||
let logger: jest.Mocked<Logger>;
|
||||
let server: Server;
|
||||
let serverFactory: jest.Mocked<HttpServerFactory>;
|
||||
|
||||
let initializer: ServerInitializer;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
logger = { info: jest.fn() } as any;
|
||||
(getLoggerFor as jest.MockedFn<() => Logger>).mockReturnValue(logger);
|
||||
|
||||
server = {
|
||||
listen: jest.fn(),
|
||||
close: jest.fn((fn: () => void): void => fn()),
|
||||
} as any;
|
||||
serverFactory = {
|
||||
startServer: jest.fn().mockReturnValue(server),
|
||||
createServer: jest.fn().mockReturnValue(server),
|
||||
};
|
||||
initializer = new ServerInitializer(serverFactory, 3000);
|
||||
});
|
||||
|
||||
it('starts an HTTP server.', async(): Promise<void> => {
|
||||
await initializer.handle();
|
||||
expect(serverFactory.startServer).toHaveBeenCalledWith(3000);
|
||||
expect(serverFactory.createServer).toHaveBeenCalledTimes(1);
|
||||
expect(server.listen).toHaveBeenCalledTimes(1);
|
||||
expect(server.listen).toHaveBeenLastCalledWith(3000);
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenLastCalledWith(`Listening to server at http://localhost:3000/`);
|
||||
});
|
||||
|
||||
it('correctly logs the protocol in case of an HTTPS server.', async(): Promise<void> => {
|
||||
server = new HttpsServer();
|
||||
serverFactory.createServer.mockResolvedValue(server);
|
||||
await initializer.handle();
|
||||
expect(serverFactory.createServer).toHaveBeenCalledTimes(1);
|
||||
expect(server.listen).toHaveBeenCalledTimes(1);
|
||||
expect(server.listen).toHaveBeenLastCalledWith(3000);
|
||||
expect(logger.info).toHaveBeenCalledTimes(1);
|
||||
expect(logger.info).toHaveBeenLastCalledWith(`Listening to server at https://localhost:3000/`);
|
||||
});
|
||||
|
||||
it('can stop the server.', async(): Promise<void> => {
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import type { Server } from 'http';
|
||||
import request from 'supertest';
|
||||
import type { BaseHttpServerOptions } from '../../../src/server/BaseHttpServerFactory';
|
||||
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';
|
||||
|
||||
const port = getPort('BaseHttpServerFactory');
|
||||
|
||||
const handler: jest.Mocked<HttpHandler> = {
|
||||
handleSafe: jest.fn(async(input: { response: HttpResponse }): Promise<void> => {
|
||||
input.response.writeHead(200);
|
||||
input.response.end();
|
||||
}),
|
||||
} as any;
|
||||
|
||||
describe('A BaseHttpServerFactory', (): void => {
|
||||
let server: Server;
|
||||
|
||||
const options: [string, BaseHttpServerOptions | undefined][] = [
|
||||
[ 'http', undefined ],
|
||||
[ 'https', {
|
||||
https: true,
|
||||
key: joinFilePath(__dirname, '../../assets/https/server.key'),
|
||||
cert: joinFilePath(__dirname, '../../assets/https/server.cert'),
|
||||
}],
|
||||
];
|
||||
|
||||
describe.each(options)('with %s', (protocol, httpOptions): void => {
|
||||
let rejectTls: string | undefined;
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// Allow self-signed certificate
|
||||
rejectTls = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||
server = factory.startServer(port);
|
||||
});
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectTls;
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('sends incoming requests to the handler.', async(): Promise<void> => {
|
||||
await request(server).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('returns a 404 when the handler does not do anything.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockResolvedValueOnce(undefined);
|
||||
|
||||
await expect(request(server).get('/').expect(404)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('writes an error to the HTTP response without the stack trace.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockRejectedValueOnce(new Error('dummyError'));
|
||||
|
||||
const res = await request(server).get('/').expect(500);
|
||||
expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
|
||||
expect(res.text).toBe('Error: dummyError\n');
|
||||
});
|
||||
|
||||
it('does not write an error if the response had been started.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockImplementationOnce(async(input: { response: HttpResponse }): Promise<void> => {
|
||||
input.response.write('content');
|
||||
throw new Error('dummyError');
|
||||
});
|
||||
|
||||
const res = await request(server).get('/');
|
||||
expect(res.text).not.toContain('dummyError');
|
||||
});
|
||||
|
||||
it('throws unknown errors if its handler throw non-Error objects.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockRejectedValueOnce('apple');
|
||||
|
||||
const res = await request(server).get('/').expect(500);
|
||||
expect(res.text).toContain('Unknown error: apple.');
|
||||
});
|
||||
|
||||
it('can handle errors on the HttpResponse.', async(): Promise<void> => {
|
||||
// This just makes sure the logging line is covered.
|
||||
// Actually destroying the request to trigger an error causes issues for supertest
|
||||
handler.handleSafe.mockImplementationOnce(async(input): Promise<void> => {
|
||||
input.request.emit('error', new Error('bad request'));
|
||||
});
|
||||
await request(server).get('/').expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with showStackTrace enabled', (): void => {
|
||||
const httpOptions = {
|
||||
http: true,
|
||||
showStackTrace: true,
|
||||
};
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const factory = new BaseHttpServerFactory(handler, httpOptions);
|
||||
server = factory.startServer(port);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('does not print the stack if that option is disabled.', async(): Promise<void> => {
|
||||
const error = new Error('dummyError');
|
||||
handler.handleSafe.mockRejectedValueOnce(error);
|
||||
|
||||
const res = await request(server).get('/').expect(500);
|
||||
expect(res.headers['content-type']).toBe('text/plain; charset=utf-8');
|
||||
expect(res.text).toBe(`${error.stack}\n`);
|
||||
});
|
||||
});
|
||||
});
|
||||
75
test/unit/server/BaseServerFactory.test.ts
Normal file
75
test/unit/server/BaseServerFactory.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { RequestListener, Server } from 'http';
|
||||
import request from 'supertest';
|
||||
import type { BaseServerFactoryOptions } from '../../../src/server/BaseServerFactory';
|
||||
import { BaseServerFactory } from '../../../src/server/BaseServerFactory';
|
||||
import type { ServerConfigurator } from '../../../src/server/ServerConfigurator';
|
||||
import { joinFilePath } from '../../../src/util/PathUtil';
|
||||
import { getPort } from '../../util/Util';
|
||||
|
||||
const port = getPort('BaseServerFactory');
|
||||
|
||||
describe('A BaseServerFactory', (): void => {
|
||||
let server: Server;
|
||||
|
||||
const options: [string, BaseServerFactoryOptions | undefined][] = [
|
||||
[ 'http', undefined ],
|
||||
[ 'https', {
|
||||
https: true,
|
||||
key: joinFilePath(__dirname, '../../assets/https/server.key'),
|
||||
cert: joinFilePath(__dirname, '../../assets/https/server.cert'),
|
||||
}],
|
||||
];
|
||||
|
||||
describe.each(options)('with %s', (protocol, httpOptions): void => {
|
||||
let rejectTls: string | undefined;
|
||||
let configurator: ServerConfigurator;
|
||||
let mockRequestHandler: jest.MockedFn<RequestListener>;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
// Allow self-signed certificate
|
||||
rejectTls = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
mockRequestHandler = jest.fn();
|
||||
|
||||
configurator = {
|
||||
async handleSafe(serv: Server): Promise<void> {
|
||||
serv.on('request', mockRequestHandler);
|
||||
},
|
||||
} as any;
|
||||
|
||||
const factory = new BaseServerFactory(configurator, httpOptions);
|
||||
server = await factory.createServer();
|
||||
|
||||
server.listen(port);
|
||||
});
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectTls;
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('emits a request event on requests.', async(): Promise<void> => {
|
||||
let resolveProm: (value: unknown) => void;
|
||||
const requestProm = new Promise((resolve): void => {
|
||||
resolveProm = resolve;
|
||||
});
|
||||
server.on('request', (req, res): void => {
|
||||
resolveProm(req);
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
});
|
||||
await request(server).get('/').set('Host', 'test.com').expect(200);
|
||||
|
||||
await expect(requestProm).resolves.toEqual(expect.objectContaining({
|
||||
headers: expect.objectContaining({ host: 'test.com' }),
|
||||
}));
|
||||
|
||||
expect(mockRequestHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
143
test/unit/server/HandlerServerConfigurator.test.ts
Normal file
143
test/unit/server/HandlerServerConfigurator.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { ServerResponse, IncomingMessage, Server } from 'http';
|
||||
import { Readable } from 'stream';
|
||||
import type { Logger } from '../../../src/logging/Logger';
|
||||
import { getLoggerFor } from '../../../src/logging/LogUtil';
|
||||
import { HandlerServerConfigurator } from '../../../src/server/HandlerServerConfigurator';
|
||||
import type { HttpHandler } from '../../../src/server/HttpHandler';
|
||||
import { flushPromises } from '../../util/Util';
|
||||
|
||||
jest.mock('../../../src/logging/LogUtil', (): any => {
|
||||
const logger: Logger =
|
||||
{ error: jest.fn(), info: jest.fn() } as any;
|
||||
return { getLoggerFor: (): Logger => logger };
|
||||
});
|
||||
|
||||
describe('A HandlerServerConfigurator', (): void => {
|
||||
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
||||
let request: jest.Mocked<IncomingMessage>;
|
||||
let response: jest.Mocked<ServerResponse>;
|
||||
let server: Server;
|
||||
let handler: jest.Mocked<HttpHandler>;
|
||||
let listener: HandlerServerConfigurator;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
// Clearing the logger mock
|
||||
jest.clearAllMocks();
|
||||
request = Readable.from('') as any;
|
||||
request.method = 'GET';
|
||||
request.url = '/';
|
||||
|
||||
response = {
|
||||
headersSent: false,
|
||||
end: jest.fn(),
|
||||
setHeader: jest.fn(),
|
||||
writeHead: jest.fn(),
|
||||
} as any;
|
||||
response.end.mockImplementation((): any => {
|
||||
response.headersSent = true;
|
||||
});
|
||||
response.writeHead.mockReturnValue(response);
|
||||
|
||||
server = new EventEmitter() as any;
|
||||
|
||||
handler = {
|
||||
handleSafe: jest.fn((): void => {
|
||||
response.headersSent = true;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
listener = new HandlerServerConfigurator(handler);
|
||||
await listener.handle(server);
|
||||
});
|
||||
|
||||
it('sends incoming requests to the handler.', async(): Promise<void> => {
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(handler.handleSafe).toHaveBeenCalledTimes(1);
|
||||
expect(handler.handleSafe).toHaveBeenLastCalledWith({ request, response });
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledTimes(0);
|
||||
expect(response.writeHead).toHaveBeenCalledTimes(0);
|
||||
expect(response.end).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('returns a 404 when the handler does not do anything.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockImplementation(jest.fn());
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledTimes(0);
|
||||
expect(response.writeHead).toHaveBeenCalledTimes(1);
|
||||
expect(response.writeHead).toHaveBeenLastCalledWith(404);
|
||||
expect(response.end).toHaveBeenCalledTimes(1);
|
||||
expect(response.end).toHaveBeenLastCalledWith();
|
||||
});
|
||||
|
||||
it('writes an error to the HTTP response without the stack trace.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockRejectedValueOnce(new Error('dummyError'));
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledTimes(1);
|
||||
expect(response.setHeader).toHaveBeenLastCalledWith('Content-Type', 'text/plain; charset=utf-8');
|
||||
expect(response.writeHead).toHaveBeenCalledTimes(1);
|
||||
expect(response.writeHead).toHaveBeenLastCalledWith(500);
|
||||
expect(response.end).toHaveBeenCalledTimes(1);
|
||||
expect(response.end).toHaveBeenLastCalledWith('Error: dummyError\n');
|
||||
});
|
||||
|
||||
it('does not write an error if the response had been started.', async(): Promise<void> => {
|
||||
response.headersSent = true;
|
||||
handler.handleSafe.mockRejectedValueOnce(new Error('dummyError'));
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledTimes(0);
|
||||
expect(response.writeHead).toHaveBeenCalledTimes(0);
|
||||
expect(response.end).toHaveBeenCalledTimes(1);
|
||||
expect(response.end).toHaveBeenLastCalledWith();
|
||||
});
|
||||
|
||||
it('throws unknown errors if its handler throw non-Error objects.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockRejectedValueOnce('apple');
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledTimes(1);
|
||||
expect(response.setHeader).toHaveBeenLastCalledWith('Content-Type', 'text/plain; charset=utf-8');
|
||||
expect(response.writeHead).toHaveBeenCalledTimes(1);
|
||||
expect(response.writeHead).toHaveBeenLastCalledWith(500);
|
||||
expect(response.end).toHaveBeenCalledTimes(1);
|
||||
expect(response.end).toHaveBeenLastCalledWith('Unknown error: apple.\n');
|
||||
});
|
||||
|
||||
it('can handle errors on the HttpResponse.', async(): Promise<void> => {
|
||||
handler.handleSafe.mockImplementationOnce(async(input): Promise<void> => {
|
||||
input.request.emit('error', new Error('bad request'));
|
||||
});
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenLastCalledWith('Request error: bad request');
|
||||
});
|
||||
|
||||
it('prints the stack trace if that option is enabled.', async(): Promise<void> => {
|
||||
server.removeAllListeners();
|
||||
listener = new HandlerServerConfigurator(handler, true);
|
||||
await listener.handle(server);
|
||||
const error = new Error('dummyError');
|
||||
handler.handleSafe.mockRejectedValueOnce(error);
|
||||
server.emit('request', request, response);
|
||||
await flushPromises();
|
||||
|
||||
expect(response.setHeader).toHaveBeenCalledTimes(1);
|
||||
expect(response.setHeader).toHaveBeenLastCalledWith('Content-Type', 'text/plain; charset=utf-8');
|
||||
expect(response.writeHead).toHaveBeenCalledTimes(1);
|
||||
expect(response.writeHead).toHaveBeenLastCalledWith(500);
|
||||
expect(response.end).toHaveBeenCalledTimes(1);
|
||||
expect(response.end).toHaveBeenLastCalledWith(`${error.stack}\n`);
|
||||
});
|
||||
});
|
||||
80
test/unit/server/WebSocketServerConfigurator.test.ts
Normal file
80
test/unit/server/WebSocketServerConfigurator.test.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import type { Server } from 'http';
|
||||
import type { WebSocket } from 'ws';
|
||||
import type { Logger } from '../../../src/logging/Logger';
|
||||
import { getLoggerFor } from '../../../src/logging/LogUtil';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import { WebSocketServerConfigurator } from '../../../src/server/WebSocketServerConfigurator';
|
||||
import { flushPromises } from '../../util/Util';
|
||||
|
||||
jest.mock('ws', (): any => ({
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
WebSocketServer: jest.fn().mockImplementation((): any => ({
|
||||
handleUpgrade(upgradeRequest: any, socket: any, head: any, callback: any): void {
|
||||
callback(socket, upgradeRequest);
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../../src/logging/LogUtil', (): any => {
|
||||
const logger: Logger =
|
||||
{ error: jest.fn(), info: jest.fn() } as any;
|
||||
return { getLoggerFor: (): Logger => logger };
|
||||
});
|
||||
|
||||
class SimpleWebSocketConfigurator extends WebSocketServerConfigurator {
|
||||
public async handleConnection(): Promise<void> {
|
||||
// Will be overwritten
|
||||
}
|
||||
}
|
||||
|
||||
describe('A WebSocketServerConfigurator', (): void => {
|
||||
const logger: jest.Mocked<Logger> = getLoggerFor('mock') as any;
|
||||
let server: Server;
|
||||
let webSocket: WebSocket;
|
||||
let upgradeRequest: HttpRequest;
|
||||
let listener: jest.Mocked<SimpleWebSocketConfigurator>;
|
||||
|
||||
beforeEach(async(): Promise<void> => {
|
||||
// Clearing the logger mock
|
||||
jest.clearAllMocks();
|
||||
server = new EventEmitter() as any;
|
||||
webSocket = new EventEmitter() as any;
|
||||
webSocket.send = jest.fn();
|
||||
webSocket.close = jest.fn();
|
||||
|
||||
upgradeRequest = { url: `/foo` } as any;
|
||||
|
||||
listener = new SimpleWebSocketConfigurator() as any;
|
||||
listener.handleConnection = jest.fn().mockResolvedValue('');
|
||||
await listener.handle(server);
|
||||
});
|
||||
|
||||
it('attaches an upgrade listener to any server it gets.', async(): Promise<void> => {
|
||||
server = new EventEmitter() as any;
|
||||
expect(server.listenerCount('upgrade')).toBe(0);
|
||||
await listener.handle(server);
|
||||
expect(server.listenerCount('upgrade')).toBe(1);
|
||||
});
|
||||
|
||||
it('calls the handleConnection function when there is a new WebSocket.', async(): Promise<void> => {
|
||||
server.emit('upgrade', upgradeRequest, webSocket);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(listener.handleConnection).toHaveBeenCalledTimes(1);
|
||||
expect(listener.handleConnection).toHaveBeenLastCalledWith(webSocket, upgradeRequest);
|
||||
expect(logger.error).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('logs an error if something went wrong handling the connection.', async(): Promise<void> => {
|
||||
listener.handleConnection.mockRejectedValue(new Error('bad input'));
|
||||
server.emit('upgrade', upgradeRequest, webSocket);
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(listener.handleConnection).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenCalledTimes(1);
|
||||
expect(logger.error).toHaveBeenLastCalledWith('Something went wrong handling a WebSocket connection: bad input');
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { Server } from 'http';
|
||||
import request from 'supertest';
|
||||
import { WebSocket } from 'ws';
|
||||
import { BaseHttpServerFactory } from '../../../src/server/BaseHttpServerFactory';
|
||||
import type { HttpHandlerInput } from '../../../src/server/HttpHandler';
|
||||
import { HttpHandler } from '../../../src/server/HttpHandler';
|
||||
import type { HttpRequest } from '../../../src/server/HttpRequest';
|
||||
import { WebSocketHandler } from '../../../src/server/WebSocketHandler';
|
||||
import { WebSocketServerFactory } from '../../../src/server/WebSocketServerFactory';
|
||||
|
||||
class SimpleHttpHandler extends HttpHandler {
|
||||
public async handle(input: HttpHandlerInput): Promise<void> {
|
||||
input.response.end('SimpleHttpHandler');
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleWebSocketHandler extends WebSocketHandler {
|
||||
public host: any;
|
||||
|
||||
public async handle(input: { webSocket: WebSocket; upgradeRequest: HttpRequest }): Promise<void> {
|
||||
input.webSocket.send('SimpleWebSocketHandler');
|
||||
input.webSocket.close();
|
||||
this.host = input.upgradeRequest.headers.host;
|
||||
}
|
||||
}
|
||||
|
||||
describe('SimpleWebSocketHandler', (): void => {
|
||||
let webSocketHandler: SimpleWebSocketHandler;
|
||||
let server: Server;
|
||||
|
||||
beforeAll(async(): Promise<void> => {
|
||||
const httpHandler = new SimpleHttpHandler();
|
||||
webSocketHandler = new SimpleWebSocketHandler();
|
||||
const httpServerFactory = new BaseHttpServerFactory(httpHandler);
|
||||
const webSocketServerFactory = new WebSocketServerFactory(httpServerFactory, webSocketHandler);
|
||||
server = webSocketServerFactory.startServer(5556);
|
||||
});
|
||||
|
||||
afterAll(async(): Promise<void> => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
it('has a functioning HTTP interface.', async(): Promise<void> => {
|
||||
const result = await request(server).get('/').expect('SimpleHttpHandler');
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('has a functioning WebSockets interface.', async(): Promise<void> => {
|
||||
const client = new WebSocket('ws://localhost:5556');
|
||||
const buffer = await new Promise<Buffer>((resolve): any => client.on('message', resolve));
|
||||
expect(buffer.toString()).toBe('SimpleWebSocketHandler');
|
||||
expect(webSocketHandler.host).toBe('localhost:5556');
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ const portNames = [
|
||||
'FileBackend',
|
||||
'GlobalQuota',
|
||||
'Identity',
|
||||
'LegacyWebSocketsProtocol',
|
||||
'LpdHandlerWithAuth',
|
||||
'LpdHandlerWithoutAuth',
|
||||
'Middleware',
|
||||
@@ -28,10 +29,9 @@ const portNames = [
|
||||
'SetupMemory',
|
||||
'SparqlStorage',
|
||||
'Subdomains',
|
||||
'WebSocketsProtocol',
|
||||
|
||||
// Unit
|
||||
'BaseHttpServerFactory',
|
||||
'BaseServerFactory',
|
||||
] as const;
|
||||
|
||||
export function getPort(name: typeof portNames[number]): number {
|
||||
|
||||
Reference in New Issue
Block a user