Net-Base מגזין

23.05.2026

Reverse Proxy עם nginx וDelphi: ניהול מסודר של Forwarded, כתובת IP מקורית של הלקוח ובסיסי URL חסינים

כאשר שרתי Delphi-REST פועלים מאחורי nginx, לעתים קרובות כתובת ה-Client-IP, זיהוי HTTPS וכתובות URL מוחלטות משתבשות. קטע קוד זה מציג טיפול יציב ב-Forwarded וב-X-Forwarded (כולל רשימת Trust-Proxy), הגדרות nginx טיפוסיות והערות דיבוג לתפעול.

23.05.2026

פרוקסי הפוך (Reverse Proxy) עם nginx ו Delphi הוא ברוב המקרים אינו סתם „nice to have“, אלא ההפרדה הברורה בין קצה האינטרנט ליישום: TLS-Terminierung (HTTPS-Offloading), כללי Header-/CORS מרכזיים, Rate-Limits, לוגים אחידים, Blue/Green-Rollouts או פשוט אירוח מספר שירותים תחת דומיין אחד. מה שנוטים להמעיט בחשיבותו: ברגע ש-nginx עומד „לפני“ השרת של Delphi, השרת רואה רק את כתובת ה‑IP של הפרוקסי, לעיתים רק „http“ במקום „https“ ויוצר קישורים מוחלטים שגויים (Redirects, Callback-URLs, OpenAPI-Server-URL). בדיוק שלוש הנקודות האלה גורמות מאוחר יותר לזמן דיבוג בתפעול.

קטע קוד זה מציג דפוס יציב כיצד לנתח בDelphi את ה-Forwarded או את ה-X-Forwarded-* בצורה נקייה — כולל Trust-Proxy-Liste (חשוב נגד Header-Spoofing) ו-Request-Base-URL עקבית. בנוסף יש תצורות nginx פרקטיות והערות למקרי קצה כגון WebSockets, העלאות גדולות ו-Timeouts.

מדוע תצורות Reverse Proxy מבלבלות את שרתי Delphi

nginx, בתפקידו כ-Reverse Proxy, מתקשר עם שירות Delphi בדרך כלל בצורה לא מוצפנת (HTTP) ברשת פנימית או על localhost, בעוד הלקוח מחוץ מגיע דרך HTTPS. ללא כותרות נוספות, Delphi אינו יודע על:

  • Original-Schema (https מול http) — רלוונטי עבור Redirects וכתובות URL מוחלטות.
  • Original-Host (דומיין מותאם ללקוח, פורט) — רלוונטי עבור תצורות Multi-Tenant, CORS ו-Callback-URLs.
  • Original-Client-IP — רלוונטי עבור Audit, 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 מצרף את כתובת ה-proxy הנוכחית לשרשרת קיימת אם ישנה; זה טוב להגדרות עם מספר פרוקסים, אך הופך את הניתוח הנכון בצד Delphi לחשוב אף יותר. מכשול: אם ב-nginx לא תקבעו את כותרת ה-Host, Delphi עלול לראות רק את ה-Upstream-Host (127.0.0.1), מה שישבור הפניות ובדיקות מקור.

Delphi קטע קוד מקור: ניתוח אמין של כותרות Forwarded ו-X-Forwarded (עם רשימת פרוקסי מהימנים)

הקוד הבא מנוסח במכוון ללא תלות ב-framework: הוא פועל על ממשק מינימלי (Header + RemoteIP) וניתן להתאמה ל-WebBroker, RAD Server או Horse. נקודות מפתח:

  • עדיפות: RFC Forwarded (אם קיים) לפני X-Forwarded-*.
  • אמון: לנתח את כותרות ה-Forwarded רק אם העמית הישיר (RemoteIP) הוא פרוקסי מוכר.
  • ניתוח: להתחשב ב-IPv6, במרכאות, ביציאות ובשרשראות ב-X-Forwarded-For.
  • פלט: כתובת 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 in []: [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); // defensiv
      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.

מטרה: תקבלו מכל Request מבט עקבי על ClientIP, Proto ו-Host וכן על BaseUrl. מידע זה ניתן להשתמש בו מרכזית לרישום (Logging), לקבלת החלטות אבטחה (למשל IP-Allowlist) וליצירת קישורים.

מדוע רשימת Trust-Proxy נחוצה: ללא בדיקת אמון (Trust-Prüfung) תוקף עלול להגיע ישירות לפורט של Delphi (תצורה שגויה, ניתוב פנימי, VPN) ולשלוח פשוט X-Forwarded-For: 127.0.0.1. בכך יהיו פגיעים Audit-Trails, Rate-Limits או נקודות קצה ש“מותר רק פנימית“. סמכו על כותרות Forwarded רק אם ה-peer הישיר (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 חייבת לייצג את ה-peer של TCP, ולא ערך כותרת כלשהו.

ניסיון מעשי: אתחלו בעת הפעלת ה-service TTrustedProxyList מתוך התצורה (INI/ENV), למשל „127.0.0.1“ עבור הגדרות nginx מקומיות או ה-IP של ה-Load Balancer שלכם. לאחר מכן קראו ל-ResolveForwardedInfo עבור כל Request וכתבו את השדות ל-logging המובנה שלכם (JSON-Log, Syslog או Windows Event Log).

ניפוי שגיאות בתפעול: כך תמצאו תקלות בדקות במקום בשעות

כאשר Requests נראים „מוזרים“, נדיר שהבעיה היא ב-Delphi-HTTP עצמו; בדרך כלל מדובר בשילוב של כותרות פרוקסי, לוגיקת הפניות ו-timeouts. שלוש בדיקות דיבאג שמצדיקות את עצמן בשגרה:

  1. Header-Dump (ממוקד): רשמו ללוג עבור 4xx/5xx בנוסף את Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent ו-Request-URI. אבל רק במקרה של שגיאות – אחרת הלוג יהפוך ליקר ולא מאורגן.
  2. בדיקת Base-URL: כאשר Redirects או Callback-URLs נכשלים, רשמו ללוג את ForwardedInfo.BaseUrl. שגיאות רבות נראות מיד („http://127.0.0.1“ במקום „https://api…“).
  3. קורלציית Timeout: 504 מהפרוקסי אינו זהה ל-timeout בצד Delphi. הגדרת nginx proxy_read_timeout ו-timeoutים בצד Delphi (Idle/Read) חייבים להתאים זה לזה.

מקרים קצה: WebSockets, Streaming ובקשות גדולות

WebSockets מאחורי nginx

לעבודה עם WebSockets נדרש שנג’ינקס יטפל נכון ב-Upgrade וב-Connection. בנוסף, ה-backend לא צריך לסגור „מוקדם מדי“. בצד Delphi רלוונטי שתשתית ה-WebSocket שלכם (או נקודת SSE/Streaming) תדע להתמודד עם Reverse Proxies ושתמיכת Heartbeats/Keep-Alives מיושמת כראוי.

העלאות גדולות ושגיאות 413

מקרה קלאסי: Delphi מקבל את ההעלאה, אך nginx חוסם מוקדם יותר עם 413 Request Entity Too Large. שלטו בכך במפורש באמצעות client_max_body_size והתאימו את גבולות ה-Request בצד Delphi. עבור פתרונות תוכנה קרובים לתהליך הכוללים מסמכים או נתוני תמונה זה אינו חריג, אלא מצב תפעולי רגיל.

HTTPS-Offloading und „Secure Cookies“

אם ה-Delphi-שירות שלכם מגדיר Session-Cookies, בדרך כלל עוגיות אלה חייבות להיות מסומנות כ-Secure כאשר החיבור החיצוני הוא באמצעות HTTPS. האם היישום שלכם עושה זאת תלוי לעתים קרובות בכך שהוא ‚יודע‘ שהבקשה המקורית הייתה ב-HTTPS. כאן בדיוק עוזרת הערכה עקבית של X-Forwarded-Proto/Forwarded.

מתי המאמץ משתלם — והיכן הוא עלול להיכשל

הגישה המוצגת משתלמת תמיד כאשר ה-Delphi-שירות אינו עוד „ערום“ ברשת המקומית (LAN), אלא חלק מתשתית קצה פרודוקטיבית: מספר דומיינים, ממשקי SSO/SAML, Public APIs, תמיכה בריבוי לקוחות או דרישות ביקורת מחמירות יותר. היא עלולה להיכשל כשנותנים אמון עיוור ל-Forwarded-Headers או כשטופולוגיות הפרוקסי אינן מתועדות (שלבי Ingress מרובים, Cloud-LB פלוס nginx פלוס Sidecar). אז כתובת ה-Client-IP וה-Scheme הופכות במהירות ל’משהו‘.

גבול ברור: אם אתם זקוקים לכללי אמון מורכבים (CIDR, רשתות IPv6, כתובות LB דינמיות), יש להרחיב את בדיקת האמון (ניתוח אמיתי של כתובות IP, מסכות רשת) או לעצב את התשתית כך שרק פרוקסי מוגדר יוכל להגיע לפורט של Delphi (Firewall/Security Groups). בסופו של דבר זו בדרך כלל ההחלטה התפעולית העמידה יותר.

מסקנה: תפעול נקי של Reverse Proxy עם nginx ו-Delphi משמעותו „לטפל ב-Forwarded כראוי“

פרוקסי הפוך עם nginx מהווה רכיב סטנדרטי מתאים עבור Delphi-REST-שרת – אך רק הטיפול הנכון ב-Forwarded וב-X-Forwarded-* הופך את ההתקנה ליציבה בתפעול. היסוד פשוט: לקבל כותרות רק מפרוקסי שניתן לסמוך עליהם, לחלץ בצורה עקבית Client-IP/Scheme/Host וליישם את הבסיס הזה בהפניות, ברישום ובבדיקות אבטחה. עם הקטע למעלה יש לכם בסיס נקי, מתאים-ל-legacy, שניתן לשלב ב-WebBroker, Horse או בשרתי HTTP משלכם.

אם אתם מתכוונים לקונסולידציה של Backend קיים של Delphi מאחורי nginx או למודרניזציה לכיוון Delphi REST-API וREST-שרת עם קו תפעולי ברור, סקירה טכנית של שרשרת הפרוקסי ודרך הערכת הכותרות היא לעתים המנוף המהיר ביותר. צרו קשר עם Net-Base לקבלת הערכה טכנית קצרה.

בסביבה מקצועית גם Nginx Reverse Proxy וכותרות Forwarded ממלאות תפקיד חשוב כשיש צורך בשילוב נקי של אינטגרציות, זרימות נתונים ופיתוח מתמשך.

לשוחח על פרויקט או מהלך מודרניזציה עם Net-Base.

שתף פוסט

לשתף את הפוסט הזה ישירות

LinkedIn, X, XING, Facebook, WhatsApp ודוא"ל זמינים מיידית. עבור Instagram אנו מכינים קישור וטקסט קצר באופן מיידי.

דוא״ל

אינסטגרם נפתח בכרטיסייה חדשה. הקישור וטקסט קצר מועתקים מראש ללוח.