Skip to content

Commit

Permalink
use node https instead of uwebsockets
Browse files Browse the repository at this point in the history
  • Loading branch information
theoephraim committed Dec 12, 2024
1 parent 1a52755 commit 1e1287f
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 320 deletions.
1 change: 0 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@
"socket.io": "^4.8.0",
"svgo": "catalog:",
"typescript": "catalog:",
"uWebSockets.js": "github:uNetworking/uWebSockets.js#semver:^20.49.0",
"validate-npm-package-name": "^5.0.0",
"vite": "catalog:",
"vite-node": "catalog:",
Expand Down
13 changes: 0 additions & 13 deletions packages/core/src/cli/lib/init-process.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,6 @@
import kleur from 'kleur';

process.on('uncaughtException', (err) => {
if (err.message.includes('Error loading shared library ld-linux-')) {
console.log([
'🚨 💡 🚨 💡 🚨',
'',
kleur.bold('uWebsockets compat issue. If you are running in Docker, try adding this line to your Dockerfile:'),
'',
' RUN ln -s "/lib/libc.musl-$(uname -m).so.1" "/lib/ld-linux-$(uname -m).so.1"',
'',
'🚨 💡 🚨 💡 🚨',
'',
].join('\n'));
}

console.log(kleur.red(`UNCAUGHT EXCEPTION: ${err.message}`));
console.log(kleur.red(`UNCAUGHT EXCEPTION: ${err.stack}`));
// eslint-disable-next-line no-restricted-syntax
Expand Down
256 changes: 148 additions & 108 deletions packages/core/src/config-loader/dmno-server.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import https from 'node:https';

import path, { dirname } from 'node:path';
import fs from 'node:fs';
import crypto from 'node:crypto';
import { fileURLToPath } from 'node:url';
import { TLSSocket } from 'node:tls';
import { ServerResponse } from 'node:http';
import _ from 'lodash-es';
import getPort from 'get-port';
import { Server as SocketIoServer } from 'socket.io';
import uWS from 'uWebSockets.js';
import launchEditor from 'launch-editor';
import Debug from 'debug';
import { CacheMode } from '@dmno/configraph';
import { createDeferredPromise } from '@dmno/ts-lib';
import forge from 'node-forge';
import { ConfigLoader } from './config-loader';
import { loadOrCreateTlsCerts } from '../lib/certs';
import { pathExists } from '../lib/fs-utils';
import { findDmnoServices } from './find-services';
import { MIME_TYPES_BY_EXT, uwsBodyParser, uwsValidateClientCert } from '../lib/uws-utils';
import { MIME_TYPES_BY_EXT, bodyParser } from '../lib/web-server-utils';
import { UseAtPhases } from '../config-engine/configraph-adapter';

const __dirname = dirname(fileURLToPath(import.meta.url));
Expand All @@ -34,6 +37,26 @@ function getCurrentPackageNameFromPackageManager() {
if (process.env.PNPM_PACKAGE_NAME !== undefined) return process.env.PNPM_PACKAGE_NAME;
}


function writeResponse(
res: ServerResponse,
content: string | any,
statusCode = 200,
contentType = 'application/json',
) {
// res.writeHead(statusCode);
res.statusCode = statusCode;
res.setHeader('content-type', contentType);
// res.setHeader(http2.constants.HTTP2_HEADER_STATUS, statusCode);
// res.setHeader(http2.constants.HTTP2_HEADER_CONTENT_TYPE, contentType);
// res.stream.respond({
// [http2.constants.HTTP2_HEADER_STATUS]: statusCode,
// [http2.constants.HTTP2_HEADER_CONTENT_TYPE]: contentType,
// });
res.end(contentType === 'application/json' ? JSON.stringify(content) : content.toString());
}


export class DmnoServer {
private serverId?: string;
private serverPort?: number;
Expand Down Expand Up @@ -76,8 +99,9 @@ export class DmnoServer {


readonly webServerReady?: Promise<void>;
private uwsServer?: uWS.TemplatedApp;
private webServer?: https.Server;
private certs?: Awaited<ReturnType<typeof loadOrCreateTlsCerts>>;
private caStore?: forge.pki.CAStore;

private _webServerUrl?: string;
public get webServerUrl() { return this._webServerUrl; }
Expand All @@ -94,11 +118,11 @@ export class DmnoServer {
// we also probably want to let the user specify a port in workspace config
this.serverPort = await getPort({ port: DEFAULT_PORT });

const uwsServerListeningDeferred = createDeferredPromise();
const webServerListeningDeferred = createDeferredPromise();

const certDir = `${this.configLoader.workspaceRootPath}/.dmno/certs`;
this.certs = await loadOrCreateTlsCerts('localhost', certDir);

this.caStore = forge.pki.createCaStore([forge.pki.certificateFromPem(this.certs!.caCert)]);

const devUiPath = path.resolve(`${__dirname}/../dev-ui-dist/`);

Expand All @@ -112,135 +136,148 @@ export class DmnoServer {
}
}

this.uwsServer = uWS.SSLApp({
cert_file_name: path.join(certDir, 'SERVER.crt'),
key_file_name: path.join(certDir, 'SERVER_key.pem'),
ca_file_name: path.join(certDir, 'CA.crt'),
// passphrase: '1234',
})
.any('/api/:requestName', async (res, req) => {
/* Can't return or yield from here without responding or attaching an abort handler */
res.onAborted(() => {
res.aborted = true;
});
const httpsServer = https.createServer({
key: this.certs.serverKey,
cert: this.certs.serverCert,
ca: this.certs.caCert,
requestCert: true,
// rejectUnauthorized: true, // if enabled you cannot do authentication based on client cert
// checkClientCertificate: true
});

if (this.serverId !== req.getHeader('dmno-server-id')) {
res.writeStatus('409');
res.end(JSON.stringify({ error: 'Incorrect DMNO server ID' }));
return;
}
httpsServer.on('request', async (req, res) => {
if (!(req.socket instanceof TLSSocket)) throw new Error('expected TLSSocket');

if (!uwsValidateClientCert(res, this.certs!.caCert)) return;
// console.log(`${req.socket.getProtocol()} ${req.method} request from: ${req.connection.remoteAddress}`);

const reqName = req.getParameter(0) || '';
if (!(reqName in this.commands)) {
res.writeStatus('404');
res.end(JSON.stringify({ error: 'Not found!' }));
return;
}
if (!req.socket.authorized) {
return writeResponse(res, { error: 'Access denied' }, 401);
} else {
// Examine the cert itself, and even validate based on that if required
// const cert = req.socket.getPeerCertificate();
// if (cert.subject) {
// console.log(`${cert.subject.CN} issued by: ${cert.issuer.CN} has logged in!`);
// } else {
// console.log('Cert has no subject?');
// }

// parse the body to get function args
let args;
if (req.getMethod() === 'post') {
try {
args = await uwsBodyParser(res);
} catch (err) {
res.writeStatus('400');
console.log(err);
res.end(JSON.stringify({ error: 'body parsing failed' }));
return;
// VALIDATE CLIENT CERT
try {
const clientCert = req.socket.getPeerCertificate(true);
const clientCertRaw = clientCert.raw.toString('base64');

const derKey = forge.util.decode64(clientCertRaw);
const asnObj = forge.asn1.fromDer(derKey);
const asn1Cert = forge.pki.certificateFromAsn1(asnObj);
const pemCert = forge.pki.certificateToPem(asn1Cert);
const client = forge.pki.certificateFromPem(pemCert);
const certValid = forge.pki.verifyCertificateChain(this.caStore!, [client]);
if (!certValid) {
return writeResponse(res, { error: 'Unauthorized - bad client cert' }, 401);
}
} else if (req.getMethod() === 'get') {
args = [];
} else {
res.writeStatus('404');
res.end(JSON.stringify({ error: 'unsupported method' }));
return;
} catch (err) {
console.log(err);
return writeResponse(res, { error: 'Error validating client cert' }, 401);
}

// API request handler
if (req.url?.startsWith('/api/')) {
const [,,requestName] = req.url.split('/');

// @ts-ignore
const rawResponse = await this.commands[reqName].call(this, ...args);

/* If we were aborted, you cannot respond */
if (!res.aborted) {
res.cork(() => {
res.end(JSON.stringify(rawResponse));
});
}
})
.any('/*', async (res, req) => {
res.onAborted(() => {
res.aborted = true;
});
if (this.serverId !== req.headers['dmno-server-id']) {
return writeResponse(res, { error: 'Incorrect DMNO server ID' }, 409);
}

if (!uwsValidateClientCert(res, this.certs!.caCert)) return;
if (!(requestName in this.commands)) {
return writeResponse(res, { error: 'Not found!' }, 404);
}

if (!this.opts?.enableWebUi) {
res.writeStatus('404');
res.writeHeader('content-type', 'text/html');
res.end('<h1>dmno web ui is disabled</h1><p>Run `dmno dev` to boot the web dashboard</p>');
return;
}
// parse the body to get function args
let args;
if (req.method?.toLowerCase() === 'post') {
try {
args = await bodyParser(req);
} catch (err) {
console.log(err);
return writeResponse(res, { error: 'body parsing failed' }, 400);
}
} else if (req.method?.toLowerCase() === 'get') {
args = [];
} else {
return writeResponse(res, { error: 'unsupported method!' }, 404);
}

// have to use .any for the route matching to work properly
if (req.getMethod() !== 'get') {
res.writeStatus('404');
res.end(JSON.stringify({ error: 'method not supported' }));
}
// @ts-ignore
const rawResponse = await this.commands[requestName].call(this, ...args);
return writeResponse(res, rawResponse);

// dev ui
} else {
if (!this.opts?.enableWebUi) {
return writeResponse(
res,
'<h1>dmno web ui is disabled</h1><p>Run `dmno dev` to boot the web dashboard</p>',
404,
'text/html',
);
}
if (req.method?.toLowerCase() !== 'get') {
return writeResponse(res, { error: 'unsupported method!' }, 404);
}

let reqPath = req.getUrl();
if (!reqPath || reqPath === '/') reqPath = '/index.html';
// debugWeb('http request', reqPath);

let reqPath = req.url;
if (!reqPath || reqPath === '/') reqPath = '/index.html';
// debugWeb('http request', reqPath);

const fullPath = path.join(devUiPath, reqPath);
const extension = fullPath.split('.').pop();
const fullPath = path.join(devUiPath, reqPath);
const extension = fullPath.split('.').pop();

let fileContents = devUiIndexHtml;
let contentType = 'text/html';
let fileContents = devUiIndexHtml;
let contentType = 'text/html';

try {
fileContents = await fs.promises.readFile(fullPath, 'utf-8');
contentType = (MIME_TYPES_BY_EXT as any)[extension || ''];
} catch (err) {
if ((err as any).code === 'ENOENT') {
if (reqPath.startsWith('/assets/')) {
res.writeStatus('404');
res.writeHeader('content-type', 'text/html');
res.end('<h1>oops! file does not exist</h1>');
return;
try {
fileContents = await fs.promises.readFile(fullPath, 'utf-8');
contentType = (MIME_TYPES_BY_EXT as any)[extension || ''];
} catch (err) {
if ((err as any).code === 'ENOENT') {
if (reqPath.startsWith('/assets/')) {
return writeResponse(
res,
'<h1>oops! file does not exist</h1>',
404,
'text/html',
);
}
} else {
throw err;
}
} else {
throw err;
}
}

if (!res.aborted) {
res.cork(() => {
res.writeStatus('200');
res.writeHeader('content-type', contentType);
res.end(fileContents);
});

return writeResponse(res, fileContents, 200, contentType);
}
})
.listen(this.serverPort, (token) => {
this._webServerUrl = `https://localhost:${this.serverPort}`;
if (!token) throw new Error('uWS failed to bind to port?');
uwsServerListeningDeferred.resolve();
});
}
});

this.webServer = httpsServer;

httpsServer.listen(this.serverPort, () => {
let host = (httpsServer.address() as any).address;
if (host === '::') host = 'localhost';
this._webServerUrl = `https://${host}:${this.serverPort}`;
webServerListeningDeferred.resolve();
});

process.on('exit', (code) => {
// TODO: can be smarter about tracking what needs to be shut down
try {
this.uwsServer?.close();
this.webServer?.close();
} catch (err) {

}
});

await uwsServerListeningDeferred.promise;
await webServerListeningDeferred.promise;

if (this.opts?.enableWebUi) {
await this.initSocketIoServer();
Expand All @@ -252,7 +289,7 @@ export class DmnoServer {

const debugWeb = Debug('dmno:webserver');

this.socketIoServer = new SocketIoServer({
this.socketIoServer = new SocketIoServer(this.webServer, {
path: '/ws',
serveClient: false,
// allowRequest: (req, callback) => {
Expand All @@ -261,7 +298,6 @@ export class DmnoServer {
// },
cors: { origin: '*' },
});
this.socketIoServer.attachApp(this.uwsServer);
this.socketIoServer.on('connection', (socket) => {
debugWeb('socket connection');
// let handshake = socket.handshake;
Expand Down Expand Up @@ -298,7 +334,7 @@ export class DmnoServer {


shutdown() {
this.uwsServer?.close();
this.webServer?.close();
this.configLoader?.shutdown().catch(() => {
console.log('error shutting down dmno vite dev server');
});
Expand Down Expand Up @@ -354,6 +390,10 @@ export class DmnoServer {
cert: this.certs.clientCert,
ca: [this.certs.caCert],
rejectUnauthorized: false,
// enableHttp2: false,
// onlyHttp2: false,
// allowHTTP1: true,
// minVersion: 'TLSv1.2',
};

const req = https.request(clientOptions, (res) => {
Expand Down
Loading

0 comments on commit 1e1287f

Please sign in to comment.