unosquare / embedio

A tiny, cross-platform, module based web server for .NET
http://unosquare.github.io/embedio
Other
1.46k stars 176 forks source link

HttpContext.Redirect failing with SSL and relative route #454

Closed SaricVr closed 4 years ago

SaricVr commented 4 years ago

Hello,

I'm facing a strange behavior with a simple page redirection inside a controller. This is my code:

class WebController : WebApiController
{
        [Route(HttpVerbs.Get, "/")]
        public void Page()
        {
            HttpContext.Redirect("/page2");
        }

        [Route(HttpVerbs.Get, "/page2")]
        public string Page2()
        {
            return "Page 2";
        }
}

Now, the following redirection from "/" to "/page2" works with HTTP, but fails with HTTPS. If instead i change the redirect to a fully qualified url such as HttpContext.Redirect("https://localhost:9696/page2") the code works also with HTTPS. I'm using a self-signed certificate for testing purposes and accessing the page with the Google Chrome (the specific error is ERR_CONNECTION_RESET.

Now, I may be lacking a fundamental concept behind HTTPS that I'm not aware of (although I did research the issue), but if you could clarify the situation to me it would be great.

Thank you

rdeago commented 4 years ago

Hello @SaricVr, thanks for using EmbedIO!

As far as I know, there is no specific restriction on the use of relative URLs for redirection in HTTPS.

Let's rule out some possible external causes first:

As for the chance of this being caused by a bug in EmbedIO, the only possible malfunction I can think of at the moment would be EmbedIO closing the connection without properly announcing so to the client via a Connection: close header. I can't figure out why it would be happening, much less why it would depend on relative vs. absolute URL in a response's Location header, but you may want to give it a shot by changing your Page method as follows:

        [Route(HttpVerbs.Get, "/")]
        public void Page()
        {
            HttpContext.Redirect("/page2");
            HttpContext.Response.KeepAlive = false;
        }

This will force EmbedIO to close the connection and send a Connection: close header. If this makes relative redirection work, then we'll have to look closely at how EmbedIO manages secure connections.

SaricVr commented 4 years ago

Hi @rdeago, thank you for your advice.

I tried another browser and the problem remains. I also disabled the antivirus and nothing changed. Also setting KeepAlive = false doesn't change anything.

Maybe the issue is related to the certificate I'm using? I created it with the powershell command New-SelfSignedCertificate -certstorelocation cert:\currentuser\my -friendlyname "Test certificate" -DnsName "localhost"

It would be useful if someone could try to reproduce the issue, so to exclude it's a problem related to my machine (I also tried to connect to the web server on my machine from my phone (LAN) and the error persists).

Thanks

rdeago commented 4 years ago

I'll try to reproduce the issue. Can you post your server initialization code, so I can be sure to use your same options?

SaricVr commented 4 years ago

Here is the code:

public static void Start()
{
    var urls = new string[] {"https://localhost:9696"};
    using (var server = CreateWebServer(urls))
        server.RunAsync();
}

private static WebServer CreateWebServer(string[] urls)
{
    var cert = new X509Certificate2("cert.pfx", "12345678", X509KeyStorageFlags.DefaultKeySet);
    var server = new WebServer(o => o
                .WithUrlPrefixes(urls)
                .WithMode(HttpListenerMode.EmbedIO).WithCertificate(cert))
                .WithLocalSessionManager().WithWebApi("/", m => m
                .WithController<WebController>());
    return server;
}

As I said, the certificate has been created with the powershell command reported in my previous post, manually exported from the Windows certificate store as a .pfx file.

Thank you

SaricVr commented 4 years ago

Hello, a small update on the problem... this is the log I observe when requesting "page1:

[hXVXgRXzZk60UZhWBueAzw] GET /page1: "302 Found" sent in 39ms (0 bytes)
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in EmbedIO.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in EmbedIO.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in System.dll
Eccezione generata: 'System.IO.IOException' in EmbedIO.dll

Hope this helps a bit.

P.S. "Eccezione generata" is just "Exception thrown".

rdeago commented 4 years ago

Hello @SaricVr, sorry for not coming back to you in the meantime. I've been busy with work. Good to see you've been further investigating the issue.

(Also, thanks for the translation on behalf of the rest of the team. As for me, I'm Italian too. 😁)

Back to the issue. We see from the log that a 302 response is actually generated and sent to the client (the first log entry in your comment was generated after flushing the response stream.)

Then all hell breaks loose. 😱

To me, those "Exception thrown" messages look more like they come straight from .NET Framework (or Core) rather than being part of the log. You can verify this by logging to a file:

using Swan.Logging;

    // ...
    Logger.RegisterLogger(new FileLogger("PATH_TO_LOG_FILE"));
    // ... then initialize and run your server as usual.

If those messages are not written to your log file, it obviously means that they were written directly to the console - not by EmbedIO, of course, but rather from the .NET runtime. This distinction is important to start understanding where the exceptions come from.

Another possible test is requesting "/" from a non-browser client, such as curl, that does not follow redirections. This way you can see whether the exceptions are thrown while closing the first response or accepting the second request. If you use Linux, you either have curl already installed on your machine, or available through your distro's package manager (e.g. sudo apt-get install curl on Debian and derivatives.) For Windows, you can download it from here (no installation required, just extract the contents of the bin folder from the ZIP somewhere handy.) What do you see in the log file when requesting with curl?

curl https://127.0.0.1/

Finally, you can re-enable write exceptions in the HTTP listener:

    Logger.RegisterLogger(new FileLogger("PATH_TO_LOG_FILE"));
    using (var server = CreateWebServer())
    {
        server.Listener.IgnoreWriteExceptions = false;
        server.RunAsync();
    }

The differences between logs with IgnoreWriteExceptions set to false and set to true could be revealing. (The default value is true for HttpListenerMode.EmbedIO, false for HttpListenerMode.Microsoft - oops, my fault!)

SaricVr commented 4 years ago

Hi @rdeago, thanks for the help.

So, I did all the tests you suggested and:

HTTP/1.0 302 Found
Expires: Sat, 26 Jul 1997 05:00:00 GMT
Last-Modified: Thu, 05 Mar 2020 09:39:46 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: http://localhost:9696/page2
Content-Type: application/octet-stream; charset=utf-8
Server: EmbedIO/3.4.2
Date: Thu, 05 Mar 2020 09:39:46 GMT
Content-Length: 0
Connection: close

curl: (56) Recv failure: Connection was reset

So it seems to me that the redirect is followed, but the connection is closed (probably due to internal exception of EmbedIO).

Just to help me with the debugging... How can I make EmbedIO throw the exception so I can handle it in the code instead of logging it only? I really can't figure it out from the code or documentation.

Thanks again

rdeago commented 4 years ago

Unfortunately, there's currently no official way to debug EmbedIO.

Try the following:

Version 4 will have SourceLink and symbol packages, I'm currently experimenting with them.

SaricVr commented 4 years ago

Here I am again. I found the root cause of the problem. So, first of all, being able to debug the code... actually helps you debugging the code!

Essentially, in file HttpListenerRequest.cs, line 279-280 (I guess there's a way to create a link to that line on github but I don't know how) I found this (I'm referring to the 3.X branch):

// var baseUri = $"{(IsSecureConnection ? "https" : "http")}://{host}:{LocalEndPoint.Port}";
 var baseUri = $"http://{host}:{LocalEndPoint.Port}";

As you can see, the baseUri is fixed to "http". The funny thing is that the correct line (the previous one) is commented out :) By keeping only line 279, redirects work also with "https".

Also, in the output of curl in my previous post you could notice Location: http://localhost:9696/page2 which I didn't notice at first.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

panManfredini commented 4 years ago

I also see a similar issue, maybe worth adding an overload to HttpContext.Redirect to be able to do relative redirection.

A work around for now could be:

public static Task RelativeRedirect(IHttpContext ctx, string url){
            ctx.Response.StatusCode = 303;
            // Relative redirection if url is relative
            ctx.Response.Headers.Add(HttpResponseHeader.Location, url);
            ctx.SetHandled();
            return Task.CompletedTask;
        }