Documentation : behind a reverse proxy
christopheblin opened this issue ยท 8 comments
Here is a code snippet you could include in the documentation reltive to RootUrl https://github.com/domaindrivendev/Swashbuckle#rooturl
I think I am facing a common problem (2 chained reverse proxies) and many users can benefit from this...
public static string ComputeHostAsSeenByOriginalClient(HttpRequestMessage req)
{
if (req.Headers.Contains("X-Forwarded-Host"))
{
//we are behind a reverse proxy, use the host that was used by the client
var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
//when multiple apache httpd are chained, each proxy append to the header
//with a comma (see //https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers).
//so we need to take only the first host because it is the host that was
//requested by the original client.
//note that other reverse proxies may behave differently but
//we are not taking care of them...
var firstForwardedHost = xForwardedHost.Split(',')[0];
//now that we have the host, we also need to determine the protocol used by the
//original client.
//if present, we are using the de facto standard header X-Forwarded-Proto, assuming
//that only the first reverse proxy in the chain added it.
//otherwise, we fallback to http
//note that this is extremely brittle, either because the first proxy
//can "forget" to set the header or because another proxy can rewrite it...
var xForwardedProto = req.Headers.Contains("X-Forwarded-Proto")
? req.Headers.GetValues("X-Forwarded-Proto").First()
: "http";
return xForwardedProto + "://" + firstForwardedHost;
}
else
{
//no reverse proxy mean we can directly use the RequestUri
return req.RequestUri.Scheme + "://" + req.RequestUri.Authority;
}
}
just an addition to this,
I got into problems with X-Forwarded-Proto
, it was also having "https , https". So needed to do the split for Proto also.
@Tazer do you mean something like this ?
public static string ComputeHostAsSeenByOriginalClient(HttpRequestMessage req)
{
if (req.Headers.Contains("X-Forwarded-Host"))
{
//we are behind a reverse proxy, use the host that was used by the client
var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
//when multiple apache httpd are chained, each proxy append to the header
//with a comma (see //https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers).
//so we need to take only the first host because it is the host that was
//requested by the original client.
//note that other reverse proxies may behave differently but
//we are not taking care of them...
var firstForwardedHost = xForwardedHost.Split(',')[0];
//now that we have the host, we also need to determine the protocol used by the
//original client.
//if present, we are using the de facto standard header X-Forwarded-Proto
//otherwise, we fallback to http
//note that this is extremely brittle, either because the first proxy
//can "forget" to set the header or because another proxy can rewrite it...
var xForwardedProto = req.Headers.Contains("X-Forwarded-Proto")
? req.Headers.GetValues("X-Forwarded-Proto").First()
: "http";
if (xForwardedProto.IndexOf(",") != -1)
{
//when multiple apache, X-Forwarded-Proto is also multiple ...
xForwardedProto = = xForwardedProto.Split(',')[0];
}
return xForwardedProto + "://" + firstForwardedHost;
}
else
{
//no reverse proxy mean we can directly use the RequestUri
return req.RequestUri.Scheme + "://" + req.RequestUri.Authority;
}
}
@christopheblin I would even suggestion splitting up host and proto , cause there can be cases where you just have the proto header :) something like this ( I havn't updated your comments just moved the code around abit)
public static string ComputeHostAsSeenByOriginalClient(HttpRequestMessage req)
{
var authority = req.RequestUri.Authority;
var scheme = req.RequestUri.Scheme
if (req.Headers.Contains("X-Forwarded-Host"))
{
//we are behind a reverse proxy, use the host that was used by the client
var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
//when multiple apache httpd are chained, each proxy append to the header
//with a comma (see //https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers).
//so we need to take only the first host because it is the host that was
//requested by the original client.
//note that other reverse proxies may behave differently but
//we are not taking care of them...
var firstForwardedHost = xForwardedHost.Split(',')[0];
Authority = firstForwardedHost;
}
if (req.Headers.Contains("X-Forwarded-Proto"))
{
//now that we have the host, we also need to determine the protocol used by the
//original client.
//if present, we are using the de facto standard header X-Forwarded-Proto
//otherwise, we fallback to http
//note that this is extremely brittle, either because the first proxy
//can "forget" to set the header or because another proxy can rewrite it...
var xForwardedProto = req.Headers.GetValues("X-Forwarded-Proto").First()
if (xForwardedProto.IndexOf(",") != -1)
{
//when multiple apache, X-Forwarded-Proto is also multiple ...
xForwardedProto = = xForwardedProto.Split(',')[0];
}
scheme = xForwardedProto;
}
//no reverse proxy mean we can directly use the RequestUri
return scheme + "://" + authority;
}
Added to readme
Hi,
For anyone looking for a solution, there are a few typos in the code above, and it also doesn't account for the possibility that the original request is made on a different port than expected.
So, here is a full extension class:
public static class HttpRequestMessageExtensions
{
public static string ComputeHostAsSeenByOriginalClient(this HttpRequestMessage req)
{
var authority = req.RequestUri.Authority;
var scheme = req.RequestUri.Scheme;
var port = req.RequestUri.Port;
if (req.Headers.Contains("X-Forwarded-Host"))
{
// we are behind a reverse proxy, use the host that was used by the client
var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
/*
When multiple apache httpd are chained, each proxy append to the header
with a comma (see //https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers).
so we need to take only the first host because it is the host that was
requested by the original client.
note that other reverse proxies may behave differently but
we are not taking care of them...
*/
var firstForwardedHost = xForwardedHost.Split(',')[0];
authority = firstForwardedHost;
}
if (req.Headers.Contains("X-Forwarded-Proto"))
{
/*
now that we have the host, we also need to determine the protocol used by the
original client.
if present, we are using the de facto standard header X-Forwarded-Proto
otherwise, we fallback to http
note that this is extremely brittle, either because the first proxy
can "forget" to set the header or because another proxy can rewrite it...
*/
var xForwardedProto = req.Headers.GetValues("X-Forwarded-Proto").First();
if (xForwardedProto.IndexOf(",") != -1)
{
// >hen multiple apache, X-Forwarded-Proto is also multiple ...
xForwardedProto = xForwardedProto.Split(',')[0];
}
scheme = xForwardedProto;
}
if (req.Headers.Contains("X-Forwarded-Port"))
{
var xForwardedPort = req.Headers.GetValues("X-Forwarded-Port").First();
if (xForwardedPort.IndexOf(",") != -1)
{
// When multiple apache, X-Forwarded-Proto is also multiple ...
xForwardedPort = xForwardedPort.Split(',')[0];
}
int.TryParse(xForwardedPort, out port);
}
// If we have standard scheme + port, leave out the port in the resulting Url.
if (("http".Equals(scheme, StringComparison.InvariantCultureIgnoreCase) && port == 80)
|| ("https".Equals(scheme, StringComparison.InvariantCultureIgnoreCase) && port == 443))
{
return scheme + "://" + authority;
}
return scheme + "://" + authority + ":" + port.ToString();
}
}
And here is how to use it:
GlobalConfiguration.Configuration
.EnableSwagger(c =>
{
c.RootUrl(req => req.ComputeHostAsSeenByOriginalClient());
//...
});
@ottomatic RequestUri.Authority already contains port number. ะ non-correct address will be generated because of it, like http://localhost:19292:19292/swagger/docs/v1
@nsdev0 Ah, OK. That was not the case in my setting. In that case the code needs to be modified even further. If there is an X-Forwarded-Host header, then the "authority" variable will only contain a hostname. If not, then it might contain a port which may need to be parsed out / captured.
Hi, @ottomatic thanks for the code snippet! It didn't really work with my configuration, however (ASP.NET MVC app on IIS behind a Caddy reverse proxy). Basically the changes were to use the host portion of the URI instead of authority, and specifying default ports if X-Forwarded-Port
is not defined. Here's the modified code:
static string ComputeHostAsSeenByOriginalClient(HttpRequestMessage req)
{
var host = req.RequestUri.Host;
var scheme = req.RequestUri.Scheme;
var port = req.RequestUri.Port;
if (req.Headers.Contains("X-Forwarded-Host"))
{
// we are behind a reverse proxy, use the host that was used by the client
var xForwardedHost = req.Headers.GetValues("X-Forwarded-Host").First();
/*
When multiple apache httpd are chained, each proxy append to the header
with a comma (see //https://httpd.apache.org/docs/2.4/mod/mod_proxy.html#x-headers).
so we need to take only the first host because it is the host that was
requested by the original client.
note that other reverse proxies may behave differently but
we are not taking care of them...
*/
var firstForwardedHost = xForwardedHost.Split(',')[0];
host = firstForwardedHost;
}
if (req.Headers.Contains("X-Forwarded-Proto"))
{
/*
now that we have the host, we also need to determine the protocol used by the
original client.
if present, we are using the de facto standard header X-Forwarded-Proto
otherwise, we fallback to http
note that this is extremely brittle, either because the first proxy
can "forget" to set the header or because another proxy can rewrite it...
*/
var xForwardedProto = req.Headers.GetValues("X-Forwarded-Proto").First();
if (xForwardedProto.IndexOf(",") != -1)
{
// >hen multiple apache, X-Forwarded-Proto is also multiple ...
xForwardedProto = xForwardedProto.Split(',')[0];
}
scheme = xForwardedProto;
}
if (req.Headers.Contains("X-Forwarded-Port"))
{
var xForwardedPort = req.Headers.GetValues("X-Forwarded-Port").First();
if (xForwardedPort.IndexOf(",") != -1)
{
// When multiple apache, X-Forwarded-Proto is also multiple ...
xForwardedPort = xForwardedPort.Split(',')[0];
}
int.TryParse(xForwardedPort, out port);
}
else
{
// If port is missing, set port to defaults
if (("http".Equals(scheme, StringComparison.InvariantCultureIgnoreCase))) port = 80;
if (("https".Equals(scheme, StringComparison.InvariantCultureIgnoreCase))) port = 443;
}
// If we have standard scheme + port, leave out the port in the resulting Url.
if (("http".Equals(scheme, StringComparison.InvariantCultureIgnoreCase) && port == 80)
|| ("https".Equals(scheme, StringComparison.InvariantCultureIgnoreCase) && port == 443))
{
return scheme + "://" + host;
}
return scheme + "://" + host + ":" + port.ToString();
}