SOAP, HTTP, HTTPS и редиректы, а так же PHP и Python

Всем привет!

Сегодня, работая с soap-сервисами столкнулся с неожиданной для меня проблемой: по неопределенной причине soap-сервис прекрасно работал из php-клиента (SoapClient) но напрочь отказывался работать из под python (suds).

Описание сервиса

Вначале стоит рассказать об программном окружении. Это:

  • Nginx как web-сервер;
  • Gunicorn 18.0 как wsgi-сервер;
  • Django 1.4 как web-фреймворк;
  • Spyne 2.11 как инструментарий для создания SOAP-сервисов.

В общем – ничего особенного за исключением того, что сервис работал только по протоколу HTTPS, а по HTTP отдавался редирект на него.

Конфигурация Nginx самая обычная:

location /soap/ {
    if ( $scheme = "http" ) {
        rewrite ^/(.*)$ https://$host/$1 permanent;
    }

    proxy_pass http://unix:/var/tmp/backend.sock:$uri$is_args$args;
}

Проблема в WSDL-схеме

Несмотря на то, что сервис изначально отдавал свою WSDL-схему по HTTPS-протоколу, схема, описывающая все его методы и типы данных содержала в качестве адресов сервисов (элемент wsdl:address из wsdl:port из wsdl:service) адреса с протоколом HTTP вместо исходного HTTPS.

<wsdl:service name="MySoapService">
    <wsdl:port name="MySoapService" binding="tns:MySoapService">
        <soap:address location="http://my-domain.ru/soap/"/>
    </wsdl:port>
</wsdl:service>

Так происходило  потому, что Gunicorn не знал, по какому протоколу был запрошен ресурс и не заполнял переменную окружения wsgi.url_scheme, а nginx ему об этом не сообщал нужными заголовками X-FORWARDED-PROTOCOL, X-FORWARDED-PROTO и X-FORWARDED-SSL.

Работа из php через SoapClient

Cамое интересное, что даже с неправильными адресами soap-сервисов из схемы (HTTP вместо HTTPS) сервис прекрасно работает через класс SoapClient из стандартной библиотеки php.

$client = new SoapClient('https://my-domain.ru/soap/?wsdl');
// ---> GET-запрос на https://my-domain.ru/soap/?wsdl

$client->someMethod();
// ---> POST-запрос <SOAP:Envelope>...</...> на http://my-domain.ru/soap/
// <--- 301 REDIRECT на https://my-domain.ru/soap/
//
// ---> повторный POST-запроса <SOAP:Envelope>...</...> на https://my-domain.ru/soap/
// <--- 200 OK
//      <SOAP:Envelope>...</...>

Почему SoapClient повторно отправляет POST-запрос после получения перенаправления я не знаю, но эту особенность стоит помнить.

Работа из python через Suds

В отличии от SoapClient, suds написанный на python и использующий urllib2 в качестве транспорта такой особенностью не обладает, и не отправляет повторно все тело первого запроса после направления:

from suds.client import Client

client = Client('https://my-domain.ru/soap/?wsdl') 
// ---> GET-запрос на https://my-domain.ru/soap/?wsdl

client.service.someMethod()
// ---> POST-запрос <SOAP:Envelope>...</...> на http://my-domain.ru/soap/
// <--- 301 REDIRECT на https://my-domain.ru/soap/
//
// ---> GET-запрос на https://my-domain.ru/soap/
// <--- 500 Internal server error
//      Ошибка вызова сервиса без тела запроса. Ошибка парсинга XML

Исправление проблемы между nginx и gunicorn

Для исправления моей проблемы достаточно было просто добавить в секцию location отправку заголовка X-Forwarded-Proto:

location /soap/ {
    if ( $scheme = "http" ) {
        rewrite ^/(.*)$ https://$host/$1 permanent;
    }

    proxy_pass http://unix:/var/tmp/backend.sock:$uri$is_args$args;
    proxy_set_header        X-Forwarded-Proto $scheme;
}

И оба клиента заработали одинаково.

Leave a Reply

Your email address will not be published. Required fields are marked *