Інструмент Reverse Proxy з nginx і Delphi на практиці зазвичай не є «nice to have», а забезпечує чітке розділення між інтернет-краєм і аплікацією: термінація TLS (HTTPS-offloading), центральні правила заголовків/CORS, обмеження швидкості запитів, уніфіковані логи, blue/green‑розгортання або просто хостинг кількох сервісів під однією доменною зоною. Те, що часто недооцінюють: як тільки nginx стоїть «перед» сервером Delphi, цей сервер бачить лише IP проксі, часто тільки «http» замість «https» і генерує некоректні абсолютні посилання (редиректи, callback-URL, URL сервера OpenAPI). Саме ці три моменти пізніше створюють час на налагодження в експлуатації.
Цей фрагмент коду показує стійкий патерн, як у Delphi коректно розбирати Forwarded та X-Forwarded-* — включно зі списком довірених проксі (важливо проти підробки заголовків) і послідовною Request-Base-URL. Також наведені практичні конфігурації nginx і вказівки щодо крайових випадків, як-от WebSockets, великі завантаження і таймаути.
Чому налаштування Reverse Proxy «заплутують» сервери Delphi
nginx як Reverse Proxy зазвичай спілкується з сервісом Delphi незашифровано (HTTP) у внутрішній мережі або на localhost, тоді як клієнт звертається ззовні через HTTPS. Без додаткових заголовків Delphi не знає про:
- Оригінальна схема (https vs. http) – важливо для редиректів та абсолютних URL-ів.
- Оригінальний хост (домен клієнта, порт) – важливо для мультиорендних налаштувань, CORS і callback-URL.
- Оригінальна IP клієнта – важливо для аудиту, обмежень частоти запитів, геоперевірок та аналізу безпеки.
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, обмеженнями швидкості запитів і журналами доступу.
# (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, IP клієнта та схему надійно переданими. Передумова: $proxy_add_x_forwarded_for приписує поточну IP проксі до можливо наявного ланцюжка; це корисно для Multi-Proxy-Setup-ів, але робить правильний розбір на боці Delphi тим важливішим. Підводний камінь: Якщо Ви в nginx не встановите заголовок Host, 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 лише якщо прямий пір (RemoteIP) — відомий проксі.
- Розбір: враховувати IPv6, лапки, порти та ланцюжки в X-Forwarded-For.
- Вивід: базову 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: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" (якщо немає проксі-заголовків)
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) та генерації посилань.
Чому потрібен список Trust-Proxy: Без перевірки довіри зловмисник може напряму дістатися до вашого Delphi-порту (помилка конфігурації, внутрішнє маршрутування, VPN) і просто відправити X-Forwarded-For: 127.0.0.1. Це робить вразливими audit-trails, rate-limits або кінцеві точки «лише для внутрішнього використання». Довіряйте Forwarded-заголовкам тільки якщо прямий пір (RemoteIP) — це проксі, яким ви контролюєте (наприклад, 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).
Підводні камені: IPv6 без квадратних дужок у нотації Host:port не однозначний. В HTTP-Host-заголовку IPv6 зазвичай зазначається в []; дотримуйтеся цього. Для складніших діапазонів IP (CIDR) вам доведеться розширити список довіри (наприклад, за рахунок реального парсингу IP).
Інтеграція в WebBroker/Horse/RAD Server: де код «підключається»
У WebBroker (TWebRequest) заголовки зазвичай доступні через ContentFields або GetFieldByName, RemoteIP залежить від серверного бекенду. В Horse (або інших HTTP-фреймворках) зазвичай є Req.Headers та властивість RemoteIP. Важливий принцип: RemoteIP має бути TCP‑співрозмовником, а не якимось значенням із заголовка.
Практично випробувано: створіть при старті сервісу TTrustedProxyList з конфігурації (INI/ENV), наприклад «127.0.0.1» для локальних nginx‑налаштувань або IP вашого Load Balancer. Потім викликайте ResolveForwardedInfo для кожного запиту і записуйте поля у ваше структуроване логування (JSON-Log, Syslog або Windows Event Log).
Налагодження в експлуатації: як знаходити помилки за хвилини замість годин
Якщо запити виглядають «дивно», проблема рідко в самому Delphi-HTTP, скоріше в поєднанні proxy‑заголовків, логіки редиректів та таймаутів. Три перевірки для налагодження, що економлять час у повсякденній роботі:
- Дамп заголовків (цілеспрямовано): логируйте при 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, Streaming та великі запити
WebSockets за nginx
Для WebSockets nginx потребує коректних заголовків Upgrade і Connection. Додатково бекенд не повинен закривати з’єднання «занадто рано». На боці Delphi важливо, щоб ваша WebSocket‑компонента (або SSE/стрімінговий ендпоінт) могла працювати з реверс‑проксі і щоб Heartbeats/Keep‑Alives були реалізовані чисто.
Великі завантаження та помилки 413
Класика: Delphi приймає завантаження, але nginx блокує його раніше з 413 Request Entity Too Large. Керуйте цим явно через client_max_body_size і налаштуйте на боці Delphi ліміти запитів. Для процесоорієнтованих програмних рішень з документами або зображеннями це не виняток, а штатна робота.
HTTPS-Offloading und „Secure Cookies“
Якщо ваш Delphi-сервіс встановлює сесійні Cookie, то при зовнішньому HTTPS вони, як правило, мають бути позначені як Secure. Чи робить це ваша аплікація, часто залежить від того, чи вона «знає», що початковий запит був HTTPS. Саме тут допомагає послідовний аналіз X-Forwarded-Proto/Forwarded.
Коли ці зусилля виправдані — і де вони можуть дати збій
Показаний підхід виправданий завжди, коли Delphi-сервіс більше не знаходиться «голим» у LAN, а стає частиною продуктивного краю: кілька доменів, SSO/SAML-інтерфейси, публічні API, багатоклієнтність або жорсткіші вимоги аудиту. Він дає збій там, де заголовкам Forwarded сліпо довіряють або топології проксі не документовані (кілька рівнів Ingress, Cloud-LB плюс nginx плюс Sidecar). У таких випадках Client-IP та схема швидко стають невизначеними.
Чітка межа: якщо вам потрібні складні правила довіри (CIDR, IPv6-мережі, динамічні IP-адреси балансувальника (LB)), слід розширити перевірку довіри (реальний розбір IP, мережеві маски) або спроектувати інфраструктуру так, щоб тільки визначений проксі міг досягати порту Delphi (Firewall/Security Groups). Зрештою це зазвичай більш надійне операційне рішення.
Висновок: експлуатація Reverse Proxy з nginx і Delphi вимагає «правильної обробки Forwarded»
Reverse Proxy з nginx є хорошим стандартним компонентом для Delphi-REST-серверів — але лише коректна обробка Forwarded та X-Forwarded-* робить налаштування стабільним в експлуатації. Суть проста: приймати заголовки лише від довірених проксі, послідовно виводити Client-IP/Scheme/Host і використовувати цю основу в редиректах, логуванні та перевірках безпеки. Наведеним вище снипетом ви отримуєте чисту, сумісну з legacy основу, яку можна інтегрувати в WebBroker, Horse або власні HTTP-сервери.
Якщо ви консолідуєте існуюче Delphi-бекенд за nginx або хочете модернізувати його в бік Delphi REST-API та REST-серверів з чіткою операційною лінією, технічне рев’ю ланцюжка проксі та аналізу заголовків часто є найшвидшим важелем. Зв’яжіться з Net-Base для короткої технічної оцінки.
У профільному середовищі Nginx Reverse Proxy та заголовки Forwarded також відіграють важливу роль, коли інтеграції, потоки даних і подальший розвиток повинні працювати узгоджено.