We all need to do weird things sometimes. One assignment we’ve got was to implement an API that would totally obfuscate all parameters in a Base64 encoded string. This will clearly go against stock standard routing and action mapping that ASP.NET WebAPI comes with out of the box. But that got us thinking about ways we can achieve it nonetheless.
By default
Normally, the router will:
- get the request URI,
- match it against given templates (those
"{controller}/{action}"
things), and - invoke an
{action}
on{controller}
with whatever parameters happen to be passed along
Then we realise
We’re constrained to full .net framework on the project and fancy .net core middleware are not a thing yet. Luckily for us custom Message Handler
is a thing so theoretically we could bootstrap ourselves through that and override IHttpControllerSelector
(and potentially IHttpActionSelector
).
Setup
Writing code directly in global.asax
is an option, but as it calls through to WebApiConfig.Register()
by default:
GlobalConfiguration.Configure(WebApiConfig.Register);
it’s probably a better place for things to do with WebAPI.
App_Start/WebApiConfig.cs
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MessageHandlers.Add(new TestHandler()); // if you define a handler here it will kick in for ALL requests coming into your WebAPI (this does not affect MVC pages though)
config.MapHttpAttributeRoutes();
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config)); // you likely will want to override some more services to ensure your logic is supported, this is one example
// your default routes
config.Routes.MapHttpRoute(name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new {id = RouteParameter.Optional});
//a non-overlapping endpoint to distinguish between requests. you can limit your handler to only kick in to this pipeline
config.Routes.MapHttpRoute(name: "Base64Api", routeTemplate: "apibase64/{query}", defaults: null, constraints: null
//, handler: new TestHandler() { InnerHandler = new HttpControllerDispatcher(config) } // here's another option to define a handler
);
}
}
and then define our handler:
TestHandler.cs
public class TestHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
//suppose we've got a URL like so: http://localhost:60290/api/VmFsdWVzCg==
var b64Encoded = request.RequestUri.AbsolutePath.Remove(0, "/apibase64/".Length);
byte[] data = Convert.FromBase64String(b64Encoded);
string decodedString = Encoding.UTF8.GetString(data); // this will decode to values
request.Headers.Add("controllerToCall", decodedString); // let us say this is the controller we want to invoke
HttpResponseMessage resp = await base.SendAsync(request, cancellationToken);
return resp;
}
}
Depending on what exactly we want handler to do, we might also have to supply a custom ControllerSelector
implementation:
WebApiConfig.cs
// add this line in your Register method
config.Services.Replace(typeof(IHttpControllerSelector), new MyControllerSelector(config));
MyControllerSelector.cs
public class MyControllerSelector : DefaultHttpControllerSelector
{
public MyControllerSelector(HttpConfiguration configuration) : base(configuration)
{
}
public override string GetControllerName(HttpRequestMessage request)
{
//this is pretty minimal implementation that examines a header set from TestHandler and returns correct value
if (request.Headers.TryGetValues("controllerToCall", out var candidates))
return candidates.First();
else
{
return base.GetControllerName(request);
}
}
}
Applying this in real life?
Pretty neat theory. We however couldn’t quite figure out a way to take it to our customers that wouldn’t raise a few questions on whether we’re doing something shady there.