Un Reverse Proxy cu nginx și Delphi nu este în practică de obicei un „nice to have”, ci separarea clară între marginea Internetului și aplicație: terminarea TLS (HTTPS-Offloading), reguli centrale pentru header/CORS, rate-limits, jurnale uniforme, Blue/Green-Rollouts sau, pur și simplu, găzduirea mai multor servicii sub un domeniu. Ceea ce se subestimează frecvent: de îndată ce nginx stă „în față”, serverul Delphi vede doar IP-ul proxy-ului, adesea doar „http” în loc de „https” și generează linkuri absolute incorecte (Redirects, Callback-URLs, OpenAPI-Server-URL). Tocmai aceste trei puncte provoacă mai târziu timp de depanare în exploatare.
Acest fragment de cod arată un model robust pentru cum să evaluați în Delphi Forwarded sau X-Forwarded-* în mod corect – inclusiv o listă Trust-Proxy (importantă împotriva header-spoofing-ului) și o Request-Base-URL consecventă. Pe lângă aceasta există configurații nginx practice pentru producție și indicații privind cazuri marginale precum WebSockets, uploaduri mari și timeouts.
De ce configurațiile cu Reverse Proxy „derutează“ serverele Delphi
nginx, ca Reverse Proxy, comunică tipic cu serviciul Delphi necriptat (HTTP) în rețeaua internă sau pe localhost, în timp ce clientul vine din exterior prin HTTPS. Fără headere suplimentare, Delphi nu are informații despre:
- Schema originală (https vs. http) – relevantă pentru Redirects și URL-uri absolute.
- Host-ul original (domeniu specific clientului, port) – relevant pentru configurări multi-tenant, CORS și Callback-URLs.
- IP-ul clientului original – relevant pentru audit, Rate-Limits, verificări geo și analize de securitate.
nginx poate transporta aceste informații prin headere. Uzuale sunt X-Forwarded-For, X-Forwarded-Proto și X-Forwarded-Host; standardizat este suplimentar headerul RFC Forwarded. Important: Aceste headere nu sunt automat de încredere din perspectiva aplicației, pentru că un client le poate trimite el însuși – ele devin de încredere doar dacă provin de la un proxy cunoscut.
Configurația nginx: headerele proxy minim necesare
Un punct de plecare solid (HTTP/1.1, Keep-Alive, Upgrade pentru WebSockets) arată astfel. Fragmentul este intenționat concis; în funcție de mediu completați cu HSTS, Rate-Limits și Access-Logs.
# (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;
}
}
Scop: Aplicația primește în mod fiabil Host, Client-IP și schemă. Condiție: $proxy_add_x_forwarded_for atașează IP-ul proxy curent la un lanț eventual existent; asta e util pentru configurații multi-proxy, dar face analiza corectă pe partea Delphi cu atât mai importantă. Capcană: Dacă nu setați în nginx antetul Host, Delphi poate vedea doar hostul upstream (127.0.0.1), ceea ce strică redirecționările și verificările de origine.
Delphi Fragment sursă: evaluarea robustă a Forwarded/X-Forwarded (cu listă Trust-Proxy)
Codul următor este intenționat independent de framework: operează pe o interfață minimă (Header + RemoteIP) și poate fi adaptat în WebBroker, RAD Server sau Horse. Punctele cheie:
- Prioritate: RFC Forwarded (dacă este prezent) înainte de X-Forwarded-*.
- Trust: Evaluați anteturile Forwarded doar dacă peer-ul direct (RemoteIP) este un proxy cunoscut.
- Parsing: luați în considerare IPv6, ghilimelele, porturile și lanțurile din X-Forwarded-For.
- Output: o Base-URL pe care o puteți utiliza pentru linkuri absolute, redirecționări sau OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Interfață minimă de adaptor: implementați-o pentru WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // endpoint TCP direct (de obicei nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // de ex. 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 poate fi '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 în []: [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/Host: host:port (Atenție: pentru IPv6 neîncadrat fără [] nu este fiabil)
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
// Exemplu: 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;
// Mai multe elemente sunt separate prin virgulă; luăm primul (cel mai aproape de client)
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 poate fi IP sau 'unknown'; IPv6 poate fi între []
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// pot apărea valori de tipul for=1.2.3.4:5678
if V.Contains(':') and (not V.StartsWith('[')) then
V := FirstCsvToken(V); // preventiv
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
// Pentru o normalizare IPv6 completă ar fi necesar un parser de IP; aici adoptăm o abordare pragmatică.
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; // Valoare implicită: gazdă directă
// Evaluăm headerele Forwarded doar dacă gazda directă este un proxy cunoscut.
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-* ca rezervă/completare
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;
// În ultimă instanță preluăm Host din headerul "Host" (dacă nu există headere de proxy)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Scop: Veți obține din fiecare request o vedere consistentă asupra ClientIP, Proto și Host precum și o BaseUrl. Aceste informații le puteți folosi centralizat pentru logging, decizii de securitate (de ex. allowlist pentru IP-uri) și generare de linkuri.
De ce este necesară lista Trust-Proxy: Fără verificarea de încredere, un atacator ar putea ajunge direct la portul Delphi (configurare greșită, rutare internă, VPN) și pur și simplu să trimită X-Forwarded-For: 127.0.0.1. Astfel ar fi vulnerabile audit-trails, rate-limits sau endpoint-uri „doar intern permise”. Aveți încredere în headerele Forwarded doar dacă peer-ul direct (RemoteIP) este un proxy pe care îl controlați (de ex. 127.0.0.1, IP-ul load-balancer-ului, Kubernetes-Ingress).
Capcane: IPv6 fără paranteze drepte nu este în mod clar definit în notația Host:port. În header-ul HTTP-Host, IPv6 este în mod normal notat în []; respectați acest format. Pentru intervale IP complexe (CIDR) va trebui să extindeți lista de încredere (de ex. printr-un parsing IP real).
Integrare în WebBroker/Horse/RAD Server: unde se „atașează“ codul
În WebBroker (TWebRequest) headerele ajung de obicei prin ContentFields sau GetFieldByName, iar Remote-IP depinde de backend-ul serverului. În Horse (sau alte framework-uri HTTP) există de regulă Req.Headers și o proprietate pentru Remote-IP. Important este principiul: RemoteIP trebuie să fie contra-partea TCP, nu vreun valoare dintr-un header.
Practica indică: creați la pornirea serviciului o TTrustedProxyList din configurație (INI/ENV), de ex. „127.0.0.1” pentru setup-uri locale cu nginx sau IP-ul load balancer-ului dvs. Apoi apelați ResolveForwardedInfo pentru fiecare request și scrieți câmpurile în logging-ul structurat (JSON-Log, Syslog sau Windows Event Log).
Depanare în producție: cum găsiți erorile în minute în loc de ore
Când request-urile par „ciudate”, rar e vina HTTP-ul din Delphi în sine; de obicei e o combinație de headere de proxy, logică de redirect și timeout-uri. Trei verificări de debug care merită în practică:
- Header-Dump (țintit): Logați la 4xx/5xx suplimentar Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent și Request-URI. Dar doar la erori – altfel logul devine scump și neclar.
- Verificați Base-URL: Dacă redirecturile sau callback-urile eșuează, logați ForwardedInfo.BaseUrl. Multe erori sunt vizibile imediat („http://127.0.0.1” în loc de „https://api…”).
- Corelația timeout-urilor: Un 504 de la proxy nu este același lucru cu un timeout pe Delphi. nginx proxy_read_timeout și timeout-urile Idle-/Read pe Delphi trebuie aliniate.
Cazuri limită: WebSockets, streaming și cereri mari
WebSockets în spatele nginx
Pentru WebSockets, nginx trebuie să gestioneze corect Upgrade și Connection. În plus, backend-ul nu trebuie să se închidă „prea devreme”. Pe partea Delphi este relevant ca componenta dvs. WebSocket (sau un endpoint SSE/Streaming) să poată gestiona Reverse Proxies și să implementeze curat Heartbeats/Keep-Alives.
Încărcări mari și erori 413
Un clasic: Delphi acceptă un upload, dar nginx blochează anterior cu 413 Request Entity Too Large. Controlați asta explicit prin client_max_body_size și adaptați la nivel Delphi limitele de request. Pentru soluții software orientate pe proces, care gestionează documente sau imagini, acesta nu este un caz special, ci funcționare normală.
Offloading HTTPS și „Secure Cookies”
Dacă serviciul dvs. Delphi setează cookie‑uri de sesiune, acestea trebuie de regulă marcate ca Secure când sunt folosite prin HTTPS extern. Dacă aplicația dvs. face acest lucru depinde adesea de faptul dacă „știe” că cererea inițială a fost HTTPS. Exact aici ajută evaluarea consecventă a X-Forwarded-Proto/Forwarded.
Când merită efortul – și unde poate eșua
Abordarea prezentată merită întotdeauna când serviciul Delphi nu mai „trăiește” dezbrăcat în LAN, ci face parte dintr‑o margine productivă: mai multe domenii, interfețe SSO/SAML, API‑uri publice, multi‑tenant sau cerințe de audit mai stricte. Ea devine problematică acolo unde se are încredere oarbă în header‑ele Forwarded sau topologiile proxy nu sunt documentate (mai multe niveluri de ingress, Cloud-LB plus nginx plus sidecar). Atunci adresa IP a clientului și schema devin rapid „ceva”.
O limită clară: dacă aveți nevoie de reguli de trust complexe (CIDR, rețele IPv6, IP‑uri dinamice ale LB), ar trebui să extindeți verificarea de încredere (parsing real al IP‑urilor, măști de rețea) sau să proiectați infrastructura astfel încât doar un proxy definit să poată accesa portul Delphi (Firewall/Security Groups). În final, aceasta este de obicei decizia operațională mai robustă.
Concluzie: operarea corectă a unui reverse proxy cu nginx și Delphi înseamnă „a trata Forwarded corect”
Un reverse proxy cu nginx este pentru Delphi-REST-Server un bun element standard – dar doar tratarea corectă a Forwarded și X-Forwarded-* face ca ansamblul să fie stabil în operare. Esența este simplă: acceptați header‑urile doar de la proxy‑uri de încredere, deduceți consistent Client-IP/Scheme/Host și aplicați această bază în redirecturi, logging și verificările de securitate. Cu snippet‑ul de mai sus aveți un fundament curat, compatibil cu sisteme legacy, care se poate integra în WebBroker, Horse sau în servere HTTP proprii.
Dacă doriți să consolidați un backend Delphi existent în spatele nginx sau să îl modernizați către Delphi REST‑API și REST‑Server cu o linie de operare clară, o revizie tehnică a lanțului de proxy și a evaluării header‑elor este adesea cea mai rapidă pârghie. Contactați Net-Base pentru o scurtă evaluare tehnică.
În contextul profesional, nginx reverse proxy și header‑ele Forwarded joacă de asemenea un rol important atunci când integrările, fluxurile de date și dezvoltarea ulterioară trebuie să funcționeze coerent.
Discutați proiectul sau inițiativa de modernizare cu Net-Base.