@@ -2,7 +2,7 @@
const http = require ( 'http' ) ;
const crypto = require ( 'crypto' ) ;
const { Readable } = require ( 'stream' ) ;
const { Readable , Transform , pipeline } = require ( 'stream' ) ;
const KEY = ( process . env . CF _API _KEY || '' ) . trim ( ) ;
const PORT = Number ( process . env . PORT || 8420 ) ;
@@ -14,6 +14,15 @@ const TOKENS = new Set(
) ;
const LIMIT _PRO _MIN = Number ( process . env . RATE _LIMIT _PER _MIN || 60 ) ;
const MAX _MB = Number ( process . env . MAX _DOWNLOAD _MB || 512 ) ;
// gleichzeitige Downloads insgesamt – schützt die Bandbreite, falls jemand mit dem
// (im Launcher steckenden) Token viele große Dateien parallel zieht. 0 = keine Grenze.
const MAX _PARALLEL = Number ( process . env . MAX _PARALLEL _DOWNLOADS ? ? 8 ) ;
// wie lange CurseForge für eine Antwort brauchen darf, bevor die Anfrage abgebrochen wird
const UPSTREAM _TIMEOUT _MS = 30_000 ;
let laufendeDownloads = 0 ;
const SELFTEST _ABSTAND _MS = 30_000 ;
let selftestErgebnis = null ;
let selftestZeit = 0 ;
const PROXY _STUFEN = Math . max ( 0 , Number ( process . env . TRUST _PROXY || 0 ) || 0 ) ;
@@ -157,6 +166,7 @@ async function dateilisteAufloesen(req, res) {
method : 'POST' ,
headers : cfKopfzeilen ( { 'Content-Type' : 'application/json' } ) ,
body : JSON . stringify ( { fileIds : ids } ) ,
signal : AbortSignal . timeout ( UPSTREAM _TIMEOUT _MS ) ,
} ) ;
if ( ! antwort . ok ) {
// Schlüsselprobleme nicht im Wortlaut weiterreichen
@@ -171,33 +181,72 @@ async function dateiDurchreichen(req, res, url) {
if ( ! ziel ) return sendeJson ( res , 400 , { error : 'Adresse fehlt oder zeigt nicht auf das CurseForge-CDN.' } ) ;
// Weiterleitungen selbst verfolgen und jedes Mal den Host neu prüfen
let aktuell = ziel ;
let antwort = null ;
for ( let sprung = 0 ; sprung < 4 ; sprung ++ ) {
antwort = await fetch ( aktuell , { headers : cfKopfzeilen ( ) , redirect : 'manual' } ) ;
const weiter = antwort . headers . get ( 'location' ) ;
if ( antwort . status > = 300 && antwort . status < 400 && weiter ) {
const naechste = pruefeDownloadZiel ( new URL ( weiter , aktuell ) . toString ( ) ) ;
if ( ! naechste ) return sendeJson ( res , 502 , { error : 'Weiterleitung führt aus dem CurseForge-CDN heraus.' } ) ;
aktuell = naechste ;
continue ;
if ( MAX _PARALLEL > 0 && laufendeDownloads > = MAX _PARALLEL ) {
res . setHeader ( 'Retry-After' , '5' ) ;
return sendeJson ( res , 503 , { error : 'Gerade zu viele Downloads, bitte gleich erneut versuchen.' } ) ;
}
laufendeDownloads ++ ;
let frei = false ;
const freigeben = ( ) => { if ( ! frei ) { frei = true ; laufendeDownloads -- ; } } ;
try {
// Zeitlimit nur bis die Antwort-Köpfe da sind – der eigentliche Download darf länger dauern
const abbruch = new AbortController ( ) ;
const frist = setTimeout ( ( ) => abbruch . abort ( ) , UPSTREAM _TIMEOUT _MS ) ;
res . on ( 'close' , ( ) => abbruch . abort ( ) ) ; // Aufrufer weg -> CurseForge-Verbindung schließen
let aktuell = ziel ;
let antwort = null ;
let umgeleitet = true ;
try {
for ( let sprung = 0 ; sprung < 4 && umgeleitet ; sprung ++ ) {
antwort = await fetch ( aktuell , { headers : cfKopfzeilen ( ) , redirect : 'manual' , signal : abbruch . signal } ) ;
const weiter = antwort . headers . get ( 'location' ) ;
if ( antwort . status >= 300 && antwort . status < 400 && weiter ) {
const naechste = pruefeDownloadZiel ( new URL ( weiter , aktuell ) . toString ( ) ) ;
if ( ! naechste ) { freigeben ( ) ; return sendeJson ( res , 502 , { error : 'Weiterleitung führt aus dem CurseForge-CDN heraus.' } ) ; }
aktuell = naechste ;
} else {
umgeleitet = false ;
}
}
} finally {
clearTimeout ( frist ) ;
}
if ( umgeleitet ) { freigeben ( ) ; return sendeJson ( res , 502 , { error : 'Zu viele Weiterleitungen.' } ) ; }
if ( ! antwort || ! antwort . ok ) {
freigeben ( ) ;
return sendeJson ( res , antwort && antwort . status >= 400 ? antwort . status : 502 , { error : 'Download fehlgeschlagen.' } ) ;
}
break ;
}
if ( ! antwort || ! antwort . ok ) {
return sendeJson ( res , antwort ? antwort . status : 502 , { error : 'Download fehlgeschlagen.' } ) ;
}
const laenge = Number ( antwort . headers . get ( 'content-length' ) ) || 0 ;
if ( MAX _MB > 0 && laenge > MAX _MB * 1024 * 1024 ) {
return sendeJson ( res , 413 , { error : ` Datei größer als erlaubt ( ${ MAX _MB } MB). ` } ) ;
}
const laenge = Number ( antwort . headers . get ( 'content-length' ) ) || 0 ;
const grenze = MAX _MB > 0 ? MAX _MB * 1024 * 1024 : 0 ;
if ( grenze && laenge > grenze ) {
freigeben ( ) ;
return sendeJson ( res , 413 , { error : ` Datei größer als erlaubt ( ${ MAX _MB } MB). ` } ) ;
}
res . writeHead ( 200 , {
'Content-Type' : antwort . headers . get ( 'content-type' ) || 'application/octet-stream' ,
... ( laenge ? { 'Content-Length' : String ( laenge ) } : { } ) ,
} ) ;
Readable . fromWeb ( antwort . body ) . pipe ( res ) ;
res . writeHead ( 200 , {
'Content-Type' : antwort . headers . get ( 'content-type' ) || 'application/octet-stream' ,
... ( laenge ? { 'Content-Length' : String ( laenge ) } : { } ) ,
} ) ;
// Auch ohne Content-Length (chunked) mitzählen und bei Überschreitung abbrechen,
// sonst ließe sich die Größengrenze umgehen
let gesamt = 0 ;
const zaehlen = new Transform ( {
transform ( stueck , _enc , fertig ) {
gesamt += stueck . length ;
if ( grenze && gesamt > grenze ) return fertig ( new Error ( 'Datei größer als erlaubt' ) ) ;
fertig ( null , stueck ) ;
} ,
} ) ;
// pipeline räumt auf, wenn eine Seite abbricht (statt wie pipe() offen zu bleiben)
pipeline ( Readable . fromWeb ( antwort . body ) , zaehlen , res , ( ) => freigeben ( ) ) ;
} catch ( err ) {
freigeben ( ) ;
throw err ;
}
}
// ---------------------------------------------------------------------------
@@ -211,7 +260,11 @@ const server = http.createServer(async (req, res) => {
return sendeJson ( res , 200 , { ok : true , dienst : 'aeromc-cf-proxy' } ) ;
}
if ( ! tokenOk ( req ) ) return sendeJson ( res , 401 , { error : 'Nicht berechtigt.' } ) ;
if ( ! tokenOk ( req ) ) {
// Fehlversuche je Absender begrenzen, damit sich das Token nicht in Ruhe erraten lässt
if ( ! darfNoch ( 'falsch:' + absender ( req ) ) ) return sendeJson ( res , 429 , { error : 'Zu viele Anfragen, bitte kurz warten.' } ) ;
return sendeJson ( res , 401 , { error : 'Nicht berechtigt.' } ) ;
}
/*
* Hilfe beim Einstellen von TRUST_PROXY: zeigt, was der Dienst von DIESER
@@ -249,6 +302,16 @@ const server = http.createServer(async (req, res) => {
* niemals den Schlüssel selbst.
*/
if ( url . pathname === '/selftest' ) {
/*
* Das Token steckt im Launcher und ist auslesbar. Jeder Selbsttest kostet fünf
* CurseForge-Anfragen und läuft vor der Ratenbegrenzung – ohne Bremse ließe sich
* damit das Kontingent des Schlüssels aufbrauchen. Deshalb höchstens alle
* 30 Sekunden wirklich ausführen, dazwischen das letzte Ergebnis zeigen.
*/
const jetzt = Date . now ( ) ;
if ( selftestErgebnis && jetzt - selftestZeit < SELFTEST _ABSTAND _MS ) {
return sendeJson ( res , 200 , Object . assign ( { } , selftestErgebnis , { zwischengespeichert : true } ) ) ;
}
const roh = process . env . CF _API _KEY || '' ;
const befund = {
schluessel : {
@@ -280,6 +343,7 @@ const server = http.createServer(async (req, res) => {
method : p . method ,
headers : cfKopfzeilen ( p . body ? { 'Content-Type' : 'application/json' } : undefined ) ,
body : p . body ? JSON . stringify ( p . body ) : undefined ,
signal : AbortSignal . timeout ( UPSTREAM _TIMEOUT _MS ) ,
} ) ;
let text = '' ;
try { text = ( await a . text ( ) ) . replace ( /\s+/g , ' ' ) . trim ( ) . slice ( 0 , 160 ) ; } catch { /* egal */ }
@@ -312,6 +376,8 @@ const server = http.createServer(async (req, res) => {
// Kurzfassung für den schnellen Blick
befund . kurz = befund . proben . map ( ( p ) => ` ${ p . name } : ${ p . status ? ? p . fehler } ` ) ;
selftestErgebnis = befund ;
selftestZeit = Date . now ( ) ;
return sendeJson ( res , 200 , befund ) ;
}