Contents

Using YARP to strangle an ASP.Net Framework MVC web application

Work with any system long enough and something that was once upto date technology goes out of date. Sometimes you’re lucky and are able to modernize them before they go out of date, other times you’ll be left supporting out of date technology.

Strangler pattern

This is a popular pattern

Getting started with YARP

1
2
dotnet new ...
dotnet add package Yarp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
"ReverseProxy": {
  "Routes": {
    "route1": {
      "ClusterId": "cluster1",
      "Match": {
        "Path": "{**catch-all}"
      }
    }
  },
  "Clusters": {
    "cluster1": {
      "Destinations": {
        "destination1": {
          "Address": "https://localhost:44397/"
        }
      }
    }
  }
}

Program.cs

1
2
3
4
5
6
7
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));

app.UseEndpoints(endpoints =>
{
    endpoints.MapReverseProxy();
});

This will be the pass through proxy.

At this point we can spin up both web apps, the legacy and YARP proxy app and we’ll see the login page load

image

However, you’ll notice something strange has happened to the URL - we’ve been redirected to the /Account/Login page which is what we were expecting, however the port that we’re running on has changed as well to https://localhost:44397 which is the port that the legacy web app is running on, however, we only want users to access the legacy site via the proxy.

X-Forwarded headers

appsettings.json

1
2
3
4
,
"Transforms": [
    { "X-Forwarded": "Set" }
]

which then becomes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
"ReverseProxy": {
  "Routes": {
    "route1": {
      "ClusterId": "cluster1",
      "Match": {
        "Path": "{**catch-all}"
      },
      "Transforms": [
        { "X-Forwarded": "Set" }
      ]
    }
  },
  "Clusters": {
    "cluster1": {
      "Destinations": {
        "destination1": {
          "Address": "https://localhost:44397/"
        }
      }
    }
  }
}

Changes for the legacy app

We need some middleware, however none exists for ASP.Net MVC Framework :-( stackoverflow to the rescue

I modified this slightly

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public static class UseForwardedHeadersMiddleware
{
    private const string ForwardedHeadersAdded = "ForwardedHeadersAdded";

    public static IAppBuilder UseForwardedHeaders(this IAppBuilder app)
    {
        if (app == null)
        {
            throw new ArgumentNullException(nameof(app));
        }

        if (!app.Properties.ContainsKey(ForwardedHeadersAdded))
        {
            app.Properties[ForwardedHeadersAdded] = true;

            app.Use(async (context, next) =>
            {
                var request = context.Request;

                var httpContext = context.Get<HttpContextBase>(typeof(HttpContextBase).FullName);
                var serverVars = httpContext.Request.ServerVariables;
                    
                if (request.Scheme != Uri.UriSchemeHttps && String.Equals(request.Headers["X-Forwarded-Proto"], Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase))
                {
                    serverVars["HTTPS"] = "on";
                    serverVars["SERVER_PORT_SECURE"] = "1";
                    serverVars["SERVER_PORT"] = "443";
                }
                serverVars["HTTP_HOST"] = request.Headers.ContainsKey("X-Forwarded-Host") ? request.Headers["X-Forwarded-Host"] : serverVars["HTTP_HOST"];

                await next.Invoke().ConfigureAwait(false);
            });
        }

        return app;
    }
}

🍪 I use Disqus for comments

Because Disqus requires cookies this site doesn't automatically load comments.

I don't mind about cookies - Show me the comments from now on (and set a cookie to remember my preference)