Ein Reverse Proxy mit nginx und Delphi je u praksi obično nije „nice to have“, nego čista separacija između internet-ruba i aplikacije: TLS-terminacija (HTTPS-Offloading), centralna pravila za Header/CORS, Rate-Limits, jedinstveni logovi, Blue/Green-Rollouts ili jednostavno hosting više servisa pod jednom domenom. Ono što se često podcijeni: čim nginx «sjedi ispred», Delphi-server vidi samo IP proxyja, često samo „http“ umjesto „https“ i generira pogrešne apsolutne linkove (Redirects, Callback-URLs, OpenAPI-Server-URL). Upravo ta tri problema kasnije stvaraju vrijeme za debugiranje u radu.
Ovaj isječak koda prikazuje robustan obrazac kako u Delphi ispravno evaluirati Forwarded odnosno X-Forwarded-* — uključujući popis pouzdanih proxyja (važno protiv spoofanja zaglavlja) i dosljedan Request-Base-URL. Uz to dolaze praktične nginx konfiguracije i napomene o rubnim slučajevima poput WebSockets, velikih uploadova i timeouta.
Warum Reverse Proxy-Setups Delphi-Server „verwirren“
nginx kao Reverse Proxy obično komunicira sa Delphi servisom nešifrirano (HTTP) u internoj mreži ili na localhostu, dok klijent izvana dolazi putem HTTPS-a. Bez dodatnih zaglavlja Delphi ne zna ništa o:
- Original-Schema (https vs. http) – relevantno za Redirects i apsolutne URL-ove.
- Original-Host (domena specifična za klijenta, port) – relevantno za Multi-Tenant-Setups, CORS i Callback-URLs.
- Original-Client-IP – relevantno za Audit, Rate-Limits, Geo-Checks i sigurnosne analize.
nginx može te informacije prenijeti preko zaglavlja. Uobičajena su X-Forwarded-For, X-Forwarded-Proto i X-Forwarded-Host; dodatno je standardizirano RFC zaglavlje Forwarded. Važno: ta zaglavlja s točke gledišta aplikacije nisu automatski pouzdana, jer ih klijent može poslati sam — postaju pouzdana tek kada dolaze od poznatog proxyja.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Solidna polazna točka (HTTP/1.1, Keep-Alive, Upgrade za WebSockets) izgleda ovako. Isječak je namjerno kratak; ovisno o okruženju dodajte HSTS, Rate-Limits i 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: Aplikacija pouzdano prima Host, Client-IP i shemu. Preduvjet: $proxy_add_x_forwarded_for dodaje trenutnu Proxy-IP na eventualni postojeći lanac; to je korisno za Multi-Proxy konfiguracije, ali čini ispravno tumačenje na strani Delphi time još važnijim. Zamka: Ako u nginxu ne postavite Host-zaglavlje, Delphi možda vidi samo Upstream-Host (127.0.0.1), što remeti preusmjerenja i provjere izvora.
Delphi Izvorni isječak: Robustno procjenjivanje Forwarded/X-Forwarded (s popisom pouzdanih proxyja)
Sljedeći kod je namjerno neutralan prema frameworku: Radi protiv minimalnog sučelja (Header + RemoteIP) i može se prilagoditi za WebBroker, RAD Server ili Horse. Ključne točke:
- Prioritet: RFC Forwarded (ako postoji) prije X-Forwarded-*.
- Povjerenje: Forwarded-zaglavlja tumačiti samo ako je izravni peer (RemoteIP) poznati proxy.
- Parsiranje: Uvažiti IPv6, navodnike, portove i lance u X-Forwarded-For.
- Ishod: bazni URL koji možete koristiti za apsolutne linkove, preusmjerenja ili OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimalno adapter-sučelje: implementirajte ga za WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // izravni TCP suprotnik (većinom 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 (Upozorenje: za golu 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 je odvojeno 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 može pojaviti
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
// Za pravu IPv6 normalizaciju trebalo bi parsirati IP; 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; // Zadana vrijednost: izravna TCP suprotnica
// Samo ako je izravna suprotnica poznati proxy, tumačimo 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/ 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;
// U krajnjem slučaju uzeti 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 dobivate konzistentan pregled ClientIP, Proto i Host kao i BaseUrl. Ove informacije možete centralno koristiti za logiranje, sigurnosne odluke (npr. IP-Allowlist) i generiranje linkova.
Zašto je potrebna Trust-Proxy lista: Bez provjere povjerenja napadač bi mogao izravno dosegnuti vaš Delphi-port (pogrešna konfiguracija, interno rutiranje, VPN) i jednostavno poslati X-Forwarded-For: 127.0.0.1. Time bi bili ugroženi audit-trailovi, rate-limitovi ili endpointi „samo interno dozvoljeno“. Vjerujte Forwarded-zaglavima samo ako je izravni peer (RemoteIP) proxy koji kontrolirate (npr. 127.0.0.1, IP load balancera, Kubernetes-Ingress).
Zamke: IPv6 bez uglatih zagrada u notaciji Host:port nije jednoznačan. U HTTP-Host-zaglavlju IPv6 je obično označen u []; pridržavajte se toga. Za složenije 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) zaglavlja obično dolaze preko ContentFields ili GetFieldByName, Remote-IP ovisi o server-backendu. U Horseu (ili drugim HTTP-frameworkima) obično postoji Req.Headers i svojstvo Remote-IP. Važno je načelo: RemoteIP mora biti TCP-peer (izravna protivstrana), a ne neka vrijednost iz zaglavlja.
U praksi provjereno: prilikom pokretanja servisa generirajte TTrustedProxyList iz konfiguracije (INI/ENV), npr. „127.0.0.1“ za lokalne nginx postavke ili IP vašeg Load Balancers. Zatim pozovite ResolveForwardedInfo za svaki request i upišite polja u vaše strukturirano logiranje (JSON-Log, Syslog ili Windows Event Log).
Debugging u radu: kako naći greške u minutama umjesto satima
Ako zahtjevi djeluju „čudno“, rijetko je kriv Delphi-HTTP sam po sebi, već kombinacija proxy-zaglavlja, redirect-logike i timeouta. Tri debug provjere koje vrijedi raditi u praksi:
- Header-Dump (ciljano): Zabilježite kod 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: Ako redirecti ili callback-URL-ovi ne uspiju, zabilježite ForwardedInfo.BaseUrl. Mnoge greške su odmah vidljive („http://127.0.0.1“ umjesto „https://api…“).
- Korelacija timeouta: 504 od proxya nije isto što i Delphi-timeout. nginx proxy_read_timeout i Delphi-na strani Idle-/Read-timeouti moraju biti usklađeni.
Rubni slučajevi: WebSockets, streaming i veliki zahtjevi
WebSockets iza nginx
Za WebSockets nginx treba imati ispravno postavljene Upgrade i Connection. Dodatno, backend ne smije zatvarati „preuranjeno“. Na Delphi-strani važno je da vaša WebSocket-komponenta (ili SSE/streaming endpoint) može raditi s reverse proxyima i da su heartbeati/keep-alive mehanizmi pravilno implementirani.
Veliki uploadi i 413-Fehler
Klasik: Delphi prihvaća upload, ali nginx ga prethodno blokira s 413 Request Entity Too Large. Kontrolirajte to eksplicitno preko client_max_body_size i prilagodite na Delphi-strani ograničenja zahtjeva. Za procesno bliske softverske rješenja koja rade s dokumentima ili slikama to nije iznimka, nego normalan rad.
HTTPS-Offloading und „Secure Cookies“
Ako vaš Delphi-servis postavlja sesijske kolačiće, oni moraju biti označeni kao Secure kada se koriste putem vanjskog HTTPS‑a. Hoće li vaša aplikacija to učiniti često ovisi o tome „zna li“ da je izvorni zahtjev bio HTTPS. Upravo tu pomaže dosljedno evaluiranje X-Forwarded-Proto/Forwarded.
Kada se trud isplati – i gdje može postati problematično
Prikazani pristup isplati se svaki put kada Delphi-servis više nije „nagnječen“ u LAN, već je dio produktivne ivice: više domena, SSO/SAML‑sučelja, javni API‑ji, podrška za više najmoprimaca ili stroži zahtjevi za reviziju. Postaje problematičan tamo gdje se Forwarded‑headereu slijepo vjeruje ili gdje topologije proxyja nisu dokumentirane (više Ingress stupnjeva, Cloud‑LB plus nginx plus sidecar). U takvim situacijama adresa klijenta i shema brzo postaju nepouzdani.
Jasna granica: ako trebate složena pravila povjerenja (CIDR, IPv6 mreže, dinamičke LB‑IP adrese), trebali biste proširiti provjeru povjerenja (stvarno parsiranje IP‑ova, mrežne maske) ili infrastrukturno osigurati da samo definiran proxy može dosegnuti Delphi‑port (Firewall/Security Groups). To je na kraju često robusnija operativna odluka.
Zaključak: ispravno održavanje Reverse Proxya s nginx i Delphi znači „Forwarded napraviti kako treba“
Reverse Proxy s nginxom je za Delphi-REST‑server dobar standardni gradivni blok – ali tek ispravno rukovanje Forwarded i X-Forwarded-* čini postavku stabilnom u radu. Jezgra je jednostavna: headere prihvaćajte samo od pouzdanih proxyja, dosljedno izvodite Client‑IP/Scheme/Host i tu osnovu provucite kroz preusmjeravanja, logiranje i sigurnosne provjere. Sa snippetom iznad imate čvrst, za legacy prikladan temelj koji se može integrirati u WebBroker, Horse ili vlastite HTTP‑servere.
Ako želite konsolidirati postojeće Delphi‑backend iza nginx‑a ili ga modernizirati prema Delphi REST‑API i REST‑server s jasno postavljenom operativnom linijom, tehnički pregled lanca proxyja i evaluacije headera često je najučinkovitiji zahvat. Kontaktirajte Net-Base za kratku tehničku procjenu.
U stručnom kontekstu Nginx Reverse Proxy i Forwarded headeri također igraju važnu ulogu kad se integracije, tokovi podataka i daljnji razvoj moraju jasno i čisto uskladiti.
Razgovarajte o projektu ili modernizacijskom zahvatu s Net-Base.