menu di navigazione del network

SDICoop: configurazione PHP SoapClient / SoapServer (Apache) per invio e ricezione di test

@omardini il KitDiTest.zip ti viene fornito dall’Agenzia delle Entrate richiedendo l’accreditamento del canale con questa procedura:

http://sdi.fatturapa.gov.it/SdI2FatturaPAWeb/AccediAlServizioAction.do?pagina=accreditamento_canale

si, ho risolto scrivendo un wrapper del SoapClient di php per supportare i diversi ceritificati e lo standard MTOM .

condivido il tutto così magari mi aiuti ad affinarlo e testarlo…

SdiSoapClient.php

 <?php 

class SdiSoapClient extends \SoapClient
{
    const USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)';
    const REGEX_ENV  = '/<soap[\s\S]*nvelope>/i';
    const REGEX_XOP  = '/<xop:include[\s\S]*cid:%s@[\s\S]*?<\/xop:Include>/i';
    const REGEX_CID  = '/cid:([0-9a-zA-Z-]+)@/i';
    const REGEX_CON  = '/Content-ID:[\s\S].+?%s[\s\S].+?>([\s\S]*?)--MIMEBoundary/i';
    
    /**
     * Path al certificato della CA
     * @var string
     */
    public $caCertFile;
    
    /**
     * Path alla chiave privata
     * @var string
     */
    public $privateKeyFile;
    
    /**
     * Path al certificato client
     * @var string
     */
    public $clientCertFile;
    
    /**
     * Url del proxy (es. host:port)
     * @var string
     */
    public $proxyUrl;
    
    /**
     * Autenticazione del proxy (es. username:password)
     * @var string
     */
    public $proxyAuth;
    
    /**
     * Headers dell'ultima richiesta
     * @var array
     */
    private $lastRequestHeaders;
    
    /**
     * Headers dell'ultima risposta
     * @var array
     */
    private $lastResponseHeaders;
    
    /**
     * Body dell'ultima richiesta
     * @var string
     */
    private $lastRequestBody;
    
    /**
     * Body dell'ultima risposta
     * @var string
     */
    private $lastResponseBody;
    
    
    /**
     * @inheritdoc
     */
    public function __doRequest($request, $location, $action, $version, $one_way = null)
    {
        // reset
        $this->lastResponseBody = '';
        $this->lastResponseHeaders = array();

        $this->lastRequestHeaders = array(
            'Content-type: text/xml;charset="utf-8"',
            'Accept: text/xml',
            'Cache-Control: no-cache',
            'Pragma: no-cache',
            'SOAPAction: '.$action,
            'Content-length: ' . strlen($request),
        );
        $this->lastRequestBody = $request;
        
        $ch = curl_init();
        
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_SSL_ENABLE_ALPN, false);
        curl_setopt($ch, CURLOPT_SSLKEY, $this->privateKeyFile);
        curl_setopt($ch, CURLOPT_SSLCERT, $this->clientCertFile);
        curl_setopt($ch, CURLOPT_CAINFO, $this->caCertFile);

        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
        curl_setopt($ch, CURLOPT_URL, $location);
        curl_setopt($ch, CURLOPT_POST , true);
        
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->lastRequestHeaders);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $this->lastRequestBody);
        
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, array(&$this, 'handleHeaderLine'));
        
        if( !empty($this->proxyUrl) ){
            curl_setopt($ch, CURLOPT_PROXY, $this->proxyUrl);
            if( !empty($this->proxyAuth) ){
                curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxyAuth);
            }
        }
        
        $this->lastResponseBody = curl_exec($ch);
        
        if ( false === $this->lastResponseBody ) {
            $err_num  = curl_errno($ch);
            $err_desc = curl_error($ch);
            $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            
            curl_close($ch);
            
            throw new \Exception('[HTTP:'. $httpcode .'] ' . $err_desc, $err_num);
        }
        
        curl_close($ch);
        
        $this->lastResponseBody = $this->__processResponse($this->lastResponseBody);
        
        return $this->lastResponseBody;
    }
    
    /**
     * Processa la risposta per supportare il formato MTOM
     * NB teniamo il metodo pubblico per favorire i test unitari
     * @param string $response
     * @throws \Exception
     * @return string
     */
    public function __processResponse($response){
        
        $xml_response = null;
        
        // recupera la risposta xml isolandola da quella mtom
        preg_match(self::REGEX_ENV, $response, $xml_response);
        
        if ( !is_array($xml_response) || count($xml_response) <= 0 ) {
            throw new \Exception('No XML has been found.');
        }
        // prendiamo il primo elemento dell'array
        $xml_response = reset($xml_response);

        // recuperiamo i tag xop
        $xop_elements = null;
        preg_match_all(sprintf(self::REGEX_XOP, '.*'), $response, $xop_elements);
        // prendiamo il primo elemento dell'array
        $xop_elements = reset($xop_elements);
        
        if ( is_array($xop_elements) && count($xop_elements) > 0 ) {
            foreach ($xop_elements as $xop_element) {
                
                // recuperiamo il cid
                $matches = null;
                preg_match(self::REGEX_CID, $xop_element, $matches);
                
                if( isset($matches[1]) ){
                    $cid = $matches[1];
                    
                    // recuperiamo il contenuto associato al cid
                    $matches = null;
                    preg_match(sprintf(self::REGEX_CON, $cid), $response, $matches);
                    
                    if( isset($matches[1]) ){
                        $binary = trim($matches[1]);
                        $binary = base64_encode($binary);
                        
                        // sostituiamo il tag xop:Include con base64_encode(binary)
                        // nota: SoapClient fa automaticamente il base64_decode(binary)
                        $old_xml_response = $xml_response;
                        $xml_response = preg_replace(sprintf(self::REGEX_XOP, $cid), $binary, $xml_response);
                        if( $old_xml_response === $xml_response ){
                            throw new \Exception('xop replace failed');
                        }
                    } else {
                        throw new \Exception('binary not found.');
                    }
                } else {
                    throw new \Exception('cid not found.');
                }
            }
        }
        
        return $xml_response;
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastRequestHeaders(){
        return implode("\n", $this->lastRequestHeaders);
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastResponseHeaders(){
        return implode("\n", $this->lastResponseHeaders);
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastRequest(){
        return $this->lastRequestBody;
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastResponse(){
        return $this->lastResponseBody;
    }
    
    /**
     * Handle singolo header richiesta cURL
     * return integer
     */
    public function handleHeaderLine($curl, $header_line){
        $this->lastResponseHeaders[] = $header_line;
        return strlen($header_line);
    }
}

esempio di utilizzo

// endpoint di test
$location = 'https://testservizi.fatturapa.it/ricevi_file';

// wsdl della soap dell'endpoint
$wsdl = '/wsdl/SdIRiceviFile_v1.0.wsdl';

// opzioni SoapClient
$options = array(
        'location' => $location,
        'cache_wsdl' => WSDL_CACHE_NONE,
        'trace' => true,
);

$sdiSoapClient = new SdiSoapClient($wsdl, $options);
    
// certificato con le root concatenate (caentrate.der + CAEntratetest.cer)
$sdiSoapClient->caCertFile = '/certificati/CA_Agenzia_delle_Entrate_all.pem';

// chiave privata del tuo certificato client
$sdiSoapClient->privateKeyFile =  '/certificati/private.key';

// certificato client in formato PEM (ASCIII)
$sdiSoapClient->clientCertFile = '/certificati/SDI-XXXXXXXXX-Client.pem';
    
// path della fattura
$fattura = '/fatture/IT01234567890_11111_MT_001.xml';

$fileSdIAccoglienza = new \stdClass();
$fileSdIAccoglienza->NomeFile = basename($fattura);
$fileSdIAccoglienza->File = base64_encode(file_get_contents($fattura));

$result = $sdiSoapClient->RiceviFile($fileSdIAccoglienza);

per generare il certificato client in formato pem (SDI-XXXXXXXXX-Client.pem):

openssl x509 -inform der -in SDI-XXXXXXXXX-Client.cer -out SDI-XXXXXXXXX-Client.pem

per generare il certificato con le root concatenate (caentrate.der + CAEntratetest.cer)

copy /b caentrate.der+CAEntratetest.cer CA_Agenzia_delle_Entrate_all.pem

PS aprilo e verifica che le sezioni BEGIN e END non siano sovrapposte sulla stessa linea (-----END CERTIFICATE----------BEGIN CERTIFICATE-----), in tal caso mettile su due linee:

-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----

con questo sono riuscito ad inviare una fattura.

Per la ricezione lato server vedi: SDICoop: configurazione PHP SoapClient per invio di test

6 Likes

Bellissimo, questo ci potrebbe essere utile per https://github.com/italia/fatturapa-testsdi/issues/21 !

2 Likes

Ottimo, finalmente sono riuscito a testare la tua classe e sono riuscito a inviare la fattura, naturalmente la riga dove inserisci il bas64 del file va modificato per agganciare il contenuto del file, ad esempio:

$fileSdIAccoglienza->File = base64_encode(file_get_contents($fattura));
1 Like

ottimo! ho anche sistemato l’esempio originale.

tuttavia io sto invece sbattendo la testa sul supporto a MTOM necessario per implementare https://testservizi.fatturapa.it/ricevi_notifica

Oggi lavorerò a questo, presumo si debba creare il server SOAP che risponda agli endpoint indicati in fase di registrazione, qui c’è un esempio di come fare, appena ho le idee più chiare le condivido.

ciao @arolando ho aggiornato il SdiSoapClient (rivedi il mio messaggio sopra) aggiungendo il supporto al formato MTOM che prima non funzionava bene.

Ti consiglio di aggiornarti.

Per quanto riguarda la parte server puoi utilizzare tranquillamante il SoapServer di php, io ho fatto così e mi funziona tranquillamente.

RicezioneFattureService.php

class RicezioneFattureService
{
    const ER01 = 'ER01';

    public function RiceviFatture($parametersIn)
    {
        $rispostaRiceviFatture = new \stdClass();
        $rispostaRiceviFatture->Esito = self::ER01;
        return $rispostaRiceviFatture;
    }
    
    public function NotificaDecorrenzaTermini($parametersIn)
    {
        
    }
}

esempio di utilizzo

$wsdl = '/wsdl/RicezioneFatture_v1.0.wsdl';

$ricezioneFattureService = new RicezioneFattureService();

$soapServer = new \SoapServer($wsdl);
$soapServer->setObject($ricezioneFattureService);
$soapServer->handle();

la parte tosta è mettere i certificati sul server, se sei su Apache

SSLEngine On

# certificato firmato dall'Agenzia (fompato ASCII/PEM)
SSLCertificateFile "/cert/CERT/sdi/SDI-XXXXXXXXX-Server.pem"

# chiave privata usata per generare la richiesta di certificato
SSLCertificateKeyFile  "/cert/CERT/sdi/private.key"

# certificato dell’autorità di certificatione dell’agenzia
SSLCertificateChainFile "/cert/CERT/sdi/caentrate.der"

# concatenazione del certificato dell'autorità di certificazione dell'agenzia 
# più il certificato dell’autorità di certificazione di test (caentrate.der + CAEntrateTest.der). 
# In questo modo lo stesso server accetta le chiamate sia dal servizio di test che dal servizio di produzione
SSLCACertificateFile "/cert/CERT/sdi/CA_Agenzia_delle_Entrate_all.pem"

SSLStrictSNIVHostCheck off

per generare il certificato server in formato pem (SDI-XXXXXXXXX-Server.pem):

openssl x509 -inform der -in SDI-XXXXXXXXX-Server.cer -out SDI-XXXXXXXXX-Server.pem

per generare il certificato con le root concatenate (caentrate.der + CAEntratetest.cer)

copy /b caentrate.der+CAEntratetest.cer CA_Agenzia_delle_Entrate_all.pem

PS aprilo e verifica che le sezioni BEGIN e END non siano sovrapposte sulla stessa linea (-----END CERTIFICATE----------BEGIN CERTIFICATE-----), in tal caso mettile su due linee:

-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
1 Like

Ciao @cesco69, stavo provando ad utilizzare la classe SoapClient per cercare di inviare la prima fattura.

Anche io ottengo l’errore:
[HTTP:0] unable to set private key file: 'mykey.key' type PEM

Questo file nel tuo esempio si chiama /certificati/private.key. Il mio mykey.key è la chiave che ho utilizzato per generare il file csr che ho caricato in fase di accreditamento. ps: quando mi chiedeva i file client e server, ho dato lo stesso .csr che avevo generato.

Quindi mykey.key è stato generato con:
openssl genrsa –out mykey.key 2048

Sbaglio file… ?

grazie mille!

1 Like

@Francesco_Biegi, devi usare SdiSoapClient, se usi la SoapClient php ti da errore.

@Francesco_Biegi, strano… se apri con notepad la tua private key (mykey.key) è in formato PEM (ASCII)?
dovrestri trovare :

-----BEGIN RSA PRIVATE KEY-----
(stringa base64)
-----END RSA PRIVATE KEY-----

Nel caso in cui non fosse in formato PEM, dovresti generare la versione pem:

openssl rsa -in mykey.key -outform PEM -out mykey.pem

Nel caso in cui fosse in formato PEM puoi verificare che stai assegnando la mykey.key alla property SdiSoapClient->privateKeyFile

In ogni caso fai attenzione che tutti i certificati passati a SdiSoapClient devono essere PEM

Lerrore cmq è dato da cURL che non riconosce la chiave privata associata al certificato o non riesce a leggerla, https://www.google.it/search?q=curl+“unable+to+set+private+key+file”

@arolando sono abbastanza cotto, è da stamani che sto facendo le prove… volevo scrivere SdiSoapClient e non SoapClient :frowning:

Cercando un pò su internet ci dovrebbe essere un modo per “capire” se le combinazioni sono giuste, e bisognerebbe eseguire questi due comandi:

openssl x509 -noout -modulus -in SDI-XXX-Client.pem | openssl md5
openssl rsa -noout -modulus -in mykey.key | openssl md5

Se questi due comandi restituiscono lo stesso md5, si sta utilizzando la coppia corretta.
A me ovviamente da due md5 diversi ma… non riesco a capire se è corretto che sia cosi (ovvero, quei due comandi non c’entrano niente con l’argomenti…) Oppure se effettivamente sto sbagliando i file.

Tu potresti fare una prova e mi dici se a te danno lo stesso md5?

Se a te funziona e l’md5 che ti restituisce non è lo stesso, devo capire dove sbaglio…

grazie

@Francesco_Biegi, i miei md5 sono uguali.

Questo mi conforta (fino ad un certo punto :D) quindi prima devo capire come far combaciare questi due valori… ora inizio a metterci dentro i file a caso…

@Francesco_Biegi, anche i miei md5 sono uguali.

Grazie ad entrambi che avete fatto la prova. Quindi devo capire come mai i miei valori sono deversi. Cmq, all’interno di mykey ho -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----

Che casino… :cry:

Il pem lo hai generato come da istruzioni di @cesco69?

openssl x509 -inform der -in SDI-XXXXXXXXX-Client.cer -out SDI-XXXXXXXXX-Client.pem

ma sei sicuro che la chiave che stai utilizzando è la stessa con la quale hai generato la request per ottenere i certificati client server?

Ho trovato il motivo.
Se confronto il mykey.key generato in ambiente di produzione, il suo md5 combacia con il file “Certificato Client” che ho scaricato da sdi.fatturapa.gov in Strumenti -> Certire il canale -> Test interoperabilità -> Downliad file per test.

Voi state state usando il .key generato dalla macchina di produzione… ?

Assolutamente si! :grinning: Probabilmente il tuo problema è quello.

Uhm… visto che la sezione sul sito si chiama “Download file per test” io stavo usando la parte “test”. Ora provo col file di produzione… tanto cambiando l’endpoint della chiamata dovrebbe andar bene… al massimo carico la prima fattura al mitico pippo in prod :unamused: