Net-Base Списание

23.05.2026

Обратен прокси с nginx и Delphi: правилно обработване на заглавката Forwarded, истинския IP на клиента и устойчиви базови URL пътища

Когато Delphi-REST сървъри работят зад nginx, често се нарушават IP адресът на клиента, разпознаването на HTTPS и абсолютните URL адреси. Този фрагмент от изходния код показва надеждна обработка на заглавките Forwarded/X-Forwarded (включително списък с доверени проксита), типични настройки за nginx и указания за отстраняване на грешки при експлоатация.

23.05.2026

От темата в списанието към проектната практика

Подходящи страници за услуги и технологии към публикацията

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.

Delphi
# (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.
Delphi
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; по-често става дума за комбинация от прокси-хедъри, логика за пренасочване и таймаути. Три дебъг-чека, които имат практическа стойност:

  1. Header-Dump (целево): Записвайте при 4xx/5xx допълнително Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent и Request-URI. Но само при грешки – в противен случай логът става скъп и нечетлив.
  2. Проверка на Base-URL: Ако пренасочвания или callback-URL-и се провалят, логвайте ForwardedInfo.BaseUrl. Много грешки са видими веднага („http://127.0.0.1“ вместо „https://api…“).
  3. Корелация на таймаути: 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-заглавките също играят важна роля, когато интеграции, потоци от данни и по-нататъшно развитие трябва да взаимодействат коректно.

Обсъдете проект или модернизационна инициатива с Net-Base.

Следваща стъпка

Когато темата прерасне в реален проект, архитектурата, съществуващото състояние и експлоатацията трябва да бъдат разгледани съвместно още в ранна фаза.

Подпомагаме не само при отделни въпроси, но и когато от фрагменти от изходен код, проблеми с наследени системи или идеи за портал трябва да бъде реализиран надежден корпоративен проект.

  • Сегашното състояние, целевото състояние и техническите рискове се оценяват съвместно.
  • REST, достъпът до данни, порталите и разгръщането не се отлагат като по-късни последици.
  • Виждате рано кой път е икономически и експлоатационно жизнеспособен.

Сподели публикацията

Споделете тази публикация директно

LinkedIn, X, XING, Facebook, WhatsApp и имейл са незабавно достъпни. За Instagram ще подготвим връзка и кратък текст.

Електронна поща

Instagram се отваря в нов раздел. Връзката и краткият текст се копират предварително в клипборда.