Net-Base Rivista

23.05.2026

Reverse Proxy con nginx e Delphi: gestione corretta dell'header Forwarded, IP reale del client e basi URL robuste

Se i server Delphi-REST sono dietro nginx, l'IP del client, il rilevamento HTTPS e le URL assolute vengono spesso alterati. Questo frammento di codice sorgente mostra una gestione robusta degli header Forwarded/X-Forwarded (inclusa una lista di proxy attendibili), impostazioni nginx tipiche e indicazioni per il debug durante il funzionamento.

23.05.2026

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.

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

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.
Delphi
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:

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

Condividi il post

Condividi direttamente questo articolo

LinkedIn, X, XING, Facebook, WhatsApp e e-mail sono immediatamente disponibili. Per Instagram prepariamo direttamente il link e un breve testo.

E-mail

Instagram si apre in una nuova scheda. Il link e il breve testo vengono copiati prima negli appunti.