Net-Base Časopis

23.05.2026

Reverse proxy s nginxom i Delphi: ispravno rukovanje Forwarded zaglavljem, stvarna IP adresa klijenta i robusne URL baze

Kad Delphi-REST serveri rade iza nginx-a, često su Client-IP, prepoznavanje HTTPS-a i apsolutni URL-ovi netačno proslijeđeni. Ovaj isječak koda prikazuje robusno rukovanje zaglavljima Forwarded/X-Forwarded (uključujući listu pouzdanih proxyja), tipične nginx postavke i smjernice za debugiranje u operativnom radu.

23.05.2026

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.

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

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:

  1. 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.
  2. 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…“).
  3. 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.

Podijeli objavu

Ovu objavu direktno proslijediti

LinkedIn, X, XING, Facebook, WhatsApp i e-pošta su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novom tabu. Link i kratak tekst se prethodno kopiraju u međuspremnik.