En Reverse Proxy med nginx och Delphi är i praktiken oftast inte ett „nice to have“, utan den rena avgränsningen mellan internetkanten och applikationen: TLS-terminering (HTTPS-offloading), centrala header-/CORS-regler, ratebegränsningar, enhetliga loggar, Blue/Green-utrullningar eller helt enkelt hosting av flera tjänster under en domän. Vad som ofta underskattas är: så fort nginx sitter „framför“ ser Delphi-servern endast proxy-IP:n, ofta endast „http“ istället för „https“ och genererar felaktiga absoluta länkar (omdirigeringar, callback-URL:er, OpenAPI-server-URL). Just dessa tre punkter leder senare till tid för felsökning i drift.
Denna kodsnutt visar ett robust mönster för hur du i Delphi kan utvärdera Forwarded respektive X-Forwarded-* korrekt – inklusive Trust-Proxy-lista (viktig mot header-spoofing) och en konsekvent Request-Base-URL. Dessutom finns praktiska nginx-konfigurationer och anmärkningar om randfall som WebSockets, stora uppladdningar och timeouts.
Varför reverse proxy-uppsättningar Delphi-servrar „förvirrar“
nginx kommunicerar som reverse proxy med Delphi-tjänsten typiskt okrypterat (HTTP) i det interna nätverket eller på localhost, medan klienten utifrån använder HTTPS. Utan ytterligare headers vet Delphi inget om:
- Ursprungligt schema (https vs. http) – relevant för omdirigeringar och absoluta URL:er.
- Ursprunglig host (kundspecifik domän, port) – relevant för multi-tenant-uppsättningar, CORS och callback-URL:er.
- Ursprunglig klient-IP – relevant för revision, ratebegränsningar, geo-kontroller och säkerhetsanalyser.
nginx kan transportera denna information via headers. Vanliga är X-Forwarded-For, X-Forwarded-Proto och X-Forwarded-Host; standardiserad finns dessutom RFC-headern Forwarded. Viktigt: Dessa headers är från applikationens perspektiv inte automatiskt betrodda, eftersom en klient själv kan skicka dem – de blir först betrodda när de kommer från en känd proxy.
nginx-konfiguration: die minimal sinnvollen Proxy-Header
En stabil startpunkt (HTTP/1.1, Keep-Alive, Upgrade för WebSockets) ser ut så här. Snippetet är avsiktligt kort; beroende på miljö kompletterar du med HSTS, ratebegränsningar och access-loggar.
# (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;
}
}
Syfte: Applikationen får Host, klient‑IP och URL‑protokoll vidarebefordrade på ett tillförlitligt sätt. Förutsättning: $proxy_add_x_forwarded_for lägger till den aktuella proxy‑IP:n i en eventuell befintlig kedja; det är bra för uppsättningar med flera proxys, men gör korrekt utvärdering på Delphi-sidan ännu viktigare. Fallgrop: Om du i nginx inte anger Host-headern, ser Delphi möjligen endast upstream‑hosten (127.0.0.1), vilket bryter omdirigeringar och origin‑kontroller.
Delphi Källkodssnutt: Robust utvärdering av Forwarded/X-Forwarded (med lista över betrodda proxys)
Följande kod är medvetet ramverksneutral: den arbetar mot ett minimalt gränssnitt (Header + RemoteIP) och kan anpassas till WebBroker, RAD Server eller Horse. Kärnpunkter:
- Prioritet: RFC Forwarded (om tillgänglig) före X-Forwarded-*.
- Trust: Utvärdera endast Forwarded‑headern när den direkta peer:n (RemoteIP) är en känd proxy.
- Parsing: Ta hänsyn till IPv6, citattecken, portar och kedjor i X-Forwarded-For.
- Output: en bas‑URL som du kan använda för absoluta länkar, omdirigeringar eller OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimales Adapter-Interface: implementieren Sie es für WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // direkte TCP-Gegenstelle (meist nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // z.B. 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 kann "client, proxy1, proxy2" sein
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 (Achtung: bei nacktem IPv6 ohne [] nicht zuverlässig)
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
// Beispiel: 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;
// Mehrere Elemente sind per Komma getrennt; wir nehmen das erste (client-naheste)
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 kann IP oder "unknown" sein; IPv6 kann in [] stehen
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 kommt vor
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
// Für echte IPv6-Normalisierung bräuchte man ein IP-Parsing; hier bewusst pragmatisch.
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 Gegenstelle
// Nur wenn die direkte Gegenstelle ein bekannter Proxy ist, werten wir Forwarded-Header aus.
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-* als Fallback/Ergänzung
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;
// Notfalls Host aus "Host" Header nehmen (wenn kein Proxy-Header da ist)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Syfte: Du får från varje Request en konsekvent vy av ClientIP, Proto och Host samt en BaseUrl. Denna information kan du använda centralt för loggning, säkerhetsbeslut (t.ex. IP-allowlist) och länkgenerering.
Varför Trust-Proxy-listan behövs: Utan trust‑kontroll skulle en angripare kunna nå din Delphi-port direkt (felkonfiguration, internt routing, VPN) och skicka t.ex. X-Forwarded-For: 127.0.0.1. Då blir auditloggar, ratebegränsningar eller „endpoints endast för internt bruk“ angripbara. Lita endast på Forwarded‑headers när den direkta peer‑adressen (RemoteIP) är en proxy som du kontrollerar (t.ex. 127.0.0.1, din Load-Balancer‑IP, Kubernetes‑Ingress).
Fallgropar: IPv6 utan hakparenteser är i Host:port‑notation inte entydigt. I HTTP Host‑header anges IPv6 normalt inom []; följ den konventionen. För komplexa IP‑intervall (CIDR) måste du utöka trust‑listan (t.ex. genom verklig IP‑parsing).
Integration i WebBroker/Horse/RAD Server: var koden „ansluter“
I WebBroker (TWebRequest) kommer headers vanligtvis via ContentFields eller GetFieldByName, Remote‑IP beror på server‑backend. I Horse (eller andra HTTP‑ramverk) finns oftast Req.Headers och en Remote‑IP‑egenskap. Det viktiga är principen: RemoteIP måste vara TCP‑motparten, inte något värde i en header.
Praktiskt brukbart: skapa vid service‑start en TTrustedProxyList från konfiguration (INI/ENV), t.ex. „127.0.0.1“ för lokala nginx‑upplägg eller IPn för din load balancer. Kör sedan ResolveForwardedInfo per Request och skriv fälten till ditt strukturerade logging (JSON‑Log, Syslog eller Windows Event Log).
Felsökning i drift: så hittar du fel på minuter istället för timmar
När Requests verkar „konstiga“ beror det sällan på Delphi‑HTTP i sig, utan på en kombination av proxy‑headers, redirect‑logik och timeouts. Tre felsökningskontroller som ofta lönar sig i vardagen:
- Header‑dump (selektiv): Logga vid 4xx/5xx dessutom Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent och Request-URI. Men bara vid fel — annars blir loggen dyr och svåröverskådlig.
- Kontrollera Base‑URL: När redirects eller callback‑URLs misslyckas, logga ForwardedInfo.BaseUrl. Många fel syns direkt („http://127.0.0.1“ istället för „https://api…“).
- Timeout‑korrelation: En 504 från proxyn är inte samma sak som ett Delphi‑timeout. nginx proxy_read_timeout och Delphi‑sida Idle/Read‑timeouts måste vara i samklang.
Särskilda fall: WebSockets, streaming och stora förfrågningar
WebSockets bakom nginx
För WebSockets krävs att nginx har Upgrade och Connection korrekt. Dessutom får backend inte stänga „för tidigt“. På Delphi‑sidan är det viktigt att din WebSocket‑komponent (eller en SSE/streaming‑endpoint) kan hantera reverse proxies och att heartbeats/keep‑alives är korrekt implementerade.
Stora uppladdningar och 413‑fel
En klassiker: Delphi accepterar en upload, men nginx blockerar tidigare med 413 Request Entity Too Large. Styr detta explicit med client_max_body_size och anpassa Delphi‑sida request‑limits. För processnära mjukvarulösningar med dokument‑ eller bilddata är detta inte en specialfall utan normal drift.
HTTPS‑avlastning och „Secure Cookies“
Om er Delphi-service sätter session-cookies måste dessa vid extern HTTPS normalt markeras som Secure. Om er applikation gör det beror ofta på om den „vet“ att den ursprungliga begäran var HTTPS. Just här hjälper en konsekvent utvärdering av X-Forwarded-Proto/Forwarded.
När insatsen är motiverad – och var den kan brista
Den visade metoden är alltid motiverad när Delphi-servicen inte längre lever „naken“ i LAN utan är en del av en produktiv kant: flera domäner, SSO/SAML-gränssnitt, publika API:er, multitenancy eller striktare revisionskrav. Den fallerar där man litar blint på Forwarded-headers eller proxy-topologier inte är dokumenterade (flera ingress-steg, Cloud-LB plus nginx plus sidecar). Då blir klient-IP och schema snabbt „något“.
En tydlig gräns: Om ni behöver komplexa trust-regler (CIDR, IPv6-nät, dynamiska LB-IP:er) bör ni utöka trust-prövningen (riktig IP-parsning, nätmasker) eller utforma infrastrukturen så att endast en definierad proxy kan nå Delphi-porten (brandvägg/säkerhetsgrupper). I slutändan är det oftast det mer robusta driftsbeslutet.
Slutsats: Att drifta en Reverse Proxy med nginx och Delphi på ett ordnat sätt betyder att hantera „Forwarded“ korrekt
En Reverse Proxy med nginx är för Delphi-REST-Server en god standardkomponent – men först den korrekta hanteringen av Forwarded och X-Forwarded-* gör uppsättningen stabil i drift. Kärnan är enkel: acceptera bara headers från betrodda proxys, härled Client-IP/Scheme/Host konsekvent och dra denna bas igenom omdirigeringar, loggning och säkerhetskontroller. Med snippeten ovan har ni en ren, legacy-kompatibel grund som går att integrera i WebBroker, Horse eller egna HTTP-servrar.
Om ni vill konsolidera ett befintligt Delphi-backend bakom nginx eller modernisera mot Delphi REST-API och REST-Server med en tydlig driftlinje, är en teknisk granskning av proxy-kedjan och headerutvärderingen ofta den snabbaste hävstången. Kontakta Net-Base för en kort teknisk bedömning.
I det tekniska sammanhanget spelar även Nginx Reverse Proxy och Forwarded-headers en viktig roll när integrationer, dataflöden och vidareutveckling måste samspela väl.
Diskutera projekt eller moderniseringsinitiativ med Net-Base.