Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HTTPS Support #1118

Merged
merged 4 commits into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions project/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,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",
Expand Down
2 changes: 1 addition & 1 deletion project/src/callbacks/HttpCallbacks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export class HttpCallbacks implements OnLoad {
constructor(@inject("HttpServer") protected httpServer: HttpServer) {}

public async onLoad(): Promise<void> {
this.httpServer.load();
await this.httpServer.load();
}

public getRoute(): string {
Expand Down
4 changes: 2 additions & 2 deletions project/src/helpers/HttpServerHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
79 changes: 68 additions & 11 deletions project/src/servers/HttpServer.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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,
Expand All @@ -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<void> {
// 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 {
Expand All @@ -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<Server> {
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<void> {
Expand Down
5 changes: 3 additions & 2 deletions project/src/servers/WebSocketServer.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion project/src/utils/DatabaseImporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading