Un Reverse Proxy con nginx e Delphi nella pratica non è di solito un ’nice to have‘, ma la separazione pulita tra il bordo Internet e l’applicazione: terminazione TLS (HTTPS-Offloading), regole centralizzate per header/CORS, rate-limit, log unificati, Blue/Green-Rollout o semplicemente l’hosting di più servizi sotto una singola domain. Quello che spesso viene sottovalutato: non appena nginx si trova „davanti“, il server Delphi vede solo l’IP del proxy, spesso solo „http“ invece di „https“ e genera link assoluti errati (redirect, callback-URL, OpenAPI-Server-URL). Proprio questi tre aspetti causano in seguito tempo di debug in esercizio.
Questo frammento di codice mostra un modello robusto per come valutare correttamente in Delphi Forwarded o X-Forwarded-* – inclusa la Trust-Proxy-Liste (importante contro lo header spoofing) e una Request-Base-URL consistente. In aggiunta ci sono configurazioni nginx pratiche e indicazioni per casi limite come WebSockets, upload di grandi dimensioni e timeout.
Perché le configurazioni con Reverse Proxy „confondono“ i server Delphi
nginx comunica come Reverse Proxy con il servizio Delphi tipicamente senza crittografia (HTTP) nella rete interna o su localhost, mentre il client esterno usa HTTPS. Senza header aggiuntivi, Delphi non sa nulla di:
- Schema originale (https vs. http) – rilevante per redirect e URL assolute.
- Host originale (dominio cliente, porta) – rilevante per setup multi-tenant, CORS e callback-URL.
- IP client originale – rilevante per audit, rate-limit, controlli geo e analisi di sicurezza.
nginx può trasportare queste informazioni tramite header. Comuni sono X-Forwarded-For, X-Forwarded-Proto e X-Forwarded-Host; standardizzato è inoltre l’header RFC Forwarded. Importante: questi header non sono automaticamente attendibili dal punto di vista dell’applicazione, perché un client può inviarli da solo – diventano attendibili solo se provengono da un proxy conosciuto.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Un punto di partenza solido (HTTP/1.1, Keep-Alive, Upgrade per WebSockets) appare così. Lo snippet è volutamente succinto; a seconda dell’ambiente completate con HSTS, rate-limit e access-log.
# (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;
}
}
Scopo: L’applicazione riceve in modo affidabile Host, IP client e schema. Condizione: $proxy_add_x_forwarded_for aggiunge l’IP dell’attuale proxy a una catena eventualmente esistente; questo è utile per setup multi-proxy, ma rende ancora più importante la corretta valutazione sul lato Delphi. Insidia: Se in nginx non impostate l’intestazione Host, Delphi potrebbe vedere solo l’host upstream (127.0.0.1), il che compromette redirect e controlli di origine.
Delphi Snippet di codice sorgente: valutazione robusta di Forwarded/X-Forwarded (con lista dei proxy attendibili)
Il codice seguente è volutamente mantenuto framework-neutral: opera su un’interfaccia minima (Header + RemoteIP) e può essere adattato in WebBroker, RAD Server o Horse. Punti chiave:
- Priorità: RFC Forwarded (se presente) prima di X-Forwarded-*.
- Trust: valutare gli header Forwarded solo se il peer diretto (RemoteIP) è un proxy noto.
- Parsing: considerare IPv6, virgolette, porte e catene in X-Forwarded-For.
- Output: una Base-URL che potete usare per link assoluti, redirect o OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Interfaccia adattatore minima: implementare per WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // controparte TCP diretta (di solito nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // z.B. 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 può essere "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 tra []: [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 (Attenzione: con IPv6 non racchiuso tra [] non affidabile)
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
// Esempio: 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;
// Più elementi sono separati da virgola; prendiamo il primo (più vicino al client)
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 può essere IP o "unknown"; IPv6 può essere racchiuso in []
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 può verificarsi
if V.Contains(':') and (not V.StartsWith('[')) then
V := FirstCsvToken(V); // per sicurezza
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
// Per una reale normalizzazione IPv6 sarebbe necessario un parsing dell'IP; qui intenzionalmente pragmatico.
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; // Fallback: controparte diretta
// Solo quando la controparte diretta è un proxy conosciuto, interpretiamo gli header Forwarded.
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-* come fallback/integrazione
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;
// Come ultima risorsa prendere l'host dall'header "Host" (se non sono presenti header del proxy)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Scopo: Ricevete da ogni request una vista consistente su ClientIP, Proto e Host oltre a una BaseUrl. Queste informazioni possono essere utilizzate in modo centrale per logging, decisioni di sicurezza (ad es. lista degli IP consentiti / allowlist) e generazione di link.
Perché è necessaria la Trust-Proxy-Liste: Senza una verifica di trust un attaccante potrebbe raggiungere direttamente la porta Delphi (errata configurazione, routing interno, VPN) e inviare semplicemente X-Forwarded-For: 127.0.0.1. In tal modo sarebbero attaccabili audit-trail, rate-limit o endpoint „solo interni“. Fidatevi degli header Forwarded solo se il peer diretto (RemoteIP) è un proxy che controllate voi (ad es. 127.0.0.1, l’IP del load balancer, Kubernetes-Ingress).
Trappole comuni: IPv6 senza parentesi quadre non è univoco nella notazione Host:port. Nell’HTTP-Host-Header l’IPv6 è solitamente indicata tra []; attenetevi a questa convenzione. Per intervalli IP complessi (CIDR) dovreste estendere la Trust-Liste (ad es. con un vero parsing degli IP).
Integrazione in WebBroker/Horse/RAD Server: dove il codice si „aggancia“
In WebBroker (TWebRequest) gli header arrivano tipicamente tramite ContentFields o GetFieldByName, la Remote-IP dipende dal backend del server. In Horse (o altri framework HTTP) esistono di solito Req.Headers e una proprietà per la Remote-IP. Il principio importante è: RemoteIP deve essere il peer TCP, non un qualsiasi valore preso da un header.
Pratica collaudata: create all’avvio del servizio una TTrustedProxyList da configurazione (INI/ENV), p.es. „127.0.0.1“ per setup locali con nginx o l’IP del vostro load balancer. Poi chiamate ResolveForwardedInfo per ogni request e scrivete i campi nel vostro logging strutturato (JSON-Log, Syslog o Windows Event Log).
Debugging in esercizio: così trovate errori in minuti invece che in ore
Se le request sembrano „strane“, raramente è colpa dell’HTTP di Delphi di per sé; più spesso è una combinazione di header del proxy, logica di redirect e timeout. Tre controlli di debug utili nella pratica:
- Dump degli header (mirato): Registrate nei log, in caso di 4xx/5xx, anche Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent e Request-URI. Ma solo su errori – altrimenti il log diventa costoso e difficilmente leggibile.
- Verificare la Base-URL: Se redirect o callback-URL falliscono, loggate ForwardedInfo.BaseUrl. Molti errori sono immediatamente evidenti („http://127.0.0.1“ invece di „https://api…“).
- Correlazione dei timeout: Un 504 dal proxy non è la stessa cosa di un timeout lato Delphi. nginx proxy_read_timeout e i timeout Idle/Read lato Delphi devono essere allineati.
Casi limite: WebSockets, streaming e richieste di grandi dimensioni
WebSockets dietro nginx
Per i WebSocket nginx deve gestire correttamente Upgrade e Connection. Inoltre il backend non deve chiudere „troppo presto“. Sul lato Delphi è rilevante che la componente WebSocket (o un endpoint SSE/Streaming) sappia convivere con reverse proxy e implementi heartbeat/keep-alive in modo affidabile.
Upload grandi ed errori 413
Un classico: Delphi accetta un upload, ma nginx blocca prima con 413 Request Entity Too Large. Gestite questo esplicitamente con client_max_body_size e adeguate i Request-Limits lato Delphi. Per soluzioni software vicine al processo che trattano documenti o immagini questo non è un caso straordinario, ma funzionamento normale.
HTTPS-Offloading und „Secure Cookies“
Se il vostro Delphi-service imposta cookie di sessione, questi devono di norma essere marcati come Secure quando l’HTTPS è esterno. Che la vostra applicazione lo faccia dipende spesso dal fatto che essa «sappia» che la richiesta originaria era HTTPS. Proprio qui aiuta una valutazione coerente di X-Forwarded-Proto/Forwarded.
Quando lo sforzo è giustificato – e dove può fallire
L’approccio mostrato vale ogni volta che il servizio Delphi non vive più „nudo“ nella LAN, ma è parte di un bordo produttivo: più domini, interfacce SSO/SAML, API pubbliche, multitenancy o requisiti di audit più stringenti. Fallisce dove si si ripone fiducia cieca negli header Forwarded o le topologie dei proxy non sono documentate (più livelli di Ingress, Cloud-LB più nginx più sidecar). In questi casi client-IP e schema diventano rapidamente „qualsiasi cosa“.
Una linea netta: se avete bisogno di regole di trust complesse (CIDR, reti IPv6, IP dinamici dei LB), dovreste estendere la verifica di trust (vero parsing degli IP, maschere di rete) oppure progettare l’infrastruttura in modo che solo un proxy definito possa raggiungere la porta Delphi (Firewall/Security Groups). Alla fine questa è di solito la decisione operativa più robusta.
Conclusione: gestire correttamente un Reverse Proxy con nginx e Delphi significa „gestire correttamente Forwarded“
Un reverse proxy con nginx è per i Delphi-REST-Server un buon componente standard – ma è solo il corretto trattamento di Forwarded e X-Forwarded-* a rendere lo setup stabile in esercizio. Il nucleo è semplice: accettare header solo da proxy affidabili, derivare in modo consistente Client-IP/Scheme/Host e applicare questa base a redirect, logging e controlli di sicurezza. Con lo snippet sopra avete una base pulita, compatibile con i sistemi legacy, che si integra in WebBroker, Horse o in server HTTP proprietari.
Se intendete consolidare un backend Delphi esistente dietro nginx o modernizzarlo verso Delphi REST-API und REST-Server con una linea operativa chiara, una review tecnica della catena di proxy e della valutazione degli header è spesso la leva più rapida. Contattate Net-Base per una breve valutazione tecnica.
Nel contesto tecnico Nginx Reverse Proxy e gli header Forwarded giocano anch’essi un ruolo importante quando integrazioni, flussi di dati e sviluppo devono cooperare in modo ordinato.
Discutere un progetto o un intervento di modernizzazione con Net-Base.