Ein Reverse Proxy mit nginx und Delphi во пракса обично не е „nice to have“, туку јасна разделба помеѓу интернет-крајот и апликацијата: TLS-терминирање (HTTPS-Offloading), централизирани правила за Header/CORS, Rate-Limits, унифицирани логови, Blue/Green-rollouts или едноставно хостирање на повеќе сервиси под една домена. Она што често се потценува е следново: откако nginx ќе застане „пред“ апликацијата, серверот Delphi ќе го види само IP-то на проксито, често само „http“ наместо „https“ и ќе генерира погрешни апсолутни линкови (Redirects, Callback-URL-и, OpenAPI-Server-URL). Токму овие три точки подоцна предизвикуваат време за дебагирање во оперативата.
Овој исечок од изворен код покажува робустен образец како да ги евалуирате Forwarded односно X-Forwarded-* во Delphi – вклучувајќи Trust-Proxy-Liste (важно против spoofing на header-и) и конзистентна Request-Base-URL. Има и практични nginx-конфигурации и напомени за крајни случаи како WebSockets, големи уплоади и timeouts.
Зошто Reverse Proxy-решенијата ги „збунуваат“ серверите Delphi
nginx како Reverse Proxy типично зборува со сервисот Delphi нешифрирано (HTTP) во внатрешната мрежа или на localhost, додека клиентот надвор доаѓа преку HTTPS. Без дополнителни header-и, Delphi не знае ништо за:
- Original-Schema (https vs. http) – релевантно за Redirects и апсолутни URL-и.
- Original-Host (кориснички домен, порт) – релевантно за Multi-Tenant-решенија, CORS и Callback-URL-и.
- Original-Client-IP – релевантно за Audit, Rate-Limits, Geo-проверки и безбедносни анализи.
nginx може да ги пренесе овие информации преку header-и. Вообичаено се користат X-Forwarded-For, X-Forwarded-Proto и X-Forwarded-Host; стандаризирано постои и RFC-header-от Forwarded. Важно: Овие header-и од гледна точка на апликацијата не се автоматски доверливи, бидејќи клиентот може самиот да ги испрати – тие стануваат доверливи кога доаѓаат од познато прокси.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Стабилна почетна точка (HTTP/1.1, Keep-Alive, Upgrade за WebSockets) изгледа вака. Исечокот е намерно краток; вие дополнувате во зависност од околината HSTS, Rate-Limits и 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;
}
}
Цел: Апликацијата добива Host, Client-IP и Schema доверливо предадени. Услов: $proxy_add_x_forwarded_for го додава тековниот IP на проксито на евентуално постоечка низа; тоа е корисно за Multi-Proxy-setup-и, но ја прави правилната евалуација на Delphi-страната уште поважна. Потенцијална замка: Ако во nginx не го поставите Host-Header-от, Delphi во некои случаи може да го види само Upstream-Host-от (127.0.0.1), што ги крши Redirects и Origin-Checks.
Delphi Исечок од изворниот код: Робустна евалуација на Forwarded/X-Forwarded (со листа на доверени проксита)
Следниот код е свесно одржан неутрален во однос на фрејмворкот: работи со минимален интерфејс (Header + RemoteIP) и може да се адаптира за WebBroker, RAD Server или Horse. Клучни точки:
- Приоритет: RFC Forwarded (ако постои) пред X-Forwarded-*.
- Доверба: Forwarded-заглавјата да се евалуираат само ако директниот peer (RemoteIP) е познат прокси.
- Парсирање: земете предвид IPv6, наводници, портови и синџири во X-Forwarded-For.
- Излез: една Base-URL што можете да ја користите за апсолутни линкови, Redirects или OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Минимален адаптер-интерфејс: имплементирајте го за WebBroker/Horse/итн.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // директна TCP-насрещна точка (обично nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // на пр. 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 може да биде „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 во []: [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 (Внимание: кај гол IPv6 без [] не е сигурно)
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
// Пример: 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;
// Повеќе елементи се одделени со запирка; користиме првиот (најблизок до клиентот)
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 може да биде IP или „unknown“; IPv6 може да стои во []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// for=1.2.3.4:5678 може да се појави
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // дефанзивно
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
// За вистинска IPv6-нормализација би било потребно парсирање на IP; овде намерно прагматично.
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; // Резервно: директна насрещна точка
// Само ако директната насрещна точка е познат прокси, ќе ги прочитаме заглавјата Forwarded.
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-* како резервно/дополнување
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;
// Во краен случај земи Host од заглавјето „Host“ (ако нема Proxy-заглавја)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Цел: Од секој Request добивате конзистентен преглед на ClientIP, Proto и Host како и на BaseUrl. Овие информации можете централизирано да ги користите за логирање, безбедносни одлуки (на пр. IP-allowlist) и генерирање линкови.
Зошто е потребна листата на доверливи проксита: Без проверка на довербата, напаѓач може директно да го достигне вашиот Delphi-порт (погрешна конфигурација, внатрешно рутирање, VPN) и едноставно да прати X-Forwarded-For: 127.0.0.1. Тоа ќе ги направи ранливи audit-trails, rate-limits или крајни точки означени како „само внатрешно дозволено“. Доверувајте им се на Forwarded-хедерите само ако директниот peer (RemoteIP) е прокси што го контролирате (на пр., 127.0.0.1, IP на Load-Balancer, Kubernetes-Ingress).
Замки: IPv6 без аголни загради не е недвосмислено во нотацијата Host:port. Во HTTP-Host-header-от IPv6 обично е означено со []; придржувајте се кон тоа. За сложени опсези на IP (CIDR) ќе треба да ја проширите Trust-листата (на пр. со вистинско парсирање на IP).
Интеграција во WebBroker/Horse/RAD Server: каде кодот „се закачува“
Во WebBroker (TWebRequest) хедерите обично доаѓаат преку ContentFields или GetFieldByName, а Remote-IP зависи од бекендот на серверот. Во Horse (или други HTTP-рамки) обично има Req.Headers и својство за Remote-IP. Важно е принципот: RemoteIP мора да биде TCP-спротивната страна, а не некоја вредност од хедер.
Практично проверено: При старт на сервисот создадете TTrustedProxyList од конфигурација (INI/ENV), на пр. „127.0.0.1“ за локални nginx-setup-ови или IP-то на вашиот Load Balancer. Потоа повикајте ResolveForwardedInfo по Request и запишете ги полјата во вашето структурирано логирање (JSON-Log, Syslog или Windows Event Log).
Дебагирање во работа: како да најдете грешки за минути наместо за часови
Кога Requests изгледаат „чудно“, ретко е до Delphi-HTTP сам по себе, туку до комбинација од proxy-хедери, redirect-логика и timeouts. Три проверки за дебагирање кои се исплатливи во пракса:
- Header-Dump (целено): Логирајте при 4xx/5xx дополнително Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent и Request-URI. Но само при грешки – инаку логот станува скап и нејасен.
- Проверете ја Base-URL: Ако Redirects или Callback-URL-ите не успеваат, логирајте го ForwardedInfo.BaseUrl. Многу грешки се веднаш видливи („http://127.0.0.1“ наместо „https://api…“).
- Корелација на timeout-ови: 504 од прокси не е исто што и timeout на Delphi. nginx proxy_read_timeout и timeout-овите на страната на Delphi за Idle/Read треба да се усогласат.
Крајни случаи: WebSockets, Streaming и големи Requests
WebSockets зад nginx
За WebSockets nginx треба правилно да ги пренесува Upgrade и Connection. Дополнително, бекендот не смее да затвори „претерано рано“. На страната на Delphi е важно вашата WebSocket-компонента (или SSE/Streaming-ендпоинт) да може да работи со Reverse Proxies и да има правилно имплементирани Heartbeats/Keep-Alives.
Големи уплоади и 413-грeшки
Класик: Delphi ја прифаќа уплоадот, но nginx претходно го блокира со 413 Request Entity Too Large. Контролирајте го тоа експлицитно преку client_max_body_size и прилагодете ги Request-лимитите на страната на Delphi. За софтверски решенија блиску до процесот со документи или слики, ова не е исклучок туку нормален оперативен случај.
HTTPS-Offloading und „Secure Cookies“
Ако вашиот Delphi-сервис поставува session-cookie-ја, тие обично мора да бидат означени како Secure при надворешен HTTPS. Дали вашата апликација го прави тоа, често зависи од тоа дали „знае“ дека оригиналниот барање бил HTTPS. Точно тука помага конзистентната евалуација на X-Forwarded-Proto/Forwarded.
Кога вложениот напор има смисла – и каде може да пропадне
Прикажаниот пристап се исплати секогаш кога Delphi-сервисот не е повеќе „гол“ во LAN, туку дел од продуктивната крајна инфраструктура: повеќе домени, SSO/SAML-интерфејси, јавни API-та, мулти-тенантност или построги барања за ревизија. Тој пропаѓа таму каде што се верува слепо на Forwarded-хедерите или кога прокси-топологиите не се документирани (неколку Ingress-степени, Cloud-LB плус nginx плус Sidecar). Тогаш IP-то на клиентот и протоколот/схемата брзо стануваат „некаков/што и да е“.
Јасна граница: Ако ви требаат комплексни правила за доверба (CIDR, IPv6-мрежи, динамички LB-IP-адреси), треба да ја проширите проверката на довербата (вистинско IP-парсирање, мрежни маски) или да ја дизајнирате инфраструктурата така што само еден дефиниран прокси може да пристапи до портот на Delphi (Firewall/Security Groups). На крајот, тоа обично е потврдената и поцврста оперативна одлука.
Заклучок: Reverse Proxy со nginx и Delphi чисто оперирање значи „Forwarded правилно направено“
Reverse Proxy со nginx е добар стандарден градежен модул за Delphi-REST-Server – но само правилната обработка на Forwarded и X-Forwarded-* го прави сетапот стабилен во оперативна употреба. Јадрото е симплотно: хедери прифаќајте само од доверливи проксита, конзистентно извлекувајте Client-IP/Scheme/Host и таа основа применете ја во пренасочувања, логирање и безбедносни проверки. Со примерот (snippet) погоре имате чиста, за legacy погодна основа која може да се интегрира во WebBroker, Horse или во сопствени HTTP-сервери.
Ако сакате да консолидирате постоечки Delphi-бекенд зад nginx или да го модернизирате кон Delphi REST-API и REST-Server со јасна оперативна линија, технички преглед на низата проксита и на евалуацијата на хедерите често е најбрзиот лост. Контактирајте го Net-Base за кратка техничка проценка.
Во стручниот контекст, Nginx Reverse Proxy и Forwarded-хедерите имаат важна улога кога интеграциите, тековите на податоци и понатамошниот развој треба да соработуваат прецизно.