domaindrivendev/Swashbuckle.WebApi

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;
        }
    }
Tazer commented

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;
    }
}
Tazer commented

@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();
}