@@ -4,31 +4,29 @@ const fs = require('fs');
44const path = require ( 'path' ) ;
55const https = require ( 'https' ) ;
66const http = require ( 'http' ) ;
7- const crypto = require ( 'crypto' ) ;
87const { rateLimit } = require ( 'express-rate-limit' ) ;
98
10- // ── Express app ───────────────────────────────────────
11- const app = express ( ) ;
12- app . use ( express . json ( { limit : '10mb' } ) ) ;
13-
14- const FRONTEND = path . join ( __dirname , '../frontend' ) ;
15- const DATA_FILE = process . env . DATA_FILE || '/data/vault.enc' ;
16- const DATA_DIR = path . dirname ( DATA_FILE ) ;
17- const CERT_DIR = process . env . CERT_DIR || '/data/certs' ;
18- const PORT = parseInt ( process . env . PORT || '3000' , 10 ) ;
19- const HTTP_PORT = parseInt ( process . env . HTTP_PORT || String ( PORT + 1 ) , 10 ) ;
9+ // ── Config ────────────────────────────────────────────
10+ const FRONTEND = path . join ( __dirname , '../frontend' ) ;
11+ const DATA_FILE = process . env . DATA_FILE || '/data/vault.enc' ;
12+ const DATA_DIR = path . dirname ( DATA_FILE ) ;
13+ const CERT_DIR = process . env . CERT_DIR || '/data/certs' ;
14+ const PORT = parseInt ( process . env . PORT || '3000' , 10 ) ; // HTTPS (main)
15+ const HEALTH_PORT = parseInt ( process . env . HEALTH_PORT || '3002' , 10 ) ; // HTTP (healthcheck only)
2016
17+ // Ensure directories exist
2118try { fs . mkdirSync ( DATA_DIR , { recursive : true } ) ; } catch { }
2219try { fs . mkdirSync ( CERT_DIR , { recursive : true } ) ; } catch { }
2320
21+ // ── Express app ───────────────────────────────────────
22+ const app = express ( ) ;
23+ app . use ( express . json ( { limit : '10mb' } ) ) ;
2424app . use ( express . static ( FRONTEND ) ) ;
2525
26- // ── Security headers ──────────────────────────────────
27- // These mark the page as a "secure context" so crypto.subtle works
28- // even when accessed via plain HTTP on a LAN IP
26+ // Headers that allow crypto.subtle on any origin (secure context hint)
2927app . use ( ( req , res , next ) => {
30- res . setHeader ( 'Cross-Origin-Opener-Policy' , 'same-origin' ) ;
31- res . setHeader ( 'Cross-Origin-Embedder-Policy' , 'require-corp' ) ;
28+ res . setHeader ( 'Cross-Origin-Opener-Policy' , 'same-origin' ) ;
29+ res . setHeader ( 'Cross-Origin-Embedder-Policy' , 'require-corp' ) ;
3230 next ( ) ;
3331} ) ;
3432
@@ -39,9 +37,9 @@ const vaultLimiter = rateLimit({
3937 message : { error : 'Too many requests, try again later.' }
4038} ) ;
4139
42- // ── API ─── ────────────────────────────────────────────
40+ // ── Routes ────────────────────────────────────────────
4341app . get ( '/api/health' , ( _ , res ) => {
44- res . json ( { status : 'ok' , writable : canWrite ( ) , https : true } ) ;
42+ res . json ( { status : 'ok' , writable : canWrite ( ) } ) ;
4543} ) ;
4644
4745app . get ( '/api/vault/exists' , vaultLimiter , ( _ , res ) => {
@@ -79,64 +77,62 @@ function canWrite() {
7977 catch { return false ; }
8078}
8179
82- // ── Self-signed certificate (generated once, persisted in /data/certs) ───────
83- // Browser will show "Not secure" once — user clicks Advanced → Proceed.
84- // After that crypto.subtle works on every visit.
80+ // ── TLS cert (generated once, stored in volume) ───────
8581const CERT_FILE = path . join ( CERT_DIR , 'cert.pem' ) ;
86- const KEY_FILE = path . join ( CERT_DIR , 'key.pem' ) ;
82+ const KEY_FILE = path . join ( CERT_DIR , 'key.pem' ) ;
8783
88- function genSelfSigned ( ) {
89- // Use openssl if available (alpine has it), otherwise fall back to pure-node
84+ function genCert ( ) {
9085 try {
9186 const { execSync } = require ( 'child_process' ) ;
9287 execSync (
9388 `openssl req -x509 -newkey rsa:2048 -keyout "${ KEY_FILE } " -out "${ CERT_FILE } " ` +
94- `-days 3650 -nodes -subj "/CN=andromeda-vault" ` +
95- `-addext "subjectAltName=IP:0.0.0.0,DNS:localhost" 2>/dev/null` ,
96- { timeout : 15000 }
89+ `-days 3650 -nodes -subj "/CN=andromeda-vault" 2>/dev/null` ,
90+ { timeout : 20000 }
9791 ) ;
98- console . log ( ' ✦ Generated self-signed TLS certificate' ) ;
9992 return true ;
10093 } catch ( e ) {
101- console . error ( ' ✗ openssl not available, falling back to HTTP only :' , e . message ) ;
94+ console . error ( ' ✗ openssl failed :' , e . message ) ;
10295 return false ;
10396 }
10497}
10598
106- function loadOrCreateCert ( ) {
107- if ( fs . existsSync ( CERT_FILE ) && fs . existsSync ( KEY_FILE ) ) {
99+ function loadCert ( ) {
100+ if ( fs . existsSync ( CERT_FILE ) && fs . existsSync ( KEY_FILE ) )
108101 return { cert : fs . readFileSync ( CERT_FILE ) , key : fs . readFileSync ( KEY_FILE ) } ;
109- }
110- if ( genSelfSigned ( ) ) {
102+ console . log ( ' ✦ Generating self-signed TLS certificate…' ) ;
103+ if ( genCert ( ) )
111104 return { cert : fs . readFileSync ( CERT_FILE ) , key : fs . readFileSync ( KEY_FILE ) } ;
112- }
113105 return null ;
114106}
115107
116108// ── Start ─────────────────────────────────────────────
117- const creds = loadOrCreateCert ( ) ;
118109
110+ // 1. Plain HTTP health-check server — always starts immediately on HEALTH_PORT.
111+ // Only used by Docker HEALTHCHECK (internal only, not exposed in compose).
112+ http . createServer ( ( req , res ) => {
113+ if ( req . url === '/health' ) {
114+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
115+ res . end ( JSON . stringify ( { status : 'ok' } ) ) ;
116+ } else {
117+ res . writeHead ( 404 ) ; res . end ( ) ;
118+ }
119+ } ) . listen ( HEALTH_PORT , '127.0.0.1' , ( ) => {
120+ console . log ( ` ✦ Health endpoint → http://127.0.0.1:${ HEALTH_PORT } /health` ) ;
121+ } ) ;
122+
123+ // 2. Main app server — HTTPS if cert available, plain HTTP otherwise.
124+ const creds = loadCert ( ) ;
119125if ( creds ) {
120- // HTTPS — crypto.subtle works on all browsers
121126 https . createServer ( creds , app ) . listen ( PORT , '0.0.0.0' , ( ) => {
122- console . log ( `\n ✦ Andromeda → https://localhost:${ PORT } (HTTPS) ` ) ;
127+ console . log ( `\n ✦ Andromeda → https://localhost:${ PORT } ` ) ;
123128 console . log ( ` ✦ Data file → ${ DATA_FILE } ` ) ;
124129 console . log ( ` ✦ Writable → ${ canWrite ( ) } ` ) ;
125130 console . log ( `\n ⚠ First visit: click "Advanced → Proceed" to accept the self-signed cert.\n` ) ;
126131 } ) ;
127- // HTTP redirect on PORT+1 (optional)
128- http . createServer ( ( req , res ) => {
129- const host = req . headers . host ?. replace ( / : .* / , '' ) || 'localhost' ;
130- res . writeHead ( 301 , { Location : `https://${ host } :${ PORT } ${ req . url } ` } ) ;
131- res . end ( ) ;
132- } ) . listen ( HTTP_PORT , '0.0.0.0' , ( ) => {
133- console . log ( ` ✦ HTTP redirect → :${ HTTP_PORT } → https://:${ PORT } ` ) ;
134- } ) ;
135132} else {
136- // HTTP fallback — crypto.subtle only works from localhost in this mode
133+ // Fallback: plain HTTP ( crypto.subtle only works from localhost)
137134 http . createServer ( app ) . listen ( PORT , '0.0.0.0' , ( ) => {
138- console . log ( `\n ✦ Andromeda → http://localhost:${ PORT } (HTTP only)` ) ;
139- console . log ( ` ⚠ WARNING: crypto.subtle unavailable on non-localhost HTTP.` ) ;
140- console . log ( ` ⚠ Access via http://localhost:${ PORT } or set up HTTPS.\n` ) ;
135+ console . log ( `\n ✦ Andromeda → http://localhost:${ PORT } (HTTP — HTTPS cert failed)` ) ;
136+ console . log ( ` ⚠ Access via http://localhost:${ PORT } only — LAN access requires HTTPS.\n` ) ;
141137 } ) ;
142138}
0 commit comments