Reverse proxy su nginx ir Delphi praktiškai dažniausiai nėra „nice to have“, o aiški atskirtis tarp interneto ribos ir aplikacijos: TLS terminavimas (HTTPS‑offloading), centralizuotos Header-/CORS taisyklės, užklausų ribojimai (Rate‑Limits), vienodi žurnalai, Blue/Green diegimai arba paprastas kelių paslaugų talpinimas po viena domenu. Dažnai nuvertinama problema: kai nginx stovi „priekyje“, Delphi serveris mato tik proxy IP, dažnai tik „http“ vietoje „https“ ir generuoja neteisingas absoliučias nuorodas (Redirects, Callback‑URL, OpenAPI server URL). Būtent šie trys aspektai vėliau prailgina derinimo laiką eksploatacijoje.
Šis kodo fragmentas demonstruoja tvirtą modelį, kaip Delphi tvarkingai įvertina Forwarded ir X-Forwarded-* antraštes – įskaitant patikimų proxy sąrašą (svarbu prieš antraščių klastojimą) ir nuoseklų Request‑Base‑URL. Pridedamos praktiškos nginx konfigūracijos ir pastabos apie kraštines situacijas, pvz. WebSockets, didelius įkėlimus ir timeout’us.
Kodėl Reverse Proxy konfigūracijos gali „suklaidinti“ Delphi serverius
nginx kaip reverse proxy paprastai bendrauja su Delphi paslauga nešifruotai (HTTP) vidiniame tinkle arba localhost, tuo tarpu klientas išorėje prisijungia per HTTPS. Be papildomų antraščių Delphi nieko nežino apie:
- Original‑Schema (https vs. http) – svarbu peradresavimams ir absoliučioms URL.
- Original‑Host (kliento domenas, portas) – svarbu daugiau‑nuomininko (Multi‑Tenant) konfigūracijoms, CORS ir callback‑URL.
- Original‑Client‑IP – svarbu audito, užklausų apribojimų, geografinio tikrinimo ir saugumo analizėms.
nginx gali perduoti šią informaciją per antraštes. Įprastai naudojami X-Forwarded-For, X-Forwarded-Proto ir X-Forwarded-Host; standartizuota taip pat RFC antraštė Forwarded. Svarbu: iš aplikacijos perspektyvos šios antraštės nėra automatiškai patikimos, nes jas gali siųsti ir klientas – jos tampa patikimos tik tuomet, kai gaunamos iš žinomo proxio.
nginx‑konfigūracija: minimaliai prasmingos proxy‑antraštės
Tvirtas pradinio taško pavyzdys (HTTP/1.1, Keep‑Alive, Upgrade WebSockets) atrodo taip. Kodo fragmentas sąmoningai trumpas; priklausomai nuo aplinkos pridėkite HSTS, užklausų apribojimus ir prieigos žurnalus.
# (nginx-Konfiguration, kein Delphi)
server {
listen 443 ssl http2;
server_name api.example.com;
# ssl_certificate ...; ssl_certificate_key ...;
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# optional, aber praktisch für absolute URLs
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# WebSockets / Upgrade
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_pass http://127.0.0.1:8080;
# Timeouts passend zu Delphi-Backend (lange Reports/Exports)
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# große Uploads explizit steuern
client_max_body_size 50m;
}
}
Paskirtis: Programa gauna Host, kliento IP ir schemą patikimai perduotus. Sąlyga: $proxy_add_x_forwarded_for prideda dabartinio proxy IP prie galimos esamos grandinės; tai naudinga Multi-Proxy konfigūracijoms, tačiau dar labiau pabrėžia teisingo vertinimo svarbą Delphi pusėje. Kliūtis: Jei nginx nepateiksite Host antraštės, Delphi gali matyti tik Upstream-Host (127.0.0.1), kas sulaužo peradresavimus ir Origin-Checks.
Delphi kodo fragmentas: patikimai apdoroti Forwarded/X-Forwarded (su patikimų proxy sąrašu)
Žemiau pateiktas kodas sąmoningai nepriklausomas nuo framework: jis dirba su minimaliu interfeisu (Header + RemoteIP) ir gali būti adaptuotas WebBroker, RAD Server arba Horse. Pagrindiniai punktai:
- Prioritetas: RFC Forwarded (jei yra) prieš X-Forwarded-*.
- Pasitikėjimas: Forwarded antraštes vertinti tik tada, kai tiesioginis peer (RemoteIP) yra žinomas proxy.
- Analizė: atsižvelgti į IPv6, kabutes, portus ir grandines X-Forwarded-For.
- Išvestis: bazinis URL, kurį galite naudoti absoliučioms nuorodoms, peradresavimams arba OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimalus adapterio sąsajos apibrėžimas: įgyvendinkite jį WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // tiesioginis TCP priešininkas (dažniausiai nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // pvz. https://api.example.com
end;
TTrustedProxyList = class
private
FSet: TDictionary;
class function NormalizeIp(const AIP: string): string; static;
public
constructor Create;
destructor Destroy; override;
procedure Add(const AIP: string);
function Contains(const AIP: string): Boolean;
end;
function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;
implementation
function StripBrackets(const S: string): string;
begin
Result := S.Trim;
if (Result.StartsWith(‚[‚)) and (Result.EndsWith(‚]‘)) then
Result := Result.Substring(1, Result.Length – 2);
end;
function RemoveQuotes(const S: string): string;
var
T: string;
begin
T := S.Trim;
if (T.Length >= 2) and (((T[1] = ‚“‚) and (T[T.Length] = ‚“‚)) or ((T[1] = ““) and (T[T.Length] = ““))) then
Result := T.Substring(1, T.Length – 2)
else
Result := T;
end;
function FirstCsvToken(const S: string): string;
var
P: Integer;
begin
// X-Forwarded-For gali būti „client, proxy1, proxy2″
P := S.IndexOf(‚,‘);
if P >= 0 then
Result := S.Substring(0, P).Trim
else
Result := S.Trim;
end;
procedure SplitHostPort(const HostPort: string; out Host: string; out Port: Integer);
var
S: string;
P: Integer;
begin
Host := “;
Port := 0;
S := HostPort.Trim;
// IPv6 tarp []: [2001:db8::1]:443
if S.StartsWith(‚[‚) then
begin
P := S.IndexOf(‚]‘);
if P >= 0 then
begin
Host := StripBrackets(S.Substring(0, P + 1));
if (P + 1 < S.Length) and (S[P + 2] = ‚:‘) then
Port := StrToIntDef(S.Substring(P + 2), 0);
Exit;
end;
end;
// IPv4/hostname: host:port (Pastaba: be [] pateiktas „plikas“ IPv6 gali veikti nepatikimai)
P := S.LastIndexOf(‚:‘);
if (P > 0) and (S.IndexOf(‚:‘) = P) then
begin
Host := S.Substring(0, P);
Port := StrToIntDef(S.Substring(P + 1), 0);
end
else
Host := S;
end;
function ParseForwardedHeader(const ForwardedValue: string; out ClientIP, Proto, Host: string): Boolean;
var
// Pavyzdys: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
Parts: TArray<string>;
I: Integer;
KV: TArray<string>;
K, V: string;
FirstElement: string;
begin
Result := False;
ClientIP := “;
Proto := “;
Host := “;
if ForwardedValue.Trim = “ then
Exit;
// Keli elementai atskirti kableliais; imam pirmą (artimiausią klientui)
FirstElement := FirstCsvToken(ForwardedValue);
Parts := FirstElement.Split([‚;‘]);
for I := 0 to High(Parts) do
begin
KV := Parts[I].Split([‚=‘], 2);
if Length(KV) <> 2 then
Continue;
K := KV[0].Trim.ToLower;
V := RemoveQuotes(KV[1]);
if K = ‚for‘ then
begin
// for gali būti IP arba „unknown“; IPv6 gali būti tarp []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// pasitaiko for=1.2.3.4:5678
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // defensyviai
ClientIP := StripBrackets(V.Split([‚:‘])[0]);
end
else if K = ‚proto‘ then
Proto := V.Trim.ToLower
else if K = ‚host‘ then
Host := V.Trim;
end;
Result := (ClientIP <> “) or (Proto <> “) or (Host <> “);
end;
{ TForwardedInfo }
function TForwardedInfo.EffectiveScheme: string;
begin
if Proto <> “ then
Exit(Proto);
Result := ‚http‘;
end;
function TForwardedInfo.EffectiveHostPort: string;
begin
if Host = “ then
Exit(“);
if (Port > 0) and not ((EffectiveScheme = ‚https‘) and (Port = 443)) and not ((EffectiveScheme = ‚http‘) and (Port = 80)) then
Result := Host + ‚:‘ + Port.ToString
else
Result := Host;
end;
function TForwardedInfo.BaseUrl: string;
var
HP: string;
begin
HP := EffectiveHostPort;
if HP = “ then
Exit(“);
Result := EffectiveScheme + ‚://‘ + HP;
end;
{ TTrustedProxyList }
constructor TTrustedProxyList.Create;
begin
inherited Create;
FSet := TDictionary<string, Boolean>.Create;
end;
destructor TTrustedProxyList.Destroy;
begin
FSet.Free;
inherited;
end;
class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
// Norint tikro IPv6 normalizavimo reikėtų IP analizės; čia sąmoningai pragmatiškas požiūris.
Result := AIP.Trim;
Result := StripBrackets(Result);
end;
procedure TTrustedProxyList.Add(const AIP: string);
begin
FSet.AddOrSetValue(NormalizeIp(AIP), True);
end;
function TTrustedProxyList.Contains(const AIP: string): Boolean;
begin
Result := FSet.ContainsKey(NormalizeIp(AIP));
end;
function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;
var
RemoteIP: string;
Fwd, XFF, XProto, XHost, XPort: string;
ClientIP, Proto, Host: string;
Port: Integer;
begin
Result := Default(TForwardedInfo);
RemoteIP := Req.GetRemoteIP;
Result.ClientIP := RemoteIP; // Atsarginė reikšmė: tiesioginis priešininkas
// Tik jei tiesioginis priešininkas yra žinomas proxy, analizuojame Forwarded antraštes.
if (TrustedProxies <> nil) and TrustedProxies.Contains(RemoteIP) then
begin
Fwd := Req.GetHeaderValue(‚Forwarded‘);
if ParseForwardedHeader(Fwd, ClientIP, Proto, Host) then
begin
if ClientIP <> “ then Result.ClientIP := ClientIP;
if Proto <> “ then Result.Proto := Proto;
if Host <> “ then
begin
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
// X-Forwarded-* kaip atsarginė/papildoma informacija
XFF := Req.GetHeaderValue(‚X-Forwarded-For‘);
if (Result.ClientIP = RemoteIP) and (XFF.Trim <> “) then
Result.ClientIP := StripBrackets(FirstCsvToken(XFF));
XProto := Req.GetHeaderValue(‚X-Forwarded-Proto‘);
if (Result.Proto = “) and (XProto.Trim <> “) then
Result.Proto := FirstCsvToken(XProto).ToLower;
XHost := Req.GetHeaderValue(‚X-Forwarded-Host‘);
if (Result.Host = “) and (XHost.Trim <> “) then
Result.Host := FirstCsvToken(XHost);
XPort := Req.GetHeaderValue(‚X-Forwarded-Port‘);
Port := StrToIntDef(FirstCsvToken(XPort), 0);
if (Result.Port = 0) and (Port > 0) then
Result.Port := Port;
end;
// Esant reikalui, imti Host iš „Host“ antraštės (jei nėra proxy antraštės)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Paskirtis: Iš kiekvieno užklausos gaunate nuoseklų vaizdą apie ClientIP, Proto ir Host, taip pat BaseUrl. Šią informaciją galite centralizuotai naudoti žurnavimui, saugumo sprendimams (pvz., IP leistinų sąrašui) ir nuorodų generavimui.
Kodėl reikalingas Trust-Proxy sąrašas: Be patikros užpuolikas galėtų tiesiogiai pasiekti jūsų Delphi-prievadą (klaidinga konfigūracija, vidinis maršrutavimas, VPN) ir paprasčiausiai siųsti X-Forwarded-For: 127.0.0.1. Tokiu atveju audito įrašai, užklausų dažnio ribojimai (rate-limits) arba galiniai taškai, skirti „tik vidiniam naudojimui“, taptų pažeidžiami. Pasitikėkite Forwarded antraštėmis tik tada, kai tiesioginis peer (RemoteIP) yra tarpinis serveris, kurį kontroliuojate (pvz., 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).
Kliūtys: IPv6 be kvadratinių skliaustų Host:port notacijoje nėra aiškus. HTTP Host antraštėje IPv6 paprastai nurodomas su []; laikykitės šios praktikos. Dėl sudėtingesnių IP diapazonų (CIDR) reikėtų išplėsti Trust-sąrašą (pvz., įgyvendinant tikrą IP-parsinimą).
Integracija į WebBroker/Horse/RAD Server: kur kodas „prisegamas“
WebBroker (TWebRequest) antraštės dažniausiai pateikiamos per ContentFields arba GetFieldByName, o Remote-IP priklauso nuo serverio backend. Horse (ar kituose HTTP framework’uose) dažniausiai turi Req.Headers ir Remote-IP savybę. Svarbiausias principas: RemoteIP turi būti TCP priešininko adresas, o ne bet koks antraštės laukas.
Praktiškai patikrinta: paleidimo metu sukonstruokite TTrustedProxyList iš konfigūracijos (INI/ENV), pvz. „127.0.0.1“ vietiniams nginx nustatymams arba jūsų Load Balancer IP. Tada kvieskite ResolveForwardedInfo kiekvienai užklausai ir rašykite laukus į struktūruotą žurnavimą (JSON-Log, Syslog arba Windows Event Log).
Derinimas veikimo metu: kaip klaidas rasti per minutes vietoje valandų
Jei užklausos atrodo „keistai“, retai kaltas yra pats Delphi-HTTP; dažniau tai proxijų antraščių, peradresavimo logikos ir timeout’ų kombinacija. Trys derinimo patikrinimai, kurie kasdien atsiperka:
- Header-Dump (taikytinas): fiksuokite esant 4xx/5xx papildomai Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent ir Request-URI. Tačiau tik klaidų atvejais — kitaip žurnalas taps brangus ir nevaldomas.
- Patikrinkite Base-URL: jei peradresavimai arba callback URL’ai žlunga, užfiksuokite ForwardedInfo.BaseUrl. Daugelis problemų iš karto tampa akis badančios (pvz., „http://127.0.0.1“ vietoje „https://api…“).
- Timeoutų koreliacija: 504 iš proxio nėra tas pats, kas Delphi-timeout. nginx proxy_read_timeout ir Delphi-pusių Idle-/Read-timeout’ai turi sutapti.
Kraštutiniai atvejai: WebSockets, srautas ir didelės užklausos
WebSockets už nginx
WebSockets veikimui nginx turi teisingai perduoti Upgrade ir Connection. Be to, backend neturi uždaryti ryšio „per anksti“. Delphi pusėje svarbu, kad jūsų WebSocket komponentas (ar SSE/Streaming endpoint’as) tinkamai veiktų su Reverse Proxies ir kad būtų tvarkingai įdiegti Heartbeats/Keep-Alives.
Dideli įkėlimai ir 413 klaidos
Įprastas atvejis: Delphi priima įkėlimą, bet nginx anksčiau blokuoja su 413 Request Entity Too Large. Valdykite tai eksplicitiai per client_max_body_size ir pritaikykite Delphi-pusių Request-Limits. Procesams artimos programinės įrangos sprendimuose, dirbančiuose su dokumentais ar vaizdais, tai nėra išimtis, o normalus darbo režimas.
HTTPS-Offloading und „Secure Cookies“
Jeigu jūsų Delphi-paslauga nustato sesijos slapukus, jie išoriniame HTTPS kontekste paprastai turi būti pažymėti kaip Secure. Ar jūsų aplikacija tai daro, dažnai priklauso nuo to, ar ji „žino“, kad pradinė užklausa buvo HTTPS. Būtent čia padeda nuoseklus X-Forwarded-Proto/Forwarded analizavimas.
Kada pastangos apsimoka – ir kur jos gali tapti problema
Pateiktas požiūris apsimoka visais atvejais, kai Delphi-servisas nebėra „tiesiogiai“ LAN tinkle, o tampa dalimi gamybinės kraštinės: kelios domenų zonos, SSO/SAML sąsajos, viešieji API, daugiamandačiškumas arba griežtesni audito reikalavimai. Jis pradeda byrėti ten, kur Forwarded-antraštėms pasitikima aklai arba proxy topologijos nėra dokumentuotos (kelios Ingress pakopos, Cloud-LB kartu su nginx ir Sidecar). Tada kliento IP ir schemos reikšmė greitai virsta „neaišku“.
Aiški riba: jei jums reikia sudėtingų trust taisyklių (CIDR, IPv6 tinklai, dinaminiai LB IP), turėtumėte išplėsti trust patikrinimą (tikslius IP-parsingo mechanizmus, tinklo kaukes) arba suprojektuoti infrastruktūrą taip, kad tik apibrėžtas proxy galėtų pasiekti Delphi prievadą (Firewall/Security Groups). Galiausiai tai dažnai yra patikimesnis eksploatacijos sprendimas.
Išvada: Reverse Proxy su nginx ir Delphi tvarkingai eksploatuoti reiškia „Forwarded teisingai apdoroti“
Reverse Proxy su nginx yra geras standartinis komponentas Delphi-REST-Server aplinkai – tačiau tik teisingas Forwarded ir X-Forwarded-* valdymas užtikrina stabilumą eksploatacijoje. Esmė paprasta: antraštes priimti tik iš patikimų proxy, nuosekliai nustatyti Client-IP/Scheme/Host ir šią bazę taikyti peradresavimuose, žurnaluose bei saugumo patikrinimuose. Su aukščiau pateiktu snippetu turite švarią, legacy-tinkamą pamatą, kurį galima integruoti į WebBroker, Horse arba savus HTTP serverius.
Jeigu konsoliduojate esamą Delphi-backend už nginx arba norite modernizuoti link Delphi REST-API ir REST-Server su aiškia eksploatacijos linija, techninis proxy grandinės ir antraščių analizės peržiūra dažnai yra greičiausias svertas. Susisiekite su Net-Base trumpai techninei įvertinčiai.
Techniniame kontekste Nginx Reverse Proxy ir Forwarded-antraštės taip pat vaidina svarbų vaidmenį, kai integracijos, duomenų srautai ir tolesnė plėtra turi veikti suderintai.