Net-Base Lehti

23.05.2026

Reverse-proxy nginx ja Delphi: asianmukainen Forwarded-käsittely, todellinen asiakas-IP ja vankat URL-perusteet

Kun Delphi-REST-palvelimet ajetaan nginxin takana, asiakas-IP, HTTPS-tunnistus ja absoluuttiset URL-osoitteet usein vääristyvät. Tämä lähdekoodikatkelma esittelee robustin Forwarded-/X-Forwarded-käsittelyn (mukaan lukien Trust-Proxy-lista), tyypilliset nginx-asetukset ja vianmääritysohjeet tuotantokäyttöä varten.

23.05.2026

Ein Reverse-proxy nginxillä ja Delphi on käytännössä harvoin „nice to have“, vaan selkeä erottelu internetreunan ja sovelluksen välillä: TLS-terminointi (HTTPS-Offloading), keskitetyt Header-/CORS-säännöt, Rate-Limits, yhtenäiset lokit, Blue/Green-Rolloutit tai yksinkertaisesti useiden palveluiden hosting saman domainin alla. Mitä usein aliarvioidaan: Kun nginx istuu „edessä“, Delphi-palvelin näkee vain proxyn IP-osoitteen, usein vain „http“ eikä „https“ ja tuottaa virheellisiä absoluuttisia linkkejä (Redirects, Callback-URL:t, OpenAPI-Server-URL). Juuri nämä kolme kohtaa aiheuttavat myöhemmin debuggausaikaa tuotannossa.

Tämä Source-Schnipsel näyttää vankan kaavan, kuinka Delphi:ssa käsitellään Forwarded– tai X-Forwarded-*-headerit oikein – mukaan lukien Trust-Proxy-Liste (tärkeä Header-Spoofingia vastaan) ja johdonmukainen Request-Base-URL. Lisäksi mukana on käytännöllisiä nginx-konfiguraatioita ja huomioita reunatapauksista kuten WebSockets, suuret uploadit ja timeouts.

Miksi Reverse-proxy-asetukset Delphi-palvelinta „hämmentävät“

nginx kommunikoi Reverse-proxyna Delphi-palvelun kanssa tyypillisesti salaamattomasti (HTTP) sisäverkossa tai localhostissa, kun taas asiakaspuoli saapuu ulkoa HTTPS:llä. Ilman lisäheaderia Delphi ei tiedä seuraavista:

  • Alkuperäinen protokolla (https vs. http) – oleellinen uudelleenohjauksille ja absoluuttisille URL-osoitteille.
  • Alkuperäinen host (asiakaskohtainen domain, portti) – merkityksellinen multi-tenant-asetuksille, CORS:lle ja callback-URL:ille.
  • Alkuperäinen asiakkaan IP-osoite – merkityksellinen auditointeihin, rate-limiteihin, geo-tarkistuksiin ja tietoturva-analyyseihin.

nginx voi välittää nämä tiedot headerien kautta. Tavallisia ovat X-Forwarded-For, X-Forwarded-Proto ja X-Forwarded-Host; standardoituna RFC-headerina on lisäksi Forwarded. Tärkeää: Näitä header-arvoja ei sovelluksen näkökulmasta kannata pitää automaattisesti luotettavina, koska asiakas voi itse lähettää ne – ne muuttuvat luotettaviksi vasta, kun ne tulevat tunnetulta proxyltä.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Vankka lähtökohta (HTTP/1.1, Keep-Alive, Upgrade WebSocket-yhteyksille) näyttää tältä. Katkelma on tarkoituksella tiivis; lisää ympäristöstä riippuen HSTS, Rate-Limits ja 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;
  }
}

Tarkoitus: Sovellus saa Hostin, asiakas-IP:n ja skeeman luotettavasti välitettyinä. Reunaehto: $proxy_add_x_forwarded_for liittää nykyisen proxyn IP:n mahdollisesti olemassa olevaan ketjuun; tämä on hyödyllistä moni-proxy-ympäristöissä, mutta tekee oikeasta tulkinnasta Delphi-puolella sitäkin tärkeämmän. Ansakohta: Jos et nginxissä aseta Host-otsaketta, Delphi saattaa nähdä vain Upstream-Hostin (127.0.0.1), mikä rikkoo uudelleenohjaukset ja origin-tarkistukset.

Delphi lähdekoodipätkä: Forwarded/X-Forwarded -otsakkeiden robusti tulkinta (luottoproxyluettelon kanssa)

Seuraava koodi on tarkoituksellisesti framework-neutraali: se työskentelee minimaalisen rajapinnan (Header + RemoteIP) kanssa ja on sovitettavissa WebBrokeriin, RAD Serveriin tai Horseen. Kernpunkte:

  • Prioriteetti: RFC Forwarded (jos saatavilla) ennen X-Forwarded-*.
  • Luottamus: Forwarded-otsakkeita käsitellään vain, jos suora peer (RemoteIP) on tunnettu proxy.
  • Jäsennys: Huomioi IPv6, lainausmerkit, portit ja ketjut X-Forwarded-For -kentässä.
  • Tuloste: perus-URL, jota voit käyttää absoluuttisiin linkkeihin, uudelleenohjauksiin tai OpenAPI:in.
Delphi
unit Net-Base.ProxyForwarding;

interface

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

type
  // Minimaalinen adapterirajapinta: toteuta se WebBrokerille/Horse:lle jne.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // suora TCP-vastapää (usein nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // esim. 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 voi olla "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 hakasulkeissa []: [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/isäntä: host:port (Huom: paljaalla IPv6:lla ilman [] ei ole luotettava)
  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
  // Esimerkki: 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;

  // Useat elementit on erotettu pilkulla; otamme ensimmäisen (asiakasta lähin)
  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 voi olla IP tai "unknown"; IPv6 voi olla hakasulkeissa []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 voi esiintyä
      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
  // Todelliseen IPv6-normalisointiin tarvittaisiin IP:n jäsennys; tässä lähestymistapa on tietoisesti pragmaattinen.
  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; // Vararatkaisu: suora vastapää

  // Arvioimme Forwarded-otsikot vain jos suora vastapää on tunnettu välityspalvelin.
  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-* vararatkaisuna/täydennyksenä
    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;

  // Viime kädessä ota Host "Host"-otsikosta (jos proxy-otsikkoja puuttuvat)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Tarkoitus: Saat jokaisesta pyynnöstä yhdenmukaisen näkymän ClientIP, Proto ja Host -kentistä sekä BaseUrl-arvon. Näitä tietoja voi käyttää keskitetysti lokitukseen, turvallisuuspäätöksiin (esim. IP-sallintalista) ja linkkien generointiin.

Miksi luotettujen proksyjen luettelo on tarpeen: Ilman luottamustarkistusta hyökkääjä voisi tavoittaa suoraan Delphi-porttinne (konfiguraatiovirhe, sisäinen reititys, VPN) ja yksinkertaisesti lähettää X-Forwarded-For: 127.0.0.1. Tällöin audit-jäljet, rate-limitit tai „vain sisäverkossa sallitut“ päätepisteet olisivat alttiita hyökkäyksille. Luota Forwarded-otsakkeisiin vain, jos suora peer (RemoteIP) on proksi, jota hallinnoitte (esim. 127.0.0.1, load-balancerin IP, Kubernetes-Ingress).

Varoitukset: IPv6 ilman hakasulkeita ei ole Host:port-notaatiossa yksiselitteinen. HTTP-Host-otsakkeessa IPv6 merkitään yleensä []-sulkeisiin; pidä siitä kiinni. Monimutkaisempien IP-alueiden (CIDR) käsittelyä varten sinun täytyy laajentaa luotettujen proksyjen luetteloa (esim. aidolla IP-parsinnalla).

Integrointi WebBroker/Horse/RAD Serveriin: wo der Code „andockt“

WebBrokerissa (TWebRequest) otsakkeet tulevat tyypillisesti ContentFields-kautta tai GetFieldByName-metodilla, Remote-IP riippuu server-backendistä. Horsessa (tai muissa HTTP-kehyksissä) on yleensä Req.Headers ja Remote-IP-ominaisuus. Tärkeä periaate: RemoteIP:n on oltava TCP-vastapuoli, ei jokin otsakearvo.

Käytännössä suositeltavaa: luo palvelun käynnistyksessä TTrustedProxyList konfiguraatiosta (INI/ENV), esim. „127.0.0.1“ paikallisiin nginx-asetuksiin tai load balancerin IP:hen. Sitten kutsu ResolveForwardedInfo jokaista pyyntöä kohden ja kirjoita kentät jäsenneltyyn lokitukseen (JSON-loki, Syslog tai Windows Event Log).

Virheiden etsintä tuotannossa: näin löydät virheet minuuteissa eikä tunneissa

Jos pyynnöt vaikuttavat „oudoilta“, syy harvoin on Delphi-HTTP:ssä itsessään, vaan yleensä proxy-otsakkeiden, uudelleenohjauslogiikan ja aikakatkaisujen yhdistelmä. Kolme debug-tarkistusta, jotka kannattaa tehdä arjessa:

  1. Header-dumppaus (kohdennettu): Kirjaa 4xx/5xx-tilanteissa lisäksi Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent ja Request-URI. Mutta vain virhetilanteissa – muuten lokit kasvavat kalliiksi ja epäselviksi.
  2. Tarkista Base-URL: Jos uudelleenohjaukset tai callback-URL:t epäonnistuvat, kirjaa ForwardedInfo.BaseUrl. Monet virheet näkyvät heti (‚http://127.0.0.1‘ sijaan ‚https://api…‘).
  3. Aikakatkaisujen korrelaatio: Proxylta tullut 504 ei ole sama kuin Delphi-aikakatkaisu. nginx:n proxy_read_timeout ja Delphi-puolen idle-/read-aikakatkaisut on sovitettava yhteen.

Reunatapaukset: WebSocketit, striimaus ja suuret pyynnöt

WebSocketit nginxin takana

WebSocketien osalta nginx tarvitsee Upgrade ja Connection oikein. Lisäksi backendin ei saa sulkea yhteyttä „liian aikaisin“. Delphi-puolella on oleellista, että WebSocket-komponenttinne (tai SSE/streaming-päätepiste) osaa toimia reverse-proksyjen kanssa ja että heartbeatit/keep-alivet on toteutettu siististi.

Suuret lataukset ja 413-virheet

Klassikko: Delphi hyväksyy latauksen, mutta nginx estää sen aiemmin 413 Request Entity Too Large-virheellä. Ohjaa tätä nimenomaan client_max_body_size-asetuksella ja säädä Delphi-puolen pyyntörajoituksia. Prosessiläheisissä ohjelmistoratkaisuissa, joissa käsitellään dokumentteja tai kuvatiedostoja, tämä ei ole poikkeus vaan normaalia toimintaa.

HTTPS-offloadaus ja „Secure Cookies“

Jos Delphi-palvelunne asettaa istuntoevästeitä, niiden on ulkoisen HTTPS:n tapauksessa yleensä oltava merkittyinä Secure-attribuutilla. Sen, tekeekö sovelluksenne näin, määrittää usein se, tietääkö se, että alkuperäinen pyyntö oli HTTPS. Tässä auttaa johdonmukainen X-Forwarded-Proto/Forwarded-arvojen tulkinta.

Milloin vaiva kannattaa – ja missä se voi epäonnistua

Esitetty lähestymistapa kannattaa aina, kun Delphi-palvelu ei enää elä „alastomana“ LAN:ssa, vaan on osa tuotantoreunaa: useita domaineja, SSO/SAML-käyttöliittymiä, julkisia API:ja, monen asiakkaan tuki (multi-tenant) tai tiukemmat audit-vaatimukset. Se voi pettää siellä, missä Forwarded-otsikoihin luotetaan sokeasti tai proxy-topologiat eivät ole dokumentoituja (useita Ingress-kerroksia, Cloud-LB plus nginx plus Sidecar). Tällöin asiakas-IP ja protokollaskema muuttuvat helposti epäselviksi.

Selkeä raja: jos tarvitsette monimutkaisia luottamissääntöjä (CIDR, IPv6-verkot, dynaamiset LB-IP:t), laajentakaa luottamustarkistusta (varsinainen IP-jäsentäminen, verkkomaskit) tai suunnitelkaa infrastruktuuri niin, että vain määritelty proxy voi tavoittaa Delphi-portin (Firewall/Security Groups). Usein tämä on lopulta kestävämpi operatiivinen ratkaisu.

Yhteenveto: Reverse Proxy nginx:llä ja Delphi huolellisesti ajettuna tarkoittaa „Forwardedin“ oikeaa käsittelyä

Reverse-proxy nginx:llä on Delphi-REST-servereille hyvä peruskomponentti – mutta vasta Forwarded– ja X-Forwarded-*-otsikoiden oikea käsittely tekee asetuksesta tuotannossa stabiilin. Ydin on yksinkertainen: hyväksy otsikot vain luotettavilta proxylta, päättele asiakas-IP, protokollaskema ja host johdonmukaisesti ja vie tämä perusta läpi uudelleenohjauksissa, lokituksessa ja turvatarkastuksissa. Edellä oleva koodikatkelma tarjoaa siihen siistin, legacy-yhteensopivan perustan, jonka voi integroida WebBrokeriin, Horseen tai omiin HTTP-palvelimiin.

Jos aiotte konsolidoida olemassa olevan Delphi-taustajärjestelmän nginxin taakse tai modernisoida kohti Delphi REST-APIa ja REST-serveriä selkeällä operointilinjalla, on proxyketjun ja otsikkotulkinnan tekninen review usein nopein vipu. Ota yhteyttä Net-Base lyhyttä teknistä arviointia varten.

Asiantuntijaympäristössä myös Nginx-reverse-proxy ja Forwarded-otsikot näyttelevät tärkeää roolia, kun integraatioiden, tietovirtojen ja jatkokehityksen tulee toimia siististi yhdessä.

Keskustele projektista tai modernisointihankkeesta Net-Base kanssa.

Jaa artikkeli

Jaa tämä viesti suoraan

LinkedIn, X, XING, Facebook, WhatsApp ja sähköposti ovat heti käytettävissä. Instagramia varten valmistelemme linkin ja lyhyen tekstin.

Sähköposti

Instagram avautuu uuteen välilehteen. Linkki ja lyhyt teksti kopioidaan ensin leikepöydälle.