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.

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

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

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

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

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