Обратный прокси с nginx и Delphi является на практике чаще не «nice to have», а аккуратным разграничением между краем сети и приложением: терминация TLS (HTTPS‑offloading), централизованные правила для заголовков/CORS, ограничение частоты запросов, единые логи, Blue/Green‑развертывания или просто хостинг нескольких сервисов под одним доменом. То, что часто недооценивают: как только nginx «сидит» перед сервисом, сервер Delphi видит только IP прокси, зачастую только «http» вместо «https» и генерирует неверные абсолютные ссылки (редиректы, callback‑URL, URL сервера OpenAPI). Именно эти три пункта впоследствии добавляют время на отладку в эксплуатации.
Этот фрагмент исходного кода демонстрирует надёжную схему, как в Delphi корректно разбирать заголовки Forwarded и X-Forwarded-* — включая список доверенных прокси (важно против подделки заголовков) и консистентный базовый URL запроса. Приведены практичные конфигурации nginx и указания по пограничным случаям, таким как WebSockets, большие загрузки и таймауты.
Почему настройки обратного прокси «сбивают с толку» серверы Delphi
nginx как обратный прокси обычно общается с сервисом 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‑конфигурация: минимально полезные proxy‑заголовки
Надёжная отправная точка (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, 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-*.
- Доверие: учитывать заголовки Forwarded только если прямой пир (RemoteIP) является известным прокси.
- Разбор: учитывать IPv6, кавычки, порты и цепочки в X-Forwarded-For.
- Выход: базовый 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; // Запасной вариант: прямая противоположная сторона
// Мы анализируем заголовки 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. Это сделает возможным обход аудита, лимитов скорости или эндпоинтов «только для внутренней сети». Доверяйте Forwarded-Header’ам только если прямой пёр (RemoteIP) — это прокси, которым вы управляете (например, 127.0.0.1, IP балансировщика нагрузки, Kubernetes-Ingress).
Подводные камни: IPv6 без квадратных скобок в нотации Host:port неоднозначен. В HTTP-Host-заголовке IPv6 обычно указывается в []; придерживайтесь этого. Для сложных диапазонов IP (CIDR) потребуется расширение списка доверия (например, с помощью полноценного парсинга IP).
Интеграция в WebBroker/Horse/RAD Server: где код «пришвартовывается»
В WebBroker (TWebRequest) заголовки обычно доступны через ContentFields или GetFieldByName, Remote-IP — в зависимости от серверного бэкенда. В Horse (или других HTTP-фреймворках) обычно есть Req.Headers и свойство Remote-IP. Важно принципиальное: RemoteIP должен быть TCP-собеседником, а не каким‑то значением из заголовка.
Практически проверено: при старте сервиса создайте из конфигурации (INI/ENV) TTrustedProxyList, например «127.0.0.1» для локальных nginx-настроек или IP вашего балансировщика нагрузки. Затем для каждого запроса вызывайте 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, стриминг и большие запросы
WebSockets за nginx
Для WebSockets nginx требует корректных заголовков Upgrade и Connection. Дополнительно бэкенд не должен закрывать соединение «слишком рано». На стороне Delphi важно, чтобы ваша WebSocket-компонента (или SSE/Streaming-эндпоинт) корректно работала с reverse-прокси и имела аккуратно реализованные 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. Делает ли это ваше приложение, часто зависит от того, «знает» ли оно, что исходный запрос был по HTTPS. Именно здесь помогает последовательная обработка X-Forwarded-Proto/Forwarded.
Когда эти усилия оправданы — и где они могут дать сбой
Показанный подход оправдан всегда тогда, когда Delphi-сервис больше не развёрнут «в открытую» в LAN, а является частью продуктивного периметра: несколько доменов, SSO/SAML-интерфейсы, публичные API, поддержка мультиарендности или более строгие требования аудита. Он даёт сбой там, где на заголовки Forwarded доверяются вслепую или топологии прокси не документируются (несколько уровней Ingress, Cloud-LB плюс nginx плюс Sidecar). В таких случаях клиентский IP и схема быстро становятся «какими‑то».
Чёткая граница: если вам нужны сложные правила доверия (CIDR, IPv6‑сети, динамические IP балансировщиков нагрузки), стоит расширить проверку доверия (реальный разбор IP, сетевые маски) или спроектировать инфраструктуру так, чтобы доступ к порту Delphi имел только определённый прокси (межсетевой экран/ Security Groups). В конце концов это обычно более надёжное оперативное решение.
Вывод: корректная эксплуатация обратного прокси на nginx и Delphi означает «правильную обработку Forwarded»
Обратный прокси на nginx является для Delphi-REST-Server хорошим стандартным элементом, — но лишь корректная обработка Forwarded и X-Forwarded-* делает настройку стабильной в эксплуатации. Суть проста: принимать заголовки только от доверенных прокси, последовательно выводить Client-IP/Scheme/Host и применять эту базу в редиректах, логировании и проверках безопасности. С приведённым выше сниппетом у вас есть чистая, совместимая с legacy основа, которую можно интегрировать в WebBroker, Horse или собственные HTTP‑серверы.
Если вы консолидируете существующий Delphi-бекенд за nginx или хотите модернизировать в сторону Delphi REST-API и REST-Server с ясной операционной линией, технический ревью цепочки прокси и обработки заголовков часто является самым быстрым рычагом. Свяжитесь с Net-Base для короткой технической оценки.
В профессиональном контексте обратный прокси Nginx и заголовки Forwarded также играют важную роль, когда интеграции, потоки данных и дальнейшая разработка должны работать согласованно.