Skip to content

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-111 'use strict';
e59b7f7cc6archadmin2026-10-112 const http=require('node:http');const fs=require('node:fs');const path=require('node:path');
e59b7f7cc6archadmin2026-10-113 const {readConfig}=require('./config');const {openDatabase,rows,one,run,now,syncServices}=require('./database');
e59b7f7cc6archadmin2026-10-114 const {serviceDefinitions}=require('./probes');const {startMonitor,uptimeStats,incidentMaintenanceState}=require('./monitor');
e59b7f7cc6archadmin2026-10-115 const {notifyQueue,processQueue,unsubscribeToken}=require('./notifications');
e59b7f7cc6archadmin2026-10-116 const {esc,cookies,randomToken,sha,safeEqual,verifyPassword,decrypt,verifyTotp,rateLimiter,validEmail}=require('./security');
e59b7f7cc6archadmin2026-10-117 const views=require('./views');
2109dd33b8archadmin2026-10-128 const {presentService}=require('./branding');
e59b7f7cc6archadmin2026-10-119 const MAX_POST=16384;
e59b7f7cc6archadmin2026-10-1110 function makeApp({db,cfg,disableMonitor=false}={}){
e59b7f7cc6archadmin2026-10-1111  cfg=cfg||readConfig();db=db||openDatabase(cfg.dbPath);
e59b7f7cc6archadmin2026-10-1112  syncServices(db,serviceDefinitions(cfg));
e59b7f7cc6archadmin2026-10-1113  const loginLimit=rateLimiter(15*60000,12),subscribeLimit=rateLimiter(3600000,10),modLimit=rateLimiter(60000,40);
e59b7f7cc6archadmin2026-10-1114  const style=fs.readFileSync(path.resolve(__dirname,'../public/style.css'));
e59b7f7cc6archadmin2026-10-1115  const base=new URL(cfg.publicUrl).origin;
e59b7f7cc6archadmin2026-10-1116  const root=cfg.publicUrl.replace(/\/$/,'');
e59b7f7cc6archadmin2026-10-1117  const send=(res,status,text,{type='text/html; charset=utf-8',headers={}}={})=>{
e59b7f7cc6archadmin2026-10-1118  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-1119  };
e59b7f7cc6archadmin2026-10-1120  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-1121  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-1122  function csrf(req,res,s){if(s)return s.csrf;let token=cookies(req).af_csrf;
e59b7f7cc6archadmin2026-10-1123  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-1124  }
e59b7f7cc6archadmin2026-10-1125  function checkCsrf(req,s,data){return !!data.csrf&&safeEqual(data.csrf,s?s.csrf:cookies(req).af_csrf||'');}
e59b7f7cc6archadmin2026-10-1126  const redirect=(res,url,code=303)=>send(res,code,'',{headers:{location:url}});
e59b7f7cc6archadmin2026-10-1127  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-1128  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-1229  // CSRF is enforced separately on every POST. For browser form submissions,
299229ecf6archadmin2026-10-1230  // Fetch Metadata is an additional reliable same-origin signal: browsers set
299229ecf6archadmin2026-10-1231  // Sec-Fetch-Site from the actual request initiator, not from an optional
299229ecf6archadmin2026-10-1232  // Origin/Referer header that a privacy extension can suppress or rewrite.
299229ecf6archadmin2026-10-1233  // Requests with cross-site or same-site (but not same-origin) metadata are
299229ecf6archadmin2026-10-1234  // ALWAYS blocked, even if other headers appear correct.
b0c4351f2earchadmin2026-10-1135  function validOriginHeader(value){
b0c4351f2earchadmin2026-10-1136  if(typeof value!=='string'||value.length>512||!/^https:\/\/[a-z0-9.\-]+(?::443)?\/?$/i.test(value))return false;
b0c4351f2earchadmin2026-10-1137  try{const url=new URL(value);return url.origin===base;}catch{return false;}
b0c4351f2earchadmin2026-10-1138  }
299229ecf6archadmin2026-10-1239  function validRefererHeader(value){
299229ecf6archadmin2026-10-1240  if(typeof value!=='string'||value.length>2048)return false;
299229ecf6archadmin2026-10-1241  try{const url=new URL(value);return url.protocol==='https:'&&url.origin===base&&!url.username&&!url.password;}catch{return false;}
299229ecf6archadmin2026-10-1242  }
b0c4351f2earchadmin2026-10-1143  function requireOrigin(req){
299229ecf6archadmin2026-10-1244  const site=req.headers['sec-fetch-site'];
299229ecf6archadmin2026-10-1245  if(site!==undefined && site!=='same-origin' && site!=='none')return false;
299229ecf6archadmin2026-10-1246  // Sec-Fetch-Site: same-origin is browser-generated and only applies when
299229ecf6archadmin2026-10-1247  // the initiating document is the same origin. A valid, host-only,
299229ecf6archadmin2026-10-1248  // SameSite=Strict cookie and 192-bit CSRF token are still mandatory.
299229ecf6archadmin2026-10-1249  if(site==='same-origin')return true;
b0c4351f2earchadmin2026-10-1150  const origin=req.headers.origin,referer=req.headers.referer;
b0c4351f2earchadmin2026-10-1151  if(origin!==undefined&&!validOriginHeader(origin))return false;
299229ecf6archadmin2026-10-1252  if(referer!==undefined&&!validRefererHeader(referer))return false;
299229ecf6archadmin2026-10-1253  // Older clients may omit Fetch Metadata, Origin or Referer. Strict CSRF
299229ecf6archadmin2026-10-1254  // validation below still prevents unauthorized browser form submissions.
e59b7f7cc6archadmin2026-10-1155  return true;
e59b7f7cc6archadmin2026-10-1156  }
e59b7f7cc6archadmin2026-10-1157  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-1158  async function router(req,res){let url;try{url=new URL(req.url,base);}catch{return fail(res,400,'Invalid URL');}
e59b7f7cc6archadmin2026-10-1159  const p=url.pathname,s=session(req),isAdmin=!!s;
e59b7f7cc6archadmin2026-10-1160  if(req.method==='GET'){
e59b7f7cc6archadmin2026-10-1161  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-1262  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-1163  if(p==='/robots.txt')return send(res,200,'User-agent: *\nDisallow: /admin\nDisallow: /confirm\nDisallow: /unsubscribe\n',{type:'text/plain'});
e59b7f7cc6archadmin2026-10-1164  if(p==='/'){const days=[7,30,90].includes(Number(url.searchParams.get('days')))?Number(url.searchParams.get('days')):30;
e59b7f7cc6archadmin2026-10-1165  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-1266  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-1167  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-1168  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-1169  if(p==='/subscribe')return send(res,200,views.subscribe(csrf(req,res,s)));
e59b7f7cc6archadmin2026-10-1170  const confirmation=p.match(/^\/confirm\/([\w-]{35,150})$/);
e59b7f7cc6archadmin2026-10-1171  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-1172  const unsubscribe=p.match(/^\/unsubscribe\/([\w-]{35,150})$/);
e59b7f7cc6archadmin2026-10-1173  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-1174  if(p==='/admin/login')return isAdmin?redirect(res,'/admin'):send(res,200,views.adminLogin(csrf(req,res,s)));
e59b7f7cc6archadmin2026-10-1175  if(p==='/admin')return isAdmin?adminScreen(res,req,s):redirect(res,'/admin/login');
e59b7f7cc6archadmin2026-10-1176  return fail(res,404,'Page not found');
e59b7f7cc6archadmin2026-10-1177  }
e59b7f7cc6archadmin2026-10-1178  if(req.method!=='POST')return fail(res,405,'Method not allowed');
e59b7f7cc6archadmin2026-10-1179  if(!requireOrigin(req))return fail(res,403,'Invalid origin');
e59b7f7cc6archadmin2026-10-1180  let data;try{data=await form(req);}catch(e){return fail(res,e.status||400,'Unable to read submitted form');}
e59b7f7cc6archadmin2026-10-1181  if(!checkCsrf(req,s,data))return fail(res,403,'Invalid or expired form token');
e59b7f7cc6archadmin2026-10-1182  if(p==='/subscribe'){
e59b7f7cc6archadmin2026-10-1183  if(data.website)return redirect(res,'/subscribe');
e59b7f7cc6archadmin2026-10-1184  if(!subscribeLimit(clientIP(req)))return fail(res,429,'Too many requests; please retry later');
e59b7f7cc6archadmin2026-10-1185  if(!validEmail(data.email))return fail(res,400,'Invalid email');
e59b7f7cc6archadmin2026-10-1186  if(!cfg.smtpAvailable)return send(res,503,views.subscribe(csrf(req,res,s),{message:'Email subscriptions are temporarily unavailable.'}));
e59b7f7cc6archadmin2026-10-1187  const email=data.email.trim().toLowerCase(),token=randomToken(32),time=now();
e59b7f7cc6archadmin2026-10-1188  const record=one(db,'SELECT id,confirmed FROM subscriptions WHERE email=?',email);
e59b7f7cc6archadmin2026-10-1189  if(!record||!record.confirmed){
e59b7f7cc6archadmin2026-10-1190  if(record)run(db,'UPDATE subscriptions SET confirm_hash=?,confirm_until=? WHERE id=?',sha(token),time+86400,record.id);
e59b7f7cc6archadmin2026-10-1191  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-1192  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-1193  }
e59b7f7cc6archadmin2026-10-1194  return send(res,200,views.subscribe(csrf(req,res,s),{message:'If this address can subscribe, a confirmation message will arrive shortly.'}));
e59b7f7cc6archadmin2026-10-1195  }
e59b7f7cc6archadmin2026-10-1196  const confirmation=p.match(/^\/confirm\/([\w-]{35,150})$/);
e59b7f7cc6archadmin2026-10-1197  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-1198  const unsubscribe=p.match(/^\/unsubscribe\/([\w-]{35,150})$/);
e59b7f7cc6archadmin2026-10-1199  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-11100  if(p==='/admin/login'){
e59b7f7cc6archadmin2026-10-11101  if(!loginLimit(clientIP(req)+'/'+String(data.username).slice(0,80).toLowerCase()))return fail(res,429,'Too many login attempts');
e59b7f7cc6archadmin2026-10-11102  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-11103  if(!valid)return send(res,401,views.adminLogin(csrf(req,res,s),'Invalid credentials or authenticator code'));
e59b7f7cc6archadmin2026-10-11104  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-11105  run(db,'INSERT INTO audit(event,created_at) VALUES(?,?)','Admin login',now());
e59b7f7cc6archadmin2026-10-11106  res.setHeader('Set-Cookie',`af_session=${token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=43200`);
e59b7f7cc6archadmin2026-10-11107  return redirect(res,'/admin');
e59b7f7cc6archadmin2026-10-11108  }
e59b7f7cc6archadmin2026-10-11109  if(!isAdmin)return fail(res,401,'Admin login required');
e59b7f7cc6archadmin2026-10-11110  if(!modLimit(clientIP(req)))return fail(res,429,'Too many admin requests');
e59b7f7cc6archadmin2026-10-11111  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-11112  if(p==='/admin/incidents'){
e59b7f7cc6archadmin2026-10-11113  const title=String(data.title||'').trim(),description=String(data.description||'').trim(),severity=data.severity,service=String(data.service||'');
e59b7f7cc6archadmin2026-10-11114  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-11115  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-11116  run(db,'INSERT INTO incident_updates(incident_id,state,body,created_at) VALUES(?,?,?,?)',Number(insert.lastInsertRowid),'investigating',description,time);
e59b7f7cc6archadmin2026-10-11117  notifyQueue(db,cfg,'ArchForge incident: '+title,description+'\n\n'+root);
e59b7f7cc6archadmin2026-10-11118  run(db,'INSERT INTO audit(event,created_at) VALUES(?,?)','Incident published '+insert.lastInsertRowid,time);
e59b7f7cc6archadmin2026-10-11119  return redirect(res,'/admin');
e59b7f7cc6archadmin2026-10-11120  }
e59b7f7cc6archadmin2026-10-11121  const update=p.match(/^\/admin\/incidents\/(\d+)\/update$/);
e59b7f7cc6archadmin2026-10-11122  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-11123  run(db,'UPDATE incidents SET state=?,resolved_at=? WHERE id=?',state,state==='resolved'?now():null,i.id);
e59b7f7cc6archadmin2026-10-11124  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-11125  }
e59b7f7cc6archadmin2026-10-11126  if(p==='/admin/maintenance'){
e59b7f7cc6archadmin2026-10-11127  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-11128  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-11129  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-11130  }
e59b7f7cc6archadmin2026-10-11131  const remove=p.match(/^\/admin\/maintenance\/(\d+)\/delete$/);
e59b7f7cc6archadmin2026-10-11132  if(remove){run(db,'DELETE FROM maintenance WHERE id=?',Number(remove[1]));return redirect(res,'/admin');}
e59b7f7cc6archadmin2026-10-11133  return fail(res,404,'Not found');
e59b7f7cc6archadmin2026-10-11134  }
e59b7f7cc6archadmin2026-10-11135  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-11136  const monitor=disableMonitor?null:startMonitor(db,cfg);
e59b7f7cc6archadmin2026-10-11137  return {server,db,cfg,monitor,close:async()=>{monitor?.stop();await new Promise(r=>server.close(r));db.close();}};
e59b7f7cc6archadmin2026-10-11138 }
e59b7f7cc6archadmin2026-10-11139 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-11140 module.exports={makeApp};