Laden...

HttpClient produziert bei weiterem Request StatusCode 401. Werden Credentials gecached?

Erstellt von Chronos vor 7 Jahren Letzter Beitrag vor 7 Jahren 10.846 Views
C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren
HttpClient produziert bei weiterem Request StatusCode 401. Werden Credentials gecached?

Hallo,

ich habe ein Problem mit dem HttpClient aus System.Net.Http.

ich bekomme reproduzierbar den Statuscode 401 wo er eigentlich nicht auftreten dürfte, die Credentials die ich dem Client mitgebe passen.

Folgendes Szenario:

Ein Vorgang wird gestartet. Für jeden dieser Vorgänge instanziiere ich einen eigenen HttpClient samt HttpClientHandler dem ich die Credentials mitgebe.
Der Client macht einen SoapRequest um sich benötigte Daten zu holen. In diesen Daten ist eine URL enthalten mit der ich nach dem ersten Request ein Get mache.

Für den ersten Durchlauf, kein Problem. Credentials sind Valide -> StatusCode 200
Für den zweiten und folgenden Durchlauf auch kein Problem solange der Vorgang in einem Zeitfenster von 25 Sekunden (Reproduzierbar) zum vorhergegangenen Vorgang stattfindet.

Dann nähmlich bekomme ich plötzlich den StatusCode 401.

Folgendes habe ich mittels Fiddler herausfinden können:

Der erste Request des Vorgangs wird ohne CredentialHeader gemacht. StatusCode: 401
Der zweite Request erfolgt dann mit Credentials im Header. StatusCode: 200
Der Get-Request erfolgt dann auch wieder mit Credentials. StatusCode: 200

Und jetzt fängt es an merkwürdig zu werden.

Ein zweiter Vorgang wird 25 Sekunden später angestoßen:

Der erste Request des Vorgangs wird mit (???) CredentialHeader gemacht. StatusCode: 401
An dieser stelle bekomme ich das Fehlen des StatusCodes 200 als Exception präsetiert.

Ich hätte jetzt erwartet das der Client zunächst eine Anfrage ohne Credentials macht um dann bei den folgenden diese wieder mitzuschicken.

Für mich sieht das ein wenig so aus als ob die Credentials in irgendeiner Art gecached werden und diese nach besagten 25 Sekunden nicht mehr gültig sind.
Aber wie kann das sein, immerhin ist es bei jedem Vorgang ein neuer Client der nach Abschluss auch wieder Disposed wird.

Weis jemand rat?

Falls jemand Beispielscode sehen mag bitte einfach bescheid geben. Den reiche ich dann nach.

16.807 Beiträge seit 2008
vor 7 Jahren

Was hängt denn da im Hintergrund für ein Rechtesystem dran? Basic Auth?
Kannst Du relevanten Code zeigen?

Und ja: HttpClient cached per default.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Reproduzieren kann ich das Verhalten mit folgendem Code:


private async Task TryItAsync(string userName, string password)
        {
            try
            {
                string soap = "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap-env:Envelope encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap-env:Body><GetCallList xmlns=\"urn:dslforum-org:service:X_AVM-DE_OnTel:1\" /></soap-env:Body></soap-env:Envelope>";
                string action = "urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetCallList";
                Uri uri = new Uri("https://fritz.box:49443/upnp/control/x_contact");

                for (int i = 0; i < 4; i++)
                {
                    
                    using (var handler = new HttpClientHandler())
                    {
                        handler.Credentials = new NetworkCredential(userName, password);
                        using (var client = new HttpClient(handler))
                        {
                            string url = null;
                            using (HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, uri))
                            {
                                message.Version = new Version("1.0");
                                message.Content = new StringContent(soap, Encoding.UTF8, "text/xml");
                                message.Headers.Add("SOAPAction", action);
                                using (var responseMessage = await client.SendAsync(message))
                                {
                                    Debug.WriteLine($"#1 Request StatusCode: {responseMessage.StatusCode}");
                                    if (responseMessage.IsSuccessStatusCode)
                                    {
                                        using (var responseStream = await responseMessage.Content.ReadAsStreamAsync())
                                        {
                                            using (var responseStreamReader = new StreamReader(responseStream))
                                            {
                                                var responseXml = await responseStreamReader.ReadToEndAsync();
                                                XmlDocument xml = new XmlDocument();
                                                xml.LoadXml(responseXml);

                                                var envelopeNode = xml.ChildNodes.OfType<XmlElement>().FirstOrDefault(node => (string)node.LocalName == "Envelope");
                                                var bodyNode = envelopeNode.ChildNodes.OfType<XmlElement>().FirstOrDefault(node => (string)node.LocalName == "Body");
                                                var getCallListResponseNode = bodyNode.ChildNodes.OfType<XmlElement>().FirstOrDefault(node => (string)node.LocalName == "GetCallListResponse");
                                                var newCallListURLNode = getCallListResponseNode.ChildNodes.OfType<XmlElement>().FirstOrDefault(node => (string)node.LocalName == "NewCallListURL");
                                                url = newCallListURLNode.InnerText;
                                            }
                                        }
                                    }
                                }
                            }

                            if (url != null)
                            {
                                using (var responseMessage = await client.GetAsync(url))
                                {

                                    Debug.WriteLine($"#2 Request StatusCode: {responseMessage.StatusCode}");
                                }
                            }

                        }

                    }

                    await Task.Delay(26000);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }

Authentifiziert wird mittels Digest.

Wie kann man denn dem Client denn dieses Verhalten abgewöhnen?

16.807 Beiträge seit 2008
vor 7 Jahren

Sehr wirr, was Du da mit der Fritzbox machst (finde ich). =)
Bei mir sieht meine Grundklasse so aus; wobei ich - wie von AVM empfohlen - mit der lua API arbeite.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using FritzCore;
using SchwabenCode.Fritzle.Responses;

namespace SchwabenCode.Fritzle
{
    public class FritzConnection
    {
        private readonly string _protocol;
        private readonly string _hostname;
        private readonly string _username;
        private readonly string _password;
        private bool _loggedIn;
        private string _sessionId;

        public DateTime LastActivity { get; private set; }



        /// <summary>
        ///  Session is only valid for 10 minutes by default
        /// </summary>
        public bool IsValid => DateTime.Now.Subtract( LastActivity ).TotalMinutes < 10;

        /// <summary>
        /// Creates the connection object
        /// </summary>
        public FritzConnection( string username, string password, string hostname = "fritz.box", bool secure = false )
        {
            _protocol = secure ? "https" : "http";
            _hostname = hostname;
            _username = username;
            _password = password;
        }

        /// <summary>
        /// Executes login to Fritzbox
        /// </summary>
        /// <exception cref="FritzLoginFailedException">if login fails (invalid data)</exception>
        /// <returns></returns>
        public async Task Login()
        {
            this.LastActivity = DateTime.Now;

            string responseText = await GetResponseAsync( "login_sid.lua" );
            FritzSessionInfoXmlResponse response = FritzSessionInfoXmlResponse.Parse( responseText );

            if( !response.IsValidSession )
            {
                responseText = await GetResponseAsync( "login_sid.lua", new { username = this._username, response = FormatChallenge( response.Challenge ) } );
                response = FritzSessionInfoXmlResponse.Parse( responseText );

                if( !response.IsValidSession )
                {
                    throw new FritzLoginFailedException();
                }

                _sessionId = response.SessionId;
            }

            _loggedIn = true;
        }

        private string FormatChallenge( string challenge )
        {
            return $"{challenge}-{HashChallenge( $"{challenge}-{this._password}" )}";
        }

        private static string HashChallenge( string input )
        {
            byte[ ] data = MD5.Create().ComputeHash( Encoding.Unicode.GetBytes( input ) );

            StringBuilder sBuilder = new StringBuilder();

            foreach( byte t in data )
            {
                sBuilder.Append( t.ToString( "x2" ) );
            }

            return sBuilder.ToString();
        }

        public Task<string> GetResponseAsync( string relativeUri, object parameters = null )
        {
            return GetInternalResponseAsync( relativeUri, AnonymousObjectToDictionary( parameters ) );
        }

        public Task<string> GetAuthenticatedResponseAsync( string relativeUri, object parameters = null )
        {
            IDictionary<string, object> paramBag = AnonymousObjectToDictionary( parameters );
            if( !paramBag.ContainsKey( "sid" ) )
            {
                paramBag.Add( "sid", _sessionId );
            }
            else
            {
                paramBag[ "sid" ] = _sessionId;
            }

            return GetInternalResponseAsync( relativeUri, paramBag );

        }
        private async Task<string> GetInternalResponseAsync( string relativeUri, IDictionary<string, object> parameters = null )
        {
            string uri = $"{_protocol}://{_hostname}/{relativeUri}";

            if( parameters != null )
            {
                uri += "?";
                uri = parameters.Aggregate( uri, ( current, entry ) => current + $"&{entry.Key}={entry.Value}" );
            }
            using(var httpClient = new HttpClient())
            {
                var result = await httpClient.GetStringAsync( uri )
            }

            return result;
        }

        IDictionary<string, object> AnonymousObjectToDictionary( object propertyBag )
        {
            Dictionary<string, object> result = new Dictionary<string, object>();
            if( propertyBag != null )
            {
                foreach( PropertyDescriptor property in TypeDescriptor.GetProperties( propertyBag ) )
                {
                    result.Add( property.Name, property.GetValue( propertyBag ) );
                }
            }
            return result;
        }
    }
}

Das FritzConnection Objekt behande ich nun ganz einfach als Context, wie man es zB. auch mit Datenbankschnittstellen macht.
Ich öffne also diese Verbindung und verwende diese offene Verbindung für Abfragen, zB Monitoring, Kontakte...

Wird die Session ungültig wird eine neue eröffnet.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Ah auch schonmal was für die Fritte gemacht ? ;D

Dass das supobtimal ausschaut weis ich. Ist ja auch nur zum provozieren des Fehlers ohne das Beispiel unnötig kompliziert zu machen. Solch eine Session-Klasse nutze ich auch. Diese aber hauptsächlich zum validieren der Credentials.

Ich weis das ich mit der erstellten SID auch den Link direkt abrufen oder die csv herunterladen könnte. Jedoch was wenn sich der Pfad dorthin ändert? Und die CSV enthält nicht die Infos die meine App benötigt.Außerdem wollte ich dokumentierte APIs nutzen. Sprich TR-064. Und da läuft das dann ein wenig anders. Es funktioniert ja eigentlich auch wenn das Problem mit dem 401 nicht wäre.Und das liegt ja wohl an dem caching der Credentials im Client.

Lade mir den Pfad zur Liste, lade die Liste und jage sie dann einmal durch den XmlSerializer. Fertig, eigentlich.... ^^

16.807 Beiträge seit 2008
vor 7 Jahren

Ich denke eben, dass Du diese Session ID falsch behandelst.
Kenne aber ehrlich gesagt nicht die korrekte Behandlung bei Deiner Art und Weise.

Die TR-064 API ist nicht offiziell; die kommt vom DSL Forum - nicht von AVM.

Ich hab aufgehört mit der FritzBox Schnittstellen zu arbeiten, nachdem sie alle lua-Schnittstellen mit dem neuen WebInterface verändert haben.
SOAP hab ich immer vermieden.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Die SID wird ja bei dem Vorgang on the Fly erstellt und ist dann 20Min gültig. Von dem Aspekt her sollte das kein Problem mit der vergangenen Zeit sein.

Das selbe Problem habe ich auch. Seit dem neuen Seitenlayout kann man noch nichtmal mehr den html-code zerlegen da das alles auf Ajax beruht.

16.807 Beiträge seit 2008
vor 7 Jahren

Also eigentlich wird die SID - jedenfalls kann ich das im Browser nachvollziehen - einmal erstellt (beim Login) und ist dann dauerhaft gültig.
Sie wird ungültig, wenn diese SID 10 Minuten (default) nicht genutzt hat (Idle).

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Ja das schon, nur halte ich die sid nicht für das Problem. Die wird ja nur für die Get-Anfrage benötigt. Wenn ich den StatusCode 401 bekomme komme ich ja garnicht erst zum Get.

Die Exception bekomme ich immer beim Soap-Call. Aber nur dann wenn mehr als 25 Sekunden zum vorherigen Request vergangen ist. Und das kann ich nicht recht zuordnen.

In dem Soap-Request habe ich auch nirgends was von der Sid, an stelle dessen bekomme ich den Authentication-Header

Authorization: Digest username="UserName",realm="HTTPS Access",nonce="",uri="/upnp/control/x_contact",cnonce="",nc=00000019,algorithm=MD5,response="****************",qop="auth"

Danach bekomme ich erst den Link mit der Sid die laut AVM-Doku 60 Minuten gültig ist.

Eine andere Möglichkeit wäre evtl. noch dem Envelope einen Auth-Header mitzugeben. Aber dann übernehme ich ja eigentlich die Arbeit die meiner Meinung nach der Client machen sollte/würde wenn ich ihm schon die Credentials mitgebe.

16.807 Beiträge seit 2008
vor 7 Jahren

Was passiert, wenn Du den HttpClient nur einmalig instantiierst?
Dieser ist ja sowieso concurrent, was ja der Hauptvorteil gegenüber den WebClient ist.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Der erste Request geht ohne Auth-Header an: http://fritz.box:49000/upnp/control/x_contact
StatusCode: 401

Dann schickt der Client von sich aus erneut jedoch mit Auth-Header (siehe oben) an:
http://fritz.box:49000/upnp/control/x_contact
StatusCode: 200

Danach findet der Get-Request ohne Auth-Header statt:
http://fritz.box:49000/calllist.lua?sid=******************
StatusCode: 200

Das ist ein Durchlauf.

Mehr mache ich da nicht. Die SID wird nicht angetastet außer beim Get-Request ausgelesen/übernomen.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Die Frage ist ja überhaupt warum der Client nur beim ersten mal eine Anfrage ohne Credentials macht, danach die selbe Anfrage automatisch nochmal mit Credentials macht. Um dann mit einer Wartezeit von 30 Sekunden direkt mit dem StatusCode 401 zurück kommt.

Für mein Verständnis müsste es doch eigentlich so sein.

Vorgang #1:
Anfrage ohne Credentials -> 401
Automatiches neuanfragen mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

Vorgang #2:
Anfrage ohne Credentials -> 401
Automatiches neuanfragen mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

Vorgang #3:
Anfrage ohne Credentials -> 401
Automatiches neuanfragen mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

Wartezeit: 30 Sekunden

Vorgang #n:
Anfrage ohne Credentials -> 401
Automatiches neuanfragen mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

usw....
Unter der Prämisse das jeder Vorgang vom vorangegangenen Vorgang den StatusCode nicht kennt.
Also so als ob jedes mal ein frischer und unbenutzter Client genutzt wird was ja doch eigentlich der Fall sein sollte wenn ich den Client nach jedem Vorgang Dispose. Zumindest nach meinem Verständnis.

Stattdessen:

Anfrage #1:
Anfrage ohne Credentials -> 401
Automatiches neuanfragen mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

Anfrage #2:
Anfrage mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

Anfrage #3:
Anfrage mit Credentials -> 200
Get ohne Credentials dafür aber mit SID -> 200

Wartezeit: 30 Sekunden

Anfrage #n:
Anfrage mit Credentials -> 401
Get wird nicht mehr ausgeführt.

Für mich sieht das so aus das er nicht jedesmal neue Credentials mitschickt sondern diese aus dem Cache nutzt. Und diese haben eine Verfallszeit von 30 Sekunden wenn sie nicht in diesem Zeitfenster verlängert werden.

Ich könnte mir vorstellen das eine Lösung des Problems sein könnte bei jedem Request gleich die frischen Credentials mitzuschicken.

Nur wie?

16.807 Beiträge seit 2008
vor 7 Jahren

Dann vergiss mal den HttpClient und verwende den WebRequest; auch wenn das von Dir beschriebene Verhalten beim HttpClient jetzt nicht unbedingt ganz nachvollziehbar ist.
Automatic Redirect gibts eigentlich nur beim Status Code 301, wobei das über den HttpClientHandler deaktivierbar ist.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Was ich ja noch gerne ausprobiert hätte:
Der HttpClientHandler hat eine Eigenschaft namens PreAuthenticate vom Typ Boolean.

Der Standartwert ist true.

Wenn ich diesen Wert auf False ändere sollte laut diesen Blogs wohl gewünschtes Verhalten möglich sein.

Nur wenn ich den Wert False setzte bekomme ich folgende Exception:

The value 'False' is not supported for property 'PreAuthenticate'.

Ich werde jetzt mal versuchen das ganze per WebRequest zu bewerkstelligen um dann nochmal zu schauen obs sich da ähnlich verhält.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Interessanterweise verhält sich der HttpWebRequest genau so wie der HttpClient.

Bei der Ersten Anfrage werden keine Credentials geschickt und ich bekomme den StatusCode 401. Dann wiederholt sich der Request aber diesmal mit Credentials. Ab da bekomme ich dann nurnoch den StatusCode 200. setze ich die Requests für 30 Sekunden + aus, bekomme ich nurnoch 401.

Dies ist der Code zum reproduzieren.

private async Task TryItWithHttpWebRequestAsync(string userName, string password)
        {
            try
            {
                for (int i = 0; i < 40; i++)
                {
                    string url = null;
                    Uri soapUri = new Uri("http://fritz.box:49000/upnp/control/x_contact");
                    var soapRequest = (HttpWebRequest)WebRequest.Create(soapUri);
                    soapRequest.Credentials = new NetworkCredential(userName, password);
                    soapRequest.Headers["SOAPAction"] = "urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetCallList";
                    soapRequest.ContentType = "text/xml;charset=\"utf-8\"";
                    soapRequest.Method = HttpMethod.Post.Method;
                    using (var soapRequestStream = await soapRequest.GetRequestStreamAsync())
                    {
                        var bytes = Encoding.UTF8.GetBytes("<?xml version=\"1.0\" encoding=\"utf-8\"?><soap-env:Envelope encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap-env:Body><GetCallList xmlns=\"urn:dslforum-org:service:X_AVM-DE_OnTel:1\" /></soap-env:Body></soap-env:Envelope>");
                        await soapRequestStream.WriteAsync(bytes, 0, bytes.Count());
                    }

                    using (var soapResponse = (HttpWebResponse)await soapRequest.GetResponseAsync())
                    {
                        Debug.WriteLine($"URL: {soapUri}, StatusCode: {soapResponse.StatusCode}");
                        if (soapResponse.StatusCode == HttpStatusCode.OK)
                        {
                            using (var responseStream = soapResponse.GetResponseStream())
                            {
                                using (var responseStreamReader = new StreamReader(responseStream, Encoding.UTF8))
                                {
                                    var soapXml = await responseStreamReader.ReadToEndAsync();
                                    var xml = new XmlDocument();
                                    xml.LoadXml(soapXml);

                                    var newCallListURL = xml.SelectSingleNodeNS("//s:Envelope/s:Body/u:GetCallListResponse/NewCallListURL", "xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' xmlns:u='urn:dslforum-org:service:X_AVM-DE_OnTel:1'");
                                    url = newCallListURL?.InnerText;
                                }
                            }
                        }
                    }

                    if (url != null)
                    {
                        Uri getUri = new Uri(url);
                        var getRequest = (HttpWebRequest)WebRequest.Create(getUri);
                        getRequest.Method = HttpMethod.Get.Method;
                        using (var getResponse = (HttpWebResponse)await getRequest.GetResponseAsync())
                        {
                            Debug.WriteLine($"URL: {getUri}, StatusCode: {getResponse.StatusCode}");
                            if (getResponse.StatusCode == HttpStatusCode.OK)
                            {
                                var serializer = new XmlSerializer(typeof(AVMCallList));
                                using (var getResponseStream = getResponse.GetResponseStream())
                                {
                                    var callList = serializer.Deserialize(getResponseStream) as AVMCallList;
                                    if (callList != null)
                                    {
                                        Debug.WriteLine($"Calls: {callList.Calls.Count}");
                                    }
                                }
                            }
                        }
                    }

                    int delay = i * 1000;
                    Debug.WriteLine($"Wartezeit: { delay / 1000 } Sekunde(n).");
                    await Task.Delay(delay);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
        }
16.807 Beiträge seit 2008
vor 7 Jahren

Also der WebRequest wirft bei 401 normalerweise eine WebException mit Inner Message Unauthorized.
Das passt hier irgendwie also nicht...

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Was ja auch der Fall ist.
Eine InnerException gibt's jedoch nicht.
Die Message der Exception:

The remote server returned an error: (401) Unauthorized.

Wie sieht das denn mit PreAuthenticate=false aus? Lässt sich das irgendwie setzen. Laut Doku soll HttpWebRequest genauso wie HttpClientHandler auch besagtes Property haben. IntelliSense sagt mir jedoch das die Eigenschaft nicht vorhanden ist.

Das setzen der Eigenschaft im HttpClientHandler führt bei mir zumindest zu einer Exception (siehe oben).

16.807 Beiträge seit 2008
vor 7 Jahren

Okay aus Deinem Text war (für mich) jetzt nicht eindeutig ersichtlich, dass Du eine Exception erhälst.

Ich glaub immer noch, dass die Authentifizierung eigentlich - wie in meinem Code - via Basic laufen muss.
Versuch mal folgendes:

var credCache = new CredentialCache();
credCache.Add(new Uri("<login uri der fritzbox>"), "Basic", new NetworkCredential("user", "pwd"));
var request = (HttpWebRequest)WebRequest.Create(<soap uri>);
request.Credentials = credCache;
C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Da bekomme ich folgende Exception geworfen:

The value 'System.Net.CredentialCache' is not supported for property 'Credentials'.

Ich habe nochmal die Doku der Api durchforstet

In dem Dokument AVM TR-064 first steps gibt es folgende Passage:

The default authentication mechanism is HTTP authentication using digest (MD5 hashes). Alternatively an authentication on content level is supported.
16.807 Beiträge seit 2008
vor 7 Jahren

Das Verhalten hier scheint nicht unbekannt zu sein. .NET WebRequest.PreAuthenticate – not quite what it sounds like.
Mir wars nicht bekannt 😉

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Ja nur das lustige daran ist ja das der HttpClientHandler besagtes Property hat, und ich ihm den Wert False zuweisen kann. Laut der Exception die ich dann aber bekomme ist nur true erlaubt.

Und HttpWebRequest hat dieses Property bei mir garnicht erst obwohl es laut MSDN da sein sollte.

Ich verstehe das nicht.

74 Beiträge seit 2014
vor 7 Jahren

Ich kann nichts zum eigentlichen Problem beitragen, aber:

Und HttpWebRequest hat dieses Property bei mir garnicht erst obwohl es laut MSDN da sein sollte.

Manchmal sind bestimmte Member mit einem Attribut / EditorBrowsableAttribute versehen, damit sie nicht im Editor auftauchen. Für mich ein sehr nerviges und sinnloses "Feature".

Grüße

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Aber dann müsste ich doch aber auch mit Reflection dazu in der Lage sein mir die PropertyInfo geben zu lassen?

var preAuthenticateProperty = request.GetType().GetProperty("PreAuthenticate");

gibt mir null zurück.

Auch mit

var properties= request.GetType().GetProperties();

bekomme ich PreAuthenticate auch nicht angezeigt.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Interessanterweise ist die Exception die ich beim setzten von HttpClientHandler.PreAuthenticate auf false bekomme vom Typ:

System.PlatformNotSupportedException, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e

Message:

The value 'False' is not supported for property 'PreAuthenticate'.

Aber wie kann sein das diese Funktion nicht unterstützt wird?

Die App ist eine Windows 10 UWP App. Windows ist Fully updated.

Capabilities der App:

  • Internet (Client)
  • Microphone
  • Private Networks (Client & Server)

Beim Googlen nach den Schlagwörtern PlatformNotSupportedException und PreAuthenticate bin ich dabei auf folgenden Link gestoßen:
Link

Dort ist folgender Code zu finden der zumindest ansatzweise erklären würde warum ich diesen Fehler bekomme:


        public bool PreAuthenticate
        {
            get { return true; }
            set
            {
                if (value != PreAuthenticate)
                {
                    throw new PlatformNotSupportedException(String.Format(CultureInfo.InvariantCulture,
                        SR.net_http_value_not_supported, value, nameof(PreAuthenticate)));
                }
                CheckDisposedOrStarted();
            }
        }

Ob der Code allerdings 1:1 dem von MS entspricht weis ich leider nicht da ich den Code ja nicht Debuggen kann.

Es wirft für mich aber die Frage auf warum die Möglichkeit zwar gegeben ist es abzuschalten man aber nicht gelassen wird.

D
985 Beiträge seit 2014
vor 7 Jahren

Ein zärtlicher Hinweis auf UWP /.netcore wäre ja schon freundlich gewesen, denn

https://github.com/dotnet/corefx/blob/master/src/System.Net.Http/src/System/Net/Http/HttpClientHandler.Windows.cs

dort ist diese Eigenschaft durchaus verwendbar

16.807 Beiträge seit 2008
vor 7 Jahren

Fast. UWP selbst kann kein .NET Core sein, weil .NET Core keine UI kennt und dementsprechend auch weder UWP, noch XAML noch andere UIs unterstützt.
Die Bibliotheken für UWP können aber .NET Core sein - genauer gesagt der .NET Standard Platform 1.6 entsprechen.

Der Hinweis auf UWP war jedoch aller höchste Eisenbahn und hätte in den Startpost gehört.

Ich würde in diesem Fall einfach das logische Element dazu selbst schreiben.
Korrigierte Fassung von unten sollte es eigentlich tun. Jedenfalls tut sie das testweise bei einem Digest Testauth hier lokal.

            Uri uri = new Uri("<soap url>");
            HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
            CredentialCache credentials = new CredentialCache {{uri, "Digest", new NetworkCredential("user", "pw")}};
            request.Credentials = credentials;

Ob jetzt all diese Klassen in den 4 Zeilen Code in UWP zur Verfügung stehen; weiß ich nicht.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Sorry das nicht gleich eingangs erwähnt zu haben.

Was den Code angeht so sind die Klassen zwar alle vorhanden. Aber beim ausführen des Requests kommt es wieder zu einer PlatformNotSupportedException mit der Message:

The value 'System.Net.CredentialCache' is not supported for property 'Credentials'.

Aber da es sich hier auch wieder um eine PlatformNotSupportedException handelt und der Vollständigkeitshalber, kann es evtl. mit der Targetplatform zutun haben?

Target: Universal Windows
Target Version: Windows 10 (10.0; Build 10240)
Min Version: Windows 10 (10.0; Build 10240)

16.807 Beiträge seit 2008
vor 7 Jahren

In UWP muss man wohl mit HttpClient und HttpBaseProtocolFilter arbeiten, wenn man Credentials setzen will.
Jedenfalls hab ich das aus der MSDN.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Ok, habs jetzt nochmal mit dem Windows.Web.HttpClient versucht.

Interessanter weise ploppt trotz setzen von AllowUI auf false ein AnmeldeDialog auf.

Was zu bemerken war ist, das sich dieser Client ähnlich wie die anderen Lösungen verhielt.
Sprich,
Alle <30 Sekunden ein Request, StatusCode: 200.
>30 Sekunden: StatusCode: 401 mit Dialog zur Anmeldung.

Auch habe ich zusätzlich noch die Capability Privat Networks (Client & Server) wie in dem verlinkten Beitrag erwähnt hinzugefügt.

Hier ist der Code:


private async Task TryItWithHttpClient2Async(string userName, string password)
        {
            for (int i = 0; i < 40; i++)
            {
                using (var filter = new Windows.Web.Http.Filters.HttpBaseProtocolFilter())
                {
                    
                    filter.AllowUI = false;
                    filter.ServerCredential = new Windows.Security.Credentials.PasswordCredential()
                    {
                        UserName = userName,
                        Password = password,
                        Resource = "fritz.box"
                    };
                    using (Windows.Web.Http.HttpClient client = new Windows.Web.Http.HttpClient())
                    {
                        string url = null;
                        using (var message = new Windows.Web.Http.HttpRequestMessage())
                        {
                            message.Method = new Windows.Web.Http.HttpMethod("POST");
                            message.RequestUri = new Uri("http://fritz.box:49000/upnp/control/x_contact");
                            message.Content = new Windows.Web.Http.HttpStringContent(
                                "<?xml version=\"1.0\" encoding=\"utf-8\"?><soap-env:Envelope encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\" xmlns:soap-env=\"http://schemas.xmlsoap.org/soap/envelope/\"><soap-env:Body><GetCallList xmlns=\"urn:dslforum-org:service:X_AVM-DE_OnTel:1\" /></soap-env:Body></soap-env:Envelope>",
                                Windows.Storage.Streams.UnicodeEncoding.Utf8,
                                "text/xml");
                            message.Headers.Add("SOAPAction", "urn:dslforum-org:service:X_AVM-DE_OnTel:1#GetCallList");
                            using (var responseMessage = await client.SendRequestAsync(message))
                            {
                                Debug.WriteLine($"{message.RequestUri}, StatusCode: {responseMessage.StatusCode}");
                                if (responseMessage.IsSuccessStatusCode)
                                {
                                    using (var responseInputStream = await responseMessage.Content.ReadAsInputStreamAsync().AsTask())
                                    {
                                        using (var responseStream = responseInputStream.AsStreamForRead())
                                        {
                                            using (var responseStreamReader = new StreamReader(responseStream))
                                            {
                                                var soapXml = await responseStreamReader.ReadToEndAsync();
                                                var xml = new XmlDocument();
                                                xml.LoadXml(soapXml);

                                                var newCallListURL = xml.SelectSingleNodeNS("//s:Envelope/s:Body/u:GetCallListResponse/NewCallListURL", "xmlns:s='http://schemas.xmlsoap.org/soap/envelope/' xmlns:u='urn:dslforum-org:service:X_AVM-DE_OnTel:1'");
                                                url = newCallListURL?.InnerText;
                                            }
                                        }
                                    }
                                }
                            }
                        }

                        if (url != null)
                        {
                            Uri uri = new Uri(url);
                            Debug.WriteLine($"URL: {uri}");
                            using (var responseMessage = await client.GetAsync(uri))
                            {
                                Debug.WriteLine($"URL: {uri}, StatusCode: {responseMessage.StatusCode}");
                                if (responseMessage.IsSuccessStatusCode)
                                {
                                    var callListSerializer = new XmlSerializer(typeof(AVMCallList));
                                    using (var responseInputStream = await responseMessage.Content.ReadAsInputStreamAsync())
                                    {
                                        using (var responseStream = responseInputStream.AsStreamForRead())
                                        {
                                            var callList = callListSerializer.Deserialize(responseStream) as AVMCallList;
                                            if (callList != null)
                                            {
                                                Debug.WriteLine($"Calls: { callList.Calls.Count }");
                                            }
                                        }
                                    }

                                }
                            }
                        }
                    }
                }
                int delay = i * 1000;
                Debug.WriteLine($"Wartezeit: { delay / 1000 } Sekunde(n).");
                await Task.Delay(delay);
            }
        }

Jetzt habe ich aber gesehen das es mit Build 12295 in diesem Szenario eine Möglichkeit gibt den Cache auch zu leeren. Heureka

Dies geschieht mittels:

filter.ClearAuthenticationCache();

Jetzt verhält sich der Client ansatzweise wie erwartet. Unabhängig von der ersten Anmeldung werde ich jetzt jedesmal nach Credentials gefragt.
Trotz wie gesagt AllowUI=false. Fast so als könne er mit den mitgegebenen Credentials noch nix anfangen. Ich habe für den Resourcennamen verschiedene Werte ausprobiert, jedoch ohne Erfolg.

Ausprobiert habe ich die Werte:


fritz.box
fritz.box:49000
fritz.box:49000/[UPnP-Path]
http://fritz.box
http://fritz.box:49000
http://fritz.box:49000/[UPnP-Path]

16.807 Beiträge seit 2008
vor 7 Jahren

Kleiner Tipp für die Code-Übersichtlichkeit: Du kannst bei usings die Klammern weglassen.

using (var responseInputStream = await responseMessage.Content.ReadAsInputStreamAsync())
using (var responseStream = responseInputStream.AsStreamForRead())
using( noch mehr usings.. )
{
   // ..
}

Der Code wird dadurch nicht schlechter, aber Du sparst Dir Einrückungen.
Einer der sehr sehr sehr wenigen Fällen, bei der meiner Meinung nach die Klammern weggelassen werden können

1.040 Beiträge seit 2007
vor 7 Jahren

@Abt: Das funktioniert allerdings auch nur, wenn in dem using kein weiterer Code ist, sprich nur ein weiteres using.

16.807 Beiträge seit 2008
vor 7 Jahren

Jo, das ist ja hier der Fall.. drei Mal sogar.

C
Chronos Themenstarter:in
132 Beiträge seit 2008
vor 7 Jahren

Dank für den Hinweis, werd ich berücksichtigen.

Soweit habe ich es jetzt auch zum funktionieren gebracht. Es funktioniert aber nur mit ClearAuthenticationCache

Was ich nur noch nicht ganz verstehe ist:

HttpBaseProtocolFilter.CacheControl.ReadBehavior/WriteBehavior kann man ja die Werte HttpCacheReadBehavior.NoCache/HttpCacheWriteBehavior.NoCache zuweisen. Also für mein Verständnis müsste man doch auch damit verhindern können den Cache zu nutzen. Oder verstehe ich das falsch?

Ich finde es schon ein wenig merkwürdig solch eine in meinen Augen Elementare Funktion nachzuliefern so dass man dafür die Targetversion anheben muss. Oder ist es so unüblich geworden sich auch mal abzumelden? Und ja, die Frage ist ernst gemeint ^^