En Reverse Proxy med nginx og Delphi er i praksis som regel ikke et „nice to have“, men den rene adskillelse mellem internetkanten og applikationen: TLS-terminering (HTTPS-Offloading), centrale header-/CORS-regler, ratebegrænsninger, ensartede logs, Blue/Green-Rollouts eller blot hosting af flere services under et domæne. Hvad der ofte undervurderes: Så snart nginx står „foran“, ser Delphi-serveren kun proxy-IP’en, ofte kun „http“ i stedet for „https“ og genererer forkerte absolutte links (Redirects, Callback-URLs, OpenAPI-Server-URL). Netop disse tre punkter skaber senere tid til fejlsøgning i driften.
Denne source-snip viser et robust mønster for, hvordan De i Delphi vurderer Forwarded henholdsvis X-Forwarded-* korrekt – inklusive en liste over betroede proxies (vigtigt mod header-spoofing) og en konsistent Request-Base-URL. Der følger praksisnære nginx-konfigurationer med og anvisninger til randtilfælde som WebSockets, store uploads og timeouts.
Hvorfor Reverse Proxy-opsætninger „forvirrer“ Delphi-servere
nginx kommunikerer som Reverse Proxy med Delphi-servicen typisk ukrypteret (HTTP) i det interne netværk eller på localhost, mens klienten ude fra kommer via HTTPS. Uden supplerende headers kender Delphi ikke til:
- Original-Schema (https vs. http) – relevant for Redirects og absolutte URLs.
- Original-Host (kundetilpasset domæne, port) – relevant for Multi-Tenant-opsætninger, CORS og Callback-URLs.
- Original-Client-IP – relevant for audit, ratebegrænsninger, geolokationskontrol og sikkerhedsanalyser.
nginx kan transportere disse oplysninger via HTTP-headere. Almindelige er X-Forwarded-For, X-Forwarded-Proto og X-Forwarded-Host; standardiseret er derudover RFC-headeren Forwarded. Vigtigt: Disse headere er fra applikationens synspunkt ikke automatisk tillidsværdige, fordi en klient selv kan sende dem – de bliver først tillidsværdige, når de stammer fra en kendt proxy.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Et solidt udgangspunkt (HTTP/1.1, Keep-Alive, Upgrade for WebSockets) ser sådan ud. Snippettet er bevidst kortfattet; De supplerer afhængigt af miljøet med HSTS, Rate-Limits og Access-Logs.
# (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;
}
}
Formål: Applikationen modtager Host, klient-IP og skema pålideligt videre. Forudsætning: $proxy_add_x_forwarded_for tilføjer den aktuelle proxy‑IP til en eventuel eksisterende kæde; det er godt til multi‑proxy-opsætninger, men gør den korrekte fortolkning på Delphi-siden endnu vigtigere. Faldgrube: Hvis du i nginx ikke sætter Host-headeren, ser Delphi muligvis kun upstream‑hosten (127.0.0.1), hvilket bryder redirects og origin‑checks.
Delphi Kildeudsnit: Robust evaluering af Forwarded/X-Forwarded (med Trust-Proxy-liste)
Følgende kode er bevidst framework-neutral: Den arbejder mod et minimalt interface (Header + RemoteIP) og kan tilpasses i WebBroker, RAD Server eller Horse. Kernepunkter:
- Prioritet: RFC Forwarded (hvis tilgængelig) frem for X-Forwarded-*.
- Trust: Forwarded-Header kun fortolkes, hvis den direkte peer (RemoteIP) er en kendt proxy.
- Parsing: Tag højde for IPv6, quotes, porte og kæder i X-Forwarded-For.
- Output: en base-URL, som du kan bruge til absolutte links, redirects eller OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimalt adapter-interface: implementér det for WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // direkte TCP-modpart (typisk nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // f.eks. 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 kan være "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 i []: [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 (Advarsel: ved nøgent IPv6 uden [] ikke pålideligt)
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
// Eksempel: 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;
// Flere elementer er adskilt med komma; vi tager det første (tættest på klienten)
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 kan være IP eller "unknown"; IPv6 kan være i []
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 kan forekomme
if V.Contains(':') and (not V.StartsWith('[')) then
V := FirstCsvToken(V); // defensivt
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
// For ægte IPv6-normalisering ville man bruge IP-parsing; her bevidst pragmatisk.
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: direkte modpart
// Kun hvis den direkte modpart er en kendt proxy, evaluerer vi Forwarded-headere.
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-* som fallback/tilføjelse
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;
// I nødstilfælde tag Host fra "Host"-headeren (hvis der ikke er noget proxy-header)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Formål: Fra hver request får I et konsistent overblik over ClientIP, Proto og Host samt en BaseUrl. Disse oplysninger kan bruges centralt til logging, sikkerhedsbeslutninger (f.eks. IP-allowlist) og linkgenerering.
Hvorfor Trust-Proxy-listen er nødvendig: Uden en trust-kontrol kan en angriber få direkte adgang til jeres Delphi-port (fejlkonstruktion, internt routing, VPN) og simpelthen sende X-Forwarded-For: 127.0.0.1. Dermed bliver audit-trails, rate-limits eller ‚kun internt tilladte‘ endpoints sårbare. Stol kun på Forwarded-headere, hvis den direkte peer (RemoteIP) er en proxy, I kontrollerer (f.eks. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).
Faldgruber: IPv6 uden kantede parenteser er ikke entydigt i Host:port-notation. I HTTP-Host-headeren noteres IPv6 normalt i []; hold jer til det. For komplekse IP-ranges (CIDR) skal I udvide trust-listen (f.eks. ved ægte IP-parsing).
Integration i WebBroker/Horse/RAD Server: hvor koden „tilkobles“
I WebBroker (TWebRequest) kommer headers typisk via ContentFields eller GetFieldByName, Remote-IP afhænger af server-backend. I Horse (eller andre HTTP-frameworks) findes typisk Req.Headers og en Remote-IP-egenskab. Det vigtige princip er: RemoteIP skal være TCP-modparten, ikke en hvilken som helst headerværdi.
Praktisk fremgangsmåde: Opret ved service-start en TTrustedProxyList fra konfiguration (INI/ENV), f.eks. „127.0.0.1“ til lokale nginx-setup eller IP’en på jeres Load Balancer. Kald derefter ResolveForwardedInfo per request og skriv felterne til jeres strukturerede logging (JSON-log, Syslog eller Windows Event Log).
Fejlfinding i drift: sådan finder I fejl på minutter i stedet for timer
Hvis requests virker „mærkelige“, skyldes det sjældent Delphi-HTTP i sig selv, men en kombination af proxy-headere, redirect-logik og timeouts. Tre debug-checks, der er værd at køre i praksis:
- Header-dump (målrettet): Log ved 4xx/5xx desuden Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent og Request-URI. Men kun ved fejl – ellers bliver loggen dyr og uoverskuelig.
- Kontrollér Base-URL: Hvis redirects eller callback-urls fejler, log ForwardedInfo.BaseUrl. Mange fejl bliver straks synlige („http://127.0.0.1“ i stedet for „https://api…“).
- Timeout-korrelation: En 504 fra proxyen er ikke det samme som et Delphi-timeout. nginx proxy_read_timeout og på Delphi-siden skal idle-/read-timeouts være afstemt.
Randtilfælde: WebSockets, streaming og store requests
WebSockets bag nginx
For WebSockets kræver nginx korrekte Upgrade og Connection headere. Derudover må backend ikke lukke „for tidligt“. På Delphi-siden er det vigtigt, at jeres WebSocket-komponent (eller et SSE/streaming-endpoint) kan håndtere reverse proxies, og at heartbeats/keep-alives er korrekt implementeret.
Store uploads og 413-fejl
En klassiker: Delphi accepterer et upload, men nginx blokerer tidligere med 413 Request Entity Too Large. Styr det eksplicit via client_max_body_size og tilpas request-limits på Delphi-siden. For procesnære softwareløsninger med dokument- eller billeddata er dette ikke et specialtilfælde, men normal drift.
HTTPS-offloading og „Secure Cookies“
Hvis jeres Delphi-service sætter sessioncookies, skal disse ved ekstern HTTPS som regel markeres som Secure. Om jeres applikation gør det, afhænger ofte af, om den ‚ved‘, at den oprindelige request var HTTPS. Netop her hjælper konsekvent fortolkning af X-Forwarded-Proto/Forwarded.
Hvornår indsatsen lønner sig — og hvor den kan gå galt
Den viste tilgang kan betale sig, når Delphi-servicen ikke længere kører ’nøgen‘ i LAN’et, men er en del af en produktiv edge: flere domæner, SSO/SAML-flader, Public APIs, multitenancy eller strengere audit-krav. Den bryder sammen, hvor man blindt stoler på Forwarded-headere eller ikke dokumenterer proxy-topologier (flere Ingress-stadier, Cloud-LB plus nginx plus Sidecar). Så bliver Client-IP og Scheme hurtigt ’noget‘.
En klar grænse: Hvis I har brug for komplekse Trust-regler (CIDR, IPv6-net, dynamiske LB-IPs), bør I udbygge Trust-prøvningen (ægte IP-parsing, netmasker) eller designe infrastrukturen, så kun en defineret proxy kan nå Delphi-porten (Firewall/Security Groups). Det er i sidste ende ofte den mere robuste driftsbeslutning.
Konklusion: Reverse Proxy med nginx und Delphi korrekt drive betyder „Forwarded håndteres rigtigt“
En Reverse Proxy med nginx er for Delphi-REST-Server en god standardkomponent – men først den korrekte behandling af Forwarded og X-Forwarded-* gør opsætningen stabil i drift. Kernen er simpel: accepter kun headere fra betroede proxies, udled Client-IP/Scheme/Host konsistent og gennemfør denne basis i redirects, logging og security-checks. Med snippettet ovenfor har I et rent, legacy-taugliches fundament, som kan integreres i WebBroker, Horse eller egne HTTP-Server.
Hvis I vil konsolidere et eksisterende Delphi-backend bag nginx eller modernisere mod en Delphi REST-API og REST-Server med en klar driftslinje, er et teknisk review af proxy-kæden og header-fortolkningen ofte det hurtigste greb. Kontakt Net-Base for en kort teknisk indordning.
I det faglige miljø spiller også Nginx Reverse Proxy og Forwarded Header en vigtig rolle, når integrationer, dataflows og videreudvikling skal spille sammen på en ordentlig måde.