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

Document how to work with a local ASP.NET Core backend during development #600

Closed
danroth27 opened this issue Jun 9, 2022 · 23 comments · Fixed by #642
Closed

Document how to work with a local ASP.NET Core backend during development #600

danroth27 opened this issue Jun 9, 2022 · 23 comments · Fixed by #642
Assignees
Labels
doc-idea Indicates issues that are suggestions for new topics [org] Pri1 High priority, do before Pri2 and Pri3

Comments

@danroth27
Copy link
Member

Port the following Xamarin docs on consuming backend APIs from a native client app during development:

We'll also need to migrate the Todo sample app to .NET MAUI as well.

This is needed so that we can update the ASP.NET Core tutorial on building an ASP.NET Core backend for a native client: dotnet/AspNetCore.Docs#26109.

@PRMerger17 PRMerger17 added the Pri3 label Jun 9, 2022
@dotnet-bot dotnet-bot added the ⌚ Not Triaged Not triaged label Jun 9, 2022
@adegeo adegeo added doc-idea Indicates issues that are suggestions for new topics [org] and removed ⌚ Not Triaged Not triaged labels Jun 9, 2022
@davidbritch davidbritch self-assigned this Jun 10, 2022
@davidbritch davidbritch added Pri1 High priority, do before Pri2 and Pri3 and removed Pri3 labels Jun 10, 2022
@davidbritch
Copy link
Contributor

davidbritch commented Jun 15, 2022

  • Sample
    • API
    • MAUI client
    • Use Shell navigation
    • Use DI
    • Replace ListView with CollectionView
    • Tidy up UI
    • Works on all platforms
      • Windows
        • Localhost HTTP
        • Localhost HTTPS
        • Hosted HTTPS
      • Android
        • Localhost HTTP - MAUI: add network_security_config, API: add a HTTP profile
        • Localhost HTTPS - override AndroidMessageHandler and ignore localhost SSL errors via ServerCertificateCustomValidationCallback
        • Hosted HTTPS
      • iOS
        • Localhost HTTP - enable ATS (allow local networking).
        • Localhost HTTPS - set NSUrlSessionHandler.TrustOverrideForUrl to a delegate that trusts HTTPS localhost
        • Hosted HTTPS
      • MacCatalyst
        • Localhost HTTP
        • Localhost HTTPS
        • Hosted HTTPS
  • Consume REST services doc
  • Connect to local web services doc

@davidbritch
Copy link
Contributor

In typical fashion this won't be a straight port. The old way of connecting to localhost from Android doesn't work in MAUI - see dotnet/android#6665

@danroth27
Copy link
Member Author

@davidbritch dotnet/android#6665 is a merged PR. Is there an active issue tracking the problem you are seeing?

@davidbritch
Copy link
Contributor

@danroth27 It's more a question of what modifications are required to the Android config in the MAUI app to make it connect to both HTTP and HTTPS localhost without errors. I'm working on it.

@AndreduToit
Copy link

I am also experiencing problems with connecting to localhost from Android in MAUI and searching for a solution to this issue. Would appreciate any update/solution that you may have.

@davidbritch
Copy link
Contributor

Hi @AndreduToit

There'll be docs on doing this by the end of the week. In the meantime, let's see if I can help you. Are you trying to connecting to HTTP localhost or HTTPS? The solution is different for each scheme.

@AndreduToit
Copy link

Hello @davidbritch
Thank you so much for your response.
I let you know that I have also engaged with @Eagle3386 on this issue see: dotnet/maui#8131.

I am trying to connect to HTTPS with my .NET Maui App.
The App works as expected on Windows platform but on Android platform the server responds with 'Hostname 10.0.2.2 not verified'

I have added the following to my Maui App:

  • Platforms\Android\Resources\raw\kestrel.cer
  • Platforms\Android\Resources\xml\network_security_config.xm - as per the suggestion by @Eagle3386.
  • line - android:networkSecurityConfig ="@xml/network_security_config" - under tags 'manifest/application' in AndroidManifest.xml

I re-produced kestrel.cer in dotnet with command line 'dotnet dev-certs https --clean' and 'dotnet dev-certs https --trust' and exported the certificates with certmgr.exe to the folder Android\Resources\raw in my Maui App (tried both DER encoded and Base-64 encoded formats in 'Personal' and 'Trusted Root Certificate Authorities' folders).

I would very much appreciate any tips or guidance that you can give me.

@davidbritch
Copy link
Contributor

Hi @AndreduToit

The network_security_config.xml approach is for HTTP. The issue on HTTPS is that Android doesn't trust self-signed certificates, and so you have to add code to ignore SSL errors for localhost. Add the following class to your project:

    public class HttpsClientHandlerService : IHttpsClientHandlerService
    {
        public HttpMessageHandler GetPlatformMessageHandler()
        {
#if ANDROID
            var handler = new CustomAndroidMessageHandler();
            handler.ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
            {
                if (cert != null && cert.Issuer.Equals("CN=localhost"))
                    return true;
                return errors == System.Net.Security.SslPolicyErrors.None;
            };
            return handler;
#elif IOS
            var handler = new NSUrlSessionHandler
            {
                TrustOverrideForUrl = IsHttpsLocalhost
            };
            return handler;
#elif WINDOWS || MACCATALYST
            return null;
#else
         throw new PlatformNotSupportedException("Only Android, iOS, MacCatalyst, and Windows supported.");
#endif
        }

#if ANDROID
        internal sealed class CustomAndroidMessageHandler : Xamarin.Android.Net.AndroidMessageHandler
        {
            protected override Javax.Net.Ssl.IHostnameVerifier GetSSLHostnameVerifier(Javax.Net.Ssl.HttpsURLConnection connection)
                => new CustomHostnameVerifier();

            private sealed class CustomHostnameVerifier : Java.Lang.Object, Javax.Net.Ssl.IHostnameVerifier
            {
                public bool Verify(string hostname, Javax.Net.Ssl.ISSLSession session)
                {
                    return Javax.Net.Ssl.HttpsURLConnection.DefaultHostnameVerifier.Verify(hostname, session) ||
                        hostname == "10.0.2.2" && session.PeerPrincipal?.Name == "CN=localhost";
                }
            }
        }
#elif IOS
        public bool IsHttpsLocalhost(NSUrlSessionHandler sender, string url, Security.SecTrust trust)
        {
            if (url.StartsWith("https://localhost"))
                return true;
            return false;
        }
#endif
    }

Then invoke it when you create your HttpClient object:

#if DEBUG
            clientHandlerService = new HttpsClientHandlerService();
            client = new HttpClient(clientHandlerService.GetPlatformMessageHandler());
#else
            client = new HttpClient();
#endif

Hopefully this will unblock you.

@AndreduToit
Copy link

Hi @davidbritch
Thanks for your almost immediate response and positive recommendations.
I never got as far as to figure out that the network_security_config.xml approach is for HTTP - my bad.
I am going to implement your recommendations and try to unblock - will advise.
Thanks again.

@AndreduToit
Copy link

Hello @davidbritch - I am unable to locate IHttpsClientHandlerService - assmebly/namespace - nuget.

@davidbritch
Copy link
Contributor

You could delete that reference (if you want). It's just:

	public interface IHttpsClientHandlerService
	{
        HttpMessageHandler GetPlatformMessageHandler();
    }

@AndreduToit
Copy link

@davidbritch
OK, got it thanks.

Can you advise - for HTTPS, do I remove the following:

  • Platforms\Android\Resources\raw\kestrel.cer
  • Platforms\Android\Resources\xml\network_security_config.xm - as per the suggestion by @Eagle3386.
  • line - android:networkSecurityConfig ="@xml/network_security_config" - under tags 'manifest/application' in AndroidManifest.xml

Also, let you know - I tried the same code previously as suggested by @Eilon here dotnet/maui#8131 and provided here: https://gist.github.com/Eilon/49e3c5216abfa3eba81e453d45cba2d4

Did not work before, but I try again. Will let you know.

@AndreduToit
Copy link

@davidbritch
Tried your solution and it appears to work, thank you.
Now the hard work begins to figure out what are the differences and why the previous solutions did not work......
Thanks again

@davidbritch
Copy link
Contributor

davidbritch commented Jun 21, 2022

@AndreduToit

Glad you're unblocked!

Yes, you can remove the following if your localhost service is running over HTTPS:

  • Platforms\Android\Resources\raw\kestrel.cer
  • Platforms\Android\Resources\xml\network_security_config.xm - as per the suggestion by @Eagle3386.
  • line - android:networkSecurityConfig ="@xml/network_security_config" - under tags 'manifest/application' in AndroidManifest.xml

Similarly, you don't need the code-based approach from above if your localhost service is running over HTTP.

@AndreduToit
Copy link

@davidbritch thank you for helping me to unblock this - it was super frustrating up to here but I am happy now........ can push ahead.

@Eagle3386
Copy link

Eagle3386 commented Jun 21, 2022

@davidbritch I'd like to thank you, too, for the clarification about the network_security_config.xml approach as I was once taught it's required for both, HTTP and HTTPS. 😅

Addendum: can you please explain why null instead of new() is return for Windows & Mac Catalyst?
Because that always gives me a NRE upon launching a Windows debug session while everything runs smoothly with the latter.

Additionally, from what I read over at xamarin/xamarin-macios, the NSUrlSessionHandler class is supposed to work on iOS and Mac Catalyst. Why not return the same like for iOS then, e. g. change

#elif IOS

to

#elif IOS || MACCATALYST

@AndreduToit
Copy link

@Eagle3386

' why null instead of new() is return for Windows & Mac Catalyst?'

  • my guess is that this is because HttpMessageHandler is an abstract class - cannot be instantiated - a subclass is required.

I also found that calling instantiating as follows new HttpClient(null) throws and exception.

For this reason (and because I always enable nullable in my code), I added a property for HttpClient in my helper class - which builds a HttpMessageHandler (GetPlatformMessageHandler) and then on a null test returns either new HttpClient() or new HttpClient(handler)

@davidbritch
Copy link
Contributor

@Eagle3386

Addendum: can you please explain why null instead of new() is return for Windows & Mac Catalyst? Because that always gives me a NRE upon launching a Windows debug session while everything runs smoothly with the latter.

Two things here:

  1. I pasted code from my Mac. The version on my Windows machine is the more up to date and has a null check before creating the HttpClient object. But on both machines the HttpsClientHandlerService code is identical.
  2. The default HttpClient constructor creates a new HttpClientHandler object which in turns creates the correct underlying platform implementation. So for Windows & MacCatalyst you could return a new HttpClientHandler and pass it to the HttpClient constructor, but that's no different to just doing new HttpClient(). So really it's just a personal choice to return null or a new HttpClientHandler.

Additionally, from what I read over at xamarin/xamarin-macios, the NSUrlSessionHandler class is supposed to work on iOS and Mac Catalyst. Why not return the same like for iOS then

HTTP + HTTPS localhost works out of the box on MacCatalyst, without having to modify NSUrlSessionHandler.

@davidbritch
Copy link
Contributor

@AndreduToit

For this reason (and because I always enable nullable in my code), I added a property for HttpClient in my helper class - which builds a HttpMessageHandler (GetPlatformMessageHandler) and then on a null test returns either new HttpClient() or new HttpClient(handler)

Exactly what I do in the code on my Windows machine (and is the code that will make it into the docs).

@Eagle3386
Copy link

@davidbritch

Two things here:

  1. I pasted code from my Mac. The version on my Windows machine is the more up to date and has a null check before creating the HttpClient object. But on both machines the HttpsClientHandlerService code is identical.

Understood. Then my approach is just different: I'll return a (new) handler instead of a prepared HttpClient due to my MAUI Blazor app doing normal HTTP requests as HTTP/2 & the SignalR-based stuff as HTTP/1.1 (until SignalR either supports HTTP/2 or even HTTP/3 - which is currently in development, IIRC).

  1. (...) So for Windows & MacCatalyst you could return a new HttpClientHandler and pass it to the HttpClient constructor, but that's no different to just doing new HttpClient() (...)

But I've been talking about the "force-pass localhost certs" workaround here & as I said, looking at the aforementioned Xamarin repo's code, iOS & MacCatalyst seem to use the same class, hence overriding TrustOverrideForUrl is required for MacCatalyst (just as is for iOS), isn't it? Because you also say:

HTTP + HTTPS localhost works out of the box on MacCatalyst, without having to modify NSUrlSessionHandler.

And that totally confuses me now.. 😅

@davidbritch
Copy link
Contributor

@Eagle3386

Yes, both iOS + MacCatalyst use NSUrlSessionHandler, and both iOS + MacCatalyst don't trust self-signed certificates by default. However, the self-signed localhost cert in macOS should be marked as trusted (you can look in Keychain Access) because either VSMac has given you a pop up asking if you want to trust the cert, or you've ran the dotnet dev-certs https --trust command.

Hence no need to set the TrustOverrideForUrl property when running on MacCatalyst.

@AndreduToit
Copy link

@davidbritch

I share the following for your information and in case someone else can benefit from it.

The code for Verify method of the CustomHostnameVerifier calls suggested by yourself is as follows:

              public bool Verify(string hostname, Javax.Net.Ssl.ISSLSession session)
             {
                    return Javax.Net.Ssl.HttpsURLConnection.DefaultHostnameVerifier.Verify(hostname, session) ||
                        hostname == "10.0.2.2" && session.PeerPrincipal.Name == "CN=localhost";
             }

This code results in some null reference warnings.
I tried the following code, but it does not work - results in 'Hostname 10.0.2.2 not verified' - why? I have no idea :

                public bool Verify(string? hostname, Javax.Net.Ssl.ISSLSession? session)
                {
                    return Javax.Net.Ssl.HttpsURLConnection.DefaultHostnameVerifier?.Verify(hostname, session) ?? false ||
                        hostname == "10.0.2.2" && session?.PeerPrincipal?.Name == "CN=localhost";
                }

The following code seems top be working - without any null reference warnings:

                public bool Verify(string? hostname, Javax.Net.Ssl.ISSLSession? session)
                {
                    if (string.IsNullOrEmpty(hostname) || session == null) return false;
                    var httpsDefaultVerified = Javax.Net.Ssl.HttpsURLConnection.DefaultHostnameVerifier?.Verify(hostname, session) ?? false;
                    var httpsCustomVerified = hostname == "10.0.2.2" && session.PeerPrincipal?.Name == "CN=localhost";
                    return httpsDefaultVerified || httpsCustomVerified;
                }

jonpryor pushed a commit to dotnet/android that referenced this issue Sep 8, 2022
…7246)

Fixes: #7149

Context: 48540d6
Context: https://docs.microsoft.com/en-us/answers/questions/885827/error-hostname-not-verified-when-sending-request-o.html
Context: dotnet/docs-maui#600
Context: dotnet/maui#8131
Context: dotnet/runtime#70434

Commit 48540d6 added support to
`AndroidMessageHandler.ServerCertificateCustomValidationCallback` for
self-signed certificates.  However, it didn't account for mismatches
between the server's hostname and the certificate's hostname.
This can cause issues when e.g. you use 10.0.2.2 to send requests to
a local server with a self-signed dev certificate in ASP.NET:

	var handler     = new AndroidMessageHandler {
	    // Allow any cert; DO NOT USE IN PRODUCTION
	    ServerCertificateCustomValidationCallback   =
	        (httpRequestMessage, cert, cetChain, policyErrors) => true,
	    // …
	};
	var client      = new HttpClient(handler);
	var response    = await client.SendAsync(message);

This could result an exception similar to:

	System.Net.WebException: Hostname EXAMPLE.DOMAIN not verified:
	    certificate: sha1/EXAMPLE_CERT_SHA1
	    DN: CN=EXAMPLE_CN
	    subjectAltNames: [EXAMPLE_CN]
	---> Javax.Net.Ssl.SSLPeerUnverifiedException: Hostname EXAMPLE.DOMAIN not verified:
	    certificate: sha1/EXAMPLE_CERT_SHA1
	    DN: CN=EXAMPLE_CN
	    subjectAltNames: [EXAMPLE_CN]
	   at Java.Interop.JniEnvironment.InstanceMethods.CallVoidMethod(JniObjectReference instance, JniMethodInfo method, JniArgumentValue* args)
	   at Java.Interop.JniPeerMembers.JniInstanceMethods.InvokeAbstractVoidMethod(String encodedMember, IJavaPeerable self, JniArgumentValue* parameters)
	   at Javax.Net.Ssl.HttpsURLConnectionInvoker.Connect()
	   at Xamarin.Android.Net.AndroidMessageHandler.<>c__DisplayClass125_0.<ConnectAsync>b__0()
	   at System.Threading.Tasks.Task.InnerInvoke()
	   at System.Threading.Tasks.Task.<>c.<.cctor>b__272_0(Object obj)
	   at System.Threading.ExecutionContext.RunFromThreadPoolDispatchLoop(Thread threadPoolThread, ExecutionContext executionContext, ContextCallback callback, Object state)
	--- End of managed Javax.Net.Ssl.SSLPeerUnverifiedException stack trace ---
	javax.net.ssl.SSLPeerUnverifiedException: Hostname EXAMPLE.DOMAIN not verified:
	    certificate: sha1/EXAMPLE_CERT_SHA1
	    DN: CN=EXAMPLE_CN
	    subjectAltNames: [EXAMPLE_CN]
	     at com.android.okhttp.internal.io.RealConnection.connectTls(RealConnection.java:205)
	     at com.android.okhttp.internal.io.RealConnection.connectSocket(RealConnection.java:153)
	     at com.android.okhttp.internal.io.RealConnection.connect(RealConnection.java:116)
	     at com.android.okhttp.internal.http.StreamAllocation.findConnection(StreamAllocation.java:186)
	     at com.android.okhttp.internal.http.StreamAllocation.findHealthyConnection(StreamAllocation.java:128)
	     at com.android.okhttp.internal.http.StreamAllocation.newStream(StreamAllocation.java:97)
	     at com.android.okhttp.internal.http.HttpEngine.connect(HttpEngine.java:289)
	     at com.android.okhttp.internal.http.HttpEngine.sendRequest(HttpEngine.java:232)
	     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.execute(HttpURLConnectionImpl.java:465)
	     at com.android.okhttp.internal.huc.HttpURLConnectionImpl.connect(HttpURLConnectionImpl.java:131)
	     at com.android.okhttp.internal.huc.DelegatingHttpsURLConnection.connect(DelegatingHttpsURLConnection.java:90)
	     at com.android.okhttp.internal.huc.HttpsURLConnectionImpl.connect(HttpsURLConnectionImpl.java:30)
	--- End of inner exception stack trace ---
	   at Xamarin.Android.Net.AndroidMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
	   at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)

Address this problem and aligns the behavior of
`AndroidMessageHandler` with other platforms, where hostname
verification is part of the server certificate custom validation
callback.

There are two significant changes:

  - The trust manager that wraps the custom callback now verifies the
    certificate hostname

  - Java's separate hostname verification is skipped

There is only one aspect which is a little hacky: the
[`javax.net.ssl.HostnameVerifier` interface][0] requires an instance
of [`javax.net.ssl.SSLSession`][1], which we don't have access to in
the trust manager.  The default hostname verifier instead uses just a
single getter to access the chain of certificates so it seems OK to me
to have a dummy class that implements the interface that holds just
the peer certificates.

(I tried to find access to the session from `HttpsURLConnection` but
there doesn't seem to be a way to access the `SSLEngine` and its
`HandshakeSesion` property.)

[0]: https://developer.android.com/reference/javax/net/ssl/HostnameVerifier
[1]: https://developer.android.com/reference/javax/net/ssl/SSLSession
@simonrozsival
Copy link
Member

Just FYI since dotnet/android#7246 was merged, the CustomHostnameVerifier hack won't be necessary in .NET 7 on Android.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
doc-idea Indicates issues that are suggestions for new topics [org] Pri1 High priority, do before Pri2 and Pri3
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants