En Reverse Proxy med nginx og Delphi er i praksis som regel ikke et «nice to have», men den tydelige separasjonen mellom internettsiden og applikasjonen: TLS-terminering (HTTPS-Offloading), sentrale header-/CORS-regler, rate-limits, ensartede logger, Blue/Green-rollouts eller enkelt hosting av flere tjenester under én domene. Det som ofte undervurderes: Når nginx står «foran», ser Delphi-serveren som regel bare proxy-IP-en, ofte bare «http» i stedet for «https» og genererer feil absolute lenker (redirects, callback-URLer, OpenAPI-server-URL). Nettopp disse tre punktene fører senere til debug-tid i drift.
Denne kildekodesnutten viser et robust mønster for hvordan du i Delphi kan evaluere Forwarded og X-Forwarded-* korrekt – inklusive en Trust-Proxy-Liste (viktig mot header-spoofing) og en konsistent Request-Base-URL. I tillegg finnes praktiske nginx-konfigurasjoner og kommentarer om randtilfeller som WebSockets, store opplastinger og timeouts.
Hvorfor reverse proxy-oppsett kan «forvirre» Delphi-servere
nginx kommuniserer som reverse proxy med Delphi-servicen typisk ukryptert (HTTP) i det interne nettet eller på localhost, mens klienten utvendig kommer via HTTPS. Uten tilleggshoder vet ikke Delphi noe om:
- Original-Schema (https vs. http) – relevant for redirects og absolute URL-er.
- Original-Host (kundefinert domene, port) – relevant for multi-tenant-oppsett, CORS og callback-URL-er.
- Original-Client-IP – relevant for audit, rate-limits, geo-sjekk og sikkerhetsanalyser.
nginx kan transportere denne informasjonen via headere. Vanlige er X-Forwarded-For, X-Forwarded-Proto og X-Forwarded-Host; standardisert finnes også RFC-headeren Forwarded. Viktig: Disse headerne er fra applikasjonens synspunkt ikke automatisk pålitelige, fordi en klient kan sende dem selv – de blir først pålitelige når de kommer fra en kjent proxy.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Et solidt utgangspunkt (HTTP/1.1, Keep-Alive, Upgrade for WebSockets) ser slik ut. Snippetet er bevisst kort; avhengig av miljø kompletteres det 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: Applikasjonen får Host, klient-IP og skjema pålitelig videreført. Rammebetingelse: $proxy_add_x_forwarded_for legger den aktuelle proxy-IP-en til en eventuell eksisterende kjede; det er bra for multi-proxy-oppsett, men gjør korrekt evaluering på Delphi-siden desto viktigere. Fallgruve: Hvis du i nginx ikke setter Host-headeren, ser Delphi muligens bare upstream-hosten (127.0.0.1), noe som bryter omdirigeringer og origin-sjekker.
Delphi Kodesnutt: Robust evaluering av Forwarded/X-Forwarded (med liste over betrodde proxyer)
Følgende kode er bevisst rammeverk-nøytral: Den jobber mot et minimalt grensesnitt (Header + RemoteIP) og kan tilpasses WebBroker, RAD Server eller Horse. Hovedpunkter:
- Prioritet: RFC Forwarded (hvis tilgjengelig) foran X-Forwarded-*.
- Tillit: Kun evaluer Forwarded-headeren hvis den direkte peer (RemoteIP) er en kjent proxy.
- Parsing: Ta hensyn til IPv6, anførselstegn, porter og kjeder i X-Forwarded-For.
- Output: en basis-URL som kan brukes for absolutte lenker, omdirigeringer eller OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minialt adaptergrensesnitt: implementer det for WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // direkte TCP-motpart (vanligvis 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 (Merk: ved naken IPv6 uten [] ikke pålitelig)
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 komma-separert; vi tar det første (nærmest 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 forekommer
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 ekte IPv6-normalisering ville man trengt IP-parsing; her bevisst 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 motpart
// Bare hvis den direkte motparten er en kjent proxy, tolker 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/tillegg
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;
// Som siste utvei, ta Host fra „Host“-headeren (hvis ingen proxy-header finnes)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Formål: Hver forespørsel gir en konsistent oversikt over ClientIP, Proto og Host samt en BaseUrl. Disse opplysningene kan du bruke sentralt for logging, sikkerhetsbeslutninger (f.eks. IP-allowlist) og lenke-generering.
Hvorfor Trust-Proxy-listen er nødvendig: Uten en trust-sjekk kunne en angriper nå direkte din Delphi-port (feilkonfigurasjon, intern ruting, VPN) og enkelt sende X-Forwarded-For: 127.0.0.1. Da ville audit-trails, rate-limiter eller «kun intern tillatt»-endepunkter være sårbare. Stol kun på Forwarded-headere når den direkte peer (RemoteIP) er en proxy du kontrollerer (f.eks. 127.0.0.1, IP-en til load-balanceren, Kubernetes Ingress).
Fallgruver: IPv6 uten hakeparenteser er i Host:port-notasjon ikke entydig. I HTTP-Host-headeren er IPv6 normalt notert i []; følg denne konvensjonen. For komplekse IP-områder (CIDR) må du utvide Trust-listen (f.eks. ved å bruke ekte IP-parsing).
Integrasjon i WebBroker/Horse/RAD Server: hvor koden „kobles til“
I WebBroker (TWebRequest) kommer headere typisk via ContentFields eller GetFieldByName, Remote-IP avhengig av server-backend. I Horse (eller andre HTTP-rammeverk) finnes vanligvis Req.Headers og en Remote-IP-egenskap. Viktig prinsipp: RemoteIP må være TCP-motparten, ikke en tilfeldig headerverdi.
Praktisk erfart: Opprett ved tjenestestart en TTrustedProxyList fra konfigurasjon (INI/ENV), f.eks. „127.0.0.1“ for lokale nginx-oppsett eller IP-en til din load balancer. Kall så ResolveForwardedInfo per request og skriv feltene til strukturert logging (JSON-Log, Syslog eller Windows Event Log).
Feilsøking i drift: slik finner du feil på minutter i stedet for timer
Når forespørsler virker „merkelige“, skyldes det sjelden Delphi-HTTP i seg selv, men ofte en kombinasjon av proxy-headere, redirect-logikk og timeouts. Tre debug-sjekker som lønner seg i praksis:
- Header-dump (målrettet): Logg ved 4xx/5xx i tillegg Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent og Request-URI. Men kun ved feil – ellers blir loggen kostbar og uoversiktlig.
- Sjekk Base-URL: Hvis redirects eller callback-URLer feiler, logg ForwardedInfo.BaseUrl. Mange feil blir umiddelbart synlige („http://127.0.0.1“ i stedet for „https://api…“).
- Timeout-korrelasjon: En 504 fra proxy er ikke det samme som et Delphi-timeout. nginx proxy_read_timeout og Delphi-sides Idle-/Read-Timeouts må harmonisere.
Særtilfeller: WebSockets, streaming og store forespørsler
WebSockets bak nginx
For WebSockets krever nginx at Upgrade og Connection er korrekte. I tillegg må backend ikke lukke «for tidlig». På Delphi-siden er det relevant at din WebSocket-komponent (eller et SSE/streaming-endepunkt) kan håndtere reverse proxies og at Heartbeats/Keep-Alives er implementert korrekt.
Store opplastinger og 413-feil
En klassiker: Delphi aksepterer en opplasting, men nginx blokkerer tidligere med 413 Request Entity Too Large. Styr dette eksplisitt med client_max_body_size og tilpass Delphi-siders forespørselsgrenser. For prosessnære programvareløsninger med dokument- eller bildebehandling er dette ikke et unntak, men normal drift.
HTTPS-avlastning og „Secure Cookies“
Hvis deres Delphi-service setter Session-Cookies, må disse ved eksternt HTTPS som regel være merket som Secure. Om applikasjonen deres gjør det, avhenger ofte av om den «vet» at den opprinnelige forespørselen var HTTPS. Nettopp her hjelper konsekvent evaluering av X-Forwarded-Proto/Forwarded.
Når innsatsen lønner seg – og hvor den kan svikte
Den viste tilnærmingen lønner seg alltid når Delphi-servicen ikke lenger lever «naken» i LAN-et, men er del av en produksjonskant: flere domener, SSO/SAML-grensesnitt, Public APIs, multi-tenant-støtte eller strengere revisjonskrav. Den svikter der man stoler blindt på Forwarded-headere eller proxy-topologier ikke er dokumentert (flere Ingress-nivåer, Cloud-LB pluss nginx pluss Sidecar). Da blir Client-IP og Scheme raskt uforutsigbare.
En klar grense: Hvis dere trenger komplekse tillitsregler (CIDR, IPv6-nett, dynamiske LB-IPer), bør dere utvide tillitssjekken (ekte IP-parsing, nettmasker) eller utforme infrastrukturen slik at kun en definert proxy kan nå Delphi-porten (Firewall/Security Groups). Det er ofte den mest robuste driftsbeslutningen i praksis.
Konklusjon: Å drifte Reverse Proxy med nginx og Delphi på en ryddig måte betyr «gjøre Forwarded riktig»
En reverse proxy med nginx er for Delphi-REST-Server en god standardkomponent – men først korrekt behandling av Forwarded og X-Forwarded-* gjør oppsettet stabilt i drift. Kjernen er enkel: aksepter kun headere fra betrodde proxies, utled Client-IP/Scheme/Host konsekvent og før denne basen gjennom i redirects, logging og security-sjekker. Med snippetet ovenfor har dere et rent, legacy-kompatibelt fundament som lar seg integrere i WebBroker, Horse eller egne HTTP-servere.
Hvis dere skal konsolidere et eksisterende Delphi-backend bak nginx eller modernisere mot Delphi REST-API og REST-Server med klar driftslinje, er en teknisk review av proxy-kjeden og header-evalueringen ofte det raskeste grepet. Kontakt Net-Base for en kort teknisk innordning.
I faglige sammenhenger spiller også Nginx Reverse Proxy og Forwarded-headere en viktig rolle når integrasjoner, dataflyt og videreutvikling må spille godt sammen.