【问题标题】:Perl SOAP::Lite interfacing to a vendor API on .NETPerl SOAP::Lite 与 .NET 上的供应商 API 接口
【发布时间】:2016-09-01 03:56:19
【问题描述】:

经常阅读,第一次提问。

我正在努力将 Perl 与我们的供应商之一 (LogRhythm) 提供的 SOAP API 连接起来,但似乎遇到了一些障碍。

我已经能够与 API 交互,并且克服了我最初遇到的身份验证挑战。现在,当我需要为 SOAP 调用提供参数时,我遇到了 SOAP 信封正文中的名称空间标记问题。

供应商提供的 API 是围绕 .NET 和 WCF 构建的。

我的原型代码目前如下所示:

use strict;
no strict "refs";

use Data::Dumper;
use IO::Socket::SSL qw (SSL_VERIFY_NONE) ;
use SOAP::Lite;
use Tie::IxHash;

# Username and Password
my $sUID = "<API UserID>";
my $sPWD = "<API Password>";

# WSDL definition URI.  Using a cached local copy using file:/... also works
my $LookupService_wsdl = 'https://melcapi01.soc.ipsec.net.au/LogRhythm.API/Services/LookupServiceBasicAuth.svc?singleWsdl';
my $lrns = 'http://www.logrhythm.com/webservices';

# Ensure that a consistent xmlns:soap value is used, without this we get inconsistent results.
$SOAP::Constants::PREFIX_ENV = 'SOAP-ENV';

# Don't validate SSL Certificate while testing
IO::Socket::SSL::set_defaults(SSL_verify_mode => "SSL_VERIFY_NONE");

# Construct the security header
my %authHash;
tie %authHash, "Tie::IxHash";
%authHash = (
            Username => SOAP::Data->type( '' => $sUID )->prefix('wsse'),
            Password => SOAP::Data->type( '' => $sPWD )->prefix('wsse'),
    );

my $wsse = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
my $securityHeader = SOAP::Header->new(
    name   => 'Security',
    uri    => $wsse,
    prefix => 'wsse',
    value  => \SOAP::Data->new(
        name   => 'UsernameToken',
        prefix => 'wsse',
        value  => \%authHash,
      )
);

# Error handling
on_fault => sub { my($soap, $res) = @_; 
    die ref $res ? $res->faultstring : $soap->transport->status;
};

# Define the SOAP Instance
my $lrapi = SOAP::Lite
    -> readable (1)
    -> service($LookupService_wsdl)
    -> on_action( sub {return $action});

# Set the default Namespace
$lrapi->default_ns($lrns);

# Actually get data from the LookupService
my $result;

# Build up the parameters
my @classificationType = ( SOAP::Data->new(name =>'classificationType', value => 2000));

$result = $lrapi->GetClassificationsByType($securityHeader);

print Dumper $result;

这会产生以下 SOAP 请求:

SOAPAction: "http://www.logrhythm.com/webservices/LookupService/GetClassificationsByType"

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
    xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" 
    xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:tns="http://www.logrhythm.com/webservices" 
    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" 
    xmlns:wsa10="http://www.w3.org/2005/08/addressing" 
    xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" 
    xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy" 
    xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" 
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" 
    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" 
    xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <SOAP-ENV:Header>
        <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <wsse:UsernameToken>
                <wsse:Username>reporting</wsse:Username>
                <wsse:Password>password</wsse:Password>
            </wsse:UsernameToken>
        </wsse:Security>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
        <tns:GetClassificationsByType>
            <classificationType xsi:type="xsd:int">2000</classificationType>
            <classificationTypeSpecified xsi:type="xsd:boolean">true</classificationTypeSpecified>
        </tns:GetClassificationsByType>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

不返回任何结果。我已经使用 .NET WebService Studio 进行了一些挖掘和请求修正,并确定问题是由于 SOAP 主体中的元素缺少 xmlns 标识符引起的。

SOAP 请求应如下所示:

SOAPAction: "http://www.logrhythm.com/webservices/LookupService/GetClassificationsByType"

<?xml version="1.0" encoding="UTF-8"?>
<SOAP-ENV:Envelope 
    SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/" 
    xmlns:msc="http://schemas.microsoft.com/ws/2005/12/wsdl/contract" 
    xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" 
    xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" 
    xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
    xmlns:tns="http://www.logrhythm.com/webservices" 
    xmlns:wsa="http://schemas.xmlsoap.org/ws/2004/08/addressing" 
    xmlns:wsa10="http://www.w3.org/2005/08/addressing" 
    xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" 
    xmlns:wsap="http://schemas.xmlsoap.org/ws/2004/08/addressing/policy" 
    xmlns:wsaw="http://www.w3.org/2006/05/addressing/wsdl" 
    xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" 
    xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy" 
    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" 
    xmlns:wsx="http://schemas.xmlsoap.org/ws/2004/09/mex" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <SOAP-ENV:Header>
        <wsse:Security xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
            <wsse:UsernameToken>
                <wsse:Username>reporting</wsse:Username>
                <wsse:Password>password</wsse:Password>
            </wsse:UsernameToken>
        </wsse:Security>
    </SOAP-ENV:Header>
    <SOAP-ENV:Body>
        <tns:GetClassificationsByType xmlns="http://www.logrhythm.com/webservices">
            <classificationType xsi:type="xsd:int">2000</classificationType>
            <classificationTypeSpecified xsi:type="xsd:boolean">true</classificationTypeSpecified>
        </tns:GetClassificationsByType>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>

各种支持请求表明应该通过在 SOAP::Lite 对象上手动设置 default_ns 来解决此问题,但是,这对我不起作用。

我已经挖掘了SOAP::Lite (1.20) 的源代码以查看发生了什么,并且似乎在进行 SOAP 方法调用之前设置为 1{'_use_default_ns'} 属性似乎得到了在处理呼叫期间重置为0

我还尝试手动构建 SOAP::Lite 对象,目的是使用 call() 方法来获得所需的结果,但是,我认为我遇到了自动命名空间数量的问题- 由于我目前使用的service() 机制而添加。如果我想使用call() 方法,我似乎无法使用service() 设置对象。

有没有人有任何其他建议我可以尝试,因为我现在不知所措?

任何帮助将不胜感激。

免责声明:我不是程序员/开发人员,我是具有一定编程能力的高级安全工程师/顾问/架构师,所以如果我的代码结构不正确,我提前道歉。这也是我第一次尝试摆弄 SOAP。

【问题讨论】:

  • 这是给客户的吗?你会接受多肮脏的解决方案?你的代码没有use warnings,但我的评估是它不是很乱。它只是 SOAP::Lite 和所有与之相关的东西,看起来并不像现代 Perl。
  • LogRhythm API 文档不仅糟糕,而且完全具有误导性。期望某些函数以与记录的顺序完全不同的顺序接受参数。

标签: perl soap namespaces


【解决方案1】:

这里有一个 hacky 解决方案。它连接到 SOAP::Lite 客户端的传输层并修改每个传出请求。

我使用了通过谷歌搜索找到的随机 Web 服务,因此您可以复制并粘贴示例代码并在没有凭据的情况下运行它。 The service 由美国国家气象局运营。在我的示例中,我使用他们的端点返回美国邮政编码的纬度/经度坐标。

use strict;
use warnings 'all';
use feature 'say';
use SOAP::Lite;

my $client = SOAP::Lite->new(
    proxy => 'http://graphical.weather.gov/xml/SOAP_server/ndfdXMLserver.php'
);
$client->service('http://graphical.weather.gov/xml/DWMLgen/wsdl/ndfdXML.wsdl');

# the transport layer is a subclass of LWP::UserAgent
$client->transport->add_handler(
    request_prepare => sub {
        my ( $request, $ua, $h ) = @_;
        my $content = $request->content;

        # this is the content before modification
        say 'before: ' . $content;

        # modify the content with a simple regex substitution
        $content =~ s{<LatLonListZipCode>}{<LatLonListZipCode xmlns="http://example.org">};
        $request->content($content);

        # so it doesn't throw a 'Content-Length header value 
        # was wrong, fixed' warning
        $request->header( 'Content-Length' => length $content );

        # this is the content after modification
        say 'after: ' . $content;

        # the return value is ignored
    }
);

my $res = $client->LatLonListZipCode('90210');
say $res->result;

说明

这是因为用于这种 SOAP 通信的SOAP::TransportSOAP::Transport::HTTP::ClientLWP::UserAgent 的子类。 UserAgent 非常灵活,并且为某些事件提供了很多different handlers。我们使用的是request_prepare,它允许我们在通过有线(或无线或其他方式)发送HTTP::Request 之前对其进行更改。

在发送请求之前调用处理程序,并且可以以任何它认为合适的方式修改请求。例如,这可用于向特定请求添加某些标头。

在我们添加的处理程序中,我们获取请求的内容并使用正则表达式替换将 xmlns 添加到 &lt;LatLongListZipCode&gt; 标记。虽然parsing XML with regex is not a good idea,修改一些看起来像 XML 的文本是可以的,因为那时我们并不关心了解其中的内容。

由于请求已经构建,我们需要更新 Content-Length 的头部字段。它会自动更新,但同时发出警告。我们可以通过简单地更新它来省略它。

这是程序的输出,分为三部分。

变更前的请求内容

<?xml version="1.0" encoding="UTF-8"?><soap:Envelope soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soap:Body><LatLonListZipCode><c-gensym3 xsi:type="xsd:int">90210</c-gensym3></LatLonListZipCode></soap:Body></soap:Envelope>

变更后的请求内容

<?xml version="1.0" encoding="UTF-8"?><soap:Envelope soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><soap:Body><LatLonListZipCode xmlns="http://example.org"><c-gensym3 xsi:type="xsd:int">90210</c-gensym3></LatLonListZipCode></soap:Body></soap:Envelope>

回复内容

<?xml version='1.0'?><dwml version='1.0' xmlns:xsd='http://www.w3.org/2001/XMLSchema' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:noNamespaceSchemaLocation='http://graphical.weather.gov/xml/DWMLgen/schema/DWML.xsd'><latLonList>34.0995,-118.414</latLonList></dwml>

显然更改有效,请求仍在处理中,我选择的服务不关心那个 xmlns 属性。

采用您的网络服务

对于您的用例,您必须确保仅更改无法立即使用的请求。如果要全部更改,可能就这么简单。只需在构建客户端后执行一次即可。

$lrapi->transport->add_handler(
    request_prepare => sub {
        my ( $request, $ua, $h ) = @_;
        my $content = $request->content;

        # modify the content with a simple regex substitution
        $content =~ s{<tns:GetClassificationsByType>}{<tns:GetClassificationsByType xmlns="http://www.logrhythm.com/webservices">};
        $request->content($content);

        # so it doesn't throw a 'Content-Length header value was wrong, fixed' warning
        $request->header( 'Content-Length' => length $content );
    }
);

免责声明

正如我所说,这是一个务实的解决方案。但是 SOAP 非常复杂,而且很少有客户端能够正确处理所有问题。还有很多服务器做很多不正确的事情。有时实用主义是最好的行动方针。当然,服务器中的行为可能会发生变化,在这种情况下可能会中断。但我相信这是值得冒的风险。

【讨论】:

  • 感谢您花时间整理如此详细的回复。我想我理解你的建议,并且从我对代码的探索来看,它看起来会起作用。
  • 不客气。在我看来,这种事情最难的部分是找出你必须看的地方。我可能会在某个时候自己回到这里以供参考。除非我猜你每天都在使用它们,否则你不会将这样的细节留在脑海中。 :-)
  • 唯一的潜在问题是 API 中的几个服务中的每一个发布的“方法”的数量,需要修改它们的请求,但我对如何处理有一些想法。无论如何,如果这解决了我的问题,我一定会在这里更新。再次感谢。
  • 你可以有一个标签列表,可能还有他们需要更新的东西。要么用qr 构建一个预编译的正则表达式,要么做一个哈希并通过它。如果每个请求有多个标签,请将 /g 标志添加到您的替换中。
【解决方案2】:

作为我原始帖子和@simbabque 的后续行动,我已经对此进行了测试,并且可以确认@simbabque 的建议有效。

为了满足可用方法的扩展列表(有 6 种不同的服务定义,共有 59 种方法可用。这可能会在 API 的未来版本中增加......),我已经实现了建议的 @ 987654321@如下:

$lrapi->transport->add_handler(
    request_prepare => sub {
        my ( $request, $ua, $h ) = @_;
        my $content = $request->content;

        # Get the SOAPAction Header from the current request
        my $soapAction = $request->header('SOAPAction');

        # Parse the SOAPAction Header, and determine the method being called
        $soapAction =~ m/^\"(http:\/\/[\w\.\/]+)\/(\w+)\/(\w+)\"$/;
        my ($lruri, $lrsvc, $lrmethod) = ($1, $2, $3);

        # Modify the content with a simple regex substitution
        $content =~ s{<tns:$lrmethod>}{<tns:$lrmethod xmlns="$lruri">};
        $request->content($content);

        # So it doesn't throw a 'Content-Length header value was wrong, fixed' warning
        $request->header( 'Content-Length' => length $content );
    }
);

我还必须在$lrapi-&gt;transport 上设置一个属性以确保它具有一些值。我只是在传输上设置代理属性来达到这个目的。由于某种原因,使用 WSDL 服务时传输属性的构建方式不同,从而导致错误。

在调用 add_handler 之前,我使用了类似于以下内容的方法来实现此目的:

$lrapi->proxy ("https://melcapi01.soc.ipsec.net.au/LogRhythm.API/Services/LookupServiceBasicAuth.svc");

这会导致$lrapi-&gt;transport 对象被正确构建,从而使add_handler 能够正常工作。

再次感谢@simbabque 的解决方案。

我希望这篇文章也对其他人有用。

【讨论】:

  • 很高兴它有帮助。您不需要在正则表达式中转义 ",如果您使用不同的分隔符(就像我在下面的 s{}{} 中所做的那样),您还可以去掉斜杠前面的反斜杠。在[] 内部,您只需要转义],而点只是一个点,而不是元字符。所以它可能看起来像这样:m{^"(http://[\w./]+)/(\w+)/(\w+)"$}。这样更容易阅读。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-05-08
相关资源
最近更新 更多