Skip to content

Repository files, collaboration, releases and project activity.

ARCHFORGE HISTORY

Blame · src/probes.js

Branch main · the Commit author is not necessarily a verified ArchForge account.

e59b7f7cc6archadmin2026-10-111 'use strict';
e59b7f7cc6archadmin2026-10-112 const dns=require('node:dns').promises,net=require('node:net'),tls=require('node:tls');
e59b7f7cc6archadmin2026-10-113 const {URL}=require('node:url');
e59b7f7cc6archadmin2026-10-114 const PROBES=[
6b0b538dd6archadmin2026-10-115  {id:'website',label:'Main Website',description:'HTTPS reachability (safe same-origin redirects allowed)',category:'Platform',configured:true},
e59b7f7cc6archadmin2026-10-116  {id:'api',label:'Public API',description:'Read-only HTTP API reachability',category:'Platform',configured:false},
2109dd33b8archadmin2026-10-127  {id:'git_http',label:'ArchForge HTTPS',description:'Public repository upload-pack discovery (read-only)',category:'ArchForge',configured:false},
2109dd33b8archadmin2026-10-128  {id:'git_ssh',label:'ArchForge SSH',description:'SSH server handshake/banner',category:'ArchForge',configured:true},
e59b7f7cc6archadmin2026-10-119  {id:'dns',label:'DNS',description:'Public address resolution',category:'Infrastructure',configured:true},
e59b7f7cc6archadmin2026-10-1110  {id:'tls',label:'TLS Certificate',description:'HTTPS certificate validity / expiration',category:'Infrastructure',configured:true}
e59b7f7cc6archadmin2026-10-1111 ];
2109dd33b8archadmin2026-10-1212 function serviceDefinitions(cfg){return PROBES.map(s=>({...s,configured:s.id==='api'?!!cfg.api:s.id==='git_http'?!!cfg.gitHttp&&new URL(cfg.gitHttp).pathname.endsWith('.archforge/info/refs'):s.configured}));}
e59b7f7cc6archadmin2026-10-1113 function checkError(err){return String(err?.code||err?.name||'CHECK_FAILED').slice(0,80);}
2109dd33b8archadmin2026-10-1214 // Only the public website reachability probe may follow redirects. API and ArchForge
6b0b538dd6archadmin2026-10-1115 // probes must verify their actual endpoint and must never mistake a login page
2109dd33b8archadmin2026-10-1216 // for a healthy API or a valid ArchForge HTTPS advertisement.
6b0b538dd6archadmin2026-10-1117 const REDIRECT_CODES=new Set([301,302,303,307,308]);
6b0b538dd6archadmin2026-10-1118 const MAX_WEBSITE_REDIRECTS=3;
6b0b538dd6archadmin2026-10-1119 async function checkHttp(url,timeout,{git=false,website=false}={}){
e59b7f7cc6archadmin2026-10-1120  const start=Date.now(),controller=new AbortController(),timer=setTimeout(()=>controller.abort(),timeout);
6b0b538dd6archadmin2026-10-1121  let current;
e59b7f7cc6archadmin2026-10-1122  try {
6b0b538dd6archadmin2026-10-1123  const target=new URL(url);
6b0b538dd6archadmin2026-10-1124  if(!['https:','http:'].includes(target.protocol)||target.username||target.password||target.hash)throw Error('INVALID_TARGET');
6b0b538dd6archadmin2026-10-1125  current=target;
6b0b538dd6archadmin2026-10-1126  const seen=new Set();
6b0b538dd6archadmin2026-10-1127  let redirects=0,firstRedirect=0;
6b0b538dd6archadmin2026-10-1128  for(;;){
6b0b538dd6archadmin2026-10-1129  if(seen.has(current.href))return {success:false,latency:Date.now()-start,detail:'HTTP redirect loop'};
6b0b538dd6archadmin2026-10-1130  seen.add(current.href);
2109dd33b8archadmin2026-10-1231  const response=await fetch(current.href,{method:'GET',redirect:'manual',signal:controller.signal,headers:{'user-agent':'ArchForge-External-Monitor/1.0.9','accept':'*/*'}});
6b0b538dd6archadmin2026-10-1132  const status=response.status;
6b0b538dd6archadmin2026-10-1133  const location=response.headers.get('location');
6b0b538dd6archadmin2026-10-1134  const type=String(response.headers.get('content-type')||'');
6b0b538dd6archadmin2026-10-1135  if(response.body)await response.body.cancel().catch(()=>{});
6b0b538dd6archadmin2026-10-1136  if(REDIRECT_CODES.has(status)){
6b0b538dd6archadmin2026-10-1137  // Only the public website probe follows redirects, with HTTPS and
6b0b538dd6archadmin2026-10-1138  // exact origin pinned to the configured website. No downgrade,
6b0b538dd6archadmin2026-10-1139  // cross-host hop, user-info credentials, or unlimited redirect chain.
6b0b538dd6archadmin2026-10-1140  if(!website||git)return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (redirect rejected)`};
6b0b538dd6archadmin2026-10-1141  if(!location)return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (missing Location)`};
6b0b538dd6archadmin2026-10-1142  if(redirects>=MAX_WEBSITE_REDIRECTS)return {success:false,latency:Date.now()-start,detail:'HTTP redirect limit exceeded'};
6b0b538dd6archadmin2026-10-1143  let next;
6b0b538dd6archadmin2026-10-1144  try{next=new URL(location,current);}catch{return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (invalid Location)`};}
6b0b538dd6archadmin2026-10-1145  if(next.origin!==target.origin||next.protocol!==target.protocol||next.username||next.password||next.hash)
6b0b538dd6archadmin2026-10-1146  return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (unsafe redirect rejected)`};
6b0b538dd6archadmin2026-10-1147  if(!firstRedirect)firstRedirect=status;
6b0b538dd6archadmin2026-10-1148  current=next;
6b0b538dd6archadmin2026-10-1149  redirects++;
6b0b538dd6archadmin2026-10-1150  continue;
6b0b538dd6archadmin2026-10-1151  }
6b0b538dd6archadmin2026-10-1152  const good=status===200 && (!git||type.includes('application/x-git-upload-pack-advertisement'));
6b0b538dd6archadmin2026-10-1153  const detail=good
6b0b538dd6archadmin2026-10-1154  ? redirects?`HTTP 200 via ${firstRedirect} (${redirects} same-origin hop${redirects===1?'':'s'}; reachability only)`:'HTTP 200'
2109dd33b8archadmin2026-10-1255  : `HTTP ${status}${redirects?' after safe redirect':''}${git?' (ArchForge advertisement required)':''}`;
6b0b538dd6archadmin2026-10-1156  return {success:good,latency:Date.now()-start,detail};
6b0b538dd6archadmin2026-10-1157  }
6b0b538dd6archadmin2026-10-1158  }catch(e){return {success:false,latency:Date.now()-start,detail:checkError(e)};}
e59b7f7cc6archadmin2026-10-1159  finally{clearTimeout(timer);}
e59b7f7cc6archadmin2026-10-1160 }
6b0b538dd6archadmin2026-10-1161 
e59b7f7cc6archadmin2026-10-1162 function checkTcp(host,port,timeout,ssh=false){return new Promise(resolve=>{
e59b7f7cc6archadmin2026-10-1163  const start=Date.now();let ended=false,buf='';
e59b7f7cc6archadmin2026-10-1164  const socket=net.connect({host,port});
e59b7f7cc6archadmin2026-10-1165  function done(ok,detail){if(ended)return;ended=true;socket.destroy();resolve({success:ok,latency:Date.now()-start,detail});}
e59b7f7cc6archadmin2026-10-1166  socket.setTimeout(timeout,()=>done(false,'TCP timeout'));
e59b7f7cc6archadmin2026-10-1167  socket.on('error',e=>done(false,checkError(e)));
e59b7f7cc6archadmin2026-10-1168  socket.on('connect',()=>{if(!ssh)done(true,'TCP connected');});
e59b7f7cc6archadmin2026-10-1169  socket.on('data',chunk=>{buf+=(chunk.toString('ascii')).slice(0,256);if(buf.includes('\n'))done(buf.split('\n').some(line=>/^SSH-2\.0-/.test(line)),/^SSH-2\.0-/.test(buf)?'SSH banner received':'Unexpected SSH banner');});
e59b7f7cc6archadmin2026-10-1170  socket.on('close',()=>done(false,'Connection closed before SSH banner'));
e59b7f7cc6archadmin2026-10-1171 });}
e59b7f7cc6archadmin2026-10-1172 async function checkDns(host,timeout){const start=Date.now();try{const result=await Promise.race([dns.lookup(host,{all:true}),new Promise((_,reject)=>setTimeout(()=>reject(Error('DNS timeout')),timeout).unref())]);return {success:result.length>0,latency:Date.now()-start,detail:result.length?'DNS resolved':'No address found'};}catch(e){return {success:false,latency:Date.now()-start,detail:checkError(e)};}}
e59b7f7cc6archadmin2026-10-1173 function checkTls(host,port,timeout){return new Promise(resolve=>{
e59b7f7cc6archadmin2026-10-1174  const start=Date.now();let done=false;
e59b7f7cc6archadmin2026-10-1175  const sock=tls.connect({host,port,servername:host,rejectUnauthorized:true});
e59b7f7cc6archadmin2026-10-1176  const finish=(success,detail)=>{if(done)return;done=true;sock.destroy();resolve({success,latency:Date.now()-start,detail});};
e59b7f7cc6archadmin2026-10-1177  sock.setTimeout(timeout,()=>finish(false,'TLS timeout'));
e59b7f7cc6archadmin2026-10-1178  sock.on('error',err=>finish(false,checkError(err)));
e59b7f7cc6archadmin2026-10-1179  sock.on('secureConnect',()=>{
e59b7f7cc6archadmin2026-10-1180  const cert=sock.getPeerCertificate();const days=cert.valid_to?Math.floor((Date.parse(cert.valid_to)-Date.now())/86400000):-1;
e59b7f7cc6archadmin2026-10-1181  const detail=days<0?'Certificate expired':`Certificate expires in ${days} days`;
e59b7f7cc6archadmin2026-10-1182  if(done)return;done=true;sock.destroy();resolve({success:days>=0,warning:days>=0&&days<14,latency:Date.now()-start,detail});
e59b7f7cc6archadmin2026-10-1183  });
e59b7f7cc6archadmin2026-10-1184 });}
e59b7f7cc6archadmin2026-10-1185 async function runProbe(id,cfg){switch(id){
6b0b538dd6archadmin2026-10-1186  case 'website':return checkHttp(cfg.website,cfg.timeout,{website:true});
e59b7f7cc6archadmin2026-10-1187  case 'api':return cfg.api?checkHttp(cfg.api,cfg.timeout):{success:false,latency:null,detail:'Not configured'};
e59b7f7cc6archadmin2026-10-1188  case 'git_http':{
e59b7f7cc6archadmin2026-10-1189  if(!cfg.gitHttp)return {success:false,latency:null,detail:'Not configured'};
2109dd33b8archadmin2026-10-1290  const u=new URL(cfg.gitHttp);if(!u.pathname.endsWith('/info/refs'))throw Error('ArchForge URL must end with /info/refs');
2109dd33b8archadmin2026-10-1291  if(!u.pathname.endsWith('.archforge/info/refs'))return {success:false,latency:null,detail:'Configure .archforge ArchForge HTTPS URL'};u.searchParams.set('service','git-upload-pack');return checkHttp(u.toString(),cfg.timeout,{git:true});
e59b7f7cc6archadmin2026-10-1192  }
e59b7f7cc6archadmin2026-10-1193  case 'git_ssh':return checkTcp(cfg.gitHost,cfg.gitPort,cfg.timeout,true);
e59b7f7cc6archadmin2026-10-1194  case 'dns':return checkDns(cfg.websiteHost,cfg.timeout);
e59b7f7cc6archadmin2026-10-1195  case 'tls':return checkTls(cfg.websiteHost,443,cfg.timeout);
e59b7f7cc6archadmin2026-10-1196  default:throw Error('Unknown service '+id);
e59b7f7cc6archadmin2026-10-1197 }}
e59b7f7cc6archadmin2026-10-1198 module.exports={serviceDefinitions,checkHttp,checkTcp,checkDns,checkTls,runProbe};