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.
# (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.
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:
- 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.
- 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…‘).
- 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.