Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation : behind a reverse proxy #705

Closed
christopheblin opened this issue Apr 5, 2016 · 8 comments
Closed

Documentation : behind a reverse proxy #705

christopheblin opened this issue Apr 5, 2016 · 8 comments
Milestone

Comments

@christopheblin
Copy link

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;
        }
    }
@domaindrivendev domaindrivendev added this to the v5.4.0 milestone Apr 14, 2016
@Tazer
Copy link

Tazer commented Aug 30, 2016

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.

@christopheblin
Copy link
Author

@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
Copy link

Tazer commented Sep 2, 2016

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

@domaindrivendev
Copy link
Owner

Added to readme

@ottomatic
Copy link

ottomatic commented Apr 26, 2019

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

@nsdev0
Copy link

nsdev0 commented May 23, 2019

@ottomatic RequestUri.Authority already contains port number. А non-correct address will be generated because of it, like http://localhost:19292:19292/swagger/docs/v1

@ottomatic
Copy link

@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.

@daffodilistic
Copy link

daffodilistic commented Oct 13, 2022

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants