Net-Base Revistă

23.05.2026

Reverse Proxy cu nginx și Delphi: tratare corectă a header-ului Forwarded, adresă IP reală a clientului și baze URL robuste

Când serverele Delphi-REST rulează în spatele nginx, de multe ori IP-ul clientului, detectarea HTTPS și URL‑urile absolute devin incorecte. Acest fragment de cod sursă arată un gestionare robustă a antetelor Forwarded/X-Forwarded (inclusiv lista de proxy‑uri de încredere), setări nginx tipice și indicații de depanare pentru operare.

23.05.2026

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.

Delphi
# (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.
Delphi
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ă:

  1. 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.
  2. 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…”).
  3. 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.

Partajează postarea

Distribuiți această postare direct

LinkedIn, X, XING, Facebook, WhatsApp și e-mail sunt disponibile imediat. Pentru Instagram pregătim linkul și textul scurt imediat.

E-mail

Instagram se deschide într-o filă nouă. Linkul și textul scurt se copiază în prealabil în clipboard.