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.
# (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.
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í:
- 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í.
- 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…“).
- 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.