פרוקסי הפוך (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.
# (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.
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. שלוש בדיקות דיבאג שמצדיקות את עצמן בשגרה:
- Header-Dump (ממוקד): רשמו ללוג עבור 4xx/5xx בנוסף את Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent ו-Request-URI. אבל רק במקרה של שגיאות – אחרת הלוג יהפוך ליקר ולא מאורגן.
- בדיקת Base-URL: כאשר Redirects או Callback-URLs נכשלים, רשמו ללוג את ForwardedInfo.BaseUrl. שגיאות רבות נראות מיד („http://127.0.0.1“ במקום „https://api…“).
- קורלציית 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 ממלאות תפקיד חשוב כשיש צורך בשילוב נקי של אינטגרציות, זרימות נתונים ופיתוח מתמשך.