Perl - SOAP::WSDL - wsdl2perl.pl - web-services

We are working on a system in perl that has to communicate with multiple web services that provide SOAP endpoints to work with. They provide WSDL files to describe the services (in C# .Net, "Add Service Reference" would use these). We use wsdl2perl.pl to import these files into perl modules.
All is working, except for when the services return a fault.
We call a service like this:
my $service = OurInterfaces::OurService::OurPort->new();
my $result = $service->ourMethod({ someParameter => '1234' });
die $result->get_faultstring()->serialize() if not $result;
When we try to access the fault string, our perl script dies with this:
<Fault><faultcode>SOAP-ENV:Server</faultcode><faultstring>Error deserializing message: Cannot resolve class for Fault/detail/Errors/ErrorDetail via UPSRateTypemaps::RateService at /usr/lib/perl5/site_perl/5.8.8/SOAP/WSDL/Expat/MessageParser.pm line 147.
at line 1 at /usr/lib/perl5/site_perl/5.8.8/SOAP/WSDL/Expat/Base.pm line 82
.
Message was:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"><soapenv:Header/><soapenv:Body><soapenv:Fault><faultcode>Client</faultcode><faultstring>An exception has been raised as a result of client data.</faultstring><detail><err:Errors xmlns:err="http://www.ups.com/schema/xpci/1.0/error"><err:ErrorDetail><err:Severity>Hard</err:Severity><err:PrimaryErrorCode><err:Code>10001</err:Code><err:Description>The XML document is not well formed</err:Description><err:Digest>&lt;/RateRequest> does not close tag &lt;Request>.</err:Digest></err:PrimaryErrorCode><err:Location/></err:ErrorDetail></err:Errors></detail></soapenv:Fault></soapenv:Body></soapenv:Envelope></faultstring><faultactor>urn:localhost</faultactor></Fault>
It appears that the XML being returned for faults isn't mapped to a class (Class::Std) properly. How can we fix this?
Also is there any articles out there describing what we need to do to map the error messages? For the most part wsdl2perl.pl is just plain magic to us.

Since no answers yet. I'll post what we did to get it working.
We opened up the Typemap.pm file that has our $typemap_1, and added lines like this:
'Fault/detail' => 'UPSTrackElements::Errors'
'Fault/detail/Errors' => 'UPSTrackElements::Errors',
'Fault/detail/Errors/ErrorDetail' => 'UPSTrackTypes::ErrorDetailType',
...
wsdl2perl.pl imported the error classes we needed, but it didn't hook them up properly in the typemap file. In general, we had to append 'Fault/detail' to the front of where it mapped all of our Error classes. I'm not sure why wsdl2perl.pl couldn't map them right, when it obviously had no issue generating the classes.
We had to fill out the appropriate XPath for every single field in the fault xml. It is working for us now, but was rather ugly to figure out.

Related

OTRS Webservice as Requestor Test

I'm new to OTRS (3.2) and also new to PERL but I have been given the task of setting up OTRS so that it will make a call to our remote webservice so a record can be created on our end when a ticket is set as "Closed".
I set up various dynamic fields so the customer service rep can fill in additional data that will be passed into the webservice call along with ticket details.
I couldn't get the webservice call to trigger when the ticket was "Closed" but I did get it to trigger when the "priority" was changed so I'm just using that now to test the webservice.
I'm just using the Test.pm and TestSimple.pm files that were included with OTRS.
When I look at the Debugger for the Webserice, I can see that the calls were being made:
$VAR1 = {
'TicketID' => '6'
};
My webservice currently just has one method "create" which just returns true for testing.
however I get the following from the Test.pm
"Got no TicketNumber (2014-09-02 09:20:42, error)"
and the following from the TestSimple.pm
"Error in SOAP call: 404 Not Found at /TARGET/SHARE/var/otrs/Kernel/GenericInterface/Transport/HTTP/SOAP.pm line 578 (2014-09-02 09:20:43, error)
I've spent countless hours on Google but couldn't find anything on this. All I could find is code for the Test.pm and TestSimple.pm but nothing really helpful to help me create a custom invoker for my needs and configure the webservice in OTRS to get it to work.
Does anyone have any sample invokers that I can look at to see how to set it up?
Basically I need to pass the ticket information along with my custom dynamic fields to my webservice. From there I can create the record on my end and do whatever processing.
I'm not sure how to setup the Invoker to pass the necessary ticket fields and dynamic fields and how to make it call a specific method in my remote webservice.
I guess getting the Test.pm and TestSimple.pm to work is the first step then I can modify those for my needs. I have not used PERL at all so any help is greatly appreciated.
I'm also struggling with similar set of requirements too. I've also never programmed in PERL, but I can tell you at least that the "Got no TicketNumber" in the Test.pm is right from the PrepareRequest method, there you can see this block of code:
# we need a TicketNumber
if ( !IsStringWithData( $Param{Data}->{TicketNumber} ) ) {
return $Self->{DebuggerObject}->Error( Summary => 'Got no TicketNumber' );
}
You should change all references to TicketNumber to TicketID, or remove the validation whatsoever (also there is mapping to ReturnedData variable).
Invoking specific methods on your WS interface is quite simple (but poorly documented). The Invoker name that you specify in the "OTRS as requester" section of web service configuration corresponds to the WS method that will be called. So if you have WS interface with a method called "create" just name the Invoker "create" too.
As far as the gathering of dynamic field goes, can't help you on that one yet, sorry.
Cheers

Dynamic messages with gettext (AngularJS)

I have a application with a Django backend and an AngularJS front-end.
I use the angular-gettext plugin along with Grunt to handle translations.
The thing is, I sometimes received dynamic strings from my backend through the API. For instance a MySQL error about a foreign key constraint or duplicate key entry.
How can I add this strings to the .pot file or non harcoded string in general ?
I've tried to following but of course it cannot work :
angular.module('app').factory('HttpInterceptor', ['$q', '$injector', '$rootScope', '$cookieStore', 'gettext', function ($q, $injector, $rootScope, $cookieStore, gettext) {
responseError: function (rejection) {
gettext('static string'); //it works
gettext(rejection.data.error); //does not work
$rootScope.$emit('errorModal', rejection.data);
}
// Return the promise rejection.
return $q.reject(rejection);
}
};
}]);
})();
One solution I could think of would be to write every dynamic strings into a JSON object. Send this json to server and from there, write a static file containing these strings so gettext can extract them.
What do you suggest ?
I also use angular-gettext and have strings returned from the server that need to be translated. We did not like the idea of having a separate translation system for those messages so we send them over in the default language like normal.
To allow this to work we did two things. We created a function in our backend which we can call to retrieve all the possible strings to translate. In our case it's mainly static data that only changes once in a while. Ideally this would be automated but it's fine for now.
That list is formatted properly through code into html with the translate tag. This file is not deployed, it is just there to allow the extraction task to find the strings.
Secondly we created a filter to do the translation on the interpolated value, so instead of translating {{foo}} it will translate the word bar if that's was the value of foo. We called this postTranslate and it's a simple:
angular
.module('app')
.filter('postTranslate', ['gettextCatalog', function (gettextCatalog) {
return function (s) {
return gettextCatalog.getString(s);
};
}]);
As for things that are not in the database we have another file for those where we manually put them in. So your error messages may go here.
If errors are all you are worried about though, you may rather consider not showing all the error messages directly and instead determine what user friendly error message to show. That user friendly error message is in the front end and therefore circumvents all of this other headache :)

Trying to communicate with a REST service with NSIS

All I am trying to do is send an HTTP GET with Querystring or HTTP POST to a web service from within a custom page in an NSIS installer.
The problem is that most plugins can upload/download files, but not simply call a service and return the response. The recommend plugin to use is NsisUrlLib. I tried this, and while it works with simple GET requests, it fails and throws an error if you have querystring parameters in the URL, such as:
NsisUrlLib::UrlOpen /NOUNLOAD "http://tc.hwidev.com?var=value"
Pop $connectResult
NSISdl,INetC, and other plugins work with querystrings but can only be used to upload/download files.
I tried using the ExecDos plugin to call cURL.exe from the command line like so:
ExecDos::exec /TOSTACK "curl" "http://tc.hwidev.com"
Pop $connectResult
But I can't figure out how to get the data returned from the URL passed back to NSIS. The above code just returns an integer into $connectResult which probably represents the return code from CURL. How do I get the actual HTML returned from the URL onto the NSIS stack or into a variable?
Alternatively, can anyone recommend a better way to accomplish this? Calling a webservice seems so basic that thousands of people should have already figured this out.
NSISdl, Inetc etc, aren't just for uploading/downloading files. I think you are misunderstanding this point. You can use them to call REST apis, but you'll need to save the answer to a local file, and then parse it.
Example: Calling to an REST api that answers XML:
# Call to REST API
NSISdl::download_quiet "http://remotesite?restparams" "$TEMP/result.xml"
# Parse answer
nsisXML::create
nsisXML::load "$TEMP/result.xml"
nsisXML::select "/response/status"
# Clean
delete "$TEMP/result.xml"
That's it.
NSISdl::download_quiet "http://127.0.0.1/vid.php?vid=1" "$TEMP/checkvid.txt"
FileOpen $0 $TEMP\checkvid.txt r
FileRead $0 $1
DetailPrint $1
FileClose $0
${If} $1 == "vidNTYE1203"
MessageBox MB_OK "video found"
Delete "$TEMP\checkvid.txt"
${Else}
MessageBox MB_OK "bye bye"
Delete "$TEMP\checkvid.txt"
Quit
${EndIf}
This is my work snippet for production.Working good,for me.Pay attention for 'Delete' command and '\' slash .
I would recommend INetC over NsisUrlLib, the older firefox installer used it with much success.

soap web service with symfony2

i need to create a webservices with symfony2 ive read the official article http://symfony.com/doc/current/cookbook/web_services/php_soap_extension.html
in the example it creates a instance of SoapServer with a parameter routing a .wsdl file, what is this file? i didnt found too much documentation about soap in symfony. some help with this please?
public function indexAction()
{
$server = new \SoapServer('/path/to/hello.wsdl');
$server->setObject($this->get('hello_service'));
$response = new Response();
$response->headers->set('Content-Type', 'text/xml; charset=ISO-8859-1');
ob_start();
$server->handle();
$response->setContent(ob_get_clean());
return $response;
}
I'm not sure if you've found your answer or not. Just for anybody else that might run into such a problem:
WSDL is the language in which the Web Services are defined and described. It's basically a XML file containing the input/output parameters of each and every function that are served by a server. it also contains some information about the server itself, that's providing the services.
in order to be able to create a webservice, you need to use the code you've provided, which would in fact prepare Symfony to serve the clients on "/path/to/hello.wsdl" (in my example this path is /YourDesiredPathFor/services.wsdl), and also, you need to provide a valid WSDL document that contains the information mentioned above in correct WSDL format. the problem is that Symfony (or even PHP itself for this matter) has no means of creating the file automatically.
to solve the matter, you need to use an external WSDL generator. I'd suggest using PHP-WSDL-Creator. It uses annotations written inside php files to create the WSDL file and also runs the SoapServer. that means that you won't even need the code that you've provided. it also has proper interfaces and addons that provide you with clients for different protocols and languages.
you need to tweak it a little bit though! if you want it to be by symfony standards, i think you would need to rewrite some parts of it; but if you'd like to use it as an external library, it could also work!
the way i did it was by copying the extracted files into ./vendor/php_wsdl/lib/php_wsdl/src (long it is, isn't it? maybe an easier path would also work!); then defined a php_wsdl.php in ./vender/php_wsdl/lib/php_wsdl:
<?php
require_once __DIR__. '/src/class.phpwsdl.php';
class WSDL_PhpWsdl extends PhpWsdl{
}
next, in the "./app/autoload.php", i added the following line to enable Symfony to use the created extension:
require_once __DIR__. '/../vendor/php_wsdl/lib/php_wsdl/php_wsdl.php';
just one thing! the extension needs a "cache" folder in order to cache the created wsdl files and all. unfortunately because i need to finish the project quickly, i don't have enough time to manage the cache folder as it should be. there are definitely better ways than my way, and i would really be glad to know about them.
anyway, now you need to use the extension's capabilities! to do so, i created a "ServerController" for the bundle i was using:
<?php
namespace Tara\PageControllerBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
class ServiceController extends Controller
{
function wsdlAction(){
\PhpWsdlServers::$EnableRest = false;
$soap= \WSDL_PhpWsdl::CreateInstance(
null, // PhpWsdl will determine a good namespace
$this->generateUrl('service_wsdl', array(), true), // Change this to your SOAP endpoint URI (or keep it NULL and PhpWsdl will determine it)
__DIR__. '/cache', // Change this to a folder with write access
Array( // All files with WSDL definitions in comments
dirname(__FILE__). '/../Services/MyService.php'
),
null, // The name of the class that serves the webservice will be determined by PhpWsdl
null, // This demo contains all method definitions in comments
null, // This demo contains all complex types in comments
false, // Don't send WSDL right now
false // Don't start the SOAP server right now
);
if($soap->IsWsdlRequested()) // WSDL requested by the client?
$soap->Optimize=false; // Don't optimize WSDL to send it human readable to the browser
$soap->RunServer();
}
}
as you can see, the path to the cache folder is on a local directory, which means it has to be created manually and in ./src/Tara/PageControllerBundle/Controller (obviously in my case; you will need to change the paths). I'm sure there are better ways to manage the cache folder.
there is a line there:
dirname(__FILE__). '/../Services/MyService.php
this line tells the extension where to look for annotations in order to create the WSDL page. you also need to define a route to "service_wsdl":
service_wsdl:
pattern: /YourDesiredPathFor/services.wsdl
defaults: {_controller: TaraPageControllerBundle:Service:wsdl}
as you can see, the controller is ServiceController and the function responsible for it is wsdlAction; the exact function that was defined!
just as an example, i would provide my own MyService.php:
<?php
namespace Tara\PageControllerBundle\Services;
use Tara\PageControllerBundle\Model\...
/**
* #service Tara\PageControllerBundle\Services\MyService
*/
class MyService
{
/**
* Function Create
*
* #param string $link
* #param string $title
* #param string $image
* #param string $description
* #return boolean Status of the creation
* #pw_rest POST /YourDesiredPathForAction/create Create The Object
*
*/
public function Create($link, $title, $image, $description)
{
// your code that handles the data goes here. this is not a part of the annotations!
return (bool)$result;
}
}
now, you might be able to use a SoapClient to connect to your web service at
http: //your-server.something/YourDesiredPathFor/services.wsdl?wsdl
and call the Create function! you can also check the output of the extension by opening the above written address. the extension also provides a "human-readable" version at
http: //your-server.something/YourDesiredPathFor/services.wsdl.
i would be glad to know if this was any help to anyone! :)
SOAP is a more general concept that Symfony assumes you are familiar with. There is an example WSDL at the bottom of the page that you link to. Take a look at tutorials about SOAP and WSDL and then try to recreate what they're doing in the Symfony page.
SOAP Tutorial
WSDL Tutorial

How do I debug mangled soap requests?

Lately, we've been seeing exceptions like this in our .NET (.asmx) webservices:
System.Web.Services.Protocols.SoapException: Server was unable to read request. ---> System.InvalidOperationException: There is an error in XML document (868, -3932). ---> System.Xml.XmlException: '.', hexadecimal value 0x00, is an invalid character. Line 868, position -3932.
at System.Xml.XmlTextReaderImpl.Throw(Exception e)
at System.Xml.XmlTextReaderImpl.Throw(String res, String[] args)
at System.Xml.XmlTextReaderImpl.Throw(Int32 pos, String res, String[] args)
at System.Xml.XmlTextReaderImpl.ThrowInvalidChar(Int32 pos, Char invChar)
at System.Xml.XmlTextReaderImpl.ParseNumericCharRefInline(Int32 startPos, Boolean expand, BufferBuilder internalSubsetBuilder, Int32& charCount, EntityType& entityType)
at System.Xml.XmlTextReaderImpl.ParseText(Int32& startPos, Int32& endPos, Int32& outOrChars)
at System.Xml.XmlTextReaderImpl.ParseText()
at System.Xml.XmlTextReaderImpl.ParseElementContent()
at System.Xml.XmlTextReaderImpl.Read()
at System.Xml.XmlTextReader.Read()
at System.Web.Services.Protocols.SoapServerProtocol.SoapEnvelopeReader.Read()
at System.Xml.XmlReader.ReadElementString()
at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationReader1.Read14_SendErrlog()
at Microsoft.Xml.Serialization.GeneratedAssembly.ArrayOfObjectSerializer12.Deserialize(XmlSerializationReader reader)
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
--- End of inner exception stack trace ---
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle, XmlDeserializationEvents events)
at System.Xml.Serialization.XmlSerializer.Deserialize(XmlReader xmlReader, String encodingStyle)
at System.Web.Services.Protocols.SoapServerProtocol.ReadParameters()
--- End of inner exception stack trace ---
at System.Web.Services.Protocols.SoapServerProtocol.ReadParameters()
at System.Web.Services.Protocols.WebServiceHandler.CoreProcessRequest()
How can I debug this exception? This exception is getting reported to us from a SOAP filter which looks for exceptions in message.Stage = SoapMessageStage.AfterSerialize.
Is there any way to get at the original soap request? How do I get an invalid character at line 868, column -3932? How can there a negative column 3932?
This is one of the irritating things about the Microsoft web services approach -- if the request cannot be deserialized into the objects in your web method signature then the service consumer gets a cryptic message. And to top it off, the request never makes it into your web service because it cannot be deserialzied so you can't handle the error gracefully.
What I would do to help with these types of issues is to create a new SoapExtension that simply lets you output the raw XML to a destination that is convenient for you (file or Trace to be read by DebugView or whatever else you like). The code would go in the BeforeDeserialize stage. You could enable the SoapExtension via web.config in the event you wanted to investigate one of these issues. The downside of using the web.config to add the SoapExtension is that it will be active for the entire web application. You could add some additional custom configuration that would allow your service to only log information for a specific endpoint or a specific web method if you wanted.
Usually, just by seeing the incoming XML you can see what the problem is. If not, then you could try to manually run the captured XML through small program that invokes the XML serializer and see if you can find out what is going on. Another useful tool is Web Service Studio 2 which is a test harness which lets you enter data and invoke your service (and also submit any XML you want).
In terms of your specific issue, here is my take/guess. It looks like ASCII character null is getting encoded and sent to your service which is invalid according to the XML Spec. The simple answer is not to send that character. But who is sending that character? Is it a .NET client? Do you have control over the client? If you need to work around someone else's bug, then you may have to replace the offending character(s) with another character (perhaps empty string).
You should be able to get the original message by using another SoapExtension. In fact, the same extension could probably be modified to make a copy of the input, and to discard it if there is no exception. The original input would then be available to you if an exception occurred.
You could also use an external tool like Fiddler to watch what's being sent to you.
FYI: SoapException.Message is intentionally left vague to prevent exposing too much information which could potentially be used to exploit the system.
For your particular case I'd take John's advice and install Fiddler to monitor the actual HTTP traffic and view the message on the wire.
The portion of your exception that jumps out at me is the "hexadecimal value 0x00, is an invalid character" but as you mentioned the line number it points to is bunk--so it's nothing concrete.
What kind of parameters do you pass to the service? Are you doing any kind of custom encoding with a SOAP extension? Any additional SOAP headers being added?