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

Hosting behind reverse proxy with subdirectory #2302

Closed
ygoe opened this issue Dec 9, 2017 · 24 comments
Closed

Hosting behind reverse proxy with subdirectory #2302

ygoe opened this issue Dec 9, 2017 · 24 comments
Labels
area-hosting Includes Hosting area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions enhancement This issue represents an ask for new feature or an enhancement to an existing one
Milestone

Comments

@ygoe
Copy link

ygoe commented Dec 9, 2017

Sorry to use this platform but I need to find another issue and can't find it because GitHub won't let me find issues I'm subscribed to.

I'm hosting an ASP.NET Core application on Linux with Apache web server behind a reverse proxy as recommended. I can setup my environment to only proxy a subdirectory of the domain to the app, but the app doesn't know that because the proxy translates the public subdirectory to the internal root directory. Now there was some discussion going on here about whether to let the hosting configure this (just like the port number of Kestrel web server through an environment variable) and how an application can handle it through code until then.

I need to find that code. I'm sure it was mentioned here, but neither Google nor GitHub won't find it for me. And of course I'm not archiving GitHub notification e-mails that long. Maybe I should do that. Can you please point me to the issue for that?

@Tratcher
Copy link
Member

Is the request forwarded with the url intact and you want to split it? UsePathBase works for that.

@ygoe
Copy link
Author

ygoe commented Dec 10, 2017

Now I guess somebody needs to tell Kestrel that whatever it sees as / is in fact /app and it should generate URLs to other places accordingly. I'd like this to be configurable from the outside of the ASP.NET Core app because the path is determined by the hosting environment and not by the application itself. This could be through an environment variable. But AFAIR that isn't supported yet and the app has to call some method to add this behaviour.

Adding this code doesn't do anything for me:

app.UsePathBase(new PathString("/subdir"));

@davidfowl
Copy link
Member

Did you put it first in the pipeline?

@ygoe
Copy link
Author

ygoe commented Dec 10, 2017

Yes, the first instruction in the Startup.Configure method, before UseForwardedHeaders and the env.IsDevelopment() from the MVC template.

@Tratcher
Copy link
Member

You need the reverse of UsePathBase, you're not trying to trim the path, you just need to set PathBase.

app.Use((context, next) => {
context.Request.PathBase = "/app";
next();
});

@ygoe
Copy link
Author

ygoe commented Dec 10, 2017

@Tratcher Did you mean one of these alternatives?

With missing return:

app.Use((context, next) => {
	context.Request.PathBase = "/subdir";
	return next();
});

And with async/await like in the documentation samples:

app.Use(async (context, next) => {
	context.Request.PathBase = "/subdir";
	return await next();
});

(Edit: No, these are wrong, too. Please correct.)

@Tratcher
Copy link
Member

Right, coding on my phone...

Note that PathBase is the url root used when generating links, etc.. It is not a file path. That's why I set it to "/app".

What's not working for you?

@ygoe
Copy link
Author

ygoe commented Dec 10, 2017

I've tried this and it works! This is the code I need in apps hosted on my server. And I'd like it to be default for the ASP.NET Core framework.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	string basePath = Environment.GetEnvironmentVariable("ASPNETCORE_BASEPATH");
	if (!string.IsNullOrEmpty(basePath))
	{
		app.Use(async (context, next) =>
		{
			context.Request.PathBase = basePath;
			await next.Invoke();
		});
	}
	app.UseForwardedHeaders(new ForwardedHeadersOptions
	{
		ForwardedHeaders = ForwardedHeaders.All
	});

	// existing code...
}

My starter script sets these environment variables before running the dotnet command: (This is a simplified sample, the actual script is written in PHP.)

export ASPNETCORE_URLS=http://127.0.0.1:$port
export ASPNETCORE_BASEPATH=$basePath
dotnet $applicationDllFile

And these directives are used for the Apache proxy configuration: (Again, pseudo-variables.)

ProxyPass "/$path" http://127.0.0.1:$port/ retry=15
ProxyPassReverse "/$path" http://127.0.0.1:$port/

So nobody found the original issue on this topic and I haven't received any notifications from it recently, I guess it got lost. So this is now my official request to build this base path support into ASP.NET Core. This allows hosters to map an application into a subdirectory in the URL without modifications to the application code. The environment variable ASPNETCORE_URLS already exists and serves a similar purpose, to make the application server listen on a specific port, again determined by the hoster. The new environment variable ASPNETCORE_BASEPATH would extend this to the URL path.

Maybe the older issue mentioned something like supporting ASPNETCORE_URLS=http://127.0.0.1:$port/$basePath/. But IIRC that wasn't allowed back then. I guess I'd duplicate the base path in the proxy configuration then. I'm not sure if that would serve the same purpose though because the application would probably have to handle the subdirectory more explicitly.

@ygoe
Copy link
Author

ygoe commented Dec 10, 2017

@Tratcher My last reply took too long to write, so here's my answer to your reply.

What's not working for you?

It doesn't compile. The => is underlined because not all code paths return a value. Not sure why that error is reported. See my last reply for the correction I decided to make.

@ygoe
Copy link
Author

ygoe commented Dec 10, 2017

BTW, what happens when you add an ASP.NET (Core) application in a virtual subdirectory in IIS? Will that work without modifications to the app itself? Does the IIS integration handle what I have described here?

@Tratcher
Copy link
Member

The issue you're looking for is https://github.com/aspnet/Hosting/issues/1120. The main difference is that we expect the forwarded url to contain the path base.

@shanselman
Copy link
Contributor

Might be worth doing this (reopen) so that we don't need to do this https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.2#deal-with-path-base-and-proxies-that-change-the-request-path given the existence of Azure Front Door? @rynowak

@Tratcher Tratcher reopened this Jul 2, 2019
@Tratcher Tratcher added the enhancement This issue represents an ask for new feature or an enhancement to an existing one label Jul 2, 2019
@Tratcher Tratcher added this to the Backlog milestone Jul 2, 2019
@Tratcher
Copy link
Member

Tratcher commented Jul 2, 2019

Re-opening but keeping in backlog. We'll re-triage this for a future release.

Triage note: This is related to but distinct from #5898. The proxy deleted segments from the URL and we need to put them back.

@Tratcher
Copy link
Member

Tratcher commented Jul 2, 2019

I take that back, #5898 (comment) already accounts for this.

@Tratcher Tratcher closed this as completed Jul 2, 2019
@shanselman
Copy link
Contributor

shanselman commented Jul 2, 2019

I'm pretty sure this is the inverse of PathBase. I'm having to add it back in order to get ~ to work.

I'm taking http://staging.hanselman.com/blog and pointing it to http://hanselmanblog.azurewebsites.net so I need to add it BACK not strip it. Per https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.2#deal-with-path-base-and-proxies-that-change-the-request-path

app.Use((context, next) =>
{
     context.Request.PathBase = new PathString(/blog);
     return next.Invoke();
});

@Eilon
Copy link
Member

Eilon commented Jul 3, 2019

@shanselman / @Tratcher - if this isn't resolved, would it make sense for the two of you to discuss this on a call briefly? Just suggesting 😄

@Tratcher
Copy link
Member

Tratcher commented Jul 3, 2019

We've chatted and I've redirected him to #5898. There's several related scenarios we can address together.

@Eilon
Copy link
Member

Eilon commented Jul 3, 2019

Great!

@SheepRock
Copy link

I've tried this and it works! This is the code I need in apps hosted on my server. And I'd like it to be default for the ASP.NET Core framework.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	string basePath = Environment.GetEnvironmentVariable("ASPNETCORE_BASEPATH");
	if (!string.IsNullOrEmpty(basePath))
	{
		app.Use(async (context, next) =>
		{
			context.Request.PathBase = basePath;
			await next.Invoke();
		});
	}
	app.UseForwardedHeaders(new ForwardedHeadersOptions
	{
		ForwardedHeaders = ForwardedHeaders.All
	});

	// existing code...
}

My starter script sets these environment variables before running the dotnet command: (This is a simplified sample, the actual script is written in PHP.)

export ASPNETCORE_URLS=http://127.0.0.1:$port
export ASPNETCORE_BASEPATH=$basePath
dotnet $applicationDllFile

And these directives are used for the Apache proxy configuration: (Again, pseudo-variables.)

ProxyPass "/$path" http://127.0.0.1:$port/ retry=15
ProxyPassReverse "/$path" http://127.0.0.1:$port/

So nobody found the original issue on this topic and I haven't received any notifications from it recently, I guess it got lost. So this is now my official request to build this base path support into ASP.NET Core. This allows hosters to map an application into a subdirectory in the URL without modifications to the application code. The environment variable ASPNETCORE_URLS already exists and serves a similar purpose, to make the application server listen on a specific port, again determined by the hoster. The new environment variable ASPNETCORE_BASEPATH would extend this to the URL path.

Maybe the older issue mentioned something like supporting ASPNETCORE_URLS=http://127.0.0.1:$port/$basePath/. But IIRC that wasn't allowed back then. I guess I'd duplicate the base path in the proxy configuration then. I'm not sure if that would serve the same purpose though because the application would probably have to handle the subdirectory more explicitly.

For me it worked for almost everything, but when an unlogged user tries to access an page with the [Authorize] decorator, the basepath is being add twice. I have

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
    options =>
    {
        options.LoginPath = "/Account/Login";
        options.LogoutPath = "/Account/Logout";
    });

but during the navigation, the user gets http://server/basepath/basepath/Account/Login

Anyone have this issue? Is it a bug or configuration?

@DanoThom
Copy link

string basePath = Environment.GetEnvironmentVariable("ASPNETCORE_BASEPATH");
if (!string.IsNullOrEmpty(basePath))
{
app.Use(async (context, next) =>
{
context.Request.PathBase = basePath;
await next.Invoke();
});
}
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
});

Can confirm I'm experiencing the same issue using FrontDoor --> WebApp --> Logout Auth.

The internal logout mechanics seem to be ignoring the usePathBase logic to build downstream routing.

@Tratcher
Copy link
Member

@SheepRock can you show your full middleware ordering in Startup.Configure? Changing PathBase after app.UseAuthentication() might cause that.

@SheepRock
Copy link

SheepRock commented Jul 26, 2019

@SheepRock can you show your full middleware ordering in Startup.Configure? Changing PathBase after app.UseAuthentication() might cause that.

public void ConfigureServices(IServiceCollection services)
{
	services.Configure<ForwardedHeadersOptions>(options =>
	{
		options.ForwardedHeaders = 
                        Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | 
                        Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto;
		options.KnownProxies.Add(System.Net.IPAddress.Parse(myProxyIP));
	});

	services.Configure<CookiePolicyOptions>(options =>
	{
		options.MinimumSameSitePolicy = SameSiteMode.None;                
	});
	services.Configure<FormOptions>(options =>
	{
		options.ValueCountLimit = int.MaxValue;
	});
	var constrng = Configuration.GetConnectionString("MyConString");
	
	services.AddDbContext<DgnContext>(options =>
		options.UseMySql(constrng));

	services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
		.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme,
		options =>
		{
			options.LoginPath = "/Account/Login";
			options.LogoutPath = "/Account/Logout";
			options.ExpireTimeSpan = new TimeSpan(7, 0, 0, 0);                    
		});
	services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

	services.AddAuthentication(options =>
	{
		options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
	});

}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
	app.UseForwardedHeaders(new ForwardedHeadersOptions
	{
		ForwardedHeaders = 
                        Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedFor | 
                        Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.XForwardedProto
	});
	
        app.Use(async (context, next) =>
        {
            context.Request.PathBase = "/mypathBase";
            await next.Invoke();
        });

	if (env.IsDevelopment())
	{
		app.UseDeveloperExceptionPage();
	}
	else
	{
		app.UseExceptionHandler("/Home/Error");
	}

	app.UseStaticFiles();
	app.UseCookiePolicy();
	app.UseAuthentication();
	app.UseMvc(routes =>
	{
		routes.MapRoute(
			name: "default",
			template: "{controller=Home}/{action=Index}");
	});
}

The weird thing is that it works on my dev computer, but when I deploy to the Windows Server 2008 R2 server it fails. Currently I'm using options.LoginPath = "/../Account/Login"; to make it work.

Also, it looks like ForwardedHeaders aren't working, since Request.HttpContext.Connection.RemoteIpAddress is returning the proxy ip instead of client.

@Tratcher
Copy link
Member

Tratcher commented Jul 26, 2019

In 2.2 there was an issue with KnownProxies where it didn't match because RemoteIpAddress was in an IPv6 format. Make sure the formats match. This has been fixed in 3.0.

Please open a new issue for your LoginPath problem so we can investigate that specifically. Closed issues are not assigned for investigation.

@dotnet dotnet locked as resolved and limited conversation to collaborators Dec 4, 2019
@amcasey amcasey added the area-hosting Includes Hosting label Jun 1, 2023
@amcasey amcasey added area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions and removed area-runtime labels Aug 24, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area-hosting Includes Hosting area-networking Includes servers, yarp, json patch, bedrock, websockets, http client factory, and http abstractions enhancement This issue represents an ask for new feature or an enhancement to an existing one
Projects
None yet
Development

No branches or pull requests

9 participants