Repository files, collaboration, releases and project activity.
ARCHFORGE HISTORY
Blame · src/server.js
Branch main · the Commit author is not necessarily a verified ArchForge account.
e59b7f7cc6archadmin2026-10-11
1 'use strict';e59b7f7cc6archadmin2026-10-11
2 const http=require('node:http');const fs=require('node:fs');const path=require('node:path');e59b7f7cc6archadmin2026-10-11
3 const {readConfig}=require('./config');const {openDatabase,rows,one,run,now,syncServices}=require('./database');e59b7f7cc6archadmin2026-10-11
4 const {serviceDefinitions}=require('./probes');const {startMonitor,uptimeStats,incidentMaintenanceState}=require('./monitor');e59b7f7cc6archadmin2026-10-11
5 const {notifyQueue,processQueue,unsubscribeToken}=require('./notifications');e59b7f7cc6archadmin2026-10-11
6 const {esc,cookies,randomToken,sha,safeEqual,verifyPassword,decrypt,verifyTotp,rateLimiter,validEmail}=require('./security');e59b7f7cc6archadmin2026-10-11
7 const views=require('./views');2109dd33b8archadmin2026-10-12
8 const {presentService}=require('./branding');e59b7f7cc6archadmin2026-10-11
9 const MAX_POST=16384;e59b7f7cc6archadmin2026-10-11
10 function makeApp({db,cfg,disableMonitor=false}={}){e59b7f7cc6archadmin2026-10-11
11 cfg=cfg||readConfig();db=db||openDatabase(cfg.dbPath);e59b7f7cc6archadmin2026-10-11
12 syncServices(db,serviceDefinitions(cfg));e59b7f7cc6archadmin2026-10-11
13 const loginLimit=rateLimiter(15*60000,12),subscribeLimit=rateLimiter(3600000,10),modLimit=rateLimiter(60000,40);e59b7f7cc6archadmin2026-10-11
14 const style=fs.readFileSync(path.resolve(__dirname,'../public/style.css'));e59b7f7cc6archadmin2026-10-11
15 const base=new URL(cfg.publicUrl).origin;e59b7f7cc6archadmin2026-10-11
16 const root=cfg.publicUrl.replace(/\/$/,'');e59b7f7cc6archadmin2026-10-11
17 const send=(res,status,text,{type='text/html; charset=utf-8',headers={}}={})=>{e59b7f7cc6archadmin2026-10-11
18 res.writeHead(status,{'content-type':type,'cache-control':'no-store','x-content-type-options':'nosniff','referrer-policy':'no-referrer','x-frame-options':'DENY','content-security-policy':"default-src 'none'; style-src 'self'; img-src 'self'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'",...headers});res.end(text);e59b7f7cc6archadmin2026-10-11
19 };e59b7f7cc6archadmin2026-10-11
20 function clientIP(req){const direct=req.socket.remoteAddress||'unknown';if(['127.0.0.1','::1','::ffff:127.0.0.1'].includes(direct)){const x=req.headers['x-forwarded-for'];return typeof x==='string'?x.split(',')[0].trim().slice(0,75):direct;}return direct;}e59b7f7cc6archadmin2026-10-11
21 function session(req){const token=cookies(req).af_session;if(!token||!/^[-\w]{40,}$/.test(token))return null;return one(db,'SELECT * FROM sessions WHERE token_hash=? AND expires_at>?',sha(token),now())||null;}e59b7f7cc6archadmin2026-10-11
22 function csrf(req,res,s){if(s)return s.csrf;let token=cookies(req).af_csrf;e59b7f7cc6archadmin2026-10-11
23 if(!token||!/^[\w-]{32,100}$/.test(token)){token=randomToken(24);res.setHeader('Set-Cookie',`af_csrf=${token}; Path=/; Secure; SameSite=Strict; Max-Age=3600`);}return token;e59b7f7cc6archadmin2026-10-11
24 }e59b7f7cc6archadmin2026-10-11
25 function checkCsrf(req,s,data){return !!data.csrf&&safeEqual(data.csrf,s?s.csrf:cookies(req).af_csrf||'');}e59b7f7cc6archadmin2026-10-11
26 const redirect=(res,url,code=303)=>send(res,code,'',{headers:{location:url}});e59b7f7cc6archadmin2026-10-11
27 const fail=(res,status,message)=>send(res,status,views.layout('Error',`<div class="page-head"><h1>${esc(status)}</h1><p>${esc(message)}</p><a class="back" href="/">Back to status →</a></div>`));e59b7f7cc6archadmin2026-10-11
28 async function form(req){return new Promise((resolve,reject)=>{let size=0,chunks=[];req.on('data',c=>{size+=c.length;if(size>MAX_POST){reject(Object.assign(Error('Form too large'),{status:413}));req.destroy();}else chunks.push(c);});req.on('end',()=>{try{const data=new URLSearchParams(Buffer.concat(chunks).toString('utf8'));resolve(Object.fromEntries(data));}catch(e){reject(e);}});req.on('error',reject);});}299229ecf6archadmin2026-10-12
29 // CSRF is enforced separately on every POST. For browser form submissions,299229ecf6archadmin2026-10-12
30 // Fetch Metadata is an additional reliable same-origin signal: browsers set299229ecf6archadmin2026-10-12
31 // Sec-Fetch-Site from the actual request initiator, not from an optional299229ecf6archadmin2026-10-12
32 // Origin/Referer header that a privacy extension can suppress or rewrite.299229ecf6archadmin2026-10-12
33 // Requests with cross-site or same-site (but not same-origin) metadata are299229ecf6archadmin2026-10-12
34 // ALWAYS blocked, even if other headers appear correct.b0c4351f2earchadmin2026-10-11
35 function validOriginHeader(value){b0c4351f2earchadmin2026-10-11
36 if(typeof value!=='string'||value.length>512||!/^https:\/\/[a-z0-9.\-]+(?::443)?\/?$/i.test(value))return false;b0c4351f2earchadmin2026-10-11
37 try{const url=new URL(value);return url.origin===base;}catch{return false;}b0c4351f2earchadmin2026-10-11
38 }299229ecf6archadmin2026-10-12
39 function validRefererHeader(value){299229ecf6archadmin2026-10-12
40 if(typeof value!=='string'||value.length>2048)return false;299229ecf6archadmin2026-10-12
41 try{const url=new URL(value);return url.protocol==='https:'&&url.origin===base&&!url.username&&!url.password;}catch{return false;}299229ecf6archadmin2026-10-12
42 }b0c4351f2earchadmin2026-10-11
43 function requireOrigin(req){299229ecf6archadmin2026-10-12
44 const site=req.headers['sec-fetch-site'];299229ecf6archadmin2026-10-12
45 if(site!==undefined && site!=='same-origin' && site!=='none')return false;299229ecf6archadmin2026-10-12
46 // Sec-Fetch-Site: same-origin is browser-generated and only applies when299229ecf6archadmin2026-10-12
47 // the initiating document is the same origin. A valid, host-only,299229ecf6archadmin2026-10-12
48 // SameSite=Strict cookie and 192-bit CSRF token are still mandatory.299229ecf6archadmin2026-10-12
49 if(site==='same-origin')return true;b0c4351f2earchadmin2026-10-11
50 const origin=req.headers.origin,referer=req.headers.referer;b0c4351f2earchadmin2026-10-11
51 if(origin!==undefined&&!validOriginHeader(origin))return false;299229ecf6archadmin2026-10-12
52 if(referer!==undefined&&!validRefererHeader(referer))return false;299229ecf6archadmin2026-10-12
53 // Older clients may omit Fetch Metadata, Origin or Referer. Strict CSRF299229ecf6archadmin2026-10-12
54 // validation below still prevents unauthorized browser form submissions.e59b7f7cc6archadmin2026-10-11
55 return true;e59b7f7cc6archadmin2026-10-11
56 }e59b7f7cc6archadmin2026-10-11
57 function adminScreen(res,req,s,notice=''){const token=csrf(req,res,s);send(res,200,views.adminDashboard({csrf:token,services:rows(db,'SELECT * FROM services ORDER BY category,label'),incidents:rows(db,'SELECT * FROM incidents ORDER BY started_at DESC LIMIT 20'),maintenance:rows(db,'SELECT * FROM maintenance WHERE ends_at>? ORDER BY starts_at LIMIT 20',now()),queue:one(db,'SELECT COUNT(*) c FROM messages WHERE delivered_at IS NULL AND attempts<8').c,notice}));}e59b7f7cc6archadmin2026-10-11
58 async function router(req,res){let url;try{url=new URL(req.url,base);}catch{return fail(res,400,'Invalid URL');}e59b7f7cc6archadmin2026-10-11
59 const p=url.pathname,s=session(req),isAdmin=!!s;e59b7f7cc6archadmin2026-10-11
60 if(req.method==='GET'){e59b7f7cc6archadmin2026-10-11
61 if(p==='/assets/style.css')return send(res,200,style,{type:'text/css; charset=utf-8',headers:{'cache-control':'public, max-age=3600'}});299229ecf6archadmin2026-10-12
62 if(p==='/healthz')return send(res,200,JSON.stringify({ok:true,service:'archforge-external-status',version:require('../package.json').version}),{type:'application/json; charset=utf-8'});e59b7f7cc6archadmin2026-10-11
63 if(p==='/robots.txt')return send(res,200,'User-agent: *\nDisallow: /admin\nDisallow: /confirm\nDisallow: /unsubscribe\n',{type:'text/plain'});e59b7f7cc6archadmin2026-10-11
64 if(p==='/'){const days=[7,30,90].includes(Number(url.searchParams.get('days')))?Number(url.searchParams.get('days')):30;e59b7f7cc6archadmin2026-10-11
65 return send(res,200,views.homepage({services:uptimeStats(db,days),days,incidents:rows(db,'SELECT * FROM incidents ORDER BY started_at DESC LIMIT 12'),maintenance:rows(db,'SELECT * FROM maintenance WHERE ends_at>? ORDER BY starts_at LIMIT 8',now()),config:cfg,admin:isAdmin}));}2109dd33b8archadmin2026-10-12
66 if(p==='/api/status'){const services=rows(db,'SELECT id,label,description,current_state,last_checked FROM services WHERE configured=1 ORDER BY id').map(presentService);const incidents=rows(db,'SELECT id,title,state,severity,started_at FROM incidents WHERE resolved_at IS NULL ORDER BY started_at DESC');return send(res,200,JSON.stringify({source:'independent-external-monitor',measured_at:now(),services,active_incidents:incidents}),{type:'application/json; charset=utf-8'});}e59b7f7cc6archadmin2026-10-11
67 const service=p.match(/^\/services\/([a-z_]+)$/);if(service){const days=[7,30,90].includes(Number(url.searchParams.get('days')))?Number(url.searchParams.get('days')):30;const found=uptimeStats(db,days,service[1])[0];return found?send(res,200,views.details(found,rows(db,'SELECT * FROM incidents WHERE service_id=? ORDER BY started_at DESC LIMIT 30',found.id),days,isAdmin)):fail(res,404,'Service not found');}e59b7f7cc6archadmin2026-10-11
68 const incident=p.match(/^\/incidents\/(\d+)$/);if(incident){const i=one(db,'SELECT * FROM incidents WHERE id=?',Number(incident[1]));return i?send(res,200,views.incidentPage(i,rows(db,'SELECT * FROM incident_updates WHERE incident_id=? ORDER BY created_at,id',i.id),isAdmin)):fail(res,404,'Incident not found');}e59b7f7cc6archadmin2026-10-11
69 if(p==='/subscribe')return send(res,200,views.subscribe(csrf(req,res,s)));e59b7f7cc6archadmin2026-10-11
70 const confirmation=p.match(/^\/confirm\/([\w-]{35,150})$/);e59b7f7cc6archadmin2026-10-11
71 if(confirmation)return send(res,200,views.layout('Confirm email',`<div class="page-head narrow"><h1>Confirm your email</h1><p>Activate incident notifications from the independent status server.</p></div><div class="panel narrow"><form action="${esc(p)}" method="post">${views.formCsrf?views.formCsrf(csrf(req,res,s)):`<input type="hidden" name="csrf" value="${esc(csrf(req,res,s))}">`}<button class="button">Confirm notifications</button></form></div>`));e59b7f7cc6archadmin2026-10-11
72 const unsubscribe=p.match(/^\/unsubscribe\/([\w-]{35,150})$/);e59b7f7cc6archadmin2026-10-11
73 if(unsubscribe)return send(res,200,views.layout('Unsubscribe',`<div class="page-head narrow"><h1>Unsubscribe</h1><p>You can stop email notifications at any time.</p></div><div class="panel narrow"><form action="${esc(p)}" method="post"><input type="hidden" name="csrf" value="${esc(csrf(req,res,s))}"><button class="button secondary">Unsubscribe from alerts</button></form></div>`));e59b7f7cc6archadmin2026-10-11
74 if(p==='/admin/login')return isAdmin?redirect(res,'/admin'):send(res,200,views.adminLogin(csrf(req,res,s)));e59b7f7cc6archadmin2026-10-11
75 if(p==='/admin')return isAdmin?adminScreen(res,req,s):redirect(res,'/admin/login');e59b7f7cc6archadmin2026-10-11
76 return fail(res,404,'Page not found');e59b7f7cc6archadmin2026-10-11
77 }e59b7f7cc6archadmin2026-10-11
78 if(req.method!=='POST')return fail(res,405,'Method not allowed');e59b7f7cc6archadmin2026-10-11
79 if(!requireOrigin(req))return fail(res,403,'Invalid origin');e59b7f7cc6archadmin2026-10-11
80 let data;try{data=await form(req);}catch(e){return fail(res,e.status||400,'Unable to read submitted form');}e59b7f7cc6archadmin2026-10-11
81 if(!checkCsrf(req,s,data))return fail(res,403,'Invalid or expired form token');e59b7f7cc6archadmin2026-10-11
82 if(p==='/subscribe'){e59b7f7cc6archadmin2026-10-11
83 if(data.website)return redirect(res,'/subscribe');e59b7f7cc6archadmin2026-10-11
84 if(!subscribeLimit(clientIP(req)))return fail(res,429,'Too many requests; please retry later');e59b7f7cc6archadmin2026-10-11
85 if(!validEmail(data.email))return fail(res,400,'Invalid email');e59b7f7cc6archadmin2026-10-11
86 if(!cfg.smtpAvailable)return send(res,503,views.subscribe(csrf(req,res,s),{message:'Email subscriptions are temporarily unavailable.'}));e59b7f7cc6archadmin2026-10-11
87 const email=data.email.trim().toLowerCase(),token=randomToken(32),time=now();e59b7f7cc6archadmin2026-10-11
88 const record=one(db,'SELECT id,confirmed FROM subscriptions WHERE email=?',email);e59b7f7cc6archadmin2026-10-11
89 if(!record||!record.confirmed){e59b7f7cc6archadmin2026-10-11
90 if(record)run(db,'UPDATE subscriptions SET confirm_hash=?,confirm_until=? WHERE id=?',sha(token),time+86400,record.id);e59b7f7cc6archadmin2026-10-11
91 else run(db,'INSERT INTO subscriptions(email,confirmed,confirm_hash,confirm_until,unsubscribe_hash,created_at) VALUES(?,?,?,?,?,?)',email,0,sha(token),time+86400,sha(unsubscribeToken(cfg,email)),time);e59b7f7cc6archadmin2026-10-11
92 run(db,'INSERT INTO messages(channel,destination,subject,body,created_at,next_at) VALUES(?,?,?,?,?,?)','email',email,'Confirm ArchForge Status alerts',`Confirm your subscription (valid 24 hours):\n${root}/confirm/${token}\n\nIf you did not request this, ignore this message.`,time,time);e59b7f7cc6archadmin2026-10-11
93 }e59b7f7cc6archadmin2026-10-11
94 return send(res,200,views.subscribe(csrf(req,res,s),{message:'If this address can subscribe, a confirmation message will arrive shortly.'}));e59b7f7cc6archadmin2026-10-11
95 }e59b7f7cc6archadmin2026-10-11
96 const confirmation=p.match(/^\/confirm\/([\w-]{35,150})$/);e59b7f7cc6archadmin2026-10-11
97 if(confirmation){const r=one(db,'SELECT id FROM subscriptions WHERE confirm_hash=? AND confirm_until>? AND confirmed=0',sha(confirmation[1]),now());if(!r)return fail(res,410,'Invalid or expired confirmation link');run(db,'UPDATE subscriptions SET confirmed=1,confirm_hash=NULL,confirm_until=NULL WHERE id=?',r.id);return send(res,200,views.layout('Confirmed','<div class="page-head"><h1>Email confirmed</h1><p>Independent status notifications are now enabled.</p><a href="/">Back to status →</a></div>'));}e59b7f7cc6archadmin2026-10-11
98 const unsubscribe=p.match(/^\/unsubscribe\/([\w-]{35,150})$/);e59b7f7cc6archadmin2026-10-11
99 if(unsubscribe){run(db,'DELETE FROM subscriptions WHERE unsubscribe_hash=?',sha(unsubscribe[1]));return send(res,200,views.layout('Unsubscribed','<div class="page-head"><h1>Unsubscribed</h1><p>You will no longer receive status email notifications.</p><a href="/">Back to status →</a></div>'));}e59b7f7cc6archadmin2026-10-11
100 if(p==='/admin/login'){e59b7f7cc6archadmin2026-10-11
101 if(!loginLimit(clientIP(req)+'/'+String(data.username).slice(0,80).toLowerCase()))return fail(res,429,'Too many login attempts');e59b7f7cc6archadmin2026-10-11
102 const admin=one(db,'SELECT * FROM admin WHERE id=1'),valid=admin&&data.username===admin.username&&verifyPassword(data.password||'',admin.password_hash)&&verifyTotp(decrypt(cfg.key,admin.totp_encrypted),data.totp||'');e59b7f7cc6archadmin2026-10-11
103 if(!valid)return send(res,401,views.adminLogin(csrf(req,res,s),'Invalid credentials or authenticator code'));e59b7f7cc6archadmin2026-10-11
104 const token=randomToken(40);run(db,'INSERT INTO sessions(token_hash,csrf,expires_at,created_at) VALUES(?,?,?,?)',sha(token),randomToken(32),now()+12*3600,now());e59b7f7cc6archadmin2026-10-11
105 run(db,'INSERT INTO audit(event,created_at) VALUES(?,?)','Admin login',now());e59b7f7cc6archadmin2026-10-11
106 res.setHeader('Set-Cookie',`af_session=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=43200`);e59b7f7cc6archadmin2026-10-11
107 return redirect(res,'/admin');e59b7f7cc6archadmin2026-10-11
108 }e59b7f7cc6archadmin2026-10-11
109 if(!isAdmin)return fail(res,401,'Admin login required');e59b7f7cc6archadmin2026-10-11
110 if(!modLimit(clientIP(req)))return fail(res,429,'Too many admin requests');e59b7f7cc6archadmin2026-10-11
111 if(p==='/admin/logout'){run(db,'DELETE FROM sessions WHERE token_hash=?',s.token_hash);res.setHeader('Set-Cookie','af_session=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=0');return redirect(res,'/admin/login');}e59b7f7cc6archadmin2026-10-11
112 if(p==='/admin/incidents'){e59b7f7cc6archadmin2026-10-11
113 const title=String(data.title||'').trim(),description=String(data.description||'').trim(),severity=data.severity,service=String(data.service||'');e59b7f7cc6archadmin2026-10-11
114 if(!title||title.length>140||!description||description.length>3000||!['degraded','outage'].includes(severity)||(service&&!one(db,'SELECT id FROM services WHERE id=?',service)))return fail(res,400,'Invalid incident fields');e59b7f7cc6archadmin2026-10-11
115 const time=now(),insert=run(db,'INSERT INTO incidents(service_id,title,description,state,severity,automatic,started_at) VALUES(?,?,?,?,?,?,?)',service||null,title,description,'investigating',severity,0,time);e59b7f7cc6archadmin2026-10-11
116 run(db,'INSERT INTO incident_updates(incident_id,state,body,created_at) VALUES(?,?,?,?)',Number(insert.lastInsertRowid),'investigating',description,time);e59b7f7cc6archadmin2026-10-11
117 notifyQueue(db,cfg,'ArchForge incident: '+title,description+'\n\n'+root);e59b7f7cc6archadmin2026-10-11
118 run(db,'INSERT INTO audit(event,created_at) VALUES(?,?)','Incident published '+insert.lastInsertRowid,time);e59b7f7cc6archadmin2026-10-11
119 return redirect(res,'/admin');e59b7f7cc6archadmin2026-10-11
120 }e59b7f7cc6archadmin2026-10-11
121 const update=p.match(/^\/admin\/incidents\/(\d+)\/update$/);e59b7f7cc6archadmin2026-10-11
122 if(update){const i=one(db,'SELECT * FROM incidents WHERE id=?',Number(update[1]));if(!i||i.resolved_at)return fail(res,404,'Active incident not found');const state=data.state,body=String(data.body||'').trim();if(!['investigating','identified','monitoring','resolved'].includes(state)||!body||body.length>3000)return fail(res,400,'Invalid incident update');e59b7f7cc6archadmin2026-10-11
123 run(db,'UPDATE incidents SET state=?,resolved_at=? WHERE id=?',state,state==='resolved'?now():null,i.id);e59b7f7cc6archadmin2026-10-11
124 run(db,'INSERT INTO incident_updates(incident_id,state,body,created_at) VALUES(?,?,?,?)',i.id,state,body,now());notifyQueue(db,cfg,`ArchForge incident ${state}: ${i.title}`,body+'\n\n'+root+'/incidents/'+i.id);return redirect(res,'/admin');e59b7f7cc6archadmin2026-10-11
125 }e59b7f7cc6archadmin2026-10-11
126 if(p==='/admin/maintenance'){e59b7f7cc6archadmin2026-10-11
127 const title=String(data.title||'').trim(),description=String(data.description||'').trim(),start=Date.parse(data.start+'Z')/1000,end=Date.parse(data.end+'Z')/1000,service=data.service||'';e59b7f7cc6archadmin2026-10-11
128 if(!title||title.length>140||!description||description.length>3000||!Number.isFinite(start)||!Number.isFinite(end)||end<=start||end-start>30*86400||start<now()-3600||(service&&!one(db,'SELECT id FROM services WHERE id=?',service)))return fail(res,400,'Invalid maintenance window');e59b7f7cc6archadmin2026-10-11
129 run(db,'INSERT INTO maintenance(title,description,starts_at,ends_at,service_id,created_at) VALUES(?,?,?,?,?,?)',title,description,Math.floor(start),Math.floor(end),service||null,now());notifyQueue(db,cfg,'Scheduled maintenance: '+title,description+'\nStarting: '+new Date(start*1000).toISOString()+'\n'+root);return redirect(res,'/admin');e59b7f7cc6archadmin2026-10-11
130 }e59b7f7cc6archadmin2026-10-11
131 const remove=p.match(/^\/admin\/maintenance\/(\d+)\/delete$/);e59b7f7cc6archadmin2026-10-11
132 if(remove){run(db,'DELETE FROM maintenance WHERE id=?',Number(remove[1]));return redirect(res,'/admin');}e59b7f7cc6archadmin2026-10-11
133 return fail(res,404,'Not found');e59b7f7cc6archadmin2026-10-11
134 }e59b7f7cc6archadmin2026-10-11
135 const server=http.createServer((req,res)=>router(req,res).catch(err=>{console.error('Request error',err?.message);if(!res.headersSent)fail(res,500,'Internal server error');else res.destroy();}));e59b7f7cc6archadmin2026-10-11
136 const monitor=disableMonitor?null:startMonitor(db,cfg);e59b7f7cc6archadmin2026-10-11
137 return {server,db,cfg,monitor,close:async()=>{monitor?.stop();await new Promise(r=>server.close(r));db.close();}};e59b7f7cc6archadmin2026-10-11
138 }e59b7f7cc6archadmin2026-10-11
139 if(require.main===module){const cfg=readConfig();const app=makeApp({cfg});app.server.listen(cfg.port,cfg.host,()=>console.log(`Independent status listening on ${cfg.host}:${cfg.port}`));}e59b7f7cc6archadmin2026-10-11
140 module.exports={makeApp};