Net-Base Magazín

23.05.2026

Reverse proxy s nginx a Delphi: správné zpracování hlavičky Forwarded, skutečná IP klienta a robustní základní cesty URL

Když servery Delphi-REST běží za nginx, často dochází k přepsání klientské IP, k chybné detekci HTTPS a k nesprávným absolutním URL. Tento úryvek zdrojového kódu ukazuje robustní zpracování hlaviček Forwarded/X-Forwarded (včetně seznamu důvěryhodných proxy), typická nastavení nginx a diagnostické poznámky pro provoz.

23.05.2026

Reverzní proxy s nginx a Delphi je v praxi většinou nikoli „nice to have“, ale čisté oddělení mezi internetovým okrajem a aplikací: TLS-terminace (HTTPS-Offloading), centrální pravidla pro hlavičky/CORS, Rate-Limits, jednotné logy, Blue/Green nasazení nebo prostě hostování více služeb pod jednou doménou. Co se často podceňuje: jakmile nginx stojí „před“ službou, vidí Delphi-server už jen IP proxy, často jen „http“ místo „https“ a generuje nesprávné absolutní odkazy (Redirects, Callback-URLs, OpenAPI-Server-URL). Právě tyto tři body později způsobují čas na ladění v provozu.

Tento ukázkový zdrojový útržek demonstruje robustní vzor, jak ve Delphi správně vyhodnocovat Forwarded resp. X-Forwarded-* – včetně seznamu důvěryhodných proxy (důležité proti header-spoofingu) a konzistentní Request-Base-URL. K tomu jsou praktické nginx konfigurace a poznámky k okrajovým případům jako WebSockets, velké uploady a timeouts.

Proč nastavení reverzní proxy „matou“ servery Delphi

nginx jako reverzní proxy obvykle komunikuje se službou Delphi nešifrovaně (HTTP) v interní síti nebo na localhostu, zatímco klient přistupuje zvenčí přes HTTPS. Bez doplňujících hlaviček Delphi neví nic o:

  • Původním schématu (https vs. http) – relevantní pro Redirects a absolutní URL.
  • Původním hostu (zákaznická doména, port) – relevantní pro multi-tenant nasazení, CORS a Callback-URLs.
  • Původní IP klienta – relevantní pro audit, Rate-Limits, geo-checky a bezpečnostní analýzy.

nginx může tyto informace předat pomocí hlaviček. Obvyklé jsou X-Forwarded-For, X-Forwarded-Proto a X-Forwarded-Host; standardizovaně existuje navíc RFC hlavička Forwarded. Důležité: tyto hlavičky z pohledu aplikace nejsou automaticky důvěryhodné, protože je může poslat i klient sám – stanou se důvěryhodnými teprve tehdy, když pocházejí od známé proxy.

nginx konfigurace: minimálně smysluplné proxy-hlavičky

Solidní výchozí nastavení (HTTP/1.1, Keep-Alive, Upgrade pro WebSockets) může vypadat následovně. Útržek je záměrně stručný; podle prostředí doplníte HSTS, Rate-Limits a 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;
  }
}

Účel: Aplikace obdrží Host, Client-IP a schéma spolehlivě předané. Podmínka: $proxy_add_x_forwarded_for připojuje aktuální IP proxy k případně existujícímu řetězci; to je dobré pro multi-proxy nasazení, ale činí správné vyhodnocení na straně Delphi o to důležitějším. Úskalí: Pokud v nginxu nedefinujete hlavičku Host, může Delphi případně vidět jen upstream hosta (127.0.0.1), což narušuje přesměrování a ověření původu.

Delphi úryvek zdrojového kódu: Robustní vyhodnocení Forwarded/X-Forwarded (se seznamem důvěryhodných proxy)

Následující kód je záměrně framework‑nezávislý: pracuje proti minimálnímu rozhraní (Header + RemoteIP) a lze jej adaptovat pro WebBroker, RAD Server nebo Horse. Klíčové body:

  • Priorita: RFC Forwarded (pokud je přítomen) před X-Forwarded-*.
  • Trust: Forwarded‑hlavičky vyhodnocovat jen pokud je přímý peer (RemoteIP) známou proxy.
  • Parsing: zohlednit IPv6, uvozovky, porty a řetězce v X-Forwarded-For.
  • Output: Base-URL, kterou můžete použít pro absolutní odkazy, přesměrování nebo OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimální rozhraní adaptéru: implementujte ho pro WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // přímý TCP protějšek (většinou nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // např. 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 být "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: u holého IPv6 bez [] není spolehlivé)
  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
  // Pří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;

  // Více prvků je odděleno čárkou; vezmeme první (nejblíže 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 být IP nebo "unknown"; IPv6 může být v []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // vyskytuje se for=1.2.3.4:5678
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // defensivně
      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
  // Pro skutečnou normalizaci IPv6 by bylo potřeba parsování IP; zde záměrně pragmaticky.
  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; // Záložně: přímý protějšek

  // Jen pokud je přímý protějšek známý proxy, vyhodnocujeme Forwarded hlavičky.
  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-* jako záložní/ doplněk
    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;

  // V krajním případě převzít Host z hlavičky "Host" (pokud nejsou 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: Ze každého requestu získáte konzistentní pohled na ClientIP, Proto a Host i na BaseUrl. Tyto informace můžete centrálně používat pro logování, bezpečnostní rozhodnutí (např. seznam povolených IP — IP-Allowlist) a generování odkazů.

Proč je potřeba seznam důvěryhodných proxy: Bez kontroly důvěry by útočník mohl přímo dosáhnout vašeho Delphi-portu (chybné nastavení, interní směrování, VPN) a jednoduše poslat X-Forwarded-For: 127.0.0.1. Tím by byly zranitelné auditní stopy, rate-limity nebo endpointy určené pouze pro interní provoz. Důvěřujte Forwarded‑hlavičkám pouze pokud je přímý peer (RemoteIP) proxy, které kontrolujete (např. 127.0.0.1, IP load balanceru, Kubernetes‑Ingress).

Úskalí: IPv6 bez hranatých závorek není v notaci Host:port jednoznačné. V HTTP-Host-Headeru se IPv6 běžně uvádí v []; dodržujte to. Pro složitější IP‑rozsahy (CIDR) budete muset rozšířit seznam důvěryhodných proxy (např. přidáním skutečného parsování IP).

Integrace do WebBroker/Horse/RAD Server: kde se kód „připojuje“

Ve WebBrokeru (TWebRequest) se hlavičky typicky čtou přes ContentFields nebo GetFieldByName, Remote‑IP závisí na serverovém backendu. V Horse (nebo jiných HTTP frameworkech) obvykle najdete Req.Headers a vlastnost s Remote‑IP. Důležité je princip: RemoteIP musí být TCP protějšek, ne nějaká hodnota z hlavičky.

Osvědčené v praxi: při startu služby vytvořte z konfigurace (INI/ENV) TTrustedProxyList, např. „127.0.0.1“ pro lokální nginx nastavení nebo IP vašeho load balanceru. Poté voláte ResolveForwardedInfo pro každý request a zapisujete pole do strukturovaného logování (JSON‑log, Syslog nebo Windows Event Log).

Ladění v provozu: jak najít chyby za minuty místo hodin

Pokud se requesty zdají „divné“, zřídka je to samotný Delphi‑HTTP; obvykle jde o kombinaci proxy‑hlaviček, redirect logiky a timeoutů. Tři kontrolní body pro debugging, které se v praxi vyplatí:

  1. Výpis hlaviček (cíleně): Logujte při 4xx/5xx navíc Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent a Request-URI. Jen při chybách – jinak se log rychle prodraží a znepřehlední.
  2. Kontrola Base‑URL: Pokud selhávají redirecty nebo callback URL, logujte ForwardedInfo.BaseUrl. Mnoho chyb je hned zřejmých („http://127.0.0.1“ místo „https://api…“).
  3. Korelace timeoutů: 504 od proxy není totéž co Delphi‑timeout. nginx proxy_read_timeout a Delphi‑stranné Idle/Read timeouty musí ladit.

Okrajové případy: WebSockets, streamování a velké požadavky

WebSockety za nginx

Pro WebSockety vyžaduje nginx správné hlavičky Upgrade a Connection. Navíc backend nesmí „zavřít“ příliš brzy. Na straně Delphi je důležité, aby vaše WebSocket komponenta (nebo SSE/streaming endpoint) uměla pracovat s reverse proxy a aby byly korektně implementované heartbeats/keep‑alives.

Velké nahrávky a chyba 413

Klasika: Delphi přijme upload, ale nginx ho před tím zablokuje s 413 Request Entity Too Large. Řiďte to explicitně přes client_max_body_size a upravte na straně Delphi limity požadavků. Pro procesně blízké softwarové řešení pracující s dokumenty nebo obrázky to není výjimka, ale běžný provoz.

HTTPS offloading a „Secure Cookies“

Pokud váš Delphi-Service nastavuje sessionové Cookies, musí být při externím HTTPS obvykle označeny jako Secure. Zda to vaše aplikace dělá, často závisí na tom, zda „ví“, že původní požadavek byl přes HTTPS. Právě zde pomáhá konzistentní vyhodnocení X-Forwarded-Proto/Forwarded.

Kdy se námaha vyplatí – a kde může selhat

Předložený přístup se vyplatí vždy, pokud Delphi-Service už není „nahý“ v LAN, ale je součástí produkčního okraje: více domén, SSO/SAML rozhraní, veřejná API, multitenantnost nebo přísnější auditorské požadavky. Selhává tam, kde se Forwarded‑hlavičkám slepě důvěřuje nebo nejsou zdokumentovány proxy‑topologie (více ingress úrovní, Cloud‑LB plus nginx plus Sidecar). Pak se client‑IP a schéma rychle stanou „něčím“.

Jasná hranice: Pokud potřebujete komplexní pravidla důvěry (CIDR, IPv6 sítě, dynamické LB‑IP), měli byste rozšířit trust‑kontrolu (skutečné IP‑parsování, síťové masky) nebo navrhnout infrastrukturu tak, aby jen definovaný proxy mohl dosáhnout portu Delphi (Firewall/Security Groups). To je nakonec většinou robustnější provozní rozhodnutí.

Fazit: Reverse Proxy mit nginx und Delphi sauber betreiben heißt „Forwarded richtig machen“

Reverzní proxy s nginx je pro Delphi-REST-Server solidní standardní stavební kámen – ale teprve korektní zpracování Forwarded a X-Forwarded-* dělá nasazení stabilním v provozu. Jádro je prosté: hlavičky akceptovat pouze od důvěryhodných proxy, konzistentně odvodit Client‑IP/Scheme/Host a tuto bázi prosadit v přesměrováních, logování a bezpečnostních kontrolách. Se zmiňovaným snippetem máte pro to čisté, legacy‑přátelské fundamentum, které lze integrovat do WebBrokeru, Horse nebo vlastních HTTP serverů.

Pokud chcete konsolidovat existující Delphi‑backend za nginx nebo modernizovat směrem k Delphi REST‑API a REST‑Server s jasnou provozní linií, je technické review proxy‑řetězce a vyhodnocení hlaviček často nejrychlejší páka. Kontaktujte Net-Base pro krátké technické posouzení.

V odborném kontextu hrají nginx reverzní proxy a hlavičky Forwarded klíčovou roli, když musí integrace, datové toky a další vývoj spolu konzistentně fungovat.

Projekt nebo modernizační záměr s Net-Base projednat.

Sdílet příspěvek

Sdílet tento příspěvek přímo

LinkedIn, X, XING, Facebook, WhatsApp a e-mail jsou ihned k dispozici. Pro Instagram připravíme odkaz a stručný text.

E-mail

Instagram se otevře v nové záložce. Odkaz a krátký text budou předtím zkopírovány do schránky.