Net-Base Magazín

23.05.2026

Reverse proxy s nginx a Delphi: správne spracovanie hlavičky Forwarded, skutočná IP klienta a robustné URL základy

Ak servery Delphi-REST bežia za nginx, často dochádza k nesprávnym hodnotám klientskej IP, rozpoznávaniu HTTPS a absolútnych URL. Tento útržok zdrojového kódu ukazuje robustné spracovanie hlavičiek Forwarded/X-Forwarded (vrátane zoznamu dôveryhodných proxy), typické nastavenia nginx a poznámky na ladenie pre prevádzku.

23.05.2026

Reverse proxy s nginx a Delphi je v praxi väčšinou nie „nice to have“, ale čisté oddelenie medzi internetovou hranou a aplikáciou: TLS-terminácia (HTTPS-Offloading), centrálne pravidlá hlavičiek/CORS, rate-limity, jednotné logy, Blue/Green-Rollouts alebo jednoducho hostovanie viacerých služieb pod jednou doménou. To, čo sa často podceňuje: ak nginx „sedí pred“ službou, vidí Delphi-server len IP proxy, často len „http“ namiesto „https“ a generuje nesprávne absolútne odkazy (redirects, callback-URL, OpenAPI-Server-URL). Práve tieto tri body neskôr spôsobujú čas na ladenie v prevádzke.

Tento útržok zdrojového kódu ukazuje robustný vzor, ako v Delphi dôsledne vyhodnocovať Forwarded resp. X-Forwarded-* – vrátane zoznamu dôveryhodných proxy (dôležité proti podvrhovaniu hlavičiek) a konzistentnej Request-Base-URL. K tomu sú priložené prakticky použiteľné konfigurácie nginx a poznámky k okrajovým prípadom ako WebSockets, veľké uploady a time-outy.

Prečo setupy s reverzným proxy mätú Delphi-servery

nginx ako reverzný proxy komunikuje so službou Delphi typicky nešifrovane (HTTP) v internom sieti alebo na localhoste, zatiaľ čo klient z vonka pristupuje cez HTTPS. Bez dodatkových hlavičiek Delphi nevie nič o:

  • Pôvodnom schéme (https vs. http) – relevantné pre presmerovania a absolútne URL.
  • Pôvodnom hoste (zákaznícka doména, port) – relevantné pre multi-tenant nastavenia, CORS a callback-URL.
  • Pôvodnej IP klienta – relevantné pre audit, rate-limity, geo-checky a bezpečnostné analýzy.

nginx môže tieto informácie preniesť pomocou hlavičiek. Bežne sa používajú X-Forwarded-For, X-Forwarded-Proto a X-Forwarded-Host; štandardizovaný je navyše RFC-Header Forwarded. Dôležité: z pohľadu aplikácie tieto hlavičky nie sú automaticky dôveryhodné, lebo ich môže poslať aj klient – stanú sa dôveryhodnými až vtedy, keď pochádzajú od známej proxy.

nginx konfigurácia: minimálne zmysluplné proxy-hlavičky

Solídny východiskový bod (HTTP/1.1, keep-alive, upgrade pre WebSockets) vyzerá takto. Snippet je zámerne stručný; podľa prostredia doplníte HSTS, rate-limity a access-logy.

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;
  }
}

Účel: Aplikácia dostáva Host, klientsku IP a schému spoľahlivo odovzdané. Podmienka: $proxy_add_x_forwarded_for pripojí aktuálnu proxy-IP k prípadnému existujúcemu reťazcu; to je vhodné pre multi-proxy nastavenia, ale robí správne vyhodnocovanie na Delphi-strane o to dôležitejším. Úskalie: Ak v nginx nenastavíte hlavičku Host, môže Delphi prípadne vidieť len upstream-hosta (127.0.0.1), čo rozbije presmerovania a kontroly pôvodu.

Delphi útržok zdrojového kódu: robustné vyhodnocovanie Forwarded/X-Forwarded (s zoznamom dôveryhodných proxy)

Nasledujúci kód je zámerne nezávislý od frameworku: pracuje proti minimálnemu rozhraniu (Header + RemoteIP) a dá sa prispôsobiť pre WebBroker, RAD Server alebo Horse. Kľúčové body:

  • Priorita: RFC Forwarded (ak je prítomný) pred X-Forwarded-*.
  • Dôvera: Forwarded-hlavičky vyhodnocovať iba vtedy, keď je priamy peer (RemoteIP) známe proxy.
  • Parsovanie: zohľadniť IPv6, úvodzovky, porty a reťazce v X-Forwarded-For.
  • Výstup: základná URL, ktorú môžete použiť pre absolútne odkazy, presmerovania alebo OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

uses
  System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;

type
  // Minimálne rozhranie adaptéra: implementujte ho pre WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // priama TCP protistrana (väčšinou nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // napr. 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 môže byť "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 v []: [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 (Pozor: pri neoznačenom IPv6 bez [] nie je spoľahlivé)
  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
  // Príklad: 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;

  // Viacero položiek je oddelených čiarkou; berieme prvú (najbližšie ku klientovi)
  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 môže byť IP alebo "unknown"; IPv6 môže byť v []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // vyskytuje sa aj for=1.2.3.4:5678
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // defensiv
      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
  // Pre skutočnú normalizáciu IPv6 by bolo potrebné parsovanie IP; tu zámerne pragmatické riešenie.
  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; // Náhradne: priama protistrana

  // Iba ak je priama protistrana známym proxy, vyhodnocujeme hlavičky Forwarded.
  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-* ako záloha/doplnok
    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;

  // Ako poslednú možnosť zobrať Host z hlavičky "Host" (ak nie sú žiadne proxy-hlavičky)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Účel: Z každého requestu získate konzistentný pohľad na ClientIP, Proto a Host ako aj na BaseUrl. Tieto informácie môžete centralizovane využiť pre logovanie, bezpečnostné rozhodnutia (napr. povolený zoznam IP adries) a generovanie odkazov.

Prečo je potrebný zoznam dôveryhodných proxy (Trust-Proxy-Liste): Bez overenia dôveryhodnosti by útočník mohol priamo dosiahnuť váš Delphi-port (nesprávna konfigurácia, interné routovanie, VPN) a jednoducho poslať X-Forwarded-For: 127.0.0.1. Tým by boli zraniteľné auditné záznamy, rate-limity alebo koncové body „povolené iba interne“. Dôverujte forwarded-headers iba vtedy, keď je priamy peer (RemoteIP) proxy, ktoré kontrolujete (napr. 127.0.0.1, IP load balancera, Kubernetes-ingress).

Úskalia: IPv6 bez hranatých zátvoriek nie je v notácii Host:port jednoznačné. V HTTP-Host-headeri je IPv6 zvyčajne uvedené v []; riaďte sa týmto pravidlom. Pre komplexné IP-range (CIDR) budete musieť zoznam dôveryhodných proxy rozšíriť (napr. o skutočné parsovanie IP).

Integrácia do WebBroker/Horse/RAD Server: kde sa kód „pripája“

Vo WebBroker (TWebRequest) prichádzajú headery typicky cez ContentFields alebo GetFieldByName, Remote-IP závisí od server-backendu. V Horse (alebo iných HTTP-frameworkoch) zvyčajne nájdete Req.Headers a vlastnosť pre Remote-IP. Dôležité je princíp: RemoteIP musí byť TCP protistrana, nie ľubovoľná hodnota z headeru.

Osvedčený postup: pri štarte služby vytvorte TTrustedProxyList z konfigurácie (INI/ENV), napr. „127.0.0.1“ pre lokálne nginx nasadenia alebo IP vášho load balancera. Následne volajte ResolveForwardedInfo pre každý request a zapíšte polia do svojho štruktúrovaného logovania (JSON-Log, Syslog alebo Windows Event Log).

Debugging v prevádzke: tak nájdete chyby za minúty namiesto hodín

Ak sa requesty správajú „divne“, zriedka je chyba v samotnom Delphi-HTTP; zvyčajne ide o kombináciu proxy-headerov, redirect-logiky a timeoutov. Tri debug-checky, ktoré sa v praxi oplatia:

  1. Cielený výpis hlavičiek (Header-Dump): Pri 4xx/5xx logujte navyše Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent a Request-URI. Ale len pri chybách — inak bude log nákladný a neprehľadný.
  2. Skontrolujte Base-URL: Ak zlyhávajú presmerovania alebo callback-URL, logujte ForwardedInfo.BaseUrl. Mnohé chyby sú okamžite zrejmé („http://127.0.0.1“ namiesto „https://api…“).
  3. Korelácia timeoutov: 504 od proxy nie je to isté ako Delphi-timeout. nginx proxy_read_timeout a Delphi-stranné idle-/read-timeouty musia ladiť spolu.

Okrajové prípady: WebSockets, streaming a veľké požiadavky

WebSockets za nginxom

Pre WebSockets potrebuje nginx správne hlavičky Upgrade a Connection. Navyše backend nesmie „uzavrieť“ spojenie príliš skoro. Na strane Delphi je relevantné, že vaša WebSocket-komponenta (alebo SSE/streaming-endpoint) vie korektne pracovať s reverznými proxy a má implementované čisté heartbeats/keep-alive mechanizmy.

Veľké nahrávania a chyby 413

Klasyka: Delphi akceptuje upload, ale nginx ho predtým zablokuje s 413 Request Entity Too Large. Riadiť to explicitne cez client_max_body_size a prispôsobiť Delphi-stranne limity requestov. Pre procesne blízke softvérové riešenia pracujúce s dokumentami alebo obrazovými dátami to nie je okrajový prípad, ale bežná prevádzka.

HTTPS-Offloading und „Secure Cookies“

Ak váš Delphi-Service nastavuje session cookie, musia byť pri externom HTTPS spravidla označené ako Secure. Či to vaša aplikácia robí, často závisí od toho, či „vie“, že pôvodný request bol HTTPS. Práve tu pomáha konzistentné vyhodnocovanie X-Forwarded-Proto/Forwarded.

Kedy sa námaha oplatí – a kde môže zlyhať

Predstavený prístup sa oplatí vždy, keď Delphi-Service už nie je „naked“ v LAN, ale je súčasťou produkčnej edge: viacero domén, SSO/SAML rozhrania, verejné API, multitenantnosť alebo prísnejšie auditné požiadavky. Zlyhá tam, kde sa Forwarded‑hlavičkám slepo dôveruje alebo proxy topológie nie sú zdokumentované (viaceré Ingress‑stupne, Cloud‑LB plus nginx plus sidecar). V takých scenároch sa client‑IP a scheme rýchlo stanú „niečím“ neurčitým.

Jasná hranica: ak potrebujete komplexné pravidlá dôvery (CIDR, IPv6‑siete, dynamické LB‑IP), mali by ste rozšíriť trust‑kontrolu (skutočné IP‑parsovanie, sieťové masky) alebo navrhnúť infraštruktúru tak, aby len definovaný proxy mohol dosiahnuť port Delphi (Firewall/Security Groups). To je nakoniec zvyčajne robustnejšie prevádzkové rozhodnutie.

Záver: Prevádzkovať reverse proxy s nginx a Delphi správne znamená „Forwarded spracovať korektne“

Reverse Proxy s nginx je pre Delphi-REST-Server dobrý štandardný stavebný blok – ale až korektné zaobchádzanie s Forwarded a X-Forwarded-* robí nasadenie v prevádzke stabilným. Jadro je jednoduché: akceptovať hlavičky len od dôveryhodných proxy, konzistentne odvodiť Client‑IP/Scheme/Host a túto bázu dôsledne uplatniť pri presmerovaniach, logovaní a bezpečnostných kontrolách. So snippetom vyššie máte čisté, legacy‑priateľské fundamentum, ktoré sa dá integrovať do WebBroker, Horse alebo vlastných HTTP‑serverov.

Ak chcete konsolidovať existujúce Delphi‑backend za nginx alebo modernizovať smerom k Delphi REST‑API und REST‑Server s jasnou prevádzkovou líniou, technické review proxy reťazca a vyhodnocovania hlavičiek je často najrýchlejším spôsobom dosiahnuť zlepšenie. Kontaktujte Net-Base pre krátke technické zhodnotenie.

V odbornom prostredí zohrávajú Nginx Reverse Proxy a Forwarded‑hlavičky dôležitú úlohu, ak musia integrácie, dátové toky a ďalší vývoj hladko spolupracovať.

Prediskutovať projekt alebo modernizačný zámer s Net-Base.

Zdieľať príspevok

Tento príspevok priamo zdieľať

LinkedIn, X, XING, Facebook, WhatsApp a e-mail sú ihneď k dispozícii. Pre Instagram pripravujeme priamo odkaz a krátky text.

E-mail

Instagram sa otvorí v novej karte. Odkaz a krátky text sa predtým skopírujú do schránky.