Net-Base Žurnāls

23.05.2026

Reverse proxy ar nginx un Delphi: tīra Forwarded apstrāde, reāla klienta IP un robustas URL bāzes

Ja Delphi-REST serveri darbojas aiz nginx, bieži tiek zaudēta klienta IP adrese, traucēta HTTPS atpazīšana un izkropļotas absolūtās URL. Šis koda fragments parāda izturīgu Forwarded-/X-Forwarded apstrādi (ieskaitot Trust-Proxy sarakstu), tipiskas nginx konfigurācijas un atkļūdošanas norādes darbības nodrošināšanai.

23.05.2026

Reverse Proxy ar nginx un Delphi praksē parasti nav tikai „nice to have“, bet gan skaidra atdalīšana starp interneta malu un aplikāciju: TLS terminēšana (HTTPS offloading), centrālas Header/CORS politikas, rate-limits, vienoti žurnāli, Blue/Green izvietošanas vai vienkārši vairāku servisu hostēšana zem vienas domēnas. To, kas bieži tiek novērtēts par zemu: tiklīdz nginx «stāv priekšā», Delphi serveris redz tikai proksija IP, bieži vien tikai «http» nevis «https» un ģenerē nepareizas absolūtās saites (redirects, callback-URL, OpenAPI servera URL). Tieši šie trīs punkti vēlāk palielina debugging laiku ekspluatācijā.

Šis koda fragments demonstrē izturīgu modeli, kā Delphi pareizi apstrādāt Forwarded un X-Forwarded-* — ieskaitot uzticamo proksiju sarakstu (svarīgi pret galveņu spoofingu) un konsekventu pieprasījuma bāzes URL. Papildus ir praktiskas nginx konfigurācijas un norādes par malējām situācijām, piemēram, WebSockets, lieliem augšupielādes failiem un timeouts.

Kāpēc Reverse Proxy izvietojumi Delphi serverus „apjauc“

nginx kā Reverse Proxy parasti sazinās ar Delphi servisu nešifrēti (HTTP) iekšējā tīklā vai uz localhost, kamēr klients ārpusē nāk pa HTTPS. Bez papildu galvenēm Delphi neuzzina par:

  • Oriģinālā shēma (https vs. http) — svarīga pāradresācijām (redirects) un absolūtām URL.
  • Oriģinālais hosts (klientam specifiska domēna nosaukums, ports) — svarīgs multi-tenant izvietojumiem, CORS un callback-URL.
  • Oriģinālā klienta IP — svarīga audita, rate-limits, ģeolokācijas pārbaudēm un drošības analīzēm.

nginx var šos datus nodot galvenēs. Parasti lieto X-Forwarded-For, X-Forwarded-Proto un X-Forwarded-Host; papildus standartizēta ir RFC galvene Forwarded. Svarīgi: no aplikācijas skatpunkta šīs galvenes nav automātiski uzticamas, jo klients tās var nosūtīt pats — tās kļūst uzticamas tikai tad, ja nāk no zināma proksija.

nginx konfigurācija: minimāli lietderīgās proxy-galvenes

Labs sākumpunkts (HTTP/1.1, Keep-Alive, Upgrade WebSockets) izskatās šādi. Snippet ir apzināti īss; atkarībā no vides papildiniet HSTS, rate-limits un piekļuves žurnālus.

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

Mērķis: Lietojumprogramma saņem Host, klienta IP un shēmu uzticami nodotu. Nosacījums: $proxy_add_x_forwarded_for pievieno pašreizējo starpniekservera IP iespējamajai esošajai ķēdei; tas ir lietderīgi daudzslāņu proxy konfigurācijās, taču padara pareizu izvērtēšanu Delphi pusē vēl svarīgāku. Riska punkts: Ja nginx neuzstāda Host galveni, Delphi iespējams redzēs tikai upstream-hostu (127.0.0.1), kas izjauc pāradresācijas un Origin pārbaudes.

Delphi Avota koda fragments: Forwarded/X-Forwarded robusti izvērtēt (ar uzticamo starpniekserveru sarakstu)

Šāds kods ir apzināti neatkarīgs no ietvariem: tas darbojas pret minimālu saskarni (Header + RemoteIP) un to var pielāgot WebBroker, RAD Server vai Horse. Galvenie punkti:

  • Prioritāte: RFC Forwarded (ja pieejams) pirms X-Forwarded-*.
  • Uzticamība: Forwarded galvenes drīkst analizēt tikai tad, ja tiešais peer (RemoteIP) ir pazīstams starpniekserveris.
  • Parsēšana: Ņemt vērā IPv6, pēdiņas, portus un ķēdes X-Forwarded-For.
  • Izvade: bāzes URL, ko var izmantot absolūtām saitēm, pāradresācijām vai OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimālais adaptera interfeiss: implementējiet to WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // tiešā TCP pretējā puse (parasti nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // piemēram: 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 var būt "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 iekavās []: [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 (Uzmanību: ja IPv6 nav iekļauts iekavās [], tas var nebūt uzticami)
  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
  // Piemērs: 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;

  // Vairāki elementi atdalīti ar komatiem; mēs ņemam pirmo (klientam vistuvākais)
  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 var būt IP vai "unknown"; IPv6 var atrasties iekavās []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 var parādīties
      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
  // Lai veiktu īstu IPv6 normalizāciju, būtu nepieciešama IP parsēšana; šeit apzināti pragmatiski.
  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; // Rezerves variants: tiešā pretējā puse

  // Forwarded galvenes apstrādājam tikai tad, ja tiešā pretējā puse ir pazīstams proxy.
  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-* kā rezerves vērtība/papildinājums
    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;

  // Ja nepieciešams, iegūstam Host no "Host" galvenes (ja nav proxy galveņu)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Mērķis: No katra pieprasījuma jūs iegūstat konsekventu skatījumu uz ClientIP, Proto un Host, kā arī uz BaseUrl. Šo informāciju var izmantot centrāli žurnālfailēšanai, drošības lēmumiem (piem., IP-Allowlist) un saišu ģenerēšanai.

Kāpēc nepieciešams Trust-Proxy saraksts: Bez Trust pārbaudes uzbrucējs var tieši sasniegt jūsu Delphi portu (konfigurācijas kļūme, iekšējā maršrutēšana, VPN) un vienkārši nosūtīt X-Forwarded-For: 127.0.0.1. Tādā gadījumā tiktu apdraudēti audita ieraksti, rate-limits vai galapunkti, kas pieejami “tikai iekšēji”. Uzticieties Forwarded galvenēm tikai tad, ja tiešais savienojuma partneris (RemoteIP) ir proxies, ko jūs kontrolējat (piem., 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Bīstamie aspekti: IPv6 bez kvadrātiekavām Host:port notācijā nav viennozīmīgs. HTTP-Host galvenē IPv6 parasti tiek norādīts ar []; ievērojiet šo formātu. Arī kompleksu IP diapazonu (CIDR) atbalstam jums jāpaplašina Trust saraksts (piem., ar īstu IP-parsēšanu).

Integrācija WebBroker/Horse/RAD Server: kur kods „andockt“

WebBroker (TWebRequest) gadījumā galvenes parasti pieejamas caur ContentFields vai GetFieldByName, Remote-IP ir atkarīga no servera backend. Horse (un citi HTTP framework) parasti nodrošina Req.Headers un Remote-IP īpašību. Svarīgi principi: RemoteIP jābūt TCP pretpusei, nevis kādam galvenes vērtībai.

Praktiski pārbaudīts: izveidojiet servisa startēšanas laikā TTrustedProxyList no konfigurācijas (INI/ENV), piem., „127.0.0.1“ lokāliem nginx uzstādījumiem vai jūsu Load Balancer IP. Tad izsauciet ResolveForwardedInfo katram pieprasījumam un ierakstiet laukus savā strukturētajā žurnālfailē (JSON-Log, Syslog vai Windows Event Log).

Debugging darbībā: kā atrast kļūdas minūtēs, nevis stundās

Ja pieprasījumi izskatās „dīvaini“, retāk pie vainas ir Delphi-HTTP pats par sevi, biežāk — proxy galveņu, pāradresācijas loģikas un timeout kombinācija. Trīs debug pārbaudes, kas ikdienā atmaksājas:

  1. Header-Dump (mērķēti): pie 4xx/5xx kļūdām papildus žurnālfailā ierakstiet Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent un Request-URI. Taču tikai kļūdu gadījumos — citādi žurnāls kļūs dārgs un nepārskatāms.
  2. Base-URL pārbaudīt: ja pāradresācijas vai callback URL neizdodas, reģistrējiet ForwardedInfo.BaseUrl. Daudzas kļūdas redzamas uzreiz („http://127.0.0.1“ vietā „https://api…“).
  3. Timeout korelācija: 504 no proxy nav tas pats, kas Delphi timeout. nginx proxy_read_timeout un Delphi-pusē iestatītie Idle/Read timeouti jāsinhronizē.

Īpaši gadījumi: WebSockets, straumēšana un lieli pieprasījumi

WebSockets aiz nginx

WebSockets gadījumā nginx prasa pareizu Upgrade un Connection. Papildus backend nedrīkst slēgt savienojumu „par agru“. Delphi-pusē svarīgi, lai jūsu WebSocket komponents (vai SSE/streaming galapunkts) protētu darboties ar reversajiem proxy un ka Heartbeats/Keep-Alives ir tīri implementēti.

Lieli augšupielādes un 413 kļūdas

Klasisks piemērs: Delphi pieņem augšupielādi, bet nginx to bloķē ar 413 Request Entity Too Large. Kontrolējiet to skaidri caur client_max_body_size un pielāgojiet Delphi-pusē Request limita iestatījumus. Procesu tuvās programmatūras risinājumiem ar dokumentu vai attēlu datiem tas nav izņēmums, bet normāla darbība.

HTTPS-Offloading und „Secure Cookies“

Ja jūsu Delphi-serviss iestata sesijas sīkdatnes, tās ārējā HTTPS gadījumā parasti jāatzīmē kā Secure. Vai jūsu lietotne to dara, bieži ir atkarīgs no tā, vai tā «zina», ka sākotnējais pieprasījums bija HTTPS. Tieši šeit palīdz konsekventa X-Forwarded-Proto/Forwarded vērtēšana.

Kad ieguldījums atmaksājas — un kur tas var izjukt

Parādītā pieeja atmaksājas tad, kad Delphi-serviss vairs nedzīvo „kails“ LAN teritorijā, bet ir daļa no produktīvās malas: vairākas domēnas, SSO/SAML saskarnes, Public APIs, daudznomnieku atbalsts vai stingrākas audita prasības. Tā kļūst nedroša tur, kur Forwarded-galvenei uzticas akli vai proxy topoloģijas nav dokumentētas (vairākas Ingress pakāpes, Cloud-LB plus nginx plus Sidecar). Tad Client-IP un shēma ātri kļūst par „jebko“.

Skira robeža: ja jums nepieciešamas sarežģītas trust-noteikšanas (CIDR, IPv6-Netze, dinamiskās LB-IPs), vajadzētu paplašināt trust-pārbaudi (reāla IP-parsēšana, tīkla maskas) vai veidot infrastruktūru tā, lai tikai definēts proxy var piekļūt Delphi-portam (Firewall/Security Groups). Galarezultātā tas parasti ir robustāks ekspluatācijas lēmums.

Fazit: Reverse Proxy mit nginx und Delphi sauber betreiben heißt „Forwarded richtig machen“

Reverse Proxy ar nginx ir labs standarta elements Delphi-REST-Server vidē — taču tikai Forwarded un X-Forwarded-* korekta apstrāde padara uzstādījumu darbībā stabilu. Būtība ir vienkārša: pieņemt galvenes tikai no uzticamiem proxijiem, konsekventi izvest Client-IP/Scheme/Host un šo bāzi konsekventi izmantot pāradresācijās, žurnālfailos un drošības pārbaudēs. Ar iepriekšējo fragmentu jums ir tīra, ar legacy saderīga bāze, ko var integrēt WebBroker, Horse vai pašizveidotos HTTP-serveros.

Ja vēlaties konsolidēt esošu Delphi-backend aiz nginx vai modernizēt to virzienā uz Delphi REST-API und REST-Server ar skaidru ekspluatācijas līniju, tehnisks pārskats par proxy ķēdi un galveņu vērtēšanu bieži ir ātrākais sviras punkts. Kontaktējiet Net-Base par īsu tehnisku novērtējumu.

Profesijā Nginx Reverse Proxy un Forwarded-galvenes arī spēlē nozīmīgu lomu, ja integrācijas, datu plūsmas un turpmākā attīstība jāvirzās kopā pārdomāti.

Apspriest projektu vai modernizācijas plānu ar Net-Base.

Kopīgot ierakstu

Kopīgot šo ierakstu tieši

LinkedIn, X, XING, Facebook, WhatsApp un e-pasts ir uzreiz pieejami. Instagramam saiti un īsu tekstu sagatavosim nekavējoties.

E-pasts

Instagram atveras jaunā cilnē. Saite un īss teksts tiek iepriekš nokopēti starpliktuvē.