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-11
1 'use strict';e59b7f7cc6archadmin2026-10-11
2 const dns=require('node:dns').promises,net=require('node:net'),tls=require('node:tls');e59b7f7cc6archadmin2026-10-11
3 const {URL}=require('node:url');e59b7f7cc6archadmin2026-10-11
4 const PROBES=[6b0b538dd6archadmin2026-10-11
5 {id:'website',label:'Main Website',description:'HTTPS reachability (safe same-origin redirects allowed)',category:'Platform',configured:true},e59b7f7cc6archadmin2026-10-11
6 {id:'api',label:'Public API',description:'Read-only HTTP API reachability',category:'Platform',configured:false},2109dd33b8archadmin2026-10-12
7 {id:'git_http',label:'ArchForge HTTPS',description:'Public repository upload-pack discovery (read-only)',category:'ArchForge',configured:false},2109dd33b8archadmin2026-10-12
8 {id:'git_ssh',label:'ArchForge SSH',description:'SSH server handshake/banner',category:'ArchForge',configured:true},e59b7f7cc6archadmin2026-10-11
9 {id:'dns',label:'DNS',description:'Public address resolution',category:'Infrastructure',configured:true},e59b7f7cc6archadmin2026-10-11
10 {id:'tls',label:'TLS Certificate',description:'HTTPS certificate validity / expiration',category:'Infrastructure',configured:true}e59b7f7cc6archadmin2026-10-11
11 ];2109dd33b8archadmin2026-10-12
12 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-11
13 function checkError(err){return String(err?.code||err?.name||'CHECK_FAILED').slice(0,80);}2109dd33b8archadmin2026-10-12
14 // Only the public website reachability probe may follow redirects. API and ArchForge6b0b538dd6archadmin2026-10-11
15 // probes must verify their actual endpoint and must never mistake a login page2109dd33b8archadmin2026-10-12
16 // for a healthy API or a valid ArchForge HTTPS advertisement.6b0b538dd6archadmin2026-10-11
17 const REDIRECT_CODES=new Set([301,302,303,307,308]);6b0b538dd6archadmin2026-10-11
18 const MAX_WEBSITE_REDIRECTS=3;6b0b538dd6archadmin2026-10-11
19 async function checkHttp(url,timeout,{git=false,website=false}={}){e59b7f7cc6archadmin2026-10-11
20 const start=Date.now(),controller=new AbortController(),timer=setTimeout(()=>controller.abort(),timeout);6b0b538dd6archadmin2026-10-11
21 let current;e59b7f7cc6archadmin2026-10-11
22 try {6b0b538dd6archadmin2026-10-11
23 const target=new URL(url);6b0b538dd6archadmin2026-10-11
24 if(!['https:','http:'].includes(target.protocol)||target.username||target.password||target.hash)throw Error('INVALID_TARGET');6b0b538dd6archadmin2026-10-11
25 current=target;6b0b538dd6archadmin2026-10-11
26 const seen=new Set();6b0b538dd6archadmin2026-10-11
27 let redirects=0,firstRedirect=0;6b0b538dd6archadmin2026-10-11
28 for(;;){6b0b538dd6archadmin2026-10-11
29 if(seen.has(current.href))return {success:false,latency:Date.now()-start,detail:'HTTP redirect loop'};6b0b538dd6archadmin2026-10-11
30 seen.add(current.href);2109dd33b8archadmin2026-10-12
31 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-11
32 const status=response.status;6b0b538dd6archadmin2026-10-11
33 const location=response.headers.get('location');6b0b538dd6archadmin2026-10-11
34 const type=String(response.headers.get('content-type')||'');6b0b538dd6archadmin2026-10-11
35 if(response.body)await response.body.cancel().catch(()=>{});6b0b538dd6archadmin2026-10-11
36 if(REDIRECT_CODES.has(status)){6b0b538dd6archadmin2026-10-11
37 // Only the public website probe follows redirects, with HTTPS and6b0b538dd6archadmin2026-10-11
38 // exact origin pinned to the configured website. No downgrade,6b0b538dd6archadmin2026-10-11
39 // cross-host hop, user-info credentials, or unlimited redirect chain.6b0b538dd6archadmin2026-10-11
40 if(!website||git)return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (redirect rejected)`};6b0b538dd6archadmin2026-10-11
41 if(!location)return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (missing Location)`};6b0b538dd6archadmin2026-10-11
42 if(redirects>=MAX_WEBSITE_REDIRECTS)return {success:false,latency:Date.now()-start,detail:'HTTP redirect limit exceeded'};6b0b538dd6archadmin2026-10-11
43 let next;6b0b538dd6archadmin2026-10-11
44 try{next=new URL(location,current);}catch{return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (invalid Location)`};}6b0b538dd6archadmin2026-10-11
45 if(next.origin!==target.origin||next.protocol!==target.protocol||next.username||next.password||next.hash)6b0b538dd6archadmin2026-10-11
46 return {success:false,latency:Date.now()-start,detail:`HTTP ${status} (unsafe redirect rejected)`};6b0b538dd6archadmin2026-10-11
47 if(!firstRedirect)firstRedirect=status;6b0b538dd6archadmin2026-10-11
48 current=next;6b0b538dd6archadmin2026-10-11
49 redirects++;6b0b538dd6archadmin2026-10-11
50 continue;6b0b538dd6archadmin2026-10-11
51 }6b0b538dd6archadmin2026-10-11
52 const good=status===200 && (!git||type.includes('application/x-git-upload-pack-advertisement'));6b0b538dd6archadmin2026-10-11
53 const detail=good6b0b538dd6archadmin2026-10-11
54 ? redirects?`HTTP 200 via ${firstRedirect} (${redirects} same-origin hop${redirects===1?'':'s'}; reachability only)`:'HTTP 200'2109dd33b8archadmin2026-10-12
55 : `HTTP ${status}${redirects?' after safe redirect':''}${git?' (ArchForge advertisement required)':''}`;6b0b538dd6archadmin2026-10-11
56 return {success:good,latency:Date.now()-start,detail};6b0b538dd6archadmin2026-10-11
57 }6b0b538dd6archadmin2026-10-11
58 }catch(e){return {success:false,latency:Date.now()-start,detail:checkError(e)};}e59b7f7cc6archadmin2026-10-11
59 finally{clearTimeout(timer);}e59b7f7cc6archadmin2026-10-11
60 }6b0b538dd6archadmin2026-10-11
61 e59b7f7cc6archadmin2026-10-11
62 function checkTcp(host,port,timeout,ssh=false){return new Promise(resolve=>{e59b7f7cc6archadmin2026-10-11
63 const start=Date.now();let ended=false,buf='';e59b7f7cc6archadmin2026-10-11
64 const socket=net.connect({host,port});e59b7f7cc6archadmin2026-10-11
65 function done(ok,detail){if(ended)return;ended=true;socket.destroy();resolve({success:ok,latency:Date.now()-start,detail});}e59b7f7cc6archadmin2026-10-11
66 socket.setTimeout(timeout,()=>done(false,'TCP timeout'));e59b7f7cc6archadmin2026-10-11
67 socket.on('error',e=>done(false,checkError(e)));e59b7f7cc6archadmin2026-10-11
68 socket.on('connect',()=>{if(!ssh)done(true,'TCP connected');});e59b7f7cc6archadmin2026-10-11
69 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-11
70 socket.on('close',()=>done(false,'Connection closed before SSH banner'));e59b7f7cc6archadmin2026-10-11
71 });}e59b7f7cc6archadmin2026-10-11
72 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-11
73 function checkTls(host,port,timeout){return new Promise(resolve=>{e59b7f7cc6archadmin2026-10-11
74 const start=Date.now();let done=false;e59b7f7cc6archadmin2026-10-11
75 const sock=tls.connect({host,port,servername:host,rejectUnauthorized:true});e59b7f7cc6archadmin2026-10-11
76 const finish=(success,detail)=>{if(done)return;done=true;sock.destroy();resolve({success,latency:Date.now()-start,detail});};e59b7f7cc6archadmin2026-10-11
77 sock.setTimeout(timeout,()=>finish(false,'TLS timeout'));e59b7f7cc6archadmin2026-10-11
78 sock.on('error',err=>finish(false,checkError(err)));e59b7f7cc6archadmin2026-10-11
79 sock.on('secureConnect',()=>{e59b7f7cc6archadmin2026-10-11
80 const cert=sock.getPeerCertificate();const days=cert.valid_to?Math.floor((Date.parse(cert.valid_to)-Date.now())/86400000):-1;e59b7f7cc6archadmin2026-10-11
81 const detail=days<0?'Certificate expired':`Certificate expires in ${days} days`;e59b7f7cc6archadmin2026-10-11
82 if(done)return;done=true;sock.destroy();resolve({success:days>=0,warning:days>=0&&days<14,latency:Date.now()-start,detail});e59b7f7cc6archadmin2026-10-11
83 });e59b7f7cc6archadmin2026-10-11
84 });}e59b7f7cc6archadmin2026-10-11
85 async function runProbe(id,cfg){switch(id){6b0b538dd6archadmin2026-10-11
86 case 'website':return checkHttp(cfg.website,cfg.timeout,{website:true});e59b7f7cc6archadmin2026-10-11
87 case 'api':return cfg.api?checkHttp(cfg.api,cfg.timeout):{success:false,latency:null,detail:'Not configured'};e59b7f7cc6archadmin2026-10-11
88 case 'git_http':{e59b7f7cc6archadmin2026-10-11
89 if(!cfg.gitHttp)return {success:false,latency:null,detail:'Not configured'};2109dd33b8archadmin2026-10-12
90 const u=new URL(cfg.gitHttp);if(!u.pathname.endsWith('/info/refs'))throw Error('ArchForge URL must end with /info/refs');2109dd33b8archadmin2026-10-12
91 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-11
92 }e59b7f7cc6archadmin2026-10-11
93 case 'git_ssh':return checkTcp(cfg.gitHost,cfg.gitPort,cfg.timeout,true);e59b7f7cc6archadmin2026-10-11
94 case 'dns':return checkDns(cfg.websiteHost,cfg.timeout);e59b7f7cc6archadmin2026-10-11
95 case 'tls':return checkTls(cfg.websiteHost,443,cfg.timeout);e59b7f7cc6archadmin2026-10-11
96 default:throw Error('Unknown service '+id);e59b7f7cc6archadmin2026-10-11
97 }}e59b7f7cc6archadmin2026-10-11
98 module.exports={serviceDefinitions,checkHttp,checkTcp,checkDns,checkTls,runProbe};