Reverse Proxy ar nginx un Delphi praksē parasti nav tikai „nice to have“, bet gan skaidra atdalīšana starp interneta malu un aplikāciju: TLS terminēšana (HTTPS offloading), centrālas Header/CORS politikas, rate-limits, vienoti žurnāli, Blue/Green izvietošanas vai vienkārši vairāku servisu hostēšana zem vienas domēnas. To, kas bieži tiek novērtēts par zemu: tiklīdz nginx «stāv priekšā», Delphi serveris redz tikai proksija IP, bieži vien tikai «http» nevis «https» un ģenerē nepareizas absolūtās saites (redirects, callback-URL, OpenAPI servera URL). Tieši šie trīs punkti vēlāk palielina debugging laiku ekspluatācijā.
Šis koda fragments demonstrē izturīgu modeli, kā Delphi pareizi apstrādāt Forwarded un X-Forwarded-* — ieskaitot uzticamo proksiju sarakstu (svarīgi pret galveņu spoofingu) un konsekventu pieprasījuma bāzes URL. Papildus ir praktiskas nginx konfigurācijas un norādes par malējām situācijām, piemēram, WebSockets, lieliem augšupielādes failiem un timeouts.
Kāpēc Reverse Proxy izvietojumi Delphi serverus „apjauc“
nginx kā Reverse Proxy parasti sazinās ar Delphi servisu nešifrēti (HTTP) iekšējā tīklā vai uz localhost, kamēr klients ārpusē nāk pa HTTPS. Bez papildu galvenēm Delphi neuzzina par:
- Oriģinālā shēma (https vs. http) — svarīga pāradresācijām (redirects) un absolūtām URL.
- Oriģinālais hosts (klientam specifiska domēna nosaukums, ports) — svarīgs multi-tenant izvietojumiem, CORS un callback-URL.
- Oriģinālā klienta IP — svarīga audita, rate-limits, ģeolokācijas pārbaudēm un drošības analīzēm.
nginx var šos datus nodot galvenēs. Parasti lieto X-Forwarded-For, X-Forwarded-Proto un X-Forwarded-Host; papildus standartizēta ir RFC galvene Forwarded. Svarīgi: no aplikācijas skatpunkta šīs galvenes nav automātiski uzticamas, jo klients tās var nosūtīt pats — tās kļūst uzticamas tikai tad, ja nāk no zināma proksija.
nginx konfigurācija: minimāli lietderīgās proxy-galvenes
Labs sākumpunkts (HTTP/1.1, Keep-Alive, Upgrade WebSockets) izskatās šādi. Snippet ir apzināti īss; atkarībā no vides papildiniet HSTS, rate-limits un piekļuves žurnālus.
# (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;
}
}
Mērķis: Lietojumprogramma saņem Host, klienta IP un shēmu uzticami nodotu. Nosacījums: $proxy_add_x_forwarded_for pievieno pašreizējo starpniekservera IP iespējamajai esošajai ķēdei; tas ir lietderīgi daudzslāņu proxy konfigurācijās, taču padara pareizu izvērtēšanu Delphi pusē vēl svarīgāku. Riska punkts: Ja nginx neuzstāda Host galveni, Delphi iespējams redzēs tikai upstream-hostu (127.0.0.1), kas izjauc pāradresācijas un Origin pārbaudes.
Delphi Avota koda fragments: Forwarded/X-Forwarded robusti izvērtēt (ar uzticamo starpniekserveru sarakstu)
Šāds kods ir apzināti neatkarīgs no ietvariem: tas darbojas pret minimālu saskarni (Header + RemoteIP) un to var pielāgot WebBroker, RAD Server vai Horse. Galvenie punkti:
- Prioritāte: RFC Forwarded (ja pieejams) pirms X-Forwarded-*.
- Uzticamība: Forwarded galvenes drīkst analizēt tikai tad, ja tiešais peer (RemoteIP) ir pazīstams starpniekserveris.
- Parsēšana: Ņemt vērā IPv6, pēdiņas, portus un ķēdes X-Forwarded-For.
- Izvade: bāzes URL, ko var izmantot absolūtām saitēm, pāradresācijām vai OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimālais adaptera interfeiss: implementējiet to WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // tiešā TCP pretējā puse (parasti nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // piemēram: 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 var būt "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 iekavās []: [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 (Uzmanību: ja IPv6 nav iekļauts iekavās [], tas var nebūt uzticami)
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
// Piemērs: 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;
// Vairāki elementi atdalīti ar komatiem; mēs ņemam pirmo (klientam vistuvākais)
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 var būt IP vai "unknown"; IPv6 var atrasties iekavās []
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 var parādīties
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
// Lai veiktu īstu IPv6 normalizāciju, būtu nepieciešama IP parsēšana; šeit apzināti pragmatiski.
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; // Rezerves variants: tiešā pretējā puse
// Forwarded galvenes apstrādājam tikai tad, ja tiešā pretējā puse ir pazīstams proxy.
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-* kā rezerves vērtība/papildinājums
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;
// Ja nepieciešams, iegūstam Host no "Host" galvenes (ja nav proxy galveņu)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Mērķis: No katra pieprasījuma jūs iegūstat konsekventu skatījumu uz ClientIP, Proto un Host, kā arī uz BaseUrl. Šo informāciju var izmantot centrāli žurnālfailēšanai, drošības lēmumiem (piem., IP-Allowlist) un saišu ģenerēšanai.
Kāpēc nepieciešams Trust-Proxy saraksts: Bez Trust pārbaudes uzbrucējs var tieši sasniegt jūsu Delphi portu (konfigurācijas kļūme, iekšējā maršrutēšana, VPN) un vienkārši nosūtīt X-Forwarded-For: 127.0.0.1. Tādā gadījumā tiktu apdraudēti audita ieraksti, rate-limits vai galapunkti, kas pieejami “tikai iekšēji”. Uzticieties Forwarded galvenēm tikai tad, ja tiešais savienojuma partneris (RemoteIP) ir proxies, ko jūs kontrolējat (piem., 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).
Bīstamie aspekti: IPv6 bez kvadrātiekavām Host:port notācijā nav viennozīmīgs. HTTP-Host galvenē IPv6 parasti tiek norādīts ar []; ievērojiet šo formātu. Arī kompleksu IP diapazonu (CIDR) atbalstam jums jāpaplašina Trust saraksts (piem., ar īstu IP-parsēšanu).
Integrācija WebBroker/Horse/RAD Server: kur kods „andockt“
WebBroker (TWebRequest) gadījumā galvenes parasti pieejamas caur ContentFields vai GetFieldByName, Remote-IP ir atkarīga no servera backend. Horse (un citi HTTP framework) parasti nodrošina Req.Headers un Remote-IP īpašību. Svarīgi principi: RemoteIP jābūt TCP pretpusei, nevis kādam galvenes vērtībai.
Praktiski pārbaudīts: izveidojiet servisa startēšanas laikā TTrustedProxyList no konfigurācijas (INI/ENV), piem., „127.0.0.1“ lokāliem nginx uzstādījumiem vai jūsu Load Balancer IP. Tad izsauciet ResolveForwardedInfo katram pieprasījumam un ierakstiet laukus savā strukturētajā žurnālfailē (JSON-Log, Syslog vai Windows Event Log).
Debugging darbībā: kā atrast kļūdas minūtēs, nevis stundās
Ja pieprasījumi izskatās „dīvaini“, retāk pie vainas ir Delphi-HTTP pats par sevi, biežāk — proxy galveņu, pāradresācijas loģikas un timeout kombinācija. Trīs debug pārbaudes, kas ikdienā atmaksājas:
- Header-Dump (mērķēti): pie 4xx/5xx kļūdām papildus žurnālfailā ierakstiet Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent un Request-URI. Taču tikai kļūdu gadījumos — citādi žurnāls kļūs dārgs un nepārskatāms.
- Base-URL pārbaudīt: ja pāradresācijas vai callback URL neizdodas, reģistrējiet ForwardedInfo.BaseUrl. Daudzas kļūdas redzamas uzreiz („http://127.0.0.1“ vietā „https://api…“).
- Timeout korelācija: 504 no proxy nav tas pats, kas Delphi timeout. nginx proxy_read_timeout un Delphi-pusē iestatītie Idle/Read timeouti jāsinhronizē.
Īpaši gadījumi: WebSockets, straumēšana un lieli pieprasījumi
WebSockets aiz nginx
WebSockets gadījumā nginx prasa pareizu Upgrade un Connection. Papildus backend nedrīkst slēgt savienojumu „par agru“. Delphi-pusē svarīgi, lai jūsu WebSocket komponents (vai SSE/streaming galapunkts) protētu darboties ar reversajiem proxy un ka Heartbeats/Keep-Alives ir tīri implementēti.
Lieli augšupielādes un 413 kļūdas
Klasisks piemērs: Delphi pieņem augšupielādi, bet nginx to bloķē ar 413 Request Entity Too Large. Kontrolējiet to skaidri caur client_max_body_size un pielāgojiet Delphi-pusē Request limita iestatījumus. Procesu tuvās programmatūras risinājumiem ar dokumentu vai attēlu datiem tas nav izņēmums, bet normāla darbība.
HTTPS-Offloading und „Secure Cookies“
Ja jūsu Delphi-serviss iestata sesijas sīkdatnes, tās ārējā HTTPS gadījumā parasti jāatzīmē kā Secure. Vai jūsu lietotne to dara, bieži ir atkarīgs no tā, vai tā «zina», ka sākotnējais pieprasījums bija HTTPS. Tieši šeit palīdz konsekventa X-Forwarded-Proto/Forwarded vērtēšana.
Kad ieguldījums atmaksājas — un kur tas var izjukt
Parādītā pieeja atmaksājas tad, kad Delphi-serviss vairs nedzīvo „kails“ LAN teritorijā, bet ir daļa no produktīvās malas: vairākas domēnas, SSO/SAML saskarnes, Public APIs, daudznomnieku atbalsts vai stingrākas audita prasības. Tā kļūst nedroša tur, kur Forwarded-galvenei uzticas akli vai proxy topoloģijas nav dokumentētas (vairākas Ingress pakāpes, Cloud-LB plus nginx plus Sidecar). Tad Client-IP un shēma ātri kļūst par „jebko“.
Skira robeža: ja jums nepieciešamas sarežģītas trust-noteikšanas (CIDR, IPv6-Netze, dinamiskās LB-IPs), vajadzētu paplašināt trust-pārbaudi (reāla IP-parsēšana, tīkla maskas) vai veidot infrastruktūru tā, lai tikai definēts proxy var piekļūt Delphi-portam (Firewall/Security Groups). Galarezultātā tas parasti ir robustāks ekspluatācijas lēmums.
Fazit: Reverse Proxy mit nginx und Delphi sauber betreiben heißt „Forwarded richtig machen“
Reverse Proxy ar nginx ir labs standarta elements Delphi-REST-Server vidē — taču tikai Forwarded un X-Forwarded-* korekta apstrāde padara uzstādījumu darbībā stabilu. Būtība ir vienkārša: pieņemt galvenes tikai no uzticamiem proxijiem, konsekventi izvest Client-IP/Scheme/Host un šo bāzi konsekventi izmantot pāradresācijās, žurnālfailos un drošības pārbaudēs. Ar iepriekšējo fragmentu jums ir tīra, ar legacy saderīga bāze, ko var integrēt WebBroker, Horse vai pašizveidotos HTTP-serveros.
Ja vēlaties konsolidēt esošu Delphi-backend aiz nginx vai modernizēt to virzienā uz Delphi REST-API und REST-Server ar skaidru ekspluatācijas līniju, tehnisks pārskats par proxy ķēdi un galveņu vērtēšanu bieži ir ātrākais sviras punkts. Kontaktējiet Net-Base par īsu tehnisku novērtējumu.
Profesijā Nginx Reverse Proxy un Forwarded-galvenes arī spēlē nozīmīgu lomu, ja integrācijas, datu plūsmas un turpmākā attīstība jāvirzās kopā pārdomāti.