Net-Base Magazyn

23.05.2026

Reverse proxy z nginx i Delphi: poprawne obsługiwanie nagłówka Forwarded, rzeczywisty adres IP klienta i solidne podstawy URL

Gdy serwery Delphi-REST działają za nginx, często nieprawidłowo przekazywane są adres IP klienta, wykrywanie HTTPS oraz absolutne adresy URL. Ten fragment źródłowy pokazuje solidne obsługiwanie nagłówków Forwarded/X-Forwarded (w tym listę zaufanych proxy), typowe ustawienia nginx oraz wskazówki do debugowania w eksploatacji.

23.05.2026

Reverse Proxy z nginx i Delphi w praktyce zazwyczaj nie jest „nice to have”, lecz stanowi klarowne rozdzielenie krawędzi internetowej od aplikacji: terminacja TLS (HTTPS-Offloading), centralne zasady nagłówków/CORS, rate-limity, zunifikowane logi, wdrożenia Blue/Green lub po prostu hostowanie wielu usług pod jedną domeną. Co bywa często niedoszacowane: gdy nginx stoi „przed” serwerem, serwer Delphi widzi tylko IP proxy, często tylko „http” zamiast „https” i generuje błędne linki absolutne (redirecty, callback-URL-e, OpenAPI-Server-URL). Właśnie te trzy kwestie powodują później czasochłonne debugowanie w eksploatacji.

Ten fragment źródłowy pokazuje solidny wzorzec, jak w Delphi poprawnie analizować Forwarded oraz X-Forwarded-* — łącznie z listą zaufanych proxy (ważne przeciwko spoofingowi nagłówków) i spójną Request-Base-URL. Dołączone są praktyczne konfiguracje nginx oraz wskazówki dotyczące przypadków brzegowych, takich jak WebSockets, duże uploady i timeouty.

Dlaczego konfiguracje Reverse Proxy mylą serwery Delphi

nginx jako Reverse Proxy komunikuje się z serwisem Delphi zwykle nieszyfrowanym protokołem (HTTP) w sieci wewnętrznej lub na localhost, podczas gdy klient łączy się z zewnątrz przez HTTPS. Bez dodatkowych nagłówków Delphi nie wie o:

  • oryginalnym schemacie (https vs. http) – istotne dla redirectów i adresów absolutnych.
  • oryginalnym hoście (domena klienta, port) – istotne dla konfiguracji multi-tenant, CORS i callback-URL-i.
  • oryginalnym IP klienta – istotne dla audytu, rate-limitów, sprawdzeń geolokalizacji i analiz bezpieczeństwa.

nginx może przenieść te informacje za pomocą nagłówków. Powszechne są X-Forwarded-For, X-Forwarded-Proto i X-Forwarded-Host; dodatkowo ustandaryzowany jest nagłówek RFC Forwarded. Ważne: z punktu widzenia aplikacji te nagłówki nie są automatycznie zaufane, ponieważ klient może je sam wysłać – stają się zaufane dopiero, gdy pochodzą od znanego proxy.

nginx-Konfiguration: der minimal sinnvolle Proxy-Header

Solidny punkt startowy (HTTP/1.1, Keep-Alive, Upgrade dla WebSockets) wygląda tak. Snippet jest świadomie zwięzły; uzupełnij w zależności od środowiska HSTS, rate-limity i access-logi.

Delphi
# (konfiguracja nginx, nie 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;

    # opcjonalne, ale praktyczne dla adresów URL absolutnych
    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;

    # timeouty dopasowane do backendu Delphi (długie raporty/eksporty)
    proxy_connect_timeout 5s;
    proxy_send_timeout    60s;
    proxy_read_timeout    60s;

    # jawna kontrola dużych uploadów
    client_max_body_size 50m;
  }
}

Cel: Aplikacja otrzymuje Host, IP klienta i schemat niezawodnie przekazywane. Warunek brzegowy: $proxy_add_x_forwarded_for dopina aktualne IP proxy do ewentualnego łańcucha; to dobre dla konfiguracji z wieloma proxy, ale tym ważniejsza staje się poprawna analiza po stronie Delphi. Pułapka: Jeśli nie ustawisz nagłówka Host w nginx, Delphi może widzieć tylko hosta upstream (127.0.0.1), co łamie przekierowania i sprawdzanie origin.

Delphi Fragment źródłowy: solidne analizowanie Forwarded/X-Forwarded (z listą zaufanych proxy)

Następujący kod jest celowo niezależny od frameworku: operuje na minimalnym interfejsie (Header + RemoteIP) i można go zaadaptować w WebBroker, RAD Server lub Horse. Kluczowe punkty:

  • Priorytet: RFC Forwarded (jeśli dostępny) przed X-Forwarded-*.
  • Zaufanie: nagłówki Forwarded analizować tylko wtedy, gdy bezpośredni peer (RemoteIP) jest znanym proxy.
  • Parsowanie: uwzględniać IPv6, cudzysłowy, porty i łańcuchy w X-Forwarded-For.
  • Wyjście: Base-URL, której można użyć dla linków absolutnych, przekierowań lub OpenAPI.

unit Net-Base.ProxyForwarding;

interface

uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;

type
// Minimalny interfejs adaptera: zaimplementuj go dla WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // bezpośredni peer TCP (zwykle nginx)
end;

TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // np. 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 być „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 w []: [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 (Uwaga: przy gołym IPv6 bez [] niepewne)
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
// Przykład: 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;

// Kilka elementów oddzielonych przecinkami; bierzemy pierwszy (najbliższy klientowi)
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 być IP lub „unknown“; IPv6 może być w []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// występuje for=1.2.3.4:5678
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // defensywnie
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
// Do prawidłowej normalizacji IPv6 potrzebne byłoby parsowanie IP; tu celowo pragmatyczne.
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: bezpośredni peer

// Tylko jeśli bezpośredni peer jest znanym proxy, analizujemy nagłówki 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-* jako fallback/uzupełnienie
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;

// W ostateczności pobierz Host z nagłówka „Host“ (jeśli brak nagłówków proxy)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

end.

Cel: Z każdego żądania otrzymują Państwo spójny widok na ClientIP, Proto i Host oraz BaseUrl. Te informacje można wykorzystać centralnie do logowania, decyzji bezpieczeństwa (np. lista dozwolonych adresów IP) oraz generowania odnośników.

Dlaczego lista zaufanych proxy jest potrzebna: Bez weryfikacji zaufania atakujący może bezpośrednio dotrzeć do portu Delphi (błędna konfiguracja, wewnętrzne routing, VPN) i po prostu wysłać X-Forwarded-For: 127.0.0.1. W ten sposób ścieżki audytu, limity szybkości lub punkty końcowe „tylko wewnętrzne” stają się podatne. Ufaj nagłówkom Forwarded tylko wtedy, gdy bezpośredni peer (RemoteIP) jest proxy, które kontrolujesz (np. 127.0.0.1, adres IP load balancera, Kubernetes Ingress).

Pułapki: IPv6 bez nawiasów kwadratowych w notacji Host:port jest niejednoznaczny. W nagłówku HTTP-Host IPv6 zwykle zapisywane jest w []; trzymaj się tego formatu. Dla złożonych zakresów IP (CIDR) trzeba rozszerzyć listę zaufanych proxy (np. o prawdziwe parsowanie adresów IP).

Integracja w WebBroker/Horse/RAD Server: gdzie kod „podłącza się”

W WebBroker (TWebRequest) nagłówki zwykle trafiają przez ContentFields lub GetFieldByName, a Remote-IP zależy od backendu serwera. W Horse (lub innych frameworkach HTTP) zazwyczaj dostępne są Req.Headers i właściwość Remote-IP. Istotna jest zasada: RemoteIP musi być stroną TCP, a nie dowolną wartością z nagłówka.

Sprawdzone w praktyce: przy starcie serwisu utwórz TTrustedProxyList z konfiguracji (INI/ENV), np. „127.0.0.1” dla lokalnych konfiguracji nginx lub adres IP Twojego load balancera. Następnie wywołuj ResolveForwardedInfo dla każdego żądania i zapisuj pola w ustrukturyzowanym logowaniu (JSON-Log, Syslog lub Windows Event Log).

Debugowanie w działaniu: jak znaleźć błędy w minutach zamiast godzin

Gdy żądania wydają się „dziwne”, rzadko problem leży w samym Delphi-HTTP, częściej jest to kombinacja nagłówków proxy, logiki przekierowań i timeoutów. Trzy kontrole debugowe, które opłaca się wykonywać na co dzień:

  1. Zrzut nagłówków (selektywny): Loguj przy 4xx/5xx dodatkowo Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent i Request-URI. Jednak tylko przy błędach – inaczej logi staną się kosztowne i nieczytelne.
  2. Weryfikacja Base-URL: Gdy przekierowania lub adresy callback zawodzą, loguj ForwardedInfo.BaseUrl. Wiele błędów jest od razu widocznych („http://127.0.0.1” zamiast „https://api…”).
  3. Korelacja timeoutów: 504 od proxy to nie to samo co timeout po stronie Delphi. nginx proxy_read_timeout oraz po stronie Delphi ustawienia Idle-/Read-Timeout muszą być spójne.

Przypadki brzegowe: WebSockets, strumieniowanie i duże żądania

WebSockets za nginx

Dla WebSockets nginx wymaga prawidłowych nagłówków Upgrade i Connection. Dodatkowo backend nie może zamykać połączenia „zbyt wcześnie”. Po stronie Delphi istotne jest, aby komponent WebSocket (lub punkt końcowy SSE/streaming) potrafił obsłużyć reverse proxy oraz miał poprawnie zaimplementowane heartbeaty/keep-alive.

Duże przesyłania i błąd 413

Klasyczny przypadek: Delphi akceptuje upload, ale nginx wcześniej blokuje z 413 Request Entity Too Large. Steruj tym wyraźnie przez client_max_body_size i dopasuj limity żądań po stronie Delphi. Dla rozwiązań procesowych z dokumentami lub obrazami to nie jest wyjątek, tylko normalna eksploatacja.

HTTPS-Offloading und „Secure Cookies“

Jeżeli Państwa Delphi-Service ustawia ciasteczka sesyjne, przy zewnętrznym HTTPS zwykle muszą być one oznaczone jako Secure. To, czy Państwa aplikacja to robi, często zależy od tego, czy „wie”, że pierwotne żądanie było HTTPS. Właśnie tutaj pomaga spójna analiza X-Forwarded-Proto/Forwarded.

Kiedy wysiłek się opłaca — i gdzie może się to załamać

Pokazywane podejście opłaca się zawsze wtedy, gdy Delphi-Service nie znajduje się już „nago” w LAN, lecz stanowi część produkcyjnej krawędzi: wiele domen, interfejsy SSO/SAML, Public APIs, obsługa wielu tenantów lub surowsze wymagania audytowe. Zawodzi tam, gdzie nagłówkom Forwarded ufa się w ciemno lub topologie proxy nie są udokumentowane (wiele etapów Ingress, Cloud-LB plus nginx plus Sidecar). Wówczas Client-IP i schemat szybko stają się „cokolwiek”.

Wyraźna granica: Jeżeli potrzebują Państwo złożonych reguł zaufania (CIDR, IPv6-Netze, dynamiczne LB-IPs), powinni Państwo rozszerzyć sprawdzanie zaufania (rzeczywiste parsowanie IP, maski sieciowe) albo zaprojektować infrastrukturę tak, aby tylko zdefiniowany proxy mógł osiągnąć port Delphi (Firewall/Security Groups). To na końcu zazwyczaj bardziej odporna decyzja operacyjna.

Wniosek: Prawidłowa eksploatacja Reverse Proxy z nginx i Delphi oznacza „poprawne obsłużenie Forwarded”

Reverse Proxy z nginx jest dla Delphi-REST-Server dobrym standardowym elementem – ale dopiero poprawne traktowanie Forwarded i X-Forwarded-* sprawia, że konfiguracja jest stabilna w eksploatacji. Sedno jest proste: akceptować nagłówki tylko od zaufanych proxy, spójnie wyprowadzać Client-IP/Scheme/Host i stosować tę podstawę w przekierowaniach, logowaniu i kontrolach bezpieczeństwa. Dzięki powyższemu snippetowi mają Państwo czyste, kompatybilne z legacy fundament, który można zintegrować z WebBroker, Horse lub własnymi serwerami HTTP.

Jeżeli chcą Państwo skonsolidować istniejące Delphi-Backend za nginx lub zmodernizować je w kierunku Delphi REST-API i REST-Server z jasną linią operacyjną, techniczny przegląd łańcucha proxy i analizowania nagłówków często daje najszybszy efekt. Prosimy o kontakt z Net-Base w celu krótkiej technicznej oceny.

W środowisku merytorycznym Nginx Reverse Proxy i nagłówki Forwarded również odgrywają ważną rolę, gdy integracje, przepływy danych i dalszy rozwój muszą współgrać w sposób uporządkowany.

Omówić projekt lub przedsięwzięcie modernizacyjne z Net-Base.

Udostępnij wpis

Udostępnij ten wpis bezpośrednio

LinkedIn, X, XING, Facebook, WhatsApp i e‑mail są natychmiast dostępne. Dla Instagrama przygotowujemy od razu link i krótki tekst.

E-mail

Instagram otwiera się w nowej karcie. Link i krótki tekst są wcześniej kopiowane do schowka.