salve a tutti,
sono un niubbio totale in WCF e web services, ed ora devo provvedere a scaricare dei movimenti alberghieri all'ufficio ISTAT locale utilizzando un loro nuovo web service SOAP 1.1
il dominio di test e' https://rerprod.turitweb.it/ws/checkinV1 e la relativa definizione https://rerprod.turitweb.it/ws/checkinV1?wsdl
userName = "dataentry2"
password = "dataentry"

la documentazione molto stringata che hanno fornito indica::
authentication method is HTTP Basic, con credenziali codificate con Base64 encoding, con header esemplificativo:
POST /ws/checkinV1 HTTP/1.1
Accept: text/xml, multipart/related
Authorization: Basic TXlVc2VybmFtZTpNeVBhc3N3b3Jk
Content-Type: text/xml; charset=utf-8
SOAPAction: ""
User-Agent:JAX-WS RI 2.2.9-b130926.1035
Host: rerprod.turitweb.it
Connection: keep-alive
Content-Length: 985
<?xml version="1.0"?>
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
    <S:Body>
        <ns2:inviaMovimentazione .........


ho definito un semplicissimo e ridotto modulo di test che dovrebbe inviare un campione (gia' corretto) di dati;
mi sono connesso al wsdl per far generare a Visual Stuzio il proxie locale ed ho scritto le mie 5 triviali righe di codice per connettermi con la basic authentication utilizzando BasicHttpBinding (SOAP 1.1), con security mode System.ServiceModel.BasicHttpSecurityMode.Transport, che (come indicato in https://msdn.microsoft.com/en-us/library/ff647503.aspx) assicura che le credenziali siano trasportate con Base64 encoding, e mi pare che le specifiche siano cosi' state soddisfatte;

il config generato da Visual Studio generated e' come segue
<?xml version="1.0" encoding="utf-8"?>
<configurationSnapshot xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="urn:schemas-microsoft-com:xml-wcfconfigurationsnapshot">
    <behaviors />
    <bindings>
        <binding digest="System.ServiceModel.Configuration.BasicHttpBindingElement, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089:&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Data name="GestioneCheckinV1ImplServiceSoapBinding"&gt;&lt;security mode="Transport" /&gt;&lt;/Data&gt;" bindingType="basicHttpBinding" name="GestioneCheckinV1ImplServiceSoapBinding" />
        <binding digest="System.ServiceModel.Configuration.BasicHttpBindingElement, System.ServiceModel, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089:&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Data name="GestioneCheckinV1ImplServiceSoapBinding1" /&gt;" bindingType="basicHttpBinding" name="GestioneCheckinV1ImplServiceSoapBinding1" />
    </bindings>
    <endpoints>
        <endpoint normalizedDigest="&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Data address="https://rerprod.turitweb.it/ws/checkinV1" binding="basicHttpBinding" bindingConfiguration="GestioneCheckinV1ImplServiceSoapBinding" contract="ServiceReference1.GestioneCheckinV1Impl" name="GestioneCheckinV1ImplPort" /&gt;" digest="&lt;?xml version="1.0" encoding="utf-16"?&gt;&lt;Data address="https://rerprod.turitweb.it/ws/checkinV1" binding="basicHttpBinding" bindingConfiguration="GestioneCheckinV1ImplServiceSoapBinding" contract="ServiceReference1.GestioneCheckinV1Impl" name="GestioneCheckinV1ImplPort" /&gt;" contractName="ServiceReference1.GestioneCheckinV1Impl" name="GestioneCheckinV1ImplPort" />
    </endpoints>
</configurationSnapshot>


ed il mio codice
namespace TestIstat
{
    class Program
    {
        static void Main(string[] args)
        {

            //utilizzo di un web service presso la regione emilia romagna
            //https://rerprod.turitweb.it/ws/checkinV1 - https://rerprod.turitweb.it/ws/checkinV1?wsdl

            string userName = "dataentry2";
            string Password = "dataentry";

            string Domain = "http://rerprod.turitweb.it/ws/checkinV1";

            int TimeOut = 10;

            // minimal working data load
            ServiceReference1.xmlImportDati importDati = LoadImportDati();

            System.ServiceModel.EndpointAddress address = new System.ServiceModel.EndpointAddress(Domain);

            //'BasicHttpBinding supports SOAP 1.1. WsHttpBinding is for 1.2
            System.ServiceModel.BasicHttpBinding binding = new System.ServiceModel.BasicHttpBinding();

            binding.OpenTimeout = TimeSpan.FromSeconds(TimeOut);
            binding.CloseTimeout = TimeSpan.FromSeconds(TimeOut);
            binding.SendTimeout = TimeSpan.FromSeconds(TimeOut);
            binding.ReceiveTimeout = TimeSpan.FromSeconds(TimeOut);
           
            binding.Security.Transport.ClientCredentialType = System.ServiceModel.HttpClientCredentialType.Basic;
            binding.Security.Mode = System.ServiceModel.BasicHttpSecurityMode.Transport;

            ServiceReference1.GestioneCheckinV1ImplClient serviceClient = new ServiceReference1.GestioneCheckinV1ImplClient(binding, address);
            serviceClient.ClientCredentials.UserName.UserName = userName;
            serviceClient.ClientCredentials.UserName.Password = Password;

            try
            {
                serviceClient.Open();

                List<ServiceReference1.risultatoGiorno> results = serviceClient.inviaMovimentazione(importDati).ToList();
                foreach (ServiceReference1.risultatoGiorno result in results)
                {
                    Console.WriteLine(result.ToString());
                }

            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                if ((serviceClient != null))
                    serviceClient.Abort();
            }
            if ((serviceClient != null))
                serviceClient.Close();
            serviceClient = null;

        }

        private static ServiceReference1.xmlImportDati LoadImportDati()  {

            ServiceReference1.xmlImportDati importDati = new ServiceReference1.xmlImportDati();

            importDati.codice = "B01205";
            importDati.prodotto = "S0011";

            List<ServiceReference1.movimentoBean> movList = new List<ServiceReference1.movimentoBean>();
            ServiceReference1.movimentoBean mov = new ServiceReference1.movimentoBean();

            mov.data = "20170210";
            mov.struttura = new ServiceReference1.strutturaBean();
            mov.struttura.apertura = "NO";
            mov.struttura.cameredisponibili = "36";
            mov.struttura.camereoccupate = "0";
            mov.struttura.lettidisponibili = "57";
            movList.Add(mov);

            importDati.movimento = movList.ToArray();

            return importDati;

        }
    }
}


ma purtroppo mi viene sollevata l'eccezione "The HTTP request was forbidden with client authentication scheme 'Basic'."

ho chiesto agli sviluppatori, e loro, dopo piu' di un mese, mi hanno fornito un "workarount", che personalmente NON capisco e che mi sembra un hack...

il loro codice diventa
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Net.Http;
using System.Net;
using System.Net.Http.Headers;
using System.Web;
using System.IO;
using System.ServiceModel;
//using System.Net.Http;
using System.ServiceModel.Channels;


namespace TestIstat
{
    class Program
    {
        static void Main(string[] args)
        {

            //utilizzo di un web service presso la regione emilia romagna
            //https://rerprod.turitweb.it/ws/checkinV1 - https://rerprod.turitweb.it/ws/checkinV1?wsdl

            // minimal working data load
            ServiceReference1.xmlImportDati importDati = LoadImportDati();
            ServiceReference1.GestioneCheckinV1ImplClient serviceClient = new ServiceReference1.GestioneCheckinV1ImplClient();
            
            //Credenziali Autenticazione web service.
            serviceClient.ClientCredentials.UserName.UserName = "dataentry2";
            serviceClient.ClientCredentials.UserName.Password = "dataentry";
            
            using (OperationContextScope scope = new OperationContextScope(serviceClient.InnerChannel))
            {
                var httpRequestProperty = new HttpRequestMessageProperty();
                //Inesrisco nell'header http l'autenticazione : Authorization : Basic Username:password(converrtiti in base 64) 
                httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "Basic " +
                             Convert.ToBase64String(Encoding.ASCII.GetBytes(serviceClient.ClientCredentials.UserName.UserName + ":" +
                             serviceClient.ClientCredentials.UserName.Password));
                OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;

                try
                {
                    List<ServiceReference1.risultatoGiorno> results = serviceClient.inviaMovimentazione(importDati).ToList();
                    foreach (ServiceReference1.risultatoGiorno result in results)
                    {
                        Console.WriteLine(result.ToString());
                    }

                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                    if ((serviceClient != null))
                        serviceClient.Abort();
                }
                if ((serviceClient != null))
                    serviceClient.Close();
                serviceClient = null;

            }
        }

        private static ServiceReference1.xmlImportDati LoadImportDati()  {

            ServiceReference1.xmlImportDati importDati = new ServiceReference1.xmlImportDati();

            importDati.codice = "B01205";
            importDati.prodotto = "S0011";

            List<ServiceReference1.movimentoBean> movList = new List<ServiceReference1.movimentoBean>();
            ServiceReference1.movimentoBean mov = new ServiceReference1.movimentoBean();

            mov.data = "20170210";
            mov.struttura = new ServiceReference1.strutturaBean();
            mov.struttura.apertura = "NO";
            mov.struttura.cameredisponibili = "36";
            mov.struttura.camereoccupate = "0";
            mov.struttura.lettidisponibili = "57";
            movList.Add(mov);

            importDati.movimento = movList.ToArray();

            return importDati;

        }
    }
}


dove usano OperationContextScope per effettuare l'injection dell'header con le credenziali utente invece di utilizzare un metodo a me piu' tradizionale

domanda: e' veramente necessario scriverlo cosi'? o e' un hack risolvibile in maniera piu' tradizionale?

se cosi' non fosse ed andasse mantenuto quel codice, posso poi modificare il dominio da quello di test a quello di produzione a runtime con un
serviceClient.Endpoint.Address = new System.ServiceModel.EndpointAddress(Domain);


e finire di definire anche il timeout come segue
serviceClient.ClientCredentials.UserName.UserName = userName;
serviceClient.ClientCredentials.UserName.Password = userPassword;
serviceClient.Endpoint.Binding.CloseTimeout = TimeSpan.FromSeconds(TimeOut);
serviceClient.Endpoint.Binding.OpenTimeout = TimeSpan.FromSeconds(TimeOut);
serviceClient.Endpoint.Binding.ReceiveTimeout = TimeSpan.FromSeconds(TimeOut);
serviceClient.Endpoint.Binding.SendTimeout = TimeSpan.FromSeconds(TimeOut);


?

tra l'altro, lo sviluppatore mi dice che il suo codice "dovrebbe" funzionare anche con il dominio https di produzione (non l'ha provato)
grazie a tutti in anticipo
--
Andrea Montanari

Andrea Montanari
http://www.hotelsole.com - http://www.hotelsole.com/asql/index.php
10.265 messaggi dal 09 febbraio 2002
Contributi
Ciao Andrea,
il problema di fondo è che la loro implementazione della Basic Authentication è incompleta. Faglielo presente, in modo che altri sviluppatori non incappino nello stesso problema. Il workaround non sarebbe necessario.

Partiamo dal principio: anche se hai indicato username e password in questo modo, la richiesta NON conterrà quelle credenziali.

serviceClient.ClientCredentials.UserName.UserName = userName;
serviceClient.ClientCredentials.UserName.Password = Password;


Il client invia una prima richiesta senza credenziali, perché vuol vedere cosa risponde il server a fronte di una richiesta anonima. E' il meccanismo challenge-response descritto dalla specifica:

The 401 (unauthorized) response message is used by an origin server
to challenge the authorization of a user agent. This response must
include a WWW-Authenticate header field
containing at least one
challenge applicable to the requested resource.


Solo all'arrivo della risposta 401 che contiene l'intestazione WWW-Authenticate, allora il client proverà con una seconda richiesta inviando le credenziali in accordo con lo schema di autenticazione indicato da WWW-Authenticate.

Ora, provando ad inviare una richiesta al loro servizio, si osserva che:
  • Il server risponde con lo status 403 anziché 401. Anche se rispondere 403 non viola la specifica, ha un altro uso rispetto alla situazione attuale:
    The server understood the request, but is refusing to fulfill it. Authorization will not help.

    ...e qui invece autenticarsi aiuterebbe, eccome.
  • Non stanno includendo l'intestazione WWW-Autheticate nella risposta.


Il workaround che ti hanno fornito costringe il client ad inviare sempre e comunque le credenziali, già dalla prima richiesta. Non è propriamente un hack... diciamo che è una pezza.



se cosi' non fosse ed andasse mantenuto quel codice, posso poi modificare il dominio da quello di test a quello di produzione a runtime con un

Sì, volendo potresti mettere la configurazione anche nel web.config, ed usare le web.config transformations per alterare i valori nel momento in cui pubblichi il sito per andare in produzione.

Se preferisci configurare il client via codice C# puoi farlo comunque. Al limite usa una direttiva #if DEBUG per evitare di dover cambiare l'url ogni volta che vuoi lanciare l'applicazione in debug.

#if DEBUG
//Siamo in debug
string url = "http://sito.di.test/";
#else
//Siamo in produzione
string url = "https://sito.di.produzione/";
#endif


ciao,
Moreno
Modificato da BrightSoul il 06 settembre 2017 14.18 -

Enjoy learning and just keep making
44 messaggi dal 09 febbraio 2005
dubbio :
... ma dichiarando :

security mode="Transport" non si dovrebbe usare il sito con https ?

se ci si vuole collegare al sito in http non si dovrebbe dichiarare :
security mode="TransportCredentialOnly" ?

Insomma personalmente proverei cosi :

per il Ws esposto in https:
-----------------
....
<security mode="Transport"
<transport clientCredentilType="Basic" </transport>
</security>

per il Ws esposto in http:
-----------------
....
<security mode="TransportCredentialOnly"
<transport clientCredentilType="Basic" </transport>
</security>
salve a tutti,
e grazie per l'interessamento.... scusate il ritardo, ma torno a voi dopo un weekend del MotoGP di Misano che qui a Riccione e' stato molto interessante ed importante, sotto tutti i punti di vista :)

ho letto anche altre "cose", e tutti concordano con quanto indicato da BrigthSoul, su una quanto meno "azzardata" e comunque "non standard" implementazione della Basic Authentication...
ora sto preparando una batteria di test per altre problematiche che sto man mano riscontrando, in modo da mandare "il pacchetto completo" al dev, cosi' sicuramente mi manda a quel paese :)

tornando all'autenticazione, visto che suppongo la manterranno inalterata dato che hanno gia' divulgato la "documentazione", non ho ancora provato la connessione al WS di produzione, che si trovera' esposto in HTTPS, ma vorrei sperare di gestirla come segue:

// questi sono parametri passati alla procedura recuperati ovviamente dalla base dati,
// dove poi in produzione saranno sostituiti dal server HTTPS e dalla credenziali reali personali
String Domain = "http://rerprod.turitweb.it/ws/checkinV1";
String userName = "dataentry2";
String Password = "dataentry";
int TimeOut = 15; // '5 secondi????
// \questi sono parametri passati alla procedura recuperati ovviamente dalla base dati


//utilizzo di un web service presso la regione emilia romagna
//https://rerprod.turitweb.it/ws/checkinV1 - https://rerprod.turitweb.it/ws/checkinV1?wsdl


//' https://msdn.microsoft.com/en-us/library/ff647503.aspx
//' Transport security. When using transport security, the user credentials and claims are passed by using the transport layer. In other words, user credentials are transport-dependent, 
//'       which allows fewer authentication options compared to message security.
//' Basic. This option is available with the HTTP protocol only. The client is authenticated by using the username and password against the Microsoft Active Directory® directory service.
//'       The client credentials are transported by using a Base64 encode string, which is very similar to a clear string and therefore not the most secure option. The service is authenticated by the Secure Sockets Layer (SSL) certificate used for secure communication.


// HACK: Web Service ISTAT
// il web service NON risponde correttamente alla transazione e le credenziali NON vengono passate quindi e' necessario 
// fare injection dell'header con le stesse credenziali prima di ogni metodo di connessione
//
// https://stackoverflow.com/questions/897782/how-to-add-custom-http-header-for-c-sharp-web-service-client-consuming-axis-1-4
// http://forum.aspitalia.com/forum/post/417739/Autenticazione-WCF-Client-Side.aspx
//
// quindi non e' possibile utilizzare i tradizionali metodi di binding con impostazione della tipologia
// ma li userei comunque per l'impostazione dei timeout
//BasicHttpBinding supports SOAP 1.1. WsHttpBinding is for 1.2
// SOAP 1.1
System.ServiceModel.BasicHttpBinding binding = new System.ServiceModel.BasicHttpBinding();
// SOAP 1.2>
//System.ServiceModel.WSHttpBinding binding = new System.ServiceModel.WSHttpBinding();


//'5 secondi?
binding.OpenTimeout = TimeSpan.FromSeconds(TimeOut);
binding.CloseTimeout = TimeSpan.FromSeconds(TimeOut);
binding.SendTimeout = TimeSpan.FromSeconds(TimeOut);
binding.ReceiveTimeout = TimeSpan.FromSeconds(TimeOut);


// magari ripetitivo ma...
System.ServiceModel.EndpointAddress address = new System.ServiceModel.EndpointAddress(Domain);

// magari ripetitivo ma...
ServiceReference1.GestioneCheckinV1ImplClient serviceClient = new ServiceReference1.GestioneCheckinV1ImplClient(binding, address);

// e ridondante... sperando che serva...
serviceClient.Endpoint.Address = new System.ServiceModel.EndpointAddress(Domain);
serviceClient.ClientCredentials.UserName.UserName = userName;
serviceClient.ClientCredentials.UserName.Password = Password;


in questo modo spererei di risolvere...
ulteriori consigli/correzioni?
grazie a tutti
--
Andrea

Andrea Montanari
http://www.hotelsole.com - http://www.hotelsole.com/asql/index.php
10.265 messaggi dal 09 febbraio 2002
Contributi
Ciao,


tornando all'autenticazione, visto che suppongo la manterranno inalterata dato che hanno gia' divulgato la "documentazione"

Beh, se sistemassero l'implementazione non sarebbe una breaking change. Sono ottimista!
Se nella documentazione citano la risposta 403 dovrebbero giusto correggerla in 401.


in modo da mandare "il pacchetto completo" al dev, cosi' sicuramente mi manda a quel paese :)

Non sarà felicissimo, questo te lo garantisco :D Così lo incanali verso la via più facile che è: "Giusta o sbagliata, questa è la nostra implementazione. Conformati."

Di solito funziona meglio così: c'è un problema -> ok risolto -> grazie! -> c'è un problema -> ok risolto -> grazie! -> c'è un problema...
Se anche ad un certo punto si stufasse, ormai avete già risolto i problemi più importanti.

Non ti so dare opinioni su come configurare i timeout. Personalmente non mi è mai capitato di doverli modificare ma credo che tutto dipenda dalle prestazioni offerte dal servizio.



// e ridondante... sperando che serva...
serviceClient.Endpoint.Address = new System.ServiceModel.EndpointAddress(Domain);

Questo non serve, tanto hai già passato l'indirizzo al costruttore di GestioneCheckinV1ImplClient.

ciao ciao,
Moreno

Enjoy learning and just keep making

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.