Dostęp do webservice przez PHP

0

GUS udostępnił nam coś takiego: TERYT

Wysłałem maila, przysłali mi dane logowania z linkami do WSDL i SVC. Jest tylko jeden problem - przykład, jaki podali na połączenie się z tą usługą jest w C#. Oto on:

try {
	var proxy = new ChannelFactory < ServiceReferenceWCF.ITerytWs1 > ("custom");
	proxy.Credentials.UserName.UserName = login;
	proxy.Credentials.UserName.Password = haslo;
	var result = proxy.CreateChannel();
	var test = result.CzyZalogowany();
} catch (Exception ex) {}

Dali też "Przykładowy fragment pliku konfiguracyjnego":

<client>
	<endpoint address="https://host/TerytWs1.svc" binding="customBinding" bindingConfiguration="custom" contract="ServiceReference1.ITerytWs1" name="custom" />
</client>

<bindings>
    <customBinding>
    	<binding name="custom">
    		<security defaultAlgorithmSuite="Default" authenticationMode="UserNameOverTransport" requireDerivedKeys="true" includeTimestamp="true" messageSecurityVersion="WSSecurity11WSTrustFebruary2005WSSecureConversationFebruary2005WSSecurityPolicy11BasicSecurityProfile10">
    			<localClientSettings detectReplays="false" />
    			<localServiceSettings detectReplays="false" />
    		</security>
    		<textMessageEncoding messageVersion="Soap11WSAddressing10" />
    		<httpsTransport maxReceivedMessageSize="2147483647" maxBufferPoolSize="2147483647" />
    	</binding>
    </customBinding>
</bindings>

Próbowałem na wszelkie sposoby połączyć się z tym przez SOAP, niestety bez powodzenia. Poniżej to, co już próbowałem:

$wsdl_proto = 'https';
$wsdl_host = 'host';
$wsdl_host_path = '/terytws1.wsdl';
$wsdl_url = $wsdl_proto.'://'.$wsdl_host.$wsdl_host_path;
$connectionPG = new SoapClient($wsdl_url, array(
	'login' => "login",
	'password' => "haslo",
	'trace' => 1,
));

try{
	$logged_in = $connectionPG->CzyZalogowany();
} catch (SoapFault $fault) {
	echo $fault->getMessage()."\n\n";
	echo "REQUEST HEADERS:\n" . $connectionPG->__getLastRequestHeaders() . "\n";
	echo "REQUEST:\n" . $connectionPG->__getLastRequest() . "\n";
	echo "Response headers:\n" . $connectionPG->__getLastResponseHeaders() . "\n";
	echo "Response:\n" . $connectionPG->__getLastResponse() . "\n";
}
$functions = $connectionPG->__getFunctions();
var_dump($functions);

Skrypt zawiesza się podczas próby wykonania funkcji CzyZalogowany(). Kiedy przekroczy timeout, wchodzi do sekcji catch. Response headers są puste. Kombinowałem z ustawieniem parametru location. Doszedłem do momentu, w którym dostałem odpowiedź, że metoda POST nie jest dopuszczalną metodą odwoływania się do tego pliku svc (dostępna była m.in GET). Ściągnąłem SoapUI. Tam udało mi się zajść jeszcze dalej. W odpowiedzi otrzymywałem błąd An error occurred when verifying security for the message.
No i na tym sprawa stanęła. Ma ktoś jakiś pomysł jak do tego podejść?

0

Trudno powiedzieć co się dzieje. SOAP w PHP ma sporo błędów które są poprawiane z wersji na wersję. Spróbuj przechwycić kopertę w metodzie __doRequest() i zobacz co w rzeczywistości wysyłasz. Możesz także spróbować zobaczyć tcpdumpem co wysyłasz i dostajesz w odpowiedzi.

Nie wiem co ten webserwis daje poza dostępem do bazy, ale może lepszym rozwiązaniem byłoby przechowywanie bazy TERYT lokalnie. W projekcie nad którym pracuję właśnie tak robimy. Możesz sobie zobaczyć jak to mamy zrobione: https://github.com/lmsgit/lms, należy przeszukać repozytorium szukając "teryt" lub "location", a w https://github.com/lmsgit/lms/blob/master/bin/lms-teryt jest zrobione zaciąganie bazy danych, mergowanie z istniejącymi adresami.

0
LionNet napisał(a):

Trudno powiedzieć co się dzieje. SOAP w PHP ma sporo błędów które są poprawiane z wersji na wersję. Spróbuj przechwycić kopertę w metodzie __doRequest() i zobacz co w rzeczywistości wysyłasz. Możesz także spróbować zobaczyć tcpdumpem co wysyłasz i dostajesz w odpowiedzi.

Nie wiem co ten webserwis daje poza dostępem do bazy, ale może lepszym rozwiązaniem byłoby przechowywanie bazy TERYT lokalnie. W projekcie nad którym pracuję właśnie tak robimy. Możesz sobie zobaczyć jak to mamy zrobione: https://github.com/lmsgit/lms, należy przeszukać repozytorium szukając "teryt" lub "location", a w https://github.com/lmsgit/lms/blob/master/bin/lms-teryt jest zrobione zaciąganie bazy danych, mergowanie z istniejącymi adresami.

Masz rację, zdecydowanie lepszym rozwiązaniem jest przechowywanie bazy TERYT lokalnie - w przypadku awarii webservice zawsze będzie możliwość zapytania lokalnej bazy. Mimo wszystko chciałbym rozwiązać ten problem. Na StackOverflow zasugerowali, że problem leży w kodowaniu wiadomości - plik konfiguracyjny XML wskazuje na kodowanie WS-Addressing. Domyślnia klasa SoapClient nie jest w stanie korzystać z WS-Addressing, trzeba ją rozszerzyć i dodać tę funkcjonalność. Znalazłem kilka gotowców. Np. ten. Jak mi się uda, to napiszę tutaj jak to zrobić.

0

Wiem, że to stary wątek, ale ponieważ ostatnimi dniami musiałem skomunikować się z TERYT poprzez PHP a chciałem pozostać jak najbliżej oryginalnego SoapClient'a, to trafiłem m.in. tutaj i podejrzewam, że inni tacy jak ja też tutaj trafią - to forum jest pierwsze w wynikach Google.

No więc jak się okazuje problem komunikacji z webservice'ami wymaga zrobienia dwóch rzeczy:

  1. przemianowania namespace'a SOAP-ENV na soapenv;
  2. dodania nagłówka SOAP zgodnego z WS-ADDRESSING, WS-SECURITY i innymi WS.

Rozwiązaniem okazuje się następujące rozszerzenie SoapClient'a, które zamieściłem też u siebie na GitHubie:

class TERYT_SoapClient extends SoapClient {
  protected $user;
  protected $pass;

  public function __construct ($wsdl, $options) {
    $this->user = $options['ws-security-login'];
    $this->pass = $options['ws-security-password'];
    parent::__construct($wsdl, $options);
  }

  public function __doRequest ($request , $location , $action , $version, $one_way = 0)
  {
    $nonce_encoded = base64_encode( bin2hex( openssl_random_pseudo_bytes( 16 ) ) );
    $header = '
    <SOAP-ENV:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
  		<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  			<wsse:UsernameToken wsu:Id="UsernameToken-'. date('c') .'">
  				<wsse:Username>'. $this->user .'</wsse:Username>
  				<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">'. $this->pass .'</wsse:Password>
  				<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">'. $nonce_encoded .'</wsse:Nonce>
  				<wsu:Created>'. date('c') .'</wsu:Created>
  			</wsse:UsernameToken>
  		</wsse:Security>
  		<wsa:Action>'. $action .'</wsa:Action>
  	</SOAP-ENV:Header>
    ';
    $xml = explode('<SOAP-ENV:Body>', $request);
    $request = str_replace('SOAP-ENV', 'soapenv', $xml[0] . $header . '<SOAP-ENV:Body>' . $xml[1]);
    return parent::__doRequest($request , $location, $action, $version, $one_way);
  }
}

W opcjach przekazywanych do SoapClient'a należy odpowiednio wypełnić ws-security-login oraz ws-security-password.

1

cenna wskazówka, ale niestety nie działa u mnie :(
czy mógłby Pan podać przykład wysłanej (i działającej) koperty ?
chodzi mi głównie o parametry username i password
czy przekazuje je Pan zwykłym tekstem ?
zgonie z
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf
hasło powinno być zakodowane wg schematu
Base64 ( SHA-1 ( nonce + created + password ) )
w pana objaśnieniu nie jest jasne co Pan przekazuje
niestety, czy przekazuję zwykły tekst, czy zakodowany - ten sam komunikat "500 internal server error"
z poważaniem
Piotr Gołębiewski

0
pgoleb napisał(a):

cenna wskazówka, ale niestety nie działa u mnie :(

A co konkretnie nie działa? Jaki jest komunikat błędu i czego się tyczy? Kod, który przedstawiłem wyżej i który też jest w moim repo na GitHubie sprawdzałem przed sekundą i działał jak należy. Skutecznie wywołałem odpowiednią metodę API, aby uzyskać dane o jednostkach podziału terytorialnego.

czy mógłby Pan podać przykład wysłanej (i działającej) koperty ?

Proszę bardzo, przed chwilą wygenerowana i działająca:

<?xml version="1.0" encoding="UTF-8"?>
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns1="http://tempuri.org/">
    <soapenv:Header xmlns:wsa="http://www.w3.org/2005/08/addressing">
  		<wsse:Security soapenv:mustUnderstand="1" xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
  			<wsse:UsernameToken wsu:Id="UsernameToken-2018-01-11T00:16:02+00:00">
  				<wsse:Username>TestPubliczny</wsse:Username>
  				<wsse:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText">1234abcd</wsse:Password>
  				<wsse:Nonce EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary">NWE5MTJkOWEyZTMyMDhhYWM0YWY2YTVmMmNjM2I0NDQ=</wsse:Nonce>
  				<wsu:Created>2018-01-11T00:16:02+00:00</wsu:Created>
  			</wsse:UsernameToken>
  		</wsse:Security>
  		<wsa:Action>http://tempuri.org/ITerytWs1/PobierzSlownikRodzajowJednostek</wsa:Action>
  	</soapenv:Header>
    <soapenv:Body><ns1:PobierzSlownikRodzajowJednostek/></soapenv:Body></soapenv:Envelope>

chodzi mi głównie o parametry username i password
czy przekazuje je Pan zwykłym tekstem ?

Tak.

zgonie z
http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0.pdf
hasło powinno być zakodowane wg schematu
Base64 ( SHA-1 ( nonce + created + password ) )

To dotyczy przekazywania hasła typu wsse:PasswordDigest a ja przekazuję wsse:PasswordText. Nie pamiętam już czemu tak, ale wydaje mi się, że serwer mógł nie wspierać składowania nonce czy coś takiego. A może po prostu nie miałem do tego cierpliwości.

w pana objaśnieniu nie jest jasne co Pan przekazuje

Racja, nie jest to jasne. Mam nadzieję, że teraz już wyjaśniłem i wszystko Ci się powiedzie. W razie wątpliwości zajrzyj do mojego repo. Podstawowe użycie mojego kodu z GitHuba jest takie:

require 'TERYT_Webservices.php';

$webservice = new TERYT_Webservices('TestPubliczny', '1234abcd', 'test', true);

try {
	$result = $webservice->division_types();
	var_dump($result);
} catch (SoapFault $exception) {
	var_dump($exception);
}

Konstruktor od TERYT_Webservices odpowiednio konfiguruje Soap Clienta i m.in. przekazuje mu dane dostępowe czystym tekstem:

  public function __construct($user, $pass, $instance = 'production', $trace = false)
  {
    if ($instance == 'production')  $wsdl = 'https://uslugaterytws1.stat.gov.pl/wsdl/terytws1.wsdl';
    if ($instance == 'test')        $wsdl = 'https://uslugaterytws1test.stat.gov.pl/wsdl/terytws1.wsdl';

    $soap_options = array(
      'ws-security-login'    => $user,
      'ws-security-password' => $pass,
      'soap_version'   => SOAP_1_1,
			'cache_wsdl'     => WSDL_CACHE_MEMORY,
			'encoding'       => 'utf8',
      'keep_alive'     => false,
			'trace'					 => $trace,
			'compression'		 => SOAP_COMPRESSION_ACCEPT | SOAP_COMPRESSION_GZIP | SOAP_COMPRESSION_DEFLATE,
    );

    $this->soap_client = new TERYT_SoapClient($wsdl, $soap_options);
}

Rozmaite dalsze funkcje i funkcyjki korzystają z Soap Clienta w standardowy sposób.

niestety, czy przekazuję zwykły tekst, czy zakodowany - ten sam komunikat "500 internal server error"

Hmm, trudno wyrokować, ale może to jakiś przejściowy problem po stronie serwerów TERYTu?

0

Fatal error: Uncaught Error: Call to undefined method TERYT_Webservices::division_types()

Czy może Pan podać przykład zastosowania? np. jak wyciągnąć województwa

0

albo ulice w danym mieście?

0
Krwawy Lew napisał(a):

Fatal error: Uncaught Error: Call to undefined method TERYT_Webservices::division_types()

Wklej swój kod PHP, bez tego nie wiem skąd powyższy problem. Przykład, który podałem w poście wyżej i który wykorzystywał division_types(), testowałem na bieżąco przed zapostowaniem i działał bez problemu. Przetestowałem go też teraz i również zadziałał bez problemu. Uzyskałem w wyniku:

array(7) {
  [1]=>
  string(13) "gmina miejska"
  [2]=>
  string(13) "gmina wiejska"
  [3]=>
  string(21) "gmina miejsko-wiejska"
  [4]=>
  string(33) "miasto w gminie miejsko-wiejskiej"
  [5]=>
  string(41) "obszar wiejski w gminie miejsko-wiejskiej"
  [8]=>
  string(25) "dzielnice m. st. Warszawy"
  [9]=>
  string(53) "delegatury miast: Kraków, Łódź, Poznań, Wrocław"
}

Czy może Pan podać przykład zastosowania? np. jak wyciągnąć województwa

Przykład z województwami jest bliźniaczo podobny do tego z division_types:

require 'TERYT_Webservices.php';

$webservice = new TERYT_Webservices('TestPubliczny', '1234abcd', 'test', true);

try {
	$result = $webservice->provinces();
	var_dump($result);
} catch (SoapFault $exception) {
	var_dump($exception);
}

W wyniku przed chwilą otrzymałem 16 elementowego arraya, którego tutaj nie wklejam, bo jest obszerny a nie chcę zaciemniać.

Ech, przydałoby się, abym napisał jakąś dokumentację do mojego repo na GitHub. Pierwotnie w tym wątku napisałem jednak nie po to, aby ogólnie zaprezentować wykorzystanie mojego kodu a jedynie sposób na utworzenie SoapClient'a łączącego się z TERYT.

0
Krwawy Lew napisał(a):

albo ulice w danym mieście?

Teoretycznie poniższy kod powinien to realizować. Nie wiem, czy tak jest, ponieważ testowałem go w tej chwili i otrzymałem zero wyników. Sprawdziłem równolegle webserwisy TERYT za pomocą SoapUI i też otrzymałem zero wyników. Wskazuje to albo na błąd po stronie testowych interfejsów TERYT albo na to, że źle wypełniam request XML do tego konkretnego interfejsu zarówno manualnie (w SoapUI) jak i w sposób zautomatyzowany w kodzie PHP. Być może na interfejsach produkcyjnych wynik byłby inny - nie mam takiego dostępu, aby to sprawdzić.

require 'TERYT_Webservices.php';

$webservice = new TERYT_Webservices('TestPubliczny', '1234abcd', 'test', true);

	try {
		$towns = $webservice->town_search('Gdańsk');
		foreach ($towns as $town) {
			if ($town->Wojewodztwo == 'POMORSKIE') {
				$result = $webservice->streets(
					$town->WojSymbol,
					$town->PowSymbol,
					$town->GmiSymbol,
					$town->GmiRodzaj,
					$town->Symbol
				);
				var_dump($result);
			} else {
				// To nie ten Gdańsk, o który mi chodzi!
			}
		}
	} catch (SoapFault $exception) {
		var_dump($exception);
	}

1 użytkowników online, w tym zalogowanych: 0, gości: 1