Ein Reverse Proxy mit nginx und Delphi ist in der Praxis meist kein „nice to have“, sondern die saubere Trennung zwischen Internetkante und Applikation: TLS-Terminierung (HTTPS-Offloading), zentrale Header-/CORS-Regeln, Rate-Limits, einheitliche Logs, Blue/Green-Rollouts oder einfach das Hosting mehrerer Services unter einer Domain. Was dann gerne unterschätzt wird: Sobald nginx „davor“ sitzt, sieht der Delphi-Server nur noch die Proxy-IP, oft nur noch „http“ statt „https“ und generiert falsche absolute Links (Redirects, Callback-URLs, OpenAPI-Server-URL). Genau diese drei Punkte sorgen später für Debugging-Zeit im Betrieb.
Този фрагмент от сорс-кода показва устойчив модел за това как в Delphi да обработите коректно Forwarded и X-Forwarded-* — включително Trust-Proxy-Liste (важно срещу Header-Spoofing) и консистентен Request-Base-URL. Към това има практически nginx-конфигурации и указания за гранични случаи като WebSockets, големи upload-и и timeouts.
Защо Reverse Proxy конфигурации „объркват“ Delphi-сървърите
nginx като Reverse Proxy обикновено комуникира с Delphi-сървиса нешифровано (HTTP) в вътрешната мрежа или на localhost, докато клиентът идва отвън по HTTPS. Без допълнителни заглавки Delphi не знае нищо от следното:
- Original-Schema (https vs. http) – релевантно за редиректи и абсолютни URL-ове.
- Original-Host (kundenspezifische Domain, Port) – релевантно за Multi-Tenant-Setups, CORS и callback-URL-и.
- Original-Client-IP – релевантно за одит, Rate-Limits, гео-проверки и анализи за сигурност.
nginx може да пренесе тази информация чрез заглавки. Обичайни са X-Forwarded-For, X-Forwarded-Proto и X-Forwarded-Host; стандартизиран е допълнително RFC-заглавката Forwarded. Важно: тези заглавки от гледна точка на приложението не са автоматично доверителни, защото клиент може да ги изпрати самостоятелно — те стават доверителни едва когато идват от познат прокси.
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 и схема. Условие: $proxy_add_x_forwarded_for прикачва текущия IP на проксито към евентуална вече съществуваща верига; това е полезно за конфигурации с множество проксита, но прави коректната обработка на Delphi още по-важна. Клопка: Ако в nginx не зададете Host-хедъра, Delphi може да види само upstream-хоста (127.0.0.1), което нарушава пренасочвания и проверките на Origin.
Delphi фрагмент от изходния код: Надеждна обработка на Forwarded/X-Forwarded (с доверителен списък за проксита)
Следният код е умишлено независим от фреймуърк: работи спрямо минимален интерфейс (Header + RemoteIP) и може да се адаптира за WebBroker, RAD Server или Horse. Ключови точки:
- Приоритет: RFC Forwarded (ако е наличен) пред X-Forwarded-*.
- Trust: Обработвайте Forwarded-хедъра само ако директният партньор (RemoteIP) е познат прокси.
- Parsing: Вземете предвид IPv6, кавички, портове и вериги в X-Forwarded-For.
- Output: базов URL, който можете да използвате за абсолютни връзки, пренасочвания или 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; // Резервно: директният TCP-връстник
// Само ако директният връстник е познат прокси, ще обработим 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" хедъра (ако няма прокси-хедър)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Цел: Получавате от всяка заявка консистентен изглед към ClientIP, Proto и Host както и една BaseUrl. Тази информация можете да използвате централно за логване, вземане на решения по сигурността (например IP-Allowlist) и генериране на връзки.
Защо е необходим списък с доверени проксита: Без проверка на доверието нападател може директно да достигне до вашия Delphi-порт (грешна конфигурация, вътрешно маршрутизиране, VPN) и просто да изпрати X-Forwarded-For: 127.0.0.1. Това би компрометирало аудитни записи, ограничения за честота или крайни точки, “достъпни само вътрешно”. Доверявайте се на Forwarded-хедърите само когато директният партньор (RemoteIP) е прокси, който контролирате (напр. 127.0.0.1, IP на Load-Balancer, Kubernetes-Ingress).
Подводни камъни: IPv6 без квадратни скоби не е еднозначен в Host:port-нотацията. В HTTP-Host-Header IPv6 обикновено е означен в []; придържайте се към това. За комплексни IP-диапазони (CIDR) трябва да разширите Trust-Liste (например чрез реален 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-настройки или IP на вашия Load Balancer. След това извиквайте ResolveForwardedInfo за всяка заявка и записвайте полетата в структурираното си логване (JSON-Log, Syslog или Windows Event Log).
Отстраняване на грешки в продукция: така ще намерите проблеми за минути вместо часове
Когато заявките изглеждат „странни“, рядко причината е самият Delphi-HTTP; по-често става дума за комбинация от прокси-хедъри, логика за пренасочване и таймаути. Три дебъг-чека, които имат практическа стойност:
- Header-Dump (целево): Записвайте при 4xx/5xx допълнително Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent и Request-URI. Но само при грешки – в противен случай логът става скъп и нечетлив.
- Проверка на Base-URL: Ако пренасочвания или callback-URL-и се провалят, логвайте ForwardedInfo.BaseUrl. Много грешки са видими веднага („http://127.0.0.1“ вместо „https://api…“).
- Корелация на таймаути: 504 от проксито не е същото като таймаут на Delphi. nginx proxy_read_timeout и Delphi-страничните Idle-/Read-таймаути трябва да съответстват.
Краен случаи: WebSockets, стрийминг и големи заявки
WebSockets зад nginx
За WebSockets nginx се нуждае от коректни Upgrade и Connection хедъри. Освен това бекендът не бива да затваря „прекалено рано“. От страна на Delphi е важно вашият WebSocket-компонент (или SSE/Streaming-ендпойнт) да може да работи зад reverse proxies и да има правилно имплементирани heartbeats/keep-alives.
Големи ъплоуди и 413 грешки
Класика: Delphi приема ъплоуд, но nginx блокира преди това с 413 Request Entity Too Large. Контролирайте това експлицитно чрез client_max_body_size и настройте от страната на Delphi лимитите за заявки. За процесно ориентирани софтуерни решения с документи или изображения това не е рядък случай, а нормална работа.
HTTPS-Offloading und „Secure Cookies“
Ако вашият Delphi-сървис задава сесийни cookies, при външно HTTPS те обикновено трябва да бъдат маркирани като Secure. Дали вашето приложение го прави често зависи от това дали „знае“, че първоначалният request е бил HTTPS. Точно тук помага последователната Auswertung на X-Forwarded-Proto/Forwarded.
Кога усилието си заслужава – и къде може да се провали
Показаният подход си заслужава винаги, когато Delphi-сървисът не е повече „на голо“ в LAN-а, а е част от продуктивен ръб: няколко домейна, SSO/SAML-интерфейси, публични API-та, мултитенантност или по-строги изисквания за одит. Той се проваля там, където се вярва сляпо на Forwarded-заглавки или топологиите на прокситата не са документирани (няколко Ingress-стъпки, Cloud-LB плюс nginx плюс Sidecar). Тогава Client-IP и схема бързо стават несигурни.
Ясна граница: Ако имате нужда от комплексни правила за доверие (CIDR, IPv6-мрежи, динамични LB-IP адреси), трябва да разширите проверката на доверие (реално парсване на IP, мрежови маски) или да проектирате инфраструктурата така, че само дефиниран прокси да може да достигне порта на Delphi (Firewall/Security Groups). Това в крайна сметка обикновено е по-робустно оперативно решение.
Fazit: Обслужване на обратен прокси с nginx и Delphi означава „Forwarded правилно да се направи“
Един обратен прокси с nginx е за Delphi-REST-сървър добър стандартен компонент – но едва коректната обработка на Forwarded и X-Forwarded-* прави конфигурацията стабилна в експлоатация. Същината е проста: приемайте заглавки само от доверени проксита, консистентно извличайте Client-IP/Scheme/Host и прилагайте тази основа в пренасочвания, логиране и проверки за сигурност. С горния фрагмент имате чиста, застаряваща-съвместима основа, която може да се интегрира в WebBroker, Horse или собствени HTTP-сървъри.
Ако консолидирате съществуващ Delphi-бекенд зад nginx или искате да го модернизирате в посока Delphi REST-API и REST-сървър с ясна експлоатационна линия, техническо ревю на веригата от проксита и на обработката на заглавките често е най-бързият лост. Свържете се с Net-Base за кратко техническо становище.
В професионалната среда Nginx обратният прокси и Forwarded-заглавките също играят важна роля, когато интеграции, потоци от данни и по-нататъшно развитие трябва да взаимодействат коректно.