Net-Base Магазин

23.05.2026

Реверс прокси са nginx и Delphi: прецизно руковање заглављима Forwarded, стварна IP адреса клијента и робусне URL базе

Када Delphi-REST-сервери раде иза nginx-а, често се губе Client-IP, препознавање HTTPS и апсолутни URL-ови. Овај исечак кода показује робусно руковање заглављима Forwarded и X-Forwarded (укљ. листу поверених проксија), типичне nginx поставке и смернице за отклањање грешака у раду.

23.05.2026

Јedan Reverse Proxy са nginx-ом и Delphi у пракси обично није „nice to have“, већ чиста разграничење између Internet-раба и апликације: TLS-терминирање (HTTPS-Offloading), централна правила за Header/CORS, ограничења учесталости (Rate-Limits), јединствени логови, Blue/Green-rollouts или једноставно хостовање више сервиса под једном доменом. Оно што се често потцењује: чим nginx „испред“ стоји, Delphi-сервер види само IP адресу проксија, често само „http“ уместо „https“ и генерише погрешне апсолутне линкове (Redirects, Callback-URL-ове, OpenAPI-Server-URL). Управо та три проблема касније узрокују време за дебаговање у раду.

Овај исечак кода показује робустан образац како у Delphi-у предано обрадити Forwarded односно X-Forwarded-* – укључујући Trust-Proxy-Liste (важно против спуфовања Header-а) и конзистентан Request-Base-URL. Поред тога су наведене практичне nginx конфигурације и напомене за рубне случајеве као што су WebSockets, велики уплоади и таймаути.

Зашто Reverse Proxy конфигурације збуњују Delphi-сервере

nginx као Reverse Proxy обично комуницира са Delphi сервисом несигурно (HTTP) у унутрашњој мрежи или на localhost-у, док клиент споља долази преко HTTPS-а. Без додатних Header-а Delphi не зна ништа о:

  • Изворној шеми (https vs. http) – релевантно за преусмерења и апсолутне URL-ове.
  • Изворном хосту (клијентска/корисничка домена, порт) – релевантно за multi-tenant окружења, CORS и callback URL-ове.
  • Изворној IP адреси клијента – релевантно за аудит, ограничења учесталости (Rate-Limits), Geo-провере и безбедносне анализе.

nginx може да преноси те информације преко Header-а. Уобичајени су X-Forwarded-For, X-Forwarded-Proto и X-Forwarded-Host; додатно је стандардизован RFC-Header Forwarded. Важно: ти Header-и са становишта апликације нису аутоматски поуздани, јер их клијент може сам послати – постају поуздани тек када потичу од познатог проксија.

nginx-Konfiguration: die minimal sinnvollen Proxy-Header

Солидан почетак (HTTP/1.1, Keep-Alive, Upgrade за WebSockets) изгледа овако. Снипет је намерно кратак; у зависности од окружења допуните HSTS, Rate-Limits и логове приступа.

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, IP клијента и шема. Ограничење: $proxy_add_x_forwarded_for додаје актуелну IP адресу прокси-ја на евентуално постојећи ланац; то је добро за multi-proxy окружења, али чини исправну евалуацију на страни Delphi тим важнијом. Замка: Ако у nginx не поставите Host-заглавље, Delphi може видети само Upstream-Host (127.0.0.1), што нарушава преусмерења и провере origin-а.

Delphi Извaдак кода: Робусно тумачење Forwarded/X-Forwarded (са листом поверених прокси-ја)

Следећи код је свесно независан од фрејмворка: ради против минималног интерфејса (Header + RemoteIP) и може се прилагодити за WebBroker, RAD Server или Horse. Кључне тачке:

  • Приоритет: RFC Forwarded (ако постоји) пре X-Forwarded-*.
  • Поверење: Forwarded-заглавља обрађивати само ако је директни peer (RemoteIP) познати прокси.
  • Парсирање: узети у обзир IPv6, наводнике, портове и ланце у X-Forwarded-For.
  • Излаз: основни URL који можете користити за апсолутне линкове, преусмерења или OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

uses
  System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;

type
  // Minimalni adapter interfejs: implementirajte ga za WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // direktna TCP-krajnja tačka (uglavnom nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // npr. 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 može biti "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 u []: [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 (Pažnja: za nagi IPv6 bez [] nije pouzdano)
  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
  // Primer: 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;

  // Više elemenata je razdvojeno zarezom; uzimamo prvi (najbliži klijentu)
  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 može biti IP ili "unknown"; IPv6 može biti u []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 se može pojaviti
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // defensivno
      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
  // Za pravu IPv6 normalizaciju bila bi potrebna parsiranje IP adrese; ovde svesno pragmatično.
  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; // Rezervna opcija: direktna krajnja tačka

  // Samo ako je direktna krajnja tačka poznati proxy, procenjujemo Forwarded zaglavlja.
  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-* kao fallback/dopuna
    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;

  // U krajnjem slučaju uzeti Host iz 'Host' zaglavlja (ako nema proxy-zaglavlja)
  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-trail-ови, rate-limit-и или крајне тачке које су „дозвољене само изнутра“. Поверујте Forwarded-заглављима само ако је директни партнер (RemoteIP) прокси који контролишете (нпр. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Замке: IPv6 без угластих заграда није јасно у Host:port нотацији. У HTTP-Host-заглављу се IPv6 обично бележи у []; придржавајте се тога. За сложене IP-опсеге (CIDR) морали бисте проширити Trust-листу (нпр. правим IP-парсирањем).

Интеграција у WebBroker/Horse/RAD Server: где се код „прикачи“

У WebBroker-у (TWebRequest) заглавља обично долазе преко ContentFields или GetFieldByName, а Remote-IP зависи од server-backend-а. У Horse-у (или другим HTTP-фрејмворцима) обично постоје Req.Headers и својство за Remote-IP. Важно је принцип: RemoteIP мора бити TCP-противник, не било која вредност из заглавља.

Практично проверено: При покретању сервиса креирајте TTrustedProxyList из конфигурације (INI/ENV), нпр. „127.0.0.1“ за локалне nginx-поставке или IP вашег Load Balancera. Онда позовите ResolveForwardedInfo по захтеву и упишите поља у ваше структурирано логовање (JSON-Log, Syslog или Windows Event Log).

Дебаговање у раду: како пронаћи грешке за минуте уместо сати

Када захтеви делују „чудно“, ретко је проблем у самом Delphi-HTTP, већ у комбинацији proxy-заглавља, redirect-логике и timeout-ова. Три дебаг провере које се исплате у пракси:

  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. Корелација timeout-а: 504 од проксија није исто што и Delphi-timeout. nginx proxy_read_timeout и Delphi-стране Idle-/Read-Timeouts морају бити усклађени.

Рубни случајеви: WebSockets, Streaming и велики захтеви

WebSockets иза nginx

За WebSockets nginx захтева исправна Upgrade и Connection заглавља. Поред тога, backend не сме да затвори везу „пре времена“. На Delphi-страни је важно да ваша WebSocket-компонента (или SSE/streaming крајња тачка) уме да ради са reverse proxy-јима и да су Heartbeats/Keep-Alives правилно имплементирани.

Велики upload-и и 413 грешке

Класик: Delphi прихвата upload, али nginx претходно блокира са 413 Request Entity Too Large. Контролишите то експлицитно преко client_max_body_size и прилагодите Delphi-стране Request-Limits. За процесно-блиске софтверске решење са документима или сликама то није изузетак, већ нормалан рад.

HTTPS-Offloading и „Secure Cookies“

Ako vaš Delphi-Service postavlja Session-Cookies, они се при екстерном HTTPS у правилу морају означити као Secure. Да ли ваша апликација то ради често зависи од тога да ли „зна“ да је оригинални захтев био HTTPS. Управо овде помаже конзистентна обрада заглавља X-Forwarded-Proto/Forwarded.

Када се улагање исплати – и где може да закаже

Приказани приступ се исплати кад Delphi-Service више није „nackt“ у LAN-у, већ постане део продуктивне ивице: више домена, SSO/SAML-спојева, јавних API-ја, мултитенантности или строжих захтева за ревизију. Он почиње да закаже у случајевима кад се слепо верује Forwarded-заглављима или кад се Proxy-топологије не документују (више нивоа Ingress-а, Cloud-LB плус nginx плус Sidecar). У тим условима Client-IP и схема брзо постају „неконзистентни“.

Јасна граница: ако вам требају сложена правила поверљивости (CIDR, IPv6-мреже, динамичке LB-IP адресе), требало би проширити проверу поверења (право IP-parsing, мрежне маске) или инфраструктуру дизајнирати тако да само дефинисани прокси може да досегне Delphi-порт (Firewall/Security Groups). То је у пракси често робуснија оперативна одлука.

Закључак: Уредно руковање Reverse Proxy-јем са nginx и Delphi значи „Forwarded исправно имплементирати“

Reverse Proxy са nginx-ом је за Delphi-REST-Server добар стандардни елемент – али тек исправна обрада Forwarded и X-Forwarded-* чини конфигурацију стабилном у раду. Суштина је једноставна: прихватајте заглавља само од поузданих проксија, доследно изводите Client-IP/Scheme/Host и ту основу примените у редиректима, логовању и безбедносним проверама. Са горњим снипетом имате чисту, за legacy погодну основу која се може интегрисати у WebBroker, Horse или власте HTTP-Server-е.

Ако желите консолидацију постојећег Delphi-бекенда иза nginx-а или модернизацију у правцу Delphi REST-API и REST-Server са јасном оперативном границом, технички преглед ланца проксија и начина хендлинга заглавља често даје најбржи ефекат. Kontaktieren Sie Net-Base за кратку техничку процену.

У стручном контексту Nginx Reverse Proxy и Forwarded-заглавља такође играју важну улогу када интеграције, токови података и даљи развој морају чисто координисати.

Разговор о пројекту или плану модернизације са Net-Base.

Подели објаву

Поделите ову објаву директно

LinkedIn, X, XING, Facebook, WhatsApp и е-пошта су одмах доступни. За Instagram припремамо линк и кратак текст.

Е-пошта

Инстаграм се отвара у новој картици. Линк и кратак текст се претходно копирају у међуспремник.