Net-Base Revija

23.05.2026

Reverse proxy z nginx in Delphi: natančna obdelava zaglavja Forwarded, resnična IP odjemalca in robustne URL-baze

Ko Delphi-REST-strežniki tečejo za nginx, pogosto pride do napačnih vrednosti Client-IP, zaznave HTTPS in absolutnih URL-jev. Ta izsek iz izvorne kode prikazuje robustno obravnavo polj Forwarded/X-Forwarded (vključno s seznamom zaupanih proxyjev), tipične nastavitve nginx in napotke za razhroščevanje v obratovanju.

23.05.2026

En Reverse proxy z nginx in Delphi je v praksi ponavadi ni „nice to have“, ampak čista ločitev med internetno mejo in aplikacijo: TLS-terminacija (HTTPS-Offloading), centralna pravila za headerje/CORS, omejitve hitrosti (Rate-Limits), enotni logi, Blue/Green-rollouti ali preprosto gostovanje več storitev pod eno domeno. Kar se pogosto podcenjuje: ko nginx stoji „pred“ njim, Delphi-strežnik vidi le IP proxyja, pogosto le „http“ namesto „https“ in generira napačne absolutne povezave (preusmeritve, callback-URL-ji, OpenAPI-server-URL). Prav ti trije vidiki kasneje povzročijo čas za odpravljanje napak v produkciji.

Ta izsek izvorne kode prikazuje robusten vzorec, kako v Delphi pravilno obdelati Forwarded oziroma X-Forwarded-* — vključno s Trust-Proxy-Liste (pomembno proti ponarejanju headerjev) in dosledno Request-Base-URL. Poleg tega so priložene praktične nginx-konfiguracije in napotki za robne primere, kot so WebSockets, velika nalaganja in timeouti.

Zakaj konfiguracije z reverse proxyjem Delphi-strežnike „zmedejo“

nginx kot reverse proxy običajno komunicira z Delphi-storitvijo nešifrirano (HTTP) v notranjem omrežju ali na localhostu, medtem ko klient zunaj prihaja po HTTPS. Brez dodatnih headerjev Delphi ne ve nič o:

  • Originalna shema (https proti http) – pomembno za preusmeritve in absolutne URL-je.
  • Originalni gostitelj (specifična domena stranke, port) – pomembno za multi-tenant nastavitve, CORS in callback-URL-je.
  • Originalna IP stranke – pomembno za revizijo, omejevanje hitrosti, geolokacijske preverbe in varnostne analize.

nginx lahko te informacije prenese preko headerjev. Pogosti so X-Forwarded-For, X-Forwarded-Proto in X-Forwarded-Host; standardiziran je dodatno RFC-header Forwarded. Pomembno: ti headerji z vidika aplikacije niso avtomatično zanesljivi, ker jih lahko pošlje tudi klient sam — postanejo zanesljivi šele, ko izvirajo iz znanega proxyja.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Dober izhodiščni komplet (HTTP/1.1, Keep-Alive, Upgrade za WebSockets) izgleda takole. Izsek je namensko kratek; glede na okolje dodate HSTS, Rate-Limits in access-loge.

Delphi
# (nginx-konfiguracija, ne 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;

    # opcijsko, vendar praktično za absolutne URL-je
    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;

    # Timeouti, prilagojeni Delphi-backendu (dolgi poročili/izvozi)
    proxy_connect_timeout 5s;
    proxy_send_timeout    60s;
    proxy_read_timeout    60s;

    # eksplicitno nadziranje velikih nalaganj
    client_max_body_size 50m;
  }
}

Namen: Aplikaciji se zanesljivo posredujejo Host, odjemalčeva IP in shema. Omejitev: $proxy_add_x_forwarded_for pripne trenutno IP proxyja na morebitno obstoječo verigo; to je koristno za multi-proxy konfiguracije, vendar naredi pravilno analizo na strani Delphi še pomembnejšo. Past: Če v nginx ne nastavite Host-glave, lahko Delphi morda vidi le upstream-host (127.0.0.1), kar zlomi preusmeritve in preverjanja izvora.

Delphi Izsek izvorne kode: robustna obdelava Forwarded/X-Forwarded (s seznamom zaupanja vrednih proxyjev)

Naslednja koda je namensko neodvisna od ogrodja: deluje proti minimalnemu vmesniku (Header + RemoteIP) in jo je mogoče prilagoditi v WebBroker, RAD Server ali Horse. Ključne točke:

  • Prednost: RFC Forwarded (če obstaja) pred X-Forwarded-*.
  • Zaupanje: Forwarded-glave obdelujte le, če je neposredni vrstnik (RemoteIP) znan proxy.
  • Parsiranje: upoštevajte IPv6, narekovaje, porte in verige v X-Forwarded-For.
  • Izhod: Base-URL, ki jo lahko uporabite za absolutne povezave, preusmeritve ali OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimalni vmesnik adapterja: implementirajte ga za WebBroker/Horse/itd.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // neposredna TCP-stranka (ponavadi 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 je lahko "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 v []: [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 (Pozor: pri gola IPv6 brez [] ni zanesljivo)
  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
  // Beispiel: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
  Parts: TArray;
  I: Integer;
  KV: TArray;
  K, V: string;
  FirstElement: string;
begin
  Result := False;
  ClientIP := '';
  Proto := '';
  Host := '';

  if ForwardedValue.Trim = '' then
    Exit;

  // Več elementov je ločenih z vejico; vzamemo prvi (najbližji klientu)
  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 je lahko IP ali "unknown"; IPv6 je lahko v []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 se zgodi
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // varnostno
      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.Create;
end;

destructor TTrustedProxyList.Destroy;
begin
  FSet.Free;
  inherited;
end;

class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
  // Za resnično normalizacijo IPv6 bi bilo potrebno razčlenjevanje IP; tukaj namerno 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; // Rezerva: neposredna TCP-stranka

  // Samo če je neposredna stranka znan proxy, obdelamo Forwarded headerje.
  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-* kot rezerva/dopolnilo
    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;

  // V skrajnem primeru vzemi Host iz headerja "Host" (če ni proxy-headerja)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Namen: Iz vsake zahteve prejmete konsistenten pogled na ClientIP, Proto in Host ter na BaseUrl. Te informacije lahko centralno uporabite za beleženje, varnostne odločitve (npr. IP-allowlist) in generiranje povezav.

Zakaj je seznam zaupanja vrednih proxyjev potreben: Brez preverjanja zaupanja bi napadalec lahko neposredno dosegel vaš Delphi-Port (napaka v konfiguraciji, interno usmerjanje, VPN) in preprosto poslal X-Forwarded-For: 127.0.0.1. S tem bi bili ogroženi audit-trace, omejitve hitrosti ali končne točke »samo interna dovoljena«. Forwarded-glavam zaupajte le, če je neposredni peer (RemoteIP) proxy, ki ga nadzorujete (npr. 127.0.0.1, IP Load Balancerja, Kubernetes-Ingress).

Past: IPv6 brez oglatih oklepajev ni enoznačen v notaciji Host:port. V HTTP-Host-glavi je IPv6 običajno zapisan v []; držite se tega. Za kompleksne IP-pasove (CIDR) boste morali razširiti seznam zaupanja vrednih (npr. z dejanskim parsiranjem IP).

Integracija v WebBroker/Horse/RAD Server: kje se koda „priključi“

V WebBroker (TWebRequest) pridejo glave običajno prek ContentFields ali GetFieldByName, Remote-IP pa je odvisen od strežniškega backenda. V Horse (ali drugih HTTP- ogrodjih) obstajajo navadno Req.Headers in lastnost Remote-IP. Pomembno je načelo: RemoteIP mora biti TCP-protistran, ne kakšna vrednost iz glave.

Praktično preverjeno: ob zagonu storitve ustvarite TTrustedProxyList iz konfiguracije (INI/ENV), npr. „127.0.0.1“ za lokalne nginx-nastavitve ali IP vašega Load Balancerja. Nato na zahtevo pokličite ResolveForwardedInfo in polja zapišite v strukturirano beleženje (JSON-log, Syslog ali Windows Event Log).

Razhroščevanje v produkciji: kako najti napake v minutah namesto ur

Če so zahteve »čudne«, razlog redko leži v Delphi-HTTP samem, temveč v kombinaciji proxy-glav, logike preusmeritev in timeoutov. Tri debug-preverjanja, ki se v praksi izplačajo:

  1. Header-dump (ciljno): Pri 4xx/5xx dodatno beležite Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent in Request-URI. A le pri napakah – sicer je log drag in nepregleden.
  2. Preverite Base-URL: Če preusmeritve ali callback-URL-i odpovedo, logirajte ForwardedInfo.BaseUrl. Veliko napak je takoj vidnih („http://127.0.0.1“ namesto „https://api…“).
  3. Usklajevanje timeoutov: 504 od proxya ni enako kot timeout na strani Delphi. nginx proxy_read_timeout in na strani Delphi nastavljeni Idle-/Read-timeouti morajo biti usklajeni.

Robni primeri: WebSockets, pretakanje in veliki zahtevki

WebSockets za nginx

Za WebSockets nginx potrebuje pravilna Upgrade in Connection. Poleg tega backend ne sme »prezgodaj« zapreti povezave. Na strani Delphi je relevantno, da vaša WebSocket-komponenta (ali SSE/streaming endpoint) zna delati z reverznimi proxyji in da so heartbeati/keep-alive mehanizmi izvedeni čisto.

Veliki naloži in 413-napake

Klasika: Delphi sprejme upload, a nginx prej blokira z 413 Request Entity Too Large. Krmili to eksplicitno z client_max_body_size in prilagodite omejitve zahtev na strani Delphi. Za procesno bližnje programske rešitve z dokumenti ali slikami to ni izjema, temveč rutinski del obratovanja.

HTTPS-Offloading und „Secure Cookies“

Če vaš Delphi-storitev nastavi sejnske piškotke, morajo biti ti pri zunanjem HTTPS praviloma označeni kot Secure. Ali vaša aplikacija to naredi, pogosto zavisi od tega, ali ve, da je bil izvorni zahtevek HTTPS. Prav tu pomaga dosledna analiza X-Forwarded-Proto/Forwarded.

Kdaj se trud splača – in kje lahko odpove

Prikazani pristop se izplača vedno, ko Delphi-storitev ni več „gola“ v LAN‑u, ampak postane del produktivnega roba: več domen, SSO/SAML‑vmesniki, javne API, večstrankost ali strožje revizijske zahteve. Ne deluje tam, kjer se Forwarded‑headerjem slepo zaupa ali kjer proxy‑topologije niso dokumentirane (več stopenj ingressa, Cloud‑LB plus nginx plus Sidecar). V takih primerih postaneta Client‑IP in scheme hitro „nekaj“.

Jasna meja: če potrebujete kompleksna pravila zaupanja (CIDR, IPv6‑omrežja, dinamične LB‑IP), bi morali razširiti preverjanje zaupanja (pravo razčlenjevanje IP, mrežne maske) ali infrastrukturo zasnovati tako, da lahko do porta Delphi dostopa le definiran proxy (Firewall/Security Groups). Na koncu je to običajno bolj robustna operativna odločitev.

Zaključek: nginx Reverse Proxy in Delphi robustno obratovati pomeni „Forwarded pravilno obravnavati“

Reverse Proxy z nginx je za Delphi-REST-strežnik dober standardni gradnik – vendar šele pravilna obravnava Forwarded in X-Forwarded-* naredi postavitev v obratovanju stabilno. Jedro je preprosto: headerje sprejemati le od zaupanja vrednih proxyjev, Client-IP/Scheme/Host dosledno določiti in to osnovo dosledno uporabiti v preusmeritvah, beleženju in varnostnih kontrolah. S prikazanim snippetom imate za to čisto, za legacy primerno osnovo, ki jo lahko integrirate v WebBroker, Horse ali lastne HTTP‑strežnike.

Če želite obstoječi Delphi-backend za nginx konsolidirati ali modernizirati proti Delphi REST-API in REST-strežnik z jasno operativno linijo, je tehnični pregled verige proxyjev in analize headerjev pogosto najhitrejši ukrep. Kontaktirajte Net-Base za kratno tehnično oceno.

V strokovnem okolju imata tudi nginx Reverse Proxy in Forwarded headerji pomembno vlogo, kadar morajo integracije, podatkovni tokovi in nadaljnji razvoj delovati usklajeno.

Razpravljajte o projektu ali modernizacijskem ukrepu z Net-Base.

Deli objavo

Deli ta prispevek neposredno

LinkedIn, X, XING, Facebook, WhatsApp in e-pošta so takoj na voljo. Za Instagram bomo neposredno pripravili povezavo in kratek opis.

E-pošta

Instagram se odpre v novem zavihku. Povezava in kratek opis se pred tem kopirata v odložišče.