En Reverse proxy z nginx in Delphi je v praksi ponavadi ni „nice to have“, ampak čista ločitev med internetno mejo in aplikacijo: TLS-terminacija (HTTPS-Offloading), centralna pravila za headerje/CORS, omejitve hitrosti (Rate-Limits), enotni logi, Blue/Green-rollouti ali preprosto gostovanje več storitev pod eno domeno. Kar se pogosto podcenjuje: ko nginx stoji „pred“ njim, Delphi-strežnik vidi le IP proxyja, pogosto le „http“ namesto „https“ in generira napačne absolutne povezave (preusmeritve, callback-URL-ji, OpenAPI-server-URL). Prav ti trije vidiki kasneje povzročijo čas za odpravljanje napak v produkciji.
Ta izsek izvorne kode prikazuje robusten vzorec, kako v Delphi pravilno obdelati Forwarded oziroma X-Forwarded-* — vključno s Trust-Proxy-Liste (pomembno proti ponarejanju headerjev) in dosledno Request-Base-URL. Poleg tega so priložene praktične nginx-konfiguracije in napotki za robne primere, kot so WebSockets, velika nalaganja in timeouti.
Zakaj konfiguracije z reverse proxyjem Delphi-strežnike „zmedejo“
nginx kot reverse proxy običajno komunicira z Delphi-storitvijo nešifrirano (HTTP) v notranjem omrežju ali na localhostu, medtem ko klient zunaj prihaja po HTTPS. Brez dodatnih headerjev Delphi ne ve nič o:
- Originalna shema (https proti http) – pomembno za preusmeritve in absolutne URL-je.
- Originalni gostitelj (specifična domena stranke, port) – pomembno za multi-tenant nastavitve, CORS in callback-URL-je.
- Originalna IP stranke – pomembno za revizijo, omejevanje hitrosti, geolokacijske preverbe in varnostne analize.
nginx lahko te informacije prenese preko headerjev. Pogosti so X-Forwarded-For, X-Forwarded-Proto in X-Forwarded-Host; standardiziran je dodatno RFC-header Forwarded. Pomembno: ti headerji z vidika aplikacije niso avtomatično zanesljivi, ker jih lahko pošlje tudi klient sam — postanejo zanesljivi šele, ko izvirajo iz znanega proxyja.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Dober izhodiščni komplet (HTTP/1.1, Keep-Alive, Upgrade za WebSockets) izgleda takole. Izsek je namensko kratek; glede na okolje dodate HSTS, Rate-Limits in access-loge.
# (nginx-konfiguracija, ne 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;
# opcijsko, vendar praktično za absolutne URL-je
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;
# Timeouti, prilagojeni Delphi-backendu (dolgi poročili/izvozi)
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# eksplicitno nadziranje velikih nalaganj
client_max_body_size 50m;
}
}
Namen: Aplikaciji se zanesljivo posredujejo Host, odjemalčeva IP in shema. Omejitev: $proxy_add_x_forwarded_for pripne trenutno IP proxyja na morebitno obstoječo verigo; to je koristno za multi-proxy konfiguracije, vendar naredi pravilno analizo na strani Delphi še pomembnejšo. Past: Če v nginx ne nastavite Host-glave, lahko Delphi morda vidi le upstream-host (127.0.0.1), kar zlomi preusmeritve in preverjanja izvora.
Delphi Izsek izvorne kode: robustna obdelava Forwarded/X-Forwarded (s seznamom zaupanja vrednih proxyjev)
Naslednja koda je namensko neodvisna od ogrodja: deluje proti minimalnemu vmesniku (Header + RemoteIP) in jo je mogoče prilagoditi v WebBroker, RAD Server ali Horse. Ključne točke:
- Prednost: RFC Forwarded (če obstaja) pred X-Forwarded-*.
- Zaupanje: Forwarded-glave obdelujte le, če je neposredni vrstnik (RemoteIP) znan proxy.
- Parsiranje: upoštevajte IPv6, narekovaje, porte in verige v X-Forwarded-For.
- Izhod: Base-URL, ki jo lahko uporabite za absolutne povezave, preusmeritve ali OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimalni vmesnik adapterja: implementirajte ga za WebBroker/Horse/itd.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // neposredna TCP-stranka (ponavadi 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 je lahko "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 gola IPv6 brez [] ni zanesljivo)
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
// Beispiel: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
Parts: TArray;
I: Integer;
KV: TArray;
K, V: string;
FirstElement: string;
begin
Result := False;
ClientIP := '';
Proto := '';
Host := '';
if ForwardedValue.Trim = '' then
Exit;
// Več elementov je ločenih z vejico; vzamemo prvi (najbližji klientu)
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 je lahko IP ali "unknown"; IPv6 je lahko v []
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 se zgodi
if V.Contains(':') and (not V.StartsWith('[')) then
V := FirstCsvToken(V); // varnostno
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.Create;
end;
destructor TTrustedProxyList.Destroy;
begin
FSet.Free;
inherited;
end;
class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
// Za resnično normalizacijo IPv6 bi bilo potrebno razčlenjevanje IP; tukaj namerno 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; // Rezerva: neposredna TCP-stranka
// Samo če je neposredna stranka znan proxy, obdelamo Forwarded headerje.
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-* kot rezerva/dopolnilo
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 skrajnem primeru vzemi Host iz headerja "Host" (če ni proxy-headerja)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Namen: Iz vsake zahteve prejmete konsistenten pogled na ClientIP, Proto in Host ter na BaseUrl. Te informacije lahko centralno uporabite za beleženje, varnostne odločitve (npr. IP-allowlist) in generiranje povezav.
Zakaj je seznam zaupanja vrednih proxyjev potreben: Brez preverjanja zaupanja bi napadalec lahko neposredno dosegel vaš Delphi-Port (napaka v konfiguraciji, interno usmerjanje, VPN) in preprosto poslal X-Forwarded-For: 127.0.0.1. S tem bi bili ogroženi audit-trace, omejitve hitrosti ali končne točke »samo interna dovoljena«. Forwarded-glavam zaupajte le, če je neposredni peer (RemoteIP) proxy, ki ga nadzorujete (npr. 127.0.0.1, IP Load Balancerja, Kubernetes-Ingress).
Past: IPv6 brez oglatih oklepajev ni enoznačen v notaciji Host:port. V HTTP-Host-glavi je IPv6 običajno zapisan v []; držite se tega. Za kompleksne IP-pasove (CIDR) boste morali razširiti seznam zaupanja vrednih (npr. z dejanskim parsiranjem IP).
Integracija v WebBroker/Horse/RAD Server: kje se koda „priključi“
V WebBroker (TWebRequest) pridejo glave običajno prek ContentFields ali GetFieldByName, Remote-IP pa je odvisen od strežniškega backenda. V Horse (ali drugih HTTP- ogrodjih) obstajajo navadno Req.Headers in lastnost Remote-IP. Pomembno je načelo: RemoteIP mora biti TCP-protistran, ne kakšna vrednost iz glave.
Praktično preverjeno: ob zagonu storitve ustvarite TTrustedProxyList iz konfiguracije (INI/ENV), npr. „127.0.0.1“ za lokalne nginx-nastavitve ali IP vašega Load Balancerja. Nato na zahtevo pokličite ResolveForwardedInfo in polja zapišite v strukturirano beleženje (JSON-log, Syslog ali Windows Event Log).
Razhroščevanje v produkciji: kako najti napake v minutah namesto ur
Če so zahteve »čudne«, razlog redko leži v Delphi-HTTP samem, temveč v kombinaciji proxy-glav, logike preusmeritev in timeoutov. Tri debug-preverjanja, ki se v praksi izplačajo:
- Header-dump (ciljno): Pri 4xx/5xx dodatno beležite Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent in Request-URI. A le pri napakah – sicer je log drag in nepregleden.
- Preverite Base-URL: Če preusmeritve ali callback-URL-i odpovedo, logirajte ForwardedInfo.BaseUrl. Veliko napak je takoj vidnih („http://127.0.0.1“ namesto „https://api…“).
- Usklajevanje timeoutov: 504 od proxya ni enako kot timeout na strani Delphi. nginx proxy_read_timeout in na strani Delphi nastavljeni Idle-/Read-timeouti morajo biti usklajeni.
Robni primeri: WebSockets, pretakanje in veliki zahtevki
WebSockets za nginx
Za WebSockets nginx potrebuje pravilna Upgrade in Connection. Poleg tega backend ne sme »prezgodaj« zapreti povezave. Na strani Delphi je relevantno, da vaša WebSocket-komponenta (ali SSE/streaming endpoint) zna delati z reverznimi proxyji in da so heartbeati/keep-alive mehanizmi izvedeni čisto.
Veliki naloži in 413-napake
Klasika: Delphi sprejme upload, a nginx prej blokira z 413 Request Entity Too Large. Krmili to eksplicitno z client_max_body_size in prilagodite omejitve zahtev na strani Delphi. Za procesno bližnje programske rešitve z dokumenti ali slikami to ni izjema, temveč rutinski del obratovanja.
HTTPS-Offloading und „Secure Cookies“
Če vaš Delphi-storitev nastavi sejnske piškotke, morajo biti ti pri zunanjem HTTPS praviloma označeni kot Secure. Ali vaša aplikacija to naredi, pogosto zavisi od tega, ali ve, da je bil izvorni zahtevek HTTPS. Prav tu pomaga dosledna analiza X-Forwarded-Proto/Forwarded.
Kdaj se trud splača – in kje lahko odpove
Prikazani pristop se izplača vedno, ko Delphi-storitev ni več „gola“ v LAN‑u, ampak postane del produktivnega roba: več domen, SSO/SAML‑vmesniki, javne API, večstrankost ali strožje revizijske zahteve. Ne deluje tam, kjer se Forwarded‑headerjem slepo zaupa ali kjer proxy‑topologije niso dokumentirane (več stopenj ingressa, Cloud‑LB plus nginx plus Sidecar). V takih primerih postaneta Client‑IP in scheme hitro „nekaj“.
Jasna meja: če potrebujete kompleksna pravila zaupanja (CIDR, IPv6‑omrežja, dinamične LB‑IP), bi morali razširiti preverjanje zaupanja (pravo razčlenjevanje IP, mrežne maske) ali infrastrukturo zasnovati tako, da lahko do porta Delphi dostopa le definiran proxy (Firewall/Security Groups). Na koncu je to običajno bolj robustna operativna odločitev.
Zaključek: nginx Reverse Proxy in Delphi robustno obratovati pomeni „Forwarded pravilno obravnavati“
Reverse Proxy z nginx je za Delphi-REST-strežnik dober standardni gradnik – vendar šele pravilna obravnava Forwarded in X-Forwarded-* naredi postavitev v obratovanju stabilno. Jedro je preprosto: headerje sprejemati le od zaupanja vrednih proxyjev, Client-IP/Scheme/Host dosledno določiti in to osnovo dosledno uporabiti v preusmeritvah, beleženju in varnostnih kontrolah. S prikazanim snippetom imate za to čisto, za legacy primerno osnovo, ki jo lahko integrirate v WebBroker, Horse ali lastne HTTP‑strežnike.
Če želite obstoječi Delphi-backend za nginx konsolidirati ali modernizirati proti Delphi REST-API in REST-strežnik z jasno operativno linijo, je tehnični pregled verige proxyjev in analize headerjev pogosto najhitrejši ukrep. Kontaktirajte Net-Base za kratno tehnično oceno.
V strokovnem okolju imata tudi nginx Reverse Proxy in Forwarded headerji pomembno vlogo, kadar morajo integracije, podatkovni tokovi in nadaljnji razvoj delovati usklajeno.
Razpravljajte o projektu ali modernizacijskem ukrepu z Net-Base.