From 4223dcf8a48300cabc01bfa744ddf9e205808693 Mon Sep 17 00:00:00 2001 From: Joachim Van Herwegen Date: Thu, 29 Sep 2022 16:34:38 +0200 Subject: [PATCH] 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. --- .componentsignore | 1 + config/default.json | 5 +- config/dynamic.json | 5 +- config/example-https-file.json | 28 ++- config/file-acp.json | 4 +- config/file-no-setup.json | 5 +- config/file.json | 5 +- .../{no-websockets.json => default.json} | 0 .../http/middleware/handlers/updates-via.json | 11 -- config/http/middleware/websockets.json | 30 --- config/http/notifications/disabled.json | 8 + .../http/notifications/legacy-websockets.json | 27 +++ .../server-factory/configurator/default.json | 18 ++ config/http/server-factory/http.json | 14 ++ config/http/server-factory/https-example.json | 23 --- .../server-factory/https-no-cli-example.json | 17 ++ .../http/server-factory/https-websockets.json | 33 ---- .../{https-no-websockets.json => https.json} | 12 +- config/http/server-factory/no-websockets.json | 12 -- config/http/server-factory/websockets.json | 20 -- config/https-file-cli.json | 5 +- config/memory-subdomains.json | 5 +- config/path-routing.json | 5 +- config/quota-file.json | 5 +- config/restrict-idp.json | 5 +- config/sparql-endpoint-no-setup.json | 5 +- config/sparql-endpoint.json | 5 +- config/sparql-file-storage.json | 5 +- src/http/UnsecureWebSocketsProtocol.ts | 30 +-- src/index.ts | 7 +- src/init/ServerInitializer.ts | 11 +- src/server/BaseHttpServerFactory.ts | 108 ---------- src/server/BaseServerFactory.ts | 69 +++++++ src/server/HandlerServerConfigurator.ts | 68 +++++++ src/server/HttpServerFactory.ts | 12 +- src/server/ServerConfigurator.ts | 7 + src/server/WebSocketHandler.ts | 9 - src/server/WebSocketServerConfigurator.ts | 30 +++ src/server/WebSocketServerFactory.ts | 37 ---- ...st.ts => LegacyWebSocketsProtocol.test.ts} | 4 +- test/integration/Middleware.test.ts | 62 +++--- test/integration/config/ldp-with-acp.json | 4 +- test/integration/config/ldp-with-auth.json | 8 +- .../integration/config/legacy-websockets.json | 37 ++++ test/integration/config/permission-table.json | 4 +- test/integration/config/quota-global.json | 5 +- test/integration/config/quota-pod.json | 5 +- test/integration/config/restricted-idp.json | 5 +- .../config/server-dynamic-unsafe.json | 5 +- test/integration/config/server-file.json | 6 +- test/integration/config/server-memory.json | 6 +- .../integration/config/server-middleware.json | 43 +++- .../integration/config/server-redis-lock.json | 17 +- .../config/server-subdomains-unsafe.json | 6 +- .../config/server-without-auth.json | 14 +- test/integration/config/setup-memory.json | 5 +- .../http/UnsecureWebSocketsProtocol.test.ts | 184 ++++++++++-------- test/unit/init/ServerInitializer.test.ts | 33 +++- .../unit/server/BaseHttpServerFactory.test.ts | 128 ------------ test/unit/server/BaseServerFactory.test.ts | 75 +++++++ .../server/HandlerServerConfigurator.test.ts | 143 ++++++++++++++ .../WebSocketServerConfigurator.test.ts | 80 ++++++++ .../server/WebSocketServerFactory.test.ts | 54 ----- test/util/Util.ts | 4 +- 64 files changed, 949 insertions(+), 694 deletions(-) rename config/http/middleware/{no-websockets.json => default.json} (100%) delete mode 100644 config/http/middleware/handlers/updates-via.json delete mode 100644 config/http/middleware/websockets.json create mode 100644 config/http/notifications/disabled.json create mode 100644 config/http/notifications/legacy-websockets.json create mode 100644 config/http/server-factory/configurator/default.json create mode 100644 config/http/server-factory/http.json delete mode 100644 config/http/server-factory/https-example.json create mode 100644 config/http/server-factory/https-no-cli-example.json delete mode 100644 config/http/server-factory/https-websockets.json rename config/http/server-factory/{https-no-websockets.json => https.json} (52%) delete mode 100644 config/http/server-factory/no-websockets.json delete mode 100644 config/http/server-factory/websockets.json delete mode 100644 src/server/BaseHttpServerFactory.ts create mode 100644 src/server/BaseServerFactory.ts create mode 100644 src/server/HandlerServerConfigurator.ts create mode 100644 src/server/ServerConfigurator.ts delete mode 100644 src/server/WebSocketHandler.ts create mode 100644 src/server/WebSocketServerConfigurator.ts delete mode 100644 src/server/WebSocketServerFactory.ts rename test/integration/{WebSocketsProtocol.test.ts => LegacyWebSocketsProtocol.test.ts} (96%) create mode 100644 test/integration/config/legacy-websockets.json delete mode 100644 test/unit/server/BaseHttpServerFactory.test.ts create mode 100644 test/unit/server/BaseServerFactory.test.ts create mode 100644 test/unit/server/HandlerServerConfigurator.test.ts create mode 100644 test/unit/server/WebSocketServerConfigurator.test.ts delete mode 100644 test/unit/server/WebSocketServerFactory.test.ts diff --git a/.componentsignore b/.componentsignore index 594027e01..6cd1bf43e 100644 --- a/.componentsignore +++ b/.componentsignore @@ -21,6 +21,7 @@ "Promise", "Readonly", "RegExp", + "Server", "Shorthand", "Template", "TemplateEngine", diff --git a/config/default.json b/config/default.json index 2be78b1b1..7449c7359 100644 --- a/config/default.json +++ b/config/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", diff --git a/config/dynamic.json b/config/dynamic.json index 4c613ebb8..045cd9c66 100644 --- a/config/dynamic.json +++ b/config/dynamic.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", diff --git a/config/example-https-file.json b/config/example-https-file.json index 5c56ded69..6cab2b568 100644 --- a/config/example-https-file.json +++ b/config/example-https-file.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" } ] } diff --git a/config/file-acp.json b/config/file-acp.json index b63837436..e5ba6e6b6 100644 --- a/config/file-acp.json +++ b/config/file-acp.json @@ -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", diff --git a/config/file-no-setup.json b/config/file-no-setup.json index 909a0d16d..28632e949 100644 --- a/config/file-no-setup.json +++ b/config/file-no-setup.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", diff --git a/config/file.json b/config/file.json index b2308bb5f..8a678b5a9 100644 --- a/config/file.json +++ b/config/file.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", diff --git a/config/http/middleware/no-websockets.json b/config/http/middleware/default.json similarity index 100% rename from config/http/middleware/no-websockets.json rename to config/http/middleware/default.json diff --git a/config/http/middleware/handlers/updates-via.json b/config/http/middleware/handlers/updates-via.json deleted file mode 100644 index 6a0250a77..000000000 --- a/config/http/middleware/handlers/updates-via.json +++ /dev/null @@ -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" } - } - ] -} diff --git a/config/http/middleware/websockets.json b/config/http/middleware/websockets.json deleted file mode 100644 index 05d873a64..000000000 --- a/config/http/middleware/websockets.json +++ /dev/null @@ -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" - } - ] - } - ] -} diff --git a/config/http/notifications/disabled.json b/config/http/notifications/disabled.json new file mode 100644 index 000000000..29675f7d4 --- /dev/null +++ b/config/http/notifications/disabled.json @@ -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." + } + ] +} diff --git a/config/http/notifications/legacy-websockets.json b/config/http/notifications/legacy-websockets.json new file mode 100644 index 000000000..fcbc94725 --- /dev/null +++ b/config/http/notifications/legacy-websockets.json @@ -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" } + } + ] + } + ] +} diff --git a/config/http/server-factory/configurator/default.json b/config/http/server-factory/configurator/default.json new file mode 100644 index 000000000..85d266b25 --- /dev/null +++ b/config/http/server-factory/configurator/default.json @@ -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" } + } + ] + } + ] +} diff --git a/config/http/server-factory/http.json b/config/http/server-factory/http.json new file mode 100644 index 000000000..7789fd338 --- /dev/null +++ b/config/http/server-factory/http.json @@ -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" } + } + ] +} diff --git a/config/http/server-factory/https-example.json b/config/http/server-factory/https-example.json deleted file mode 100644 index 8874a6198..000000000 --- a/config/http/server-factory/https-example.json +++ /dev/null @@ -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" } - } - } - ] -} diff --git a/config/http/server-factory/https-no-cli-example.json b/config/http/server-factory/https-no-cli-example.json new file mode 100644 index 000000000..a504f6510 --- /dev/null +++ b/config/http/server-factory/https-no-cli-example.json @@ -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" + } + ] +} diff --git a/config/http/server-factory/https-websockets.json b/config/http/server-factory/https-websockets.json deleted file mode 100644 index dd3712085..000000000 --- a/config/http/server-factory/https-websockets.json +++ /dev/null @@ -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" } - } - } - ] -} diff --git a/config/http/server-factory/https-no-websockets.json b/config/http/server-factory/https.json similarity index 52% rename from config/http/server-factory/https-no-websockets.json rename to config/http/server-factory/https.json index 930dc99df..084e2af6b 100644 --- a/config/http/server-factory/https-no-websockets.json +++ b/config/http/server-factory/https.json @@ -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", diff --git a/config/http/server-factory/no-websockets.json b/config/http/server-factory/no-websockets.json deleted file mode 100644 index a8acd4ad2..000000000 --- a/config/http/server-factory/no-websockets.json +++ /dev/null @@ -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" } - } - ] -} diff --git a/config/http/server-factory/websockets.json b/config/http/server-factory/websockets.json deleted file mode 100644 index 6ed45fcda..000000000 --- a/config/http/server-factory/websockets.json +++ /dev/null @@ -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" } - } - } - ] -} diff --git a/config/https-file-cli.json b/config/https-file-cli.json index 3ba8339e1..55fcfd0ba 100644 --- a/config/https-file-cli.json +++ b/config/https-file-cli.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/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", diff --git a/config/memory-subdomains.json b/config/memory-subdomains.json index eb802f774..5e63e00ce 100644 --- a/config/memory-subdomains.json +++ b/config/memory-subdomains.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", diff --git a/config/path-routing.json b/config/path-routing.json index 7921a5da7..06a6d1374 100644 --- a/config/path-routing.json +++ b/config/path-routing.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", diff --git a/config/quota-file.json b/config/quota-file.json index 9e9463287..fa0485d78 100644 --- a/config/quota-file.json +++ b/config/quota-file.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", diff --git a/config/restrict-idp.json b/config/restrict-idp.json index 04e018eae..427287fa5 100644 --- a/config/restrict-idp.json +++ b/config/restrict-idp.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", diff --git a/config/sparql-endpoint-no-setup.json b/config/sparql-endpoint-no-setup.json index 1206054a8..675b2e7a0 100644 --- a/config/sparql-endpoint-no-setup.json +++ b/config/sparql-endpoint-no-setup.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", diff --git a/config/sparql-endpoint.json b/config/sparql-endpoint.json index 08fb27ffe..eaddc6fa7 100644 --- a/config/sparql-endpoint.json +++ b/config/sparql-endpoint.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", diff --git a/config/sparql-file-storage.json b/config/sparql-file-storage.json index 567c8a710..9aafcd06d 100644 --- a/config/sparql-file-storage.json +++ b/config/sparql-file-storage.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", diff --git a/src/http/UnsecureWebSocketsProtocol.ts b/src/http/UnsecureWebSocketsProtocol.ts index bc3d6ca65..2111697e5 100644 --- a/src/http/UnsecureWebSocketsProtocol.ts +++ b/src/http/UnsecureWebSocketsProtocol.ts @@ -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 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(); - 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 { - const listener = new WebSocketListener(input.webSocket); + protected async handleConnection(webSocket: WebSocket, upgradeRequest: IncomingMessage): Promise { + 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 { diff --git a/src/index.ts b/src/index.ts index 33219507a..afaec31d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/src/init/ServerInitializer.ts b/src/init/ServerInitializer.ts index 40438dad2..6849db020 100644 --- a/src/init/ServerInitializer.ts +++ b/src/init/ServerInitializer.ts @@ -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 { - 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 { diff --git a/src/server/BaseHttpServerFactory.ts b/src/server/BaseHttpServerFactory.ts deleted file mode 100644 index 03824f4b5..000000000 --- a/src/server/BaseHttpServerFactory.ts +++ /dev/null @@ -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 => { - 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; - } -} diff --git a/src/server/BaseServerFactory.ts b/src/server/BaseServerFactory.ts new file mode 100644 index 000000000..b2cdc7ffb --- /dev/null +++ b/src/server/BaseServerFactory.ts @@ -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 { + 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; + } +} diff --git a/src/server/HandlerServerConfigurator.ts b/src/server/HandlerServerConfigurator.ts new file mode 100644 index 000000000..9c9e688f4 --- /dev/null +++ b/src/server/HandlerServerConfigurator.ts @@ -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 { + server.on('request', + async(request: IncomingMessage, response: ServerResponse): Promise => { + 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`; + } +} diff --git a/src/server/HttpServerFactory.ts b/src/server/HttpServerFactory.ts index a0bd50f19..cd39c2198 100644 --- a/src/server/HttpServerFactory.ts +++ b/src/server/HttpServerFactory.ts @@ -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; } diff --git a/src/server/ServerConfigurator.ts b/src/server/ServerConfigurator.ts new file mode 100644 index 000000000..8a65cb14c --- /dev/null +++ b/src/server/ServerConfigurator.ts @@ -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 {} diff --git a/src/server/WebSocketHandler.ts b/src/server/WebSocketHandler.ts deleted file mode 100644 index d968edabb..000000000 --- a/src/server/WebSocketHandler.ts +++ /dev/null @@ -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 }> {} diff --git a/src/server/WebSocketServerConfigurator.ts b/src/server/WebSocketServerConfigurator.ts new file mode 100644 index 000000000..a4ae753ec --- /dev/null +++ b/src/server/WebSocketServerConfigurator.ts @@ -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 { + // 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; +} diff --git a/src/server/WebSocketServerFactory.ts b/src/server/WebSocketServerFactory.ts deleted file mode 100644 index 5826e1501..000000000 --- a/src/server/WebSocketServerFactory.ts +++ /dev/null @@ -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 => { - 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; - } -} diff --git a/test/integration/WebSocketsProtocol.test.ts b/test/integration/LegacyWebSocketsProtocol.test.ts similarity index 96% rename from test/integration/WebSocketsProtocol.test.ts rename to test/integration/LegacyWebSocketsProtocol.test.ts index 9d46fbcec..737430dfe 100644 --- a/test/integration/WebSocketsProtocol.test.ts +++ b/test/integration/LegacyWebSocketsProtocol.test.ts @@ -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 => { app = await instantiateFromConfig( 'urn:solid-server:default:App', - getTestConfigPath('server-without-auth.json'), + getTestConfigPath('legacy-websockets.json'), getDefaultVariables(port, 'https://example.pod/'), ) as App; diff --git a/test/integration/Middleware.test.ts b/test/integration/Middleware.test.ts index d4a257b0c..856182ea1 100644 --- a/test/integration/Middleware.test.ts +++ b/test/integration/Middleware.test.ts @@ -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 { - 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 => { - 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 => { 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 => { @@ -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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - 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 => { - const response = request(server).get('/').set('Host', 'test.com'); - expect(response).toBeDefined(); - await response.expect(200).expect('Hello World'); - }); }); diff --git a/test/integration/config/ldp-with-acp.json b/test/integration/config/ldp-with-acp.json index 13b083b30..2e6b98340 100644 --- a/test/integration/config/ldp-with-acp.json +++ b/test/integration/config/ldp-with-acp.json @@ -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", diff --git a/test/integration/config/ldp-with-auth.json b/test/integration/config/ldp-with-auth.json index cdd2e04ae..b56d7cdad 100644 --- a/test/integration/config/ldp-with-auth.json +++ b/test/integration/config/ldp-with-auth.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", diff --git a/test/integration/config/legacy-websockets.json b/test/integration/config/legacy-websockets.json new file mode 100644 index 000000000..3a5c0e7cc --- /dev/null +++ b/test/integration/config/legacy-websockets.json @@ -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": [ + ] +} diff --git a/test/integration/config/permission-table.json b/test/integration/config/permission-table.json index 3959dc4c5..fe2fa69e5 100644 --- a/test/integration/config/permission-table.json +++ b/test/integration/config/permission-table.json @@ -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", diff --git a/test/integration/config/quota-global.json b/test/integration/config/quota-global.json index 8102227b9..cf59e6f72 100644 --- a/test/integration/config/quota-global.json +++ b/test/integration/config/quota-global.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", diff --git a/test/integration/config/quota-pod.json b/test/integration/config/quota-pod.json index 009596f18..eed5b3550 100644 --- a/test/integration/config/quota-pod.json +++ b/test/integration/config/quota-pod.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", diff --git a/test/integration/config/restricted-idp.json b/test/integration/config/restricted-idp.json index 65a690fd1..55600a7b2 100644 --- a/test/integration/config/restricted-idp.json +++ b/test/integration/config/restricted-idp.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", diff --git a/test/integration/config/server-dynamic-unsafe.json b/test/integration/config/server-dynamic-unsafe.json index a714a7d84..86b6e2d9a 100644 --- a/test/integration/config/server-dynamic-unsafe.json +++ b/test/integration/config/server-dynamic-unsafe.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", diff --git a/test/integration/config/server-file.json b/test/integration/config/server-file.json index 48ac02443..38ce8f316 100644 --- a/test/integration/config/server-file.json +++ b/test/integration/config/server-file.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", diff --git a/test/integration/config/server-memory.json b/test/integration/config/server-memory.json index b61e35d34..2bff26e5b 100644 --- a/test/integration/config/server-memory.json +++ b/test/integration/config/server-memory.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", diff --git a/test/integration/config/server-middleware.json b/test/integration/config/server-middleware.json index 7be20b0d9..5c3f0c2a4 100644 --- a/test/integration/config/server-middleware.json +++ b/test/integration/config/server-middleware.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" } + } + ] } ] } diff --git a/test/integration/config/server-redis-lock.json b/test/integration/config/server-redis-lock.json index 898ba631e..bb2f03968 100644 --- a/test/integration/config/server-redis-lock.json +++ b/test/integration/config/server-redis-lock.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/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": [ { diff --git a/test/integration/config/server-subdomains-unsafe.json b/test/integration/config/server-subdomains-unsafe.json index 8d0cf3c46..3692cb6be 100644 --- a/test/integration/config/server-subdomains-unsafe.json +++ b/test/integration/config/server-subdomains-unsafe.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", @@ -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", diff --git a/test/integration/config/server-without-auth.json b/test/integration/config/server-without-auth.json index 72a4e441c..2598e84b3 100644 --- a/test/integration/config/server-without-auth.json +++ b/test/integration/config/server-without-auth.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": [ ] diff --git a/test/integration/config/setup-memory.json b/test/integration/config/setup-memory.json index b9a9638b7..82f9af3fe 100644 --- a/test/integration/config/setup-memory.json +++ b/test/integration/config/setup-memory.json @@ -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", diff --git a/test/unit/http/UnsecureWebSocketsProtocol.test.ts b/test/unit/http/UnsecureWebSocketsProtocol.test.ts index d3320115e..33baf4d80 100644 --- a/test/unit/http/UnsecureWebSocketsProtocol.test.ts +++ b/test/unit/http/UnsecureWebSocketsProtocol.test.ts @@ -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(); @@ -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 => { + 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 => { - 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 => { + server = new EventEmitter() as any; + webSocket = new DummySocket(); + protocol = new UnsecureWebSocketsProtocol(source); + await protocol.handle(server); + }); - it('unsubscribes when a socket errors.', async(): Promise => { - 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 => { + 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 => { - 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 => { + 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 => { - 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 => { + 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 => { - 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 => { + 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 => { - 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 => { + 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 => { + 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'); + }); }); }); diff --git a/test/unit/init/ServerInitializer.test.ts b/test/unit/init/ServerInitializer.test.ts index 3beb23baf..24a63e597 100644 --- a/test/unit/init/ServerInitializer.test.ts +++ b/test/unit/init/ServerInitializer.test.ts @@ -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; let server: Server; let serverFactory: jest.Mocked; - let initializer: ServerInitializer; + beforeEach(async(): Promise => { + 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 => { 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 => { + 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 => { diff --git a/test/unit/server/BaseHttpServerFactory.test.ts b/test/unit/server/BaseHttpServerFactory.test.ts deleted file mode 100644 index e380431cf..000000000 --- a/test/unit/server/BaseHttpServerFactory.test.ts +++ /dev/null @@ -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 = { - handleSafe: jest.fn(async(input: { response: HttpResponse }): Promise => { - 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 => { - // 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 => { - jest.clearAllMocks(); - }); - - afterAll(async(): Promise => { - process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectTls; - server.close(); - }); - - it('sends incoming requests to the handler.', async(): Promise => { - 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 => { - 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 => { - 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 => { - handler.handleSafe.mockImplementationOnce(async(input: { response: HttpResponse }): Promise => { - 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 => { - 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 => { - // 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 => { - 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 => { - const factory = new BaseHttpServerFactory(handler, httpOptions); - server = factory.startServer(port); - }); - - afterAll(async(): Promise => { - server.close(); - }); - - it('does not print the stack if that option is disabled.', async(): Promise => { - 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`); - }); - }); -}); diff --git a/test/unit/server/BaseServerFactory.test.ts b/test/unit/server/BaseServerFactory.test.ts new file mode 100644 index 000000000..3f35a537e --- /dev/null +++ b/test/unit/server/BaseServerFactory.test.ts @@ -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; + + beforeAll(async(): Promise => { + // 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 { + serv.on('request', mockRequestHandler); + }, + } as any; + + const factory = new BaseServerFactory(configurator, httpOptions); + server = await factory.createServer(); + + server.listen(port); + }); + + beforeEach(async(): Promise => { + jest.clearAllMocks(); + }); + + afterAll(async(): Promise => { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = rejectTls; + server.close(); + }); + + it('emits a request event on requests.', async(): Promise => { + 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); + }); + }); +}); diff --git a/test/unit/server/HandlerServerConfigurator.test.ts b/test/unit/server/HandlerServerConfigurator.test.ts new file mode 100644 index 000000000..29beccd51 --- /dev/null +++ b/test/unit/server/HandlerServerConfigurator.test.ts @@ -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 = getLoggerFor('mock') as any; + let request: jest.Mocked; + let response: jest.Mocked; + let server: Server; + let handler: jest.Mocked; + let listener: HandlerServerConfigurator; + + beforeEach(async(): Promise => { + // 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + 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 => { + handler.handleSafe.mockImplementationOnce(async(input): Promise => { + 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 => { + 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`); + }); +}); diff --git a/test/unit/server/WebSocketServerConfigurator.test.ts b/test/unit/server/WebSocketServerConfigurator.test.ts new file mode 100644 index 000000000..d2b4508e6 --- /dev/null +++ b/test/unit/server/WebSocketServerConfigurator.test.ts @@ -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 { + // Will be overwritten + } +} + +describe('A WebSocketServerConfigurator', (): void => { + const logger: jest.Mocked = getLoggerFor('mock') as any; + let server: Server; + let webSocket: WebSocket; + let upgradeRequest: HttpRequest; + let listener: jest.Mocked; + + beforeEach(async(): Promise => { + // 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 => { + 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 => { + 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 => { + 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'); + }); +}); diff --git a/test/unit/server/WebSocketServerFactory.test.ts b/test/unit/server/WebSocketServerFactory.test.ts deleted file mode 100644 index 7605cf4cd..000000000 --- a/test/unit/server/WebSocketServerFactory.test.ts +++ /dev/null @@ -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 { - input.response.end('SimpleHttpHandler'); - } -} - -class SimpleWebSocketHandler extends WebSocketHandler { - public host: any; - - public async handle(input: { webSocket: WebSocket; upgradeRequest: HttpRequest }): Promise { - 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 => { - 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 => { - server.close(); - }); - - it('has a functioning HTTP interface.', async(): Promise => { - const result = await request(server).get('/').expect('SimpleHttpHandler'); - expect(result).toBeDefined(); - }); - - it('has a functioning WebSockets interface.', async(): Promise => { - const client = new WebSocket('ws://localhost:5556'); - const buffer = await new Promise((resolve): any => client.on('message', resolve)); - expect(buffer.toString()).toBe('SimpleWebSocketHandler'); - expect(webSocketHandler.host).toBe('localhost:5556'); - }); -}); diff --git a/test/util/Util.ts b/test/util/Util.ts index efeecb8d4..f25e23274 100644 --- a/test/util/Util.ts +++ b/test/util/Util.ts @@ -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 {