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