Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I allow URL encoded path segments in Azure

I have an ASP.NET 6 MVC application running in Azure. I have a controller with an action like

[HttpDelete]
[Route("{image_url}")]
public async Task<IActionResult> RemoveImageUrl([FromRoute(Name = "image_url")] String imageUrlString)

I then call it like

api/https%3A%2F%2Frepocreator.zoltu.io%2Fimages%2FZoltu-Logo-Full-Size.png"

This application works fine when self hosting with Kestrel, but as soon as I deploy to Azure I get 500 errors. I have debugged as much as I can and after a lot of Googling and poking it appears that IIS is attempting to helpfully URL Decode the request before forwarding it on to ASP.NET to handle. The problem, of course, is that even if I can convince IIS to accept the request with

<system.webServer>
    <security>
        <requestFiltering allowDoubleEscaping="true" />
    </security>
</system.webServer>
<system.web>
    <httpRuntime requestValidationMode="2.0" requestPathInvalidCharacters="" relaxedUrlToFileSystemMapping="true"/>
    <pages validateRequest="false" />
</system.web>

It still decodes the URL and passes the decoded URL on to ASP.NET which doesn't find a matching route.

What can I do to tell IIS to stop trying to be helpful here and just pass along whatever URL it gets, without doing any sort of pre-validation or rewriting along the way? Note: this is an Azure Web App, so I don't have direct access to IIS settings.

like image 348
Micah Zoltu Avatar asked May 12 '16 06:05

Micah Zoltu


1 Answers

You could just update your route definition so it matches a decoded image url parameter.

As per the documentation, when defining route templates:

You can use the * character as a prefix to a route value name to bind to the rest of the URI. For example, blog/{*slug} would match any URI that started with /blog/ and had any value following it (which would be assigned to the slug route value).

So you could create an action matching the route [Route("{*image_url}")]:

[Route("{*image_url}")]
public IActionResult RemoveImageUrl([FromRoute(Name = "image_url")]String imageUrlString)
{
    return Json(new { ReceivedImage = imageUrlString });
}

The only problem I have seen is that the protocol part is decoded as http:/ with a single /. You have a couple of options:

  • You could manually fix it in the controller. Even better, you could create a model binder and a parameter convention to do that automatically:

    [HttpDelete]
    [Route("{*image_url}")]
    public IActionResult RemoveImageUrl([FullUrlFromEncodedRouteParam(Name = "image_url")] String imageUrlString)            
    {
        return Json(new { ReceivedImage = imageUrlString });
    }
    
    public class FullUrlFromUrlEncodedPathSegmentModelBinder : IModelBinder
    {
        //Matches a url that starts with protocol string and is followed by exactly ":/" instead of "://"
        private static Regex incorrectProtocolRegex = new Regex(@"^([a-z][\w-]+:)\/{1}(?!\/)");
    
        //A url path included as a url encoded path segment like http://localhost:39216/image2/https%3A%2F%2Frepocreator.zoltu.io%2Fimages%2FZoltu-Logo-Web.png
        //will be decoded by IIS as https:/repocreator.zoltu.io/images/Zoltu-Logo-Web.png, note the single '/' after the protocol
        //This model binder will fix it replacing "http:/" with "http://"
        public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext.ValueProvider.GetValue(bindingContext.ModelName) == null)
                return Task.FromResult(ModelBindingResult.NoResult);                            
    
            var val = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue as string;
            var fixedVal = incorrectProtocolRegex.Replace(val, @"$1//");                                                
            return Task.FromResult(ModelBindingResult.Success(bindingContext.ModelName, fixedVal));                        
        }
    }
    
    public class FullUrlFromEncodedRouteParamAttribute : Attribute, IParameterModelConvention
    {
        public string Name { get; set; }
    
        public void Apply(ParameterModel parameter)
        {
            parameter.BindingInfo = parameter.BindingInfo ?? new BindingInfo();
            parameter.BindingInfo.BinderModelName = Name;
            parameter.BindingInfo.BindingSource = BindingSource.Path;
            parameter.BindingInfo.BinderType = typeof(FullUrlFromUrlEncodedPathSegmentModelBinder);         
        }
    }
    
  • A better approach might be updating your api so you don't even use the protocol part in the image key. That will let you add the proper protocol to the full image url when you need to render it, depending on whether it needs http or https (Even the host could be omitted from the urls). You wouldn't even need to worry about url encoding the image path on your client side, you could just invoke it like http://localhost:39216/api/repocreator.zoltu.io/images/Zoltu-Logo-Full-Size.png.

IMHO I would prefer the second approach. If you really need the full url encoded in the route, then at least you have a way of implementing it in a clean way outside the controller.

Note: If you want to keep the protocol part in the image url, it looks like the static files middleware does not like them so it has to be added after MVC in Startup.configure, otherwise it will throw errors.

like image 81
Daniel J.G. Avatar answered Sep 30 '22 17:09

Daniel J.G.