How do I allow URL encoded path segments in Azure - 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.

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.

Related

Change ASP.NET_sessionid cookie path using SessionIDManager

I'm renaming the cookie and made it to target to a different path, rather than targeting to a default path "/".
Below is the web.config settings:
<sessionState sessionIDManagerType="MyNamespace.MySessionIDManager" cookieName="AppCookie"/>
Below is the backend class used to create the cookie:
public class MySessionIDManager : SessionIDManager, ISessionIDManager
{
void ISessionIDManager.SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded)
{
base.SaveSessionID(context, id, out redirected, out cookieAdded);
if (cookieAdded)
{
var name = "AppCookie";
var cookie = context.Response.Cookies[name];
cookie.Path = "/Forms";
}
}
}
This fix is working fine for me locally. The cookie is successfully pointing to the given path i.e "/Forms".
But when I deploy my application to IIS, I'm not able to login to the application.
It is not throwing any error, but not allowing me to login to the web application.
If I use to below web.config settings, it is working fine.
<sessionState mode="InProc" timeout="30" cookieName="AppCookie" />
Please let me know what issue it is causing in the IIS.
Any input is much appreciated.
Thank you all in advance.
Thanks and Regards,
Dada.
please try to change the URL from http://www.website/login/ to http://www.website/forms/login/ and then you can see the cookie will send in the request header, and you will auto login.
This is caused by you change the cookie URL to /forms, it means that only the http URL has /forms string will send the session cookie which has created with the aspnet_sessionID.
If you don't change the cookie URL, default path '/' means the cookie can be shared.
You can see this link: https://learn.microsoft.com/en-us/dotnet/api/system.web.httpcookie.path?view=netframework-4.8
I fixed this issue with the below piece of code;
public class CookieManager : SessionIDManager, ISessionIDManager
{
void ISessionIDManager.SaveSessionID(HttpContext context, string id, out bool redirected, out bool cookieAdded)
{
base.SaveSessionID(context, id, out redirected, out cookieAdded);
if (cookieAdded)
{
SessionStateSection sessionStateSection = (System.Web.Configuration.SessionStateSection)ConfigurationManager.GetSection("system.web/sessionState");
var cookie = context.Response.Cookies[sessionStateSection.CookieName];
cookie.Path = context.Request.ApplicationPath;
}
}
}
And update the web.config as follows;
<sessionState sessionIDManagerType="ANJU.Reports.WebUI.Library.CookieManager" timeout="30" cookieName="CookieName"/>
Now when I host my application on the IIS, it'll fetch the directory where I have my build.
All my cookies will point to the root directory of the build.

Swagger authentication in Azure App Service

In my Azure Mobile .NET backend I want to use Azure Mobile .NET Server Swagger . I'm looking for fast way to hide swagger UI from public access ? Is there any way to provide access only for selected users ?
First a disclaimer: Even if you protect your Swagger UI from public consumption, you are not protecting your APIs from public consumption. You have to assume that everyone knows all of your routes and have the appropriate security in place to protect any requests that may come in.
That being said, there's still not a simple way to do this. Swashbuckle (the piece that adds Swagger to Web API) adds a custom HttpMessageHandler to the /swagger/ui route (as seen here). If you look at the Web API pipeline, you can see that if you specify a custom handler, you can bypass all of the Controller selection, Auth filters, etc. This is what happens here.
Some solutions:
Use an app setting to conditionally call ConfigureSwagger(config) in debug modes only. This would prevent all /swagger routes from making it into production. Or you could use a staging slot and only add it there.
You can wrap the SwaggerUiHandler with something like this Basic Auth MessageHandler. This would prompt the user for basic creds if they went to the /swagger/ui route. See below for my modified version of this code.
Maybe with a little more thought we can come up with a better solution -- I see a couple of issues (here and here) in the Swashbuckle repo that indicate you're not the first one to hit this.
Modified BasicAuthHandler (from here):
Warning: minimally tested (and be sure to change how you verify user/pass)
public class BasicAuthMessageHandler : DelegatingHandler
{
private const string BasicAuthResponseHeader = "WWW-Authenticate";
private const string BasicAuthResponseHeaderValue = "Basic";
public BasicAuthMessageHandler(HttpMessageHandler innerHandler)
{
this.InnerHandler = innerHandler;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
AuthenticationHeaderValue authValue = request.Headers.Authorization;
HttpResponseMessage unauthorizedResponse = request.CreateUnauthorizedResponse();
if (authValue != null && !string.IsNullOrWhiteSpace(authValue.Parameter))
{
Credentials parsedCredentials = ParseAuthorizationHeader(authValue.Parameter);
if (parsedCredentials != null)
{
// TODO: Check that the user/pass are valid
if (parsedCredentials.Username == "user" &&
parsedCredentials.Password == "pass")
{
// If match, pass along to the inner handler
return base.SendAsync(request, cancellationToken);
}
}
}
else
{
// Prompt for creds
unauthorizedResponse.Headers.Add(BasicAuthResponseHeader, BasicAuthResponseHeaderValue);
}
return Task.FromResult(unauthorizedResponse);
}
private Credentials ParseAuthorizationHeader(string authHeader)
{
string[] credentials = Encoding.ASCII.GetString(Convert
.FromBase64String(authHeader))
.Split(
new[] { ':' });
if (credentials.Length != 2 || string.IsNullOrEmpty(credentials[0])
|| string.IsNullOrEmpty(credentials[1])) return null;
return new Credentials()
{
Username = credentials[0],
Password = credentials[1],
};
}
}
Registering with Swagger route
// Do this after calling ConfigureSwagger
ConfigureSwagger(config);
// Remove the swagger_ui route and re-add it with the wrapped handler.
var route = config.Routes["swagger_ui"];
config.Routes.Remove("swagger_ui");
config.Routes.MapHttpRoute("swagger_ui", route.RouteTemplate, route.Defaults, route.Constraints, new BasicAuthMessageHandler(route.Handler));

SharePoint OPTIONS preflight request

I have a Service Stack service hosted within a SharePoint 2013 site. When attempting to make a cross domain request to one of the services, a preflight OPTIONS request is made, as expected.
The problem is that the response always comes back as 401 Unauthorized, due to the fact that authentication info is not sent across with the request. I have tried putting some request filters via servicestack to try and bypass the authentication, but these filters are not firing - it seems like something prior to service stack is sending the response.
Is there any way of specifying that OPTIONS requests to the sharepoint site do not need to be authenticated? If not, does anyone have a workaround for this scenario?
I tried 'fooling' the browser in to not sending a preflight request by changing the data type from application/json to text/plain in my ajax request, but then the data I send is not being deserialised in to the correct RequestDTO for the service calls on the server side.
Any help would be appreciated.
We ended up having to write our own HTTP module in order to support the options request. We basically add a key specifying which domains to allow the CORS requests from (can support more than one) and then have this HTTP module registered:
public class ECSPreFlightModule : IHttpModule
{
/// <summary>
/// You will need to configure this module in the Web.config file of your
/// web and register it with IIS before being able to use it. For more information
/// see the following link: http://go.microsoft.com/?linkid=8101007
/// </summary>
public void Dispose()
{
//clean-up code here.
}
private const string OptionsHeader = "OPTIONS";
private const string OriginHeader = "ORIGIN";
private const string AccessAllowOrigin = "Access-Control-Allow-Origin";
private string AllowedOriginUrlsArray
{
get
{
return GetWebConfigValue("CORSAllowedOriginUrls");
}
}
private string GetWebConfigValue(string key)
{
var configuration = WebConfigurationManager.OpenWebConfiguration("~");
object o = configuration.GetSection("system.web/httpModules");
var section = o as HttpModulesSection;
return section.CurrentConfiguration.AppSettings.Settings[key].Value;
}
public void Init(HttpApplication context)
{
context.PreSendRequestHeaders += (sender, e) =>
{
var splitUrls = AllowedOriginUrlsArray.Split('|');
var response = context.Response;
var originHeader = context.Request.Headers.Get(OriginHeader);
if (!String.IsNullOrEmpty(originHeader) && splitUrls.Length > 0)
{
foreach (var url in splitUrls)
{
var urlLower = url.ToLower();
var originHeaderLower = originHeader.ToLower();
// if the method being requested is an OPTIONS request and the url is the url specified in the web.config then return an OK response.
if (context.Request.HttpMethod.ToLowerInvariant() == OptionsHeader.ToLowerInvariant() &&
(urlLower == originHeaderLower))
{
response.StatusCode = (int)HttpStatusCode.OK;
}
// If the originating header url is equal to the url specified in the web.config then grant the access control
if (originHeaderLower == urlLower)
{
response.AddHeader(AccessAllowOrigin, originHeader);
break;
}
}
}
};
}
}
}
The above module was wrapped in a sharepoint feature that, when activated, made the appropriate changes to the web.config, namely registering the module and adding the following keys:
<add name='Access-Control-Allow-Credentials' value='true' />
<add name='Access-Control-Allow-Headers' value='Authorization, X-Requested-With, Content-Type, Origin, Accept, X-RequestDigest' />
<add name='Access-Control-Allow-Methods' value='GET,POST,OPTIONS,PUT, DELETE' />

Web API attribute routing HTTP 404

I can't make attribute routing to work in Web API 2. I did not start with Web API template project, I think it was Empty project. Convention based routing works, but when I add Route attribute to controller actions, I get 404 error. I find a lot of posts regarding this, but none of them helped me. It could be related to the project template, something I have to do manually since I started with Empty project. I do call MapHttpAttributeRoutes method in WebApiConfig and the class iheriting ApiController is public. What else do I have to do in WebApiConfig class and Application_Start method?
Thanks in advance.
Here is my WebApiConfig file:
public static void Register(HttpConfiguration config)
{
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{action}/{oId}/{oTypeCode}/{oTypeName}/{email}",
defaults: new { controller = "Xrm", email = RouteParameter.Optional, oId = RouteParameter.Optional, oTypeCode = RouteParameter.Optional, oTypeName = RouteParameter.Optional}
);
}
And Application_Start method:
protected void Application_Start()
{
System.Web.Mvc.AreaRegistration.RegisterAllAreas();
GlobalConfiguration.Configure(WebApiConfig.Register);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
RouteConfig.RegisterRoutes(RouteTable.Routes);
log4net.Config.XmlConfigurator.Configure();
}
Of course, my fault. I was trying to pass an email address as a path parameter. Sending it as query string parameter works. I am not sure how would it look like to send as part of the path, but it works for me this way.

How do I call my own service from a request/response filter in ServiceStack?

My problem is...
...I have a DTO like this
[Route("/route/to/dto/{Id}", "GET")]
public class Foo : IReturn<Bar>
{
public string Id { get; set; }
}
and need to call the service that implements the method with this signature
public Bar Get(Foo)
from a request and/or response filter. I don't know what class implements it (don't want to need to know). What I need is something like the LocalServiceClient class in the example below:
var client = new LocalServiceClient();
Bar bar = client.Get(new Foo());
Does this LocalServiceClient thing exists? JsonServiceClient has a pretty similar interface, but using it would be inneficient (I need to call my own service, I shouldn't need an extra round-trip, even to localhost, just to do this).
I'm aware of ResolveService method from Service class, but it requires me to have a service instance and to know what class will handle the request.
I think this LocalServiceClient is possible because I have all the data that a remote client (e.g. JsonServiceClient) needs to call the service - request DTO, route, verb - but couldn't find how to do it. Actually, it should be easier to implement than JsonServiceClient.
JsonServiceClient would do it, but there must be a better way, using the same request context.
What I want to do (skip this if you're not curious about why I'm doing this)
Actually, my DTOs are like this:
[EmbedRequestedLinks]
[Route("/route/to/dto/{Id}", "GET")]
public class MyResponseDto
{
public string Id { get; set; }
public EmbeddableLink<AResponseDto> RelatedResource { get; set; }
public EmbeddableLink<AnotherResponteDto> AnotherRelatedResource { get; set; }
}
EmbedRequestedLinksAttribute is a request/response filter. This filter checks if there is a query argument named "embed" in the request. If so, the filter need to "embed" the comma-separated related resources referenced by the argument into the response to this request. EmbeddableLink<T> instances can be obtained by using extension methods like these:
1) public static EmbeddableLink<T> ToEmbeddableLink<T>(this IReturn<T> requestDto)
2) public static EmbeddableLink<T> ToEmbeddableLink<T>(this T resource)
Assume a client places this request:
GET /route/to/dto/123456?embed=relatedResource HTTP/1.1
The service that will handle this request will return an instance of MyResponseDto with EmbeddableLinks created using signature (1). Then my response filter will see the embed query argument and will call the Get method of the appropriate service, replacing the RelatedResource with another instance of EmbeddableLink, this time created using extension method (2):
var client = new LocalServiceClient();
response.RelatedResource = client.Get(response.RelatedResource.RequestDto)
.ToEmbeddableLink();
The serialization routine of EmbeddableLink takes care of the rest.
In case an embeddable link is not included in the embed list the serialization routine will call the extension method ToUrl (provided by ServiceStack), that takes a verb and converts a request DTO into a URL. In this example the client will get this response:
{
"id": "9asc09dcd80a98",
"relatedResource": { "id": "ioijo0909801", ... },
"anotherRelatedResource":
{
"$link": { "href": "/route/to/another/dto/1sdf89879s" }
}
}
I know the creators of ServiceStack think that polymorphic request/responses are bad things but this case seems OK to me because I'm not creating services, instead I'm extending the framework to help me create services the way I (and possibly other users of ServiceStack) need. I'm also creating other hypermedia extensions to ServiceStack. (I hope my boss allow me to publish these extensions on github)
If you really want to do this then look the source code for ServiceStack. Look at the ServiceManager and ServiceController. These classes are responsible for registering and resolving services. You might even be able to use reflection to create services on the fly with the static EndpointHost.Metadata like so:
var operation = EndpointHost.Metadata.Operations
.FirstOrDefault(x => x.RequestType == typeof(Person));
if (operation != null)
{
var svc = Activator.CreateInstance(operation.ServiceType);
var method = operation.ServiceType.GetMethod("Get");
var response = method.Invoke(svc, new[] { new Person() });
}
This kinda works but you will get NULL exceptions if there is other code calling
var httpRequest = RequestContext.Get<IHttpRequest>();
But I would not suggest this.
Instead if you create your own Business Service classes that do all the CRUD operations (POST/PUT/GET ect). Then make the ServiceStack Services thin wrappers over them. Now you can call your own services whenever you want without worrying about the HTTP Request and ServiceStack. Only use the ServiceStack Service when you are dealing with HTTP requests
You can call the static AppHostBase.Resolve() method as demonstrated here, calling a SeviceStack Service from an MVC controller:
var helloService = AppHostBase.Resolve<HelloService>();
helloService.RequestContext = System.Web.HttpContext.Current.ToRequestContext();
var response = (HelloResponse)helloService.Any(new HelloRequest { Name = User.Identity.Name });
However, I would take #kampsj's approach of making your ServiceStack services a thin wrapper around your application service classes and only deal with HTTP/Session specific stuff in the ServiceStack service.

Resources