Net-Base Časopis

23.05.2026

Reverse proxy s nginxom i Delphi: ispravno rukovanje zaglavljima Forwarded, stvarna IP adresa klijenta i robusne URL baze

Ako Delphi-REST-serveri rade iza nginx-a, često dolazi do gubitka Client-IP, prepoznavanja HTTPS-a i apsolutnih URL-ova. Ovaj isječak izvornog koda prikazuje robustno rukovanje Forwarded-/X-Forwarded zaglavljima (uključujući listu pouzdanih proxyja), tipične nginx postavke i smjernice za debugiranje u operativnom radu.

23.05.2026

Ein Reverse Proxy mit nginx und Delphi je u praksi obično nije „nice to have“, nego čista separacija između internet-ruba i aplikacije: TLS-terminacija (HTTPS-Offloading), centralna pravila za Header/CORS, Rate-Limits, jedinstveni logovi, Blue/Green-Rollouts ili jednostavno hosting više servisa pod jednom domenom. Ono što se često podcijeni: čim nginx «sjedi ispred», Delphi-server vidi samo IP proxyja, često samo „http“ umjesto „https“ i generira pogrešne apsolutne linkove (Redirects, Callback-URLs, OpenAPI-Server-URL). Upravo ta tri problema kasnije stvaraju vrijeme za debugiranje u radu.

Ovaj isječak koda prikazuje robustan obrazac kako u Delphi ispravno evaluirati Forwarded odnosno X-Forwarded-* — uključujući popis pouzdanih proxyja (važno protiv spoofanja zaglavlja) i dosljedan Request-Base-URL. Uz to dolaze praktične nginx konfiguracije i napomene o rubnim slučajevima poput WebSockets, velikih uploadova i timeouta.

Warum Reverse Proxy-Setups Delphi-Server „verwirren“

nginx kao Reverse Proxy obično komunicira sa Delphi servisom nešifrirano (HTTP) u internoj mreži ili na localhostu, dok klijent izvana dolazi putem HTTPS-a. Bez dodatnih zaglavlja Delphi ne zna ništa o:

  • Original-Schema (https vs. http) – relevantno za Redirects i apsolutne URL-ove.
  • Original-Host (domena specifična za klijenta, port) – relevantno za Multi-Tenant-Setups, CORS i Callback-URLs.
  • Original-Client-IP – relevantno za Audit, Rate-Limits, Geo-Checks i sigurnosne analize.

nginx može te informacije prenijeti preko zaglavlja. Uobičajena su X-Forwarded-For, X-Forwarded-Proto i X-Forwarded-Host; dodatno je standardizirano RFC zaglavlje Forwarded. Važno: ta zaglavlja s točke gledišta aplikacije nisu automatski pouzdana, jer ih klijent može poslati sam — postaju pouzdana tek kada dolaze od poznatog proxyja.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Solidna polazna točka (HTTP/1.1, Keep-Alive, Upgrade za WebSockets) izgleda ovako. Isječak je namjerno kratak; ovisno o okruženju dodajte HSTS, Rate-Limits i Access-Logs.

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

Svrha: Aplikacija pouzdano prima Host, Client-IP i shemu. Preduvjet: $proxy_add_x_forwarded_for dodaje trenutnu Proxy-IP na eventualni postojeći lanac; to je korisno za Multi-Proxy konfiguracije, ali čini ispravno tumačenje na strani Delphi time još važnijim. Zamka: Ako u nginxu ne postavite Host-zaglavlje, Delphi možda vidi samo Upstream-Host (127.0.0.1), što remeti preusmjerenja i provjere izvora.

Delphi Izvorni isječak: Robustno procjenjivanje Forwarded/X-Forwarded (s popisom pouzdanih proxyja)

Sljedeći kod je namjerno neutralan prema frameworku: Radi protiv minimalnog sučelja (Header + RemoteIP) i može se prilagoditi za WebBroker, RAD Server ili Horse. Ključne točke:

  • Prioritet: RFC Forwarded (ako postoji) prije X-Forwarded-*.
  • Povjerenje: Forwarded-zaglavlja tumačiti samo ako je izravni peer (RemoteIP) poznati proxy.
  • Parsiranje: Uvažiti IPv6, navodnike, portove i lance u X-Forwarded-For.
  • Ishod: bazni URL koji možete koristiti za apsolutne linkove, preusmjerenja ili OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimalno adapter-sučelje: implementirajte ga za WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // izravni TCP suprotnik (većinom 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 može biti "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 u []: [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 (Upozorenje: za golu IPv6 bez [] nije pouzdano)
  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
  // Primjer: 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;

  // Više elemenata je odvojeno zarezom; uzimamo prvi (najbliži klijentu)
  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 biti IP ili "unknown"; IPv6 može biti u []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 se može pojaviti
      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
  // Za pravu IPv6 normalizaciju trebalo bi parsirati IP; ovdje svjesno 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; // Zadana vrijednost: izravna TCP suprotnica

  // Samo ako je izravna suprotnica poznati proxy, tumačimo Forwarded zaglavlja.
  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-* kao rezervna/ dopuna
    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;

  // U krajnjem slučaju uzeti Host iz "Host" zaglavlja (ako nema proxy-zaglavlja)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Svrha: Iz svakog zahtjeva dobivate konzistentan pregled ClientIP, Proto i Host kao i BaseUrl. Ove informacije možete centralno koristiti za logiranje, sigurnosne odluke (npr. IP-Allowlist) i generiranje linkova.

Zašto je potrebna Trust-Proxy lista: Bez provjere povjerenja napadač bi mogao izravno dosegnuti vaš Delphi-port (pogrešna konfiguracija, interno rutiranje, VPN) i jednostavno poslati X-Forwarded-For: 127.0.0.1. Time bi bili ugroženi audit-trailovi, rate-limitovi ili endpointi „samo interno dozvoljeno“. Vjerujte Forwarded-zaglavima samo ako je izravni peer (RemoteIP) proxy koji kontrolirate (npr. 127.0.0.1, IP load balancera, Kubernetes-Ingress).

Zamke: IPv6 bez uglatih zagrada u notaciji Host:port nije jednoznačan. U HTTP-Host-zaglavlju IPv6 je obično označen u []; pridržavajte se toga. Za složenije IP-opsege (CIDR) trebali biste proširiti Trust-listu (npr. stvarnim parsiranjem IP-a).

Integracija u WebBroker/Horse/RAD Server: gdje se kod „priključuje“

U WebBrokeru (TWebRequest) zaglavlja obično dolaze preko ContentFields ili GetFieldByName, Remote-IP ovisi o server-backendu. U Horseu (ili drugim HTTP-frameworkima) obično postoji Req.Headers i svojstvo Remote-IP. Važno je načelo: RemoteIP mora biti TCP-peer (izravna protivstrana), a ne neka vrijednost iz zaglavlja.

U praksi provjereno: prilikom pokretanja servisa generirajte TTrustedProxyList iz konfiguracije (INI/ENV), npr. „127.0.0.1“ za lokalne nginx postavke ili IP vašeg Load Balancers. Zatim pozovite ResolveForwardedInfo za svaki request i upišite polja u vaše strukturirano logiranje (JSON-Log, Syslog ili Windows Event Log).

Debugging u radu: kako naći greške u minutama umjesto satima

Ako zahtjevi djeluju „čudno“, rijetko je kriv Delphi-HTTP sam po sebi, već kombinacija proxy-zaglavlja, redirect-logike i timeouta. Tri debug provjere koje vrijedi raditi u praksi:

  1. Header-Dump (ciljano): Zabilježite kod 4xx/5xx dodatno Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent i Request-URI. Ali samo kod grešaka – inače će log postati skup i nepregledan.
  2. Provjera Base-URL: Ako redirecti ili callback-URL-ovi ne uspiju, zabilježite ForwardedInfo.BaseUrl. Mnoge greške su odmah vidljive („http://127.0.0.1“ umjesto „https://api…“).
  3. Korelacija timeouta: 504 od proxya nije isto što i Delphi-timeout. nginx proxy_read_timeout i Delphi-na strani Idle-/Read-timeouti moraju biti usklađeni.

Rubni slučajevi: WebSockets, streaming i veliki zahtjevi

WebSockets iza nginx

Za WebSockets nginx treba imati ispravno postavljene Upgrade i Connection. Dodatno, backend ne smije zatvarati „preuranjeno“. Na Delphi-strani važno je da vaša WebSocket-komponenta (ili SSE/streaming endpoint) može raditi s reverse proxyima i da su heartbeati/keep-alive mehanizmi pravilno implementirani.

Veliki uploadi i 413-Fehler

Klasik: Delphi prihvaća upload, ali nginx ga prethodno blokira s 413 Request Entity Too Large. Kontrolirajte to eksplicitno preko client_max_body_size i prilagodite na Delphi-strani ograničenja zahtjeva. Za procesno bliske softverske rješenja koja rade s dokumentima ili slikama to nije iznimka, nego normalan rad.

HTTPS-Offloading und „Secure Cookies“

Ako vaš Delphi-servis postavlja sesijske kolačiće, oni moraju biti označeni kao Secure kada se koriste putem vanjskog HTTPS‑a. Hoće li vaša aplikacija to učiniti često ovisi o tome „zna li“ da je izvorni zahtjev bio HTTPS. Upravo tu pomaže dosljedno evaluiranje X-Forwarded-Proto/Forwarded.

Kada se trud isplati – i gdje može postati problematično

Prikazani pristup isplati se svaki put kada Delphi-servis više nije „nagnječen“ u LAN, već je dio produktivne ivice: više domena, SSO/SAML‑sučelja, javni API‑ji, podrška za više najmoprimaca ili stroži zahtjevi za reviziju. Postaje problematičan tamo gdje se Forwarded‑headereu slijepo vjeruje ili gdje topologije proxyja nisu dokumentirane (više Ingress stupnjeva, Cloud‑LB plus nginx plus sidecar). U takvim situacijama adresa klijenta i shema brzo postaju nepouzdani.

Jasna granica: ako trebate složena pravila povjerenja (CIDR, IPv6 mreže, dinamičke LB‑IP adrese), trebali biste proširiti provjeru povjerenja (stvarno parsiranje IP‑ova, mrežne maske) ili infrastrukturno osigurati da samo definiran proxy može dosegnuti Delphi‑port (Firewall/Security Groups). To je na kraju često robusnija operativna odluka.

Zaključak: ispravno održavanje Reverse Proxya s nginx i Delphi znači „Forwarded napraviti kako treba“

Reverse Proxy s nginxom je za Delphi-REST‑server dobar standardni gradivni blok – ali tek ispravno rukovanje Forwarded i X-Forwarded-* čini postavku stabilnom u radu. Jezgra je jednostavna: headere prihvaćajte samo od pouzdanih proxyja, dosljedno izvodite Client‑IP/Scheme/Host i tu osnovu provucite kroz preusmjeravanja, logiranje i sigurnosne provjere. Sa snippetom iznad imate čvrst, za legacy prikladan temelj koji se može integrirati u WebBroker, Horse ili vlastite HTTP‑servere.

Ako želite konsolidirati postojeće Delphi‑backend iza nginx‑a ili ga modernizirati prema Delphi REST‑API i REST‑server s jasno postavljenom operativnom linijom, tehnički pregled lanca proxyja i evaluacije headera često je najučinkovitiji zahvat. Kontaktirajte Net-Base za kratku tehničku procjenu.

U stručnom kontekstu Nginx Reverse Proxy i Forwarded headeri također igraju važnu ulogu kad se integracije, tokovi podataka i daljnji razvoj moraju jasno i čisto uskladiti.

Razgovarajte o projektu ili modernizacijskom zahvatu s Net-Base.

Podijeli objavu

Izravno proslijedite ovu objavu

LinkedIn, X, XING, Facebook, WhatsApp i e-mail su odmah dostupni. Za Instagram pripremamo link i kratak tekst.

E-pošta

Instagram se otvara u novoj kartici. Link i kratki tekst se prethodno kopiraju u međuspremnik.