Ein Reverse Proxy mit nginx und Delphi er i praksis som regel ikkje eit „nice to have“, men den ryddige skiljet mellom internettkanten og applikasjonen: TLS-terminering (HTTPS-offloading), sentrale Header-/CORS-reglar, Rate-Limits, einheitlege Logs, Blue/Green-Rollouts eller enkelt hosting av fleire tenester under eit domene. Det som gjerne blir undervurdert då: Når nginx sit „framfor“, ser Delphi-serveren berre proxy-IP-en, ofte berre „http“ i staden for „https“ og genererer feil absoluttlenker (Redirects, Callback-URLs, OpenAPI-Server-URL). Desse tre punkta fører seinare til tidkrevjande feilsøking i drift.
Dette source-schnipslet viser eit robust mønster for korleis du i Delphi Forwarded eller X-Forwarded-* kan tolke på ein ryddig måte – inklusive ei Trust-Proxy-Liste (viktig mot Header-Spoofing) og ei konsekvent Request-Base-URL. I tillegg finn du praksisnære nginx-konfigurasjonar og merknader om randtilfelle som WebSockets, store opplastingar og Timeouts.
Kvifor Reverse Proxy-oppsett forvirrar Delphi-serverar
nginx kommuniserer som Reverse Proxy med Delphi-tenesta typisk ukryptert (HTTP) i det interne nettverket eller på localhost, medan klienten kjem utanfrå via HTTPS. Uten ekstra Headerar veit Delphi ingenting om:
- Original-Schema (https vs. http) – relevant for Redirects og absolutte URL-ar.
- Original-Host (kundespesifikt domene, Port) – relevant for Multi-Tenant-Setups, CORS og Callback-URL-ar.
- Original-Client-IP – relevant for revisjon, Rate-Limits, Geo-Checks og sikkerheitsanalysar.
nginx kan transportere desse informasjonane via Headerar. Vanleg er X-Forwarded-For, X-Forwarded-Proto og X-Forwarded-Host; standardisert er i tillegg RFC-Headeren Forwarded. Viktig: Desse Headerane er frå applikasjonen sitt syn ikkje automatisk til å stole på, fordi ein klient kan sende dei sjølv – dei blir først til å stole på når dei kjem frå ein kjend proxy.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Eit solid utgangspunkt (HTTP/1.1, Keep-Alive, Upgrade for WebSockets) ser slik ut. Dette Snippetet er medvite kortfatta; du supplerer etter omgjevnaden 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 schema påliteleg vidareleidd. Randvilkår: $proxy_add_x_forwarded_for legg den aktuelle proxy-IP-en til ei eventuell eksisterande kjede; dette er bra for multi-proxy-oppsett, men gjer den korrekte tolkinga på Delphi-sida endå viktigare. Fallgruve: Hvis De i nginx ikkje set Host-headeren, ser Delphi kanskje berre upstream-vert (127.0.0.1), noko som bryt omdirigeringar og origin-sjekkar.
Delphi Source-snutt: Robust tolking av Forwarded/X-Forwarded (med Trust-Proxy-liste)
Følgjande kode er med vilje rammeverksnøytral: Den arbeider mot eit minimalt grensesnitt (Header + RemoteIP) og kan tilpassast i WebBroker, RAD Server eller Horse. Hovudpunkt:
- Prioritet: RFC Forwarded (dersom tilstades) framfor X-Forwarded-*.
- Trust: Forwarded-header skal berre tolkast når den direkte peeren (RemoteIP) er ein kjend proxy.
- Parsing: IPv6, anførselstegn, portar og kjeder i X-Forwarded-For blir tekne i betraktning.
- Output: ei base-URL som De kan bruke for absolutte lenker, omdirigeringar eller OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minst mogleg adaptergrensesnitt: implementer det for WebBroker/Horse/osv.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // direkte TCP-motpart (vanlegvis nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // t.d. 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 vere "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 in []: [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 utan [] er dette ikkje påliteleg)
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
// Døme: 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;
// Fleire element er kommadelte; vi tek det første (nærast 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 vere ei IP eller "unknown"; IPv6 kan stå i []
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 kan førekome
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
// For ekte IPv6-normalisering ville ein trenge IP-parsing; her er vi medvite pragmatiske.
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
// Vi tolkar Forwarded-header berre dersom den direkte motparten er ein kjend 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-* som fallback/utfylling
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 naudtilfelle hentar vi Host frå "Host"-headeren (dersom det ikkje finst 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: Du får frå kvar request ei konsistent oversikt over ClientIP, Proto og Host samt ei BaseUrl. Desse opplysningane kan du bruke sentralt for logging, sikkerheitsavgjerder (t.d. IP-Allowlist) og lenke-generering.
Kvarfor Trust-Proxy-lista er nødvendig: Uten ei trust-sjekk kunne ein angripar nådd direkte din Delphi-port (feilkonfigurasjon, internt routing, VPN) og enkelt sende X-Forwarded-For: 127.0.0.1. Då ville audit-trails, rate-limits eller „berre internt tillate“-endepunkt vere sårbare. Stol på Forwarded-headerar berre når den direkte motparten (RemoteIP) er ein proxy du kontrollerer (t.d. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).
Fallgruver: IPv6 utan hakeparentesar er i Host:port-notasjon ikkje entydig. I HTTP-Host-headeren er IPv6 som oftast oppført i []; hald deg til det. For komplekse IP-område (CIDR) må du utvide trust-lista (t.d. med ein reell IP-parsing).
Integrasjon i WebBroker/Horse/RAD Server: kvar koden „andockar“
I WebBroker (TWebRequest) kjem headerar typisk via ContentFields eller GetFieldByName, og Remote-IP avhengig av server-backend. I Horse (eller andre HTTP-rammeverk) finst vanlegvis Req.Headers og ei Remote-IP-eigenskap. Viktig prinsipp: RemoteIP må vere TCP-motparten, ikkje ein vilkårleg header-verdi.
Praktisk erfart: Opprett ved tenestestart ei TTrustedProxyList frå konfigurasjon (INI/ENV), t.d. „127.0.0.1“ for lokale nginx-oppsett eller IP-en til load-balanceren din. Kall så ResolveForwardedInfo per request og skriv felta til strukturert logging (JSON-logg, Syslog eller Windows Event Log).
Debugging i drift: slik finn du feil på minutt i staden for timar
Når requests verkar „komiske“, skuldast det sjeldan Delphi-HTTP sjølv, men heller ei kombinasjon av proxy-header, redirect-logikk og timeouts. Tre debug-sjekkar som lønner seg i kvardagen:
- Header-dump (målretta): Logg ved 4xx/5xx i tillegg Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent og Request-URI. Men berre ved feil — elles blir loggen dyr og uoversiktleg.
- Sjekk Base-URL: Når redirects eller callback-URLar feilar, logg ForwardedInfo.BaseUrl. Mange feil blir straks synlege („http://127.0.0.1″ i staden for „https://api…“).
- Timeout-korrelasjon: Ein 504 frå proxyen er ikkje det same som eit Delphi-timeout. nginx proxy_read_timeout og Delphi-sidelege idle-/read-timeoutar må passe saman.
Kanttilfelle: WebSockets, streaming og store requests
WebSockets bak nginx
For WebSockets krev nginx korrekte Upgrade og Connection-header. I tillegg må backend ikkje stengje „for tidleg“. På Delphi-sida er det relevant at WebSocket-komponenten din (eller eit SSE/streaming-endepunkt) kan handtere reverse-proxyar og at heartbeats/keep-alives er riktig implementert.
Store opplastingar og 413-feil
Eit klassisk tilfelle: Delphi aksepterer eit opplasting, men nginx blokkjerer det på førehand med 413 Request Entity Too Large. Styr dette eksplisitt gjennom client_max_body_size og tilpass Delphi-sidelege request-grensar. For proksessnære programvareløysingar med dokument- eller bilete-data er dette normal drift, ikkje eit unntak.
HTTPS-Offloading og „Secure Cookies“
Hvis din Delphi-teneste set Session-Cookies, må desse ved eksternt HTTPS som regel markerast som Secure. Om applikasjonen din gjer det, heng ofte på om ho «veit» at den opphavlege førespurnaden var HTTPS. Nøyaktig her hjelper konsekvent tolking av X-Forwarded-Proto/Forwarded.
Når innsatsen løner seg – og kvar ho kan svikte
Den viste tilnærminga løner seg alltid når Delphi-tenesten ikkje lenger ligg «naken» i LAN-et, men er ein del av ein produktiv kant: fleire domener, SSO-/SAML-grensesnitt, offentlege APIs, fleirtanantskap eller strengare revisjonskrav. Den sviktar der ein blindt stolar på Forwarded-header eller når proxy-topologiar ikkje er dokumenterte (fleire Ingress-lag, Cloud-LB pluss nginx pluss Sidecar). Då blir klient-IP og protokoll raskt «noko».
Eit klart skilje: Om du treng komplekse tillitsreglar (CIDR, IPv6-nett, dynamiske LB-IPar), bør du utvide tillitssjekken (ekte IP-parsing, nettmasker) eller utforme infrastrukturen slik at berre ein definert proxy kan nå Delphi-porten (Firewall/Security Groups). Det er til slutt som regel den mest robuste driftsavgjerda.
Konklusjon: Å drifte ein Reverse Proxy med nginx og Delphi på ein ryddig måte betyr «gjera Forwarded riktig»
Ein Reverse Proxy med nginx er for Delphi-REST-Server ein god standardbaustein – men fyrst den korrekte handsaminga av Forwarded og X-Forwarded-* gjer oppsettet stabilt i drift. Kjernen er enkel: godta berre header frå pålitelege proxiar, utlei klient-IP/protokoll/host konsistent og før denne basisen gjennom i Redirects, Logging og Security-Checks. Med snippetet ovanfor har du eit reint, legacy-taugleg fundament som kan integrerast i WebBroker, Horse eller eigne HTTP-serverar.
Om du vil konsolidere eit eksisterande Delphi-backend bak nginx eller modernisere mot Delphi REST-API og REST-Server med ei klar driftslinje, er ein teknisk gjennomgang av proxy-kjeda og header-tolking ofte det raskaste tiltaket. Kontakt Net-Base for ei kort teknisk vurdering.
I fagleg samanheng spelar også Nginx Reverse Proxy og Forwarded-Header ei viktig rolle når integrasjonar, dataflyt og vidareutvikling må samspela på ein ryddig måte.