diff --git a/project/package.json b/project/package.json index 737380da7..e14e99e5b 100644 --- a/project/package.json +++ b/project/package.json @@ -44,14 +44,15 @@ "logform": "2.7.0", "mongoid-js": "1.3.0", "reflect-metadata": "0.2.2", + "selfsigned": "^2.4.1", "semver": "7.6.3", "source-map-support": "0.5.21", "string-similarity-js": "2.1.4", "tsyringe": "4.8.0", + "typescript": "5.7.3", "winston": "3.17.0", "winston-daily-rotate-file": "5.0.0", - "ws": "8.18.0", - "typescript": "5.7.3" + "ws": "8.18.0" }, "devDependencies": { "@biomejs/biome": "1.9.4", diff --git a/project/src/callbacks/HttpCallbacks.ts b/project/src/callbacks/HttpCallbacks.ts index d1d2bffac..abf1b03f8 100644 --- a/project/src/callbacks/HttpCallbacks.ts +++ b/project/src/callbacks/HttpCallbacks.ts @@ -7,7 +7,7 @@ export class HttpCallbacks implements OnLoad { constructor(@inject("HttpServer") protected httpServer: HttpServer) {} public async onLoad(): Promise { - this.httpServer.load(); + await this.httpServer.load(); } public getRoute(): string { diff --git a/project/src/helpers/HttpServerHelper.ts b/project/src/helpers/HttpServerHelper.ts index 587b73f7c..afe45a0ae 100644 --- a/project/src/helpers/HttpServerHelper.ts +++ b/project/src/helpers/HttpServerHelper.ts @@ -40,12 +40,12 @@ export class HttpServerHelper { * @returns URI */ public getBackendUrl(): string { - return `http://${this.buildUrl()}`; + return `https://${this.buildUrl()}`; } /** Get websocket url + port */ public getWebsocketUrl(): string { - return `ws://${this.buildUrl()}`; + return `wss://${this.buildUrl()}`; } public sendTextJson(resp: any, output: any): void { diff --git a/project/src/servers/HttpServer.ts b/project/src/servers/HttpServer.ts index 937263352..38a69d649 100644 --- a/project/src/servers/HttpServer.ts +++ b/project/src/servers/HttpServer.ts @@ -1,4 +1,5 @@ -import http, { IncomingMessage, ServerResponse, Server } from "node:http"; +import { IncomingMessage, ServerResponse } from "node:http"; +import https, { Server } from "node:https"; import { ApplicationContext } from "@spt/context/ApplicationContext"; import { ContextVariableType } from "@spt/context/ContextVariableType"; import { HttpServerHelper } from "@spt/helpers/HttpServerHelper"; @@ -9,12 +10,18 @@ import { ConfigServer } from "@spt/servers/ConfigServer"; import { WebSocketServer } from "@spt/servers/WebSocketServer"; import { IHttpListener } from "@spt/servers/http/IHttpListener"; import { LocalisationService } from "@spt/services/LocalisationService"; +import { FileSystem } from "@spt/utils/FileSystem"; +import { Timer } from "@spt/utils/Timer"; +import { generate } from "selfsigned"; import { inject, injectAll, injectable } from "tsyringe"; @injectable() export class HttpServer { protected httpConfig: IHttpConfig; protected started = false; + protected certPath: string; + protected keyPath: string; + protected fileSystem: FileSystem; constructor( @inject("PrimaryLogger") protected logger: ILogger, @@ -24,33 +31,35 @@ export class HttpServer { @inject("ConfigServer") protected configServer: ConfigServer, @inject("ApplicationContext") protected applicationContext: ApplicationContext, @inject("WebSocketServer") protected webSocketServer: WebSocketServer, + @inject("FileSystem") fileSystem: FileSystem, // new dependency ) { this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP); + this.fileSystem = fileSystem; + this.certPath = "./user/certs/localhost.crt"; + this.keyPath = "./user/certs/localhost.key"; } /** * Handle server loading event */ - public load(): void { + public async load(): Promise { + // changed to async this.started = false; - /* create server */ - const httpServer: Server = http.createServer(); + const httpsServer: Server = await this.createHttpsServer(); - httpServer.on("request", async (req, res) => { + httpsServer.on("request", async (req: IncomingMessage, res: ServerResponse) => { await this.handleRequest(req, res); }); - /* Config server to listen on a port */ - httpServer.listen(this.httpConfig.port, this.httpConfig.ip, () => { + httpsServer.listen(this.httpConfig.port, this.httpConfig.ip, () => { this.started = true; this.logger.success( this.localisationService.getText("started_webserver_success", this.httpServerHelper.getBackendUrl()), ); }); - httpServer.on("error", (e: any) => { - /* server is already running or program using privileged port without root */ + httpsServer.on("error", (e: any) => { if (process.platform === "linux" && !(process.getuid && process.getuid() === 0) && e.port < 1024) { this.logger.error(this.localisationService.getText("linux_use_priviledged_port_non_root")); } else { @@ -59,8 +68,56 @@ export class HttpServer { } }); - // Setting up websocket - this.webSocketServer.setupWebSocket(httpServer); + // Setting up WebSocket using our https server + this.webSocketServer.setupWebSocket(httpsServer); + } + + /** + * Creates an HTTPS server using the stored certificate and key. + */ + protected async createHttpsServer(): Promise { + let credentials: { cert: string; key: string }; + try { + credentials = { + cert: await this.fileSystem.read(this.certPath), + key: await this.fileSystem.read(this.keyPath), + }; + } catch (err: unknown) { + const timer = new Timer(); + credentials = await this.generateSelfSignedCertificate(); + this.logger.debug(`Generating self-signed SSL certificate took ${timer.getTime("sec")}s`); + } + return https.createServer(credentials); + } + + /** + * Generates a self-signed certificate and returns an object with the cert and key. + */ + protected async generateSelfSignedCertificate(): Promise<{ cert: string; key: string }> { + const attrs = [{ name: "commonName", value: "localhost" }]; + const pems = generate(attrs, { + keySize: 4096, + days: 3653, // Ten years + algorithm: "sha256", + extensions: [ + { + name: "subjectAltName", + altNames: [ + { type: 2, value: "localhost" }, // DNS + { type: 7, ip: "127.0.0.1" }, // Resolving IP + ], + }, + ], + }); + + try { + await this.fileSystem.write(this.certPath, pems.cert); + await this.fileSystem.write(this.keyPath, pems.private); + } catch (err: unknown) { + this.logger.error(`There was an error writing the certificate or key to disk: ${err}`); + } + + return { cert: pems.cert, key: pems.private }; } protected async handleRequest(req: IncomingMessage, resp: ServerResponse): Promise { diff --git a/project/src/servers/WebSocketServer.ts b/project/src/servers/WebSocketServer.ts index d8c2a8bbd..9a56e39d6 100644 --- a/project/src/servers/WebSocketServer.ts +++ b/project/src/servers/WebSocketServer.ts @@ -1,4 +1,5 @@ -import http, { IncomingMessage } from "node:http"; +import { IncomingMessage } from "node:http"; +import https from "node:https"; import { ProgramStatics } from "@spt/ProgramStatics"; import { HttpServerHelper } from "@spt/helpers/HttpServerHelper"; import type { ILogger } from "@spt/models/spt/utils/ILogger"; @@ -27,7 +28,7 @@ export class WebSocketServer { return this.webSocketServer; } - public setupWebSocket(httpServer: http.Server): void { + public setupWebSocket(httpServer: https.Server): void { this.webSocketServer = new Server({ server: httpServer, WebSocket: SPTWebSocket }); this.webSocketServer.addListener("listening", () => { diff --git a/project/src/utils/DatabaseImporter.ts b/project/src/utils/DatabaseImporter.ts index 587cc9746..8388593ea 100644 --- a/project/src/utils/DatabaseImporter.ts +++ b/project/src/utils/DatabaseImporter.ts @@ -15,7 +15,7 @@ import { HashUtil } from "@spt/utils/HashUtil"; import { ImporterUtil } from "@spt/utils/ImporterUtil"; import { JsonUtil } from "@spt/utils/JsonUtil"; import { inject, injectable } from "tsyringe"; -import { Timer } from "./Timer"; +import { Timer } from "@spt/utils/Timer"; @injectable() export class DatabaseImporter implements OnLoad {