Ein Reverse Proxy mit nginx und Delphi ist in der Praxis meist kein „nice to have“, sondern die saubere Trennung zwischen Internetkante und Applikation: TLS-Terminierung (HTTPS-Offloading), zentrale Header-/CORS-Regeln, Rate-Limits, einheitliche Logs, Blue/Green-Rollouts oder einfach das Hosting mehrerer Services unter einer Domain. Was dann gerne unterschätzt wird: Sobald nginx „davor“ sitzt, sieht der Delphi-Server nur noch die Proxy-IP, oft nur noch „http“ statt „https“ und generiert falsche absolute Links (Redirects, Callback-URLs, OpenAPI-Server-URL). Genau diese drei Punkte sorgen später für Debugging-Zeit im Betrieb.
Dieser Source-Schnipsel zeigt ein robustes Muster, wie Sie in Delphi Forwarded bzw. X-Forwarded-* sauber auswerten – inklusive Trust-Proxy-Liste (wichtig gegen Header-Spoofing) und einer konsistenten Request-Base-URL. Dazu gibt es praxistaugliche nginx-Konfigurationen und Hinweise zu Randfällen wie WebSockets, große Uploads und Timeouts.
Warum Reverse Proxy-Setups Delphi-Server „verwirren“
nginx spricht als Reverse Proxy mit dem Delphi-Service typischerweise unverschlüsselt (HTTP) im internen Netz oder auf localhost, während der Client außen per HTTPS kommt. Ohne zusätzliche Header weiß Delphi nichts von:
- Original-Schema (https vs. http) – relevant für Redirects und absolute URLs.
- Original-Host (kundenspezifische Domain, Port) – relevant für Multi-Tenant-Setups, CORS und Callback-URLs.
- Original-Client-IP – relevant für Audit, Rate-Limits, Geo-Checks und Security-Auswertungen.
nginx kann diese Informationen über Header transportieren. Üblich sind X-Forwarded-For, X-Forwarded-Proto und X-Forwarded-Host; standardisiert ist zusätzlich der RFC-Header Forwarded. Wichtig: Diese Header sind aus Sicht der Applikation nicht automatisch vertrauenswürdig, weil ein Client sie selbst schicken kann – sie werden erst vertrauenswürdig, wenn sie von einem bekannten Proxy stammen.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Ein solider Startpunkt (HTTP/1.1, Keep-Alive, Upgrade für WebSockets) sieht so aus. Das Snippet ist bewusst knapp gehalten; Sie ergänzen je nach Umgebung HSTS, Rate-Limits und 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;
}
}
Svrha: Aplikaciji se pouzdano prosljeđuju Host, Client-IP i shema. Uvjet: $proxy_add_x_forwarded_for dodaje trenutnu proxy-IP na eventualni postojeći niz; to je korisno za multi-proxy okruženja, ali čini ispravnu evaluaciju na strani Delphi još važnijom. Zamka: Ako u nginx-u ne postavite Host-zaglavlje, Delphi možda vidi samo Upstream-Host (127.0.0.1), što može narušiti preusmjeravanja i provjere porijekla.
Delphi Izvorni isječak: Robusno tumačenje Forwarded/X-Forwarded (s listom povjerenih proxyja)
Sljedeći kod je namjerno zadržan neutralnim prema frameworku: radi protiv minimalnog sučelja (zaglavlja + RemoteIP) i može se prilagoditi za WebBroker, RAD Server ili Horse. Ključne tačke:
- Prioritet: RFC Forwarded (ako postoji) ispred X-Forwarded-*.
- Povjerenje: Forwarded-zaglavlja tumačiti samo ako je direktni peer (RemoteIP) poznati proxy.
- Parsiranje: Uzmite u obzir IPv6, navodnike, portove i nizove u X-Forwarded-For.
- Izlaz: bazna URL koju možete koristiti za apsolutne linkove, preusmjeravanja ili OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimalni adapter interfejs: implementirajte ga za WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // direktni TCP-peer (obično nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // npr. 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 može biti „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 u []: [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 (Pažnja: za gol IPv6 bez [] nije pouzdano)
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
// Primjer: 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;
// Više elemenata razdvojeno zarezom; uzimamo prvi (najbliži klijentu)
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 može biti IP ili „unknown“; IPv6 može biti u []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// for=1.2.3.4:5678 se pojavljuje
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // predostrožno
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
// Za pravu IPv6 normalizaciju trebalo bi parsiranje IP-a; ovdje svjesno pragmatično.
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; // Podrazumijevano: direktni peer
// Samo ako je direktni peer poznati proxy, analiziramo Forwarded zaglavlja.
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-* kao rezervna opcija/dopuna
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 ništa drugo, uzmi Host iz „Host“ zaglavlja (ako nema proxy zaglavlja)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Svrha: Iz svakog zahtjeva dobijate konzistentan prikaz ClientIP, Proto i Host kao i BaseUrl. Te informacije možete centralno koristiti za logiranje, sigurnosne odluke (npr. IP-allowlist) i generiranje linkova.
Zašto je lista pouzdanih proxyja (Trust-Proxy-Liste) potrebna: Bez provjere povjerenja napadač bi mogao direktno dosegnuti vaš Delphi-port (pogrešna konfiguracija, interno rutiranje, VPN) i jednostavno poslati X-Forwarded-For: 127.0.0.1. Time bi audit-zapisi, rate-limiti ili endpointi „samo za internu upotrebu“ bili izloženi. Vjerujte forwarded-headerima samo ako je direktni peer (RemoteIP) proxy koji kontrolirate (npr. 127.0.0.1, IP load-balancera, Kubernetes-Ingress).
Zamke: IPv6 bez uglastih zagrada nije jednoznačan u Host:port notaciji. U HTTP-Host-headeru se IPv6 obično navodi u []; pridržavajte se toga. Za kompleksne IP-opsege (CIDR) trebali biste proširiti trust-listu (npr. stvarnim parsiranjem IP-a).
Integracija u WebBroker/Horse/RAD Server: gdje se kod „priključuje“
U WebBrokeru (TWebRequest) headeri obično dolaze preko ContentFields ili GetFieldByName, a Remote-IP ovisi o server-backendu. U Horseu (ili drugim HTTP-frameworkima) obično postoje Req.Headers i svojstvo za Remote-IP. Bitno je načelo: RemoteIP mora biti TCP-protupartner, a ne neki vrijednost iz headera.
Praktično provjereno: pri pokretanju servisa napravite TTrustedProxyList iz konfiguracije (INI/ENV), npr. „127.0.0.1″ za lokalne nginx postavke ili IP vašeg load balancera. Zatim pozovite ResolveForwardedInfo po zahtjevu i upišite polja u strukturirano logiranje (JSON-Log, Syslog ili Windows Event Log).
Debugiranje u radu: kako pronaći greške u minutama umjesto sati
Ako zahtjevi izgledaju „čudno“, rijetko je kriv Delphi-HTTP sam po sebi, već kombinacija proxy-headera, redirect-logike i timeouta. Tri debug-provjere koje se isplate u praksi:
- Header-dump (ciljano): Logirajte pri 4xx/5xx dodatno Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent i Request-URI. Ali samo kod grešaka – inače će log postati skup i nepregledan.
- Provjera Base-URL-a: Ako redirecti ili callback-URL-ovi ne uspijevaju, logirajte ForwardedInfo.BaseUrl. Mnoge pogreške su odmah vidljive („http://127.0.0.1″ umjesto „https://api…“).
- Timeout-korelacija: 504 od proxyja nije isto što i Delphi-timeout. nginx proxy_read_timeout i Delphi-strani Idle-/Read-timeouti moraju biti usklađeni.
Rubni slučajevi: WebSockets, streaming i veliki zahtjevi
WebSockets iza nginx-a
Za WebSockets nginx treba imati ispravno postavljene Upgrade i Connection. Dodatno, backend ne smije „preuranjeno“ zatvoriti vezu. Na Delphi-strani je važno da vaša WebSocket-komponenta (ili SSE/streaming-endpoint) zna raditi s reverse proxyjima i da su heartbeat/keep-alive mehanizmi uredno implementirani.
Veliki uploadi i 413 greške
Klasika: Delphi prihvati upload, ali nginx prethodno blokira s 413 Request Entity Too Large. Kontrolirajte to eksplicitno preko client_max_body_size i prilagodite Delphi-strane ograničenja zahtjeva. Za softverska rješenja koja su blizu procesima s dokumentima ili slikama, ovo nije iznimka, već normalan rad.
HTTPS-Offloading und „Secure Cookies“
Ako vaš Delphi-Service postavlja Session-Cookies, oni se pri eksternom HTTPS u pravilu moraju označiti kao Secure. Da li vaša aplikacija to radi često ovisi o tome zna li da je izvorni zahtjev bio HTTPS. Upravo ovdje pomaže dosljedna evaluacija X-Forwarded-Proto/Forwarded.
Kada se ulaganje isplati – i gdje može zakazati
Prikazani pristup se isplati uvijek kada Delphi-Service više nije „golog“ u LAN-u, nego dio produkcijske ivice: više domena, SSO/SAML-sučelja, javni API-ji, multitenantnost ili strožiji zahtjevi za audit. On prestaje biti pouzdan tamo gdje se Forwarded-Headerima slijepo vjeruje ili proxy-topologije nisu dokumentirane (više Ingress-stupnjeva, Cloud-LB plus nginx plus Sidecar). U tom slučaju Client-IP i shema brzo postanu „neodređene“.
Jasna granica: Ako vam trebaju kompleksna pravila povjerenja (CIDR, IPv6-netovi, dinamičke LB-IP adrese), trebali biste proširiti Trust-provjeru (pravo IP-parsiranje, mrežne maske) ili infrastrukturu tako oblikovati da samo definisani proxy može dosegnuti Delphi-port (Firewall/Security Groups). Na kraju je to često robustniji odluka za operativu.
Fazit: Reverse Proxy mit nginx und Delphi sauber betreiben heißt „Forwarded richtig machen“
Reverse Proxy s nginx je za Delphi-REST-Server dobar standardni gradivni blok – ali tek ispravno rukovanje Forwarded i X-Forwarded-* čini setup stabilnim u produkciji. Suština je jednostavna: Header prihvaćati samo od pouzdanih proxyja, Client-IP/Scheme/Host konzistentno izvoditi i tu osnovu provući kroz redirect-e, logging i security-procese. Sa snippetom gore imate čvrst, legacy-kompatibilan temelj koji se može integrirati u WebBroker, Horse ili vlastite HTTP-servere.
Ako želite konsolidirati postojeći Delphi-backend iza nginx-a ili modernizirati prema Delphi REST-API i REST-Server s jasnom operativnom linijom, tehnički review proxy-lanca i evaluacije headera često je najbrži poluga. Kontaktirajte Net-Base za kratku tehničku procjenu.
U stručnom kontekstu Nginx Reverse Proxy i Forwarded Headeri također igraju važnu ulogu kada integracije, tokovi podataka i dalji razvoj moraju uredno surađivati.
Razgovarajte o projektu ili modernizacijskom poduhvatu s Net-Base.