|
guide.soaplite.com
SOAP::Lite for Perl
The Power Of Simplicity
|
|
- Quick Start Guide with SOAP and SOAP::Lite
- Writing a CGI-based Server
- Writing a Client
- Passing Values
- Autodispatching
- Objects access (it's 'simple OBJECT access protocol', isn't it?)
- Error handling
- Service dispatch (different services on one server)
- Types and Names
- More complex server (daemon, mod_perl and mod_soap)
- Access to Remote Services
- Access with service description (WSDL)
- Security (SSL, basic/digest authentication, cookie-based authentication, ticket-based authentication, access control)
- Handling LoLs (List of Lists, Structs, Objects, or something else)
- XML processing
- In/Out parameters
- How to write SOAP services
- COM interface
- Service packages (stubmaker)
- Headers and attributes
- Handling parameters on server side
- Custom data types
- Custom serializer and deserializer
- Different transports (SMTP/POP3, IO, TCP)
- Debugging and Troubleshooting
- Global settings
- Toolkits and Interoperability
- SOAP shell
- UDDI requests
- Oneliners
- Glossary
- References
- Copyright
- Author and contributors
SOAP (Simple Object Access Protocol) is a way for you to remotely make
method calls upon classes and objects that exist on a remote server.
It's the latest in a long series of similar projects like CORBA, DCOM,
and XML-RPC.
SOAP specifies a standard way to encode parameters and return values
in XML, and standard ways to pass them over some common network protocols
like HTTP (web) and SMTP (email). This guide is not about those
meaty technical aspects of SOAP, though, it's a very quick introduction in
writing SOAP servers and clients. We will hardly scratch the surface of
what's possible.
All examples will use SOAP::Lite module. Don't be mislead by
the 'Lite' suffix -- this refers to the effort it takes to use the module,
not its capabilities.
Here's a simple CGI-based SOAP server:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Demo')
-> handle;
package Demo;
sub hi {
return "hello, world";
}
sub bye {
return "goodbye, cruel world";
}
|
|
|
There are basically two parts to this: the first four lines set up a
SOAP wrapper around a class. Everything from 'package Demo' onward is
the class being wrapped.
In the previous version of the SOAP specification (1.0), SOAP over HTTP was
supposed to use a new HTTP method, M-POST. In practice, there are many web
servers that don't understand the M-POST method so this requirement was
weakened and now it's common to try a normal POST first and then use
M-POST if the server needs it. If you don't understand POST and
M-POST, don't worry, you don't need to know all about it to use the
module.
This program prints the results of the hi() method call:
|
#!perl -w
use SOAP::Lite;
print SOAP::Lite
-> uri('http://www.soaplite.com/Demo')
-> proxy('http://services.soaplite.com/hibye.cgi')
-> hi()
-> result;
|
|
|
The uri() identifies the class on the server, and the proxy()
identifies the CGI script that provides access to the class.
Since both look like URLs, I'll take a minute to explain the difference,
as it's quite important.
proxy()
-
proxy() is simply the address of the server to contact that provides
the methods. You can use http:, mailto:, even ftp: URLs here.
uri()
-
Each server can offer many different services through the one
proxy()
URL. Each service has a unique URI-like identifier, which you specify
to SOAP::Lite through the uri() method. If you get caught up in the
gripping saga of the SOAP documentation, the 'namespace' corresponds
to the uri() method.
Run your client and you should see:
That's it (assuming you're connected to the Internet).
If your method returns multiple values:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Demo')
-> handle;
package Demo;
sub hi {
return "hello, world";
}
sub bye {
return "goodbye, cruel world";
}
sub languages {
return ("Perl", "C", "sh");
}
|
|
|
Then the result() method will only return the first. To access
the rest, use the paramsout() method:
|
#!perl -w
use SOAP::Lite;
$soap_response = SOAP::Lite
-> uri('http://www.soaplite.com/Demo')
-> proxy('http://services.soaplite.com/hibye.cgi')
-> languages();
@res = $soap_response->paramsout;
$res = $soap_response->result;
print "Result is $res, outparams are @res\n";
|
|
|
This code will produce:
|
Result is Perl, outparams are C sh
|
|
|
Methods can take arguments. Here's a SOAP server that translates
between Fahrenheit and Celsius:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Temperatures')
-> handle;
package Temperatures;
sub f2c {
my ($class, $f) = @_;
return 5/9*($f-32);
}
sub c2f {
my ($class, $c) = @_;
return 32+$c*9/5;
}
|
|
|
And here's a sample query:
|
#!perl -w
use SOAP::Lite;
print SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi')
-> c2f(37.5)
-> result;
|
|
|
You can also create an object representing the remote class, and
then make method calls on it:
|
#!perl -w
use SOAP::Lite;
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi');
print $soap
-> c2f(37.5)
-> result;
|
|
|
Check your math, it should give you:
This being Perl, there's more than one way to do it:
|
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/Temperatures',
proxy => 'http://services.soaplite.com/temper.cgi';
print c2f(37.5);
|
|
|
After you specify the uri and proxy parameters, you are
able to call remote functions with the same syntax as local ones
(e.g., c2f). This is done with UNIVERSAL::AUTOLOAD, which catches
all unknown method calls. Be warned that all calls to undefined
methods will result in an attempt to use SOAP.
Methods can also return real objects. Let's extend our Temperatures class
with an object-oriented interface.
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Temperatures')
-> handle;
package Temperatures;
sub f2c {
my ($class, $f) = @_;
return 5/9*($f-32);
}
sub c2f {
my ($class, $c) = @_;
return 32+$c*9/5;
}
sub new {
my $self = shift;
my $class = ref($self) || $self;
bless {_temperature => shift} => $class;
}
sub as_fahrenheit {
return shift->{_temperature};
}
sub as_celsius {
return 5/9*(shift->{_temperature}-32);
}
|
|
|
Here is a client to access this class:
|
#!perl -w
use SOAP::Lite;
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi');
my $temperatures = $soap
-> call(new => 100) # accept Fahrenheits
-> result;
print $soap
-> as_celsius($temperatures)
-> result;
|
|
|
Similar code with autodispatch is shorter and easier to read.
|
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/Temperatures',
proxy => 'http://services.soaplite.com/temper.cgi';
my $temperatures = Temperatures->new(100);
print $temperatures->as_fahrenheit;
|
|
|
A SOAP call may fail for numerous reasons, such as: transport
error, incorrect parameters, an error on the server. Transport
errors (which may occur if, for example, there is a network break
between the client and the server) are dealt with below (examples
2.g, 2.h, 2.j and 2.k). All other errors are indicated by the fault() method:
|
#!perl -w
use SOAP::Lite;
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi');
my $result = $soap->c2f(37.5);
unless ($result->fault) {
print $result->result();
} else {
print join ', ',
$result->faultcode,
$result->faultstring;
}
|
|
|
faultcode() gives you information about the main reason for the error.
Possible values may be:
- Client: you provided incorrect information in the request.
-
This error may occur when parameters for remote call are incorrect,
for example, for service that returns name of state in US based on number
of this state you provide negative or too big number. Or type of parameter
is incorrect (specified
int instead of string), or likewise.
- Server: something is wrong on the server side.
-
This means that provided information is correct, but server couldn't handle
the request because of temporary difficulties, for example, unavailable
database.
- MustUnderstand: Header elements has mustUnderstand attribute, but wasn't
understood by server.
-
Basically that means that the server was able to parse the request but that
the client is requesting functionality that can't be provided.
For example, request requires execution of SQL statement and client wants
to be sure that several requests will be executed in one transaction. It
could be implemented as three differents calls with common TransactionID.
In this case, the SOAP header may be extended with a new header element
'TransactionID' which carries the transaction ID across the 3 separate
invocations. However, the server may not understand what a 'TransactionID'.
If the server does not have this understanding and tries to process the
request anyway, problems may arise if there is a problem with processing the
3 invocations together, as the server will not maintain transactional
integrity across the group of 3. To guard against this, the client may
indicate that the server 'mustUnderstand' the element 'TransactionID'. If
the server sees this and does NOT understand the meaning of the element, it
will not try and process the requests in the first place.
This functionality makes services more reliable and distributed
system more robast.
- VersionMismatch: the server can't understand the version of SOAP used
by the client.
-
This is provided for (possible) future extensions, when new versions of SOAP
will have different functionality and only clients that are knowledgeable
about it will be able to use it.
- Other errors
-
The server is allowed to create its own errors, like Client.Authentication.
faultstring() provides a readable explanation, whereas faultdetail()
gives access to more detailed information, which may be an object, or
complex structure.
For example, if you change uri to something else (let's try with 'Test' instead
of 'Temperatures'), this code will generate:
|
Client, Bad Class Name, Failed to access class (Test)
|
|
|
By default client will die with diagnostic on transport errors and do
nothing for faulted calls, so, you'll be able to get fault info from result.
You can alter this behavior with on_fault() handler either per object (will
die on both transport errors and SOAP faults):
|
#!perl -w
use SOAP::Lite;
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi')
-> on_fault(sub { my($soap, $res) = @_;
die ref $res ? $res->faultstring : $soap->transport->status, "\n";
});
|
|
|
or globally:
|
#!perl -w
use SOAP::Lite
on_fault => sub { my($soap, $res) = @_;
die ref $res ? $res->faultstring : $soap->transport->status, "\n";
};
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi');
|
|
|
Now you wrap your SOAP call into eval {} block and catch both transport
errors and SOAP faults:
|
#!perl -w
use SOAP::Lite
on_fault => sub { my($soap, $res) = @_;
die ref $res ? $res->faultstring : $soap->transport->status, "\n";
};
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi');
eval {
print $soap->c2f(37.5)->result;
1 } or die;
|
|
|
You may also consider this variant that will return empty envelope and setup
$@ on failure:
|
#!perl -w
use SOAP::Lite
on_fault => sub { my($soap, $res) = @_;
eval { die ref $res ? $res->faultstring : $soap->transport->status };
return ref $res ? $res : new SOAP::SOM;
};
my $soap = SOAP::Lite
-> uri('http://www.soaplite.com/Temperatures')
-> proxy('http://services.soaplite.com/temper.cgi');
defined (my $temp = $soap->c2f(37.5)->result) or die;
print $temp;
|
|
|
And finally, if you want to ignore errors (however, you can still check for
them with the fault() method call):
|
use SOAP::Lite
on_fault => sub {};
|
|
|
or
|
my $soap = SOAP::Lite
-> on_fault(sub{})
..... other parameters
|
|
|
So far our CGI programs have had a single class to handle incoming
SOAP calls. But we might have one CGI program that dispatches SOAP
calls to many classes. This section shows you how to do that.
Static dispatch is when you hard-code the name of the module that
the SOAP requests go to. That module can be defined by your program
or loaded when needed.
First, what is this dispatch? When server gets SOAP request it binds
it to class specified in request. This class could be already loaded on
server side (on server startup or as result of previous calls) or will be
loaded on demand according to server configuration. Dispatch is the process
of determining of what class should handle this request and loading of this
class. Static means that name of the class is specified in configuration
and dynamic means that only directory is specified and any class from
this particular directory can be accessed.
Now imagine you want to give access to two different classes on server
side, and want to provide the same address for both. What should you do?
Several options are available (surprised?):
- Static internal
-
That is something you already are familiar with:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Demo')
-> handle;
package Demo;
sub hi {
return "hello, world";
}
sub bye {
return "goodbye, cruel world";
}
1;
|
|
|
- Static external
-
Similar to
Static internal, but module is somewhere outside of server code:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Demo')
-> handle;
|
|
|
Following file should be somewhere in @INC directory:
|
package Demo;
sub hi {
return "hello, world";
}
sub bye {
return "goodbye, cruel world";
}
1;
|
|
|
- Dynamic
-
As you can see in both
Static internal and Static external modes the name
of the module is hardcoded in the server's code. But what if you want to be
able to add new modules dynamically without changing the code?
Dynamic dispatch allows you to do that. Specify the directory and any module in
this directory becomes available for dispatching:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('/home/soaplite/modules')
-> handle;
|
|
|
Then put Demo.pm in /home/soaplite/modules directory:
|
package Demo;
sub hi {
return "hello, world";
}
sub bye {
return "goodbye, cruel world";
}
1;
|
|
|
That's it. The any module you put in /home/soaplite/modules is now
available, but don't forget that the URI on the client side should match the
module/class name you want to dispatch your call to.
- Mixed
-
Why do we need this? Unfortunately, both dynamic and static dispatch have
disadvantages. During dynamic dispatch access to
@INC is disabled (due to
security reasons) and static dispatch loads modules on startup, but
this may not be what we want if we have a bunch of modules we want to access.
To avoid this, you can combine the dynamic and static approaches.
Let's assume you have 10 modules in /home/soaplite/modules directory, and
want to provide access, but don't want to load all of them on startup.
All you need to do is this:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('/home/soaplite/modules', 'Demo', 'Demo1', 'Demo2')
-> handle;
|
|
|
Now access to all of these modules is enabled and they'll be loaded on a demand
basis, only when needed. And, more importantly, all these modules now
have access to @INC array, so can do any use they want.
Since Perl is a typeless language (in the sence that there is no difference
between an integer 123 and a string '123') the transformation
process from a SOAP message to Perl data is very simple. For most simple types
we can just ignore types during this stage. However there are drawbacks also:
we need to provide additional information during the generation of our SOAP
message, because another side (server) may expect to get type information.
SOAP::Lite tries hard to do this job for you and doesn't force you to type
every parameter explicitly. It tries to guess the datatype based on the actual
value stored in the variable, and behave appropriately (according to another
Perl's motto, DWIM, 'Do What I Mean').
For example, the variable that has value 123 becomes element with type
int in the SOAP message, and a variable that has value 'abc' gets type
string. There are several more complex cases. For example, a variable that
has a value with binary zeroes "\0" will be encoded with type base64 and
objects (blessed references) will have type and name (unless specified)
according to their types.
It may not work in all cases though. There is no way to make (by default)
element with type string or type long from value 123, because
autotyping will always make type int for this variable.
You may alter this behavior in several ways. You may disable it completely
(with autotype(0)), you may change autotyping for different types, or you
may explicitly specify type for your variable:
my $var = SOAP::Data->type(string => 123);
$var is encoded as an element with type string and value 123. You
may use this variable in ANY place where you use usual variables in SOAP calls.
You may also provide not only a specific data types, but also name and
attributes.
Since many services count on names of parameters (instead of positions)
you may specify the name for your parameters through the same syntax. To
specify the name for the $var variable you may use
$var->name('myvar'), or make it in one line:
my $var = SOAP::Data->type(string => 123)->name('myvar');
# -- OR --
my $var = SOAP::Data->type('string')->name(myvar => 123);
# -- OR --
my $var = SOAP::Data->type('string')->name('myvar')->value(123);
You may always get/set the value of this variable with the value() method:
$var->value(321); # set new value
my $realvalue = $var->value; # store it in variable
You shouldn't have many problems with the CGI-based SOAP server you created;
however, performance could be significantly better. The next logical step
might be to implement SOAP services using accelerators (like PerlEx or
VelociGen) or persistent technologies (like mod_perl).
Another lightweight solution might be to implement the SOAP service as an HTTP
daemon; in that case you don't need to use a separate web server. This might
be useful in a situation where a client application accepts SOAP calls, or
for internal usage.
- HTTP daemon
-
For HTTP daemon implementation you may write this code:
|
#!perl -w
use SOAP::Transport::HTTP;
use Demo;
# don't want to die on 'Broken pipe' or Ctrl-C
$SIG{PIPE} = $SIG{INT} = 'IGNORE';
my $daemon = SOAP::Transport::HTTP::Daemon
-> new (LocalPort => 80)
-> dispatch_to('/home/soaplite/modules')
;
print "Contact to SOAP server at ", $daemon->url, "\n";
$daemon->handle;
|
|
|
Not much difference from the CGI server (Dynamic), huh?
And it makes the same interface accessible, only through the different
endpoint. This code is all you need to run the SOAP server on your computer
without anything else.
- HTTP daemon in VBScript
-
Similar code in VBScript may look like:
|
call CreateObject("SOAP.Lite") _
.server("SOAP::Transport::HTTP::Daemon", _
"LocalPort", 80) _
.dispatch_to("/home/soaplite/modules") _
.handle
|
|
|
That is all that you need to run SOAP server on Microsoft platform (and it will
run on Win9x/Me/NT/2K as soon as you register Lite.dll with regsvr32 Lite.dll).
- ASP/VB
-
ASP server could be created with VBScript or PerlScript code:
|
<%
Response.ContentType = "text/xml"
Response.Write(Server.CreateObject("SOAP.Lite") _
.server("SOAP::Server") _
.dispatch_to("/home/soaplite/modules") _
.handle(Request.BinaryRead(Request.TotalBytes)) _
)
%>
|
|
|
- Apache::Registry
-
One of the easiest ways to significantly speed up your CGI-based SOAP server
is to wrap it with mod_perl Apache::Registry module. You need to configure it
in httpd.conf file:
|
Alias /mod_perl/ "/Apache/mod_perl/"
<Location /mod_perl>
SetHandler perl-script
PerlHandler Apache::Registry
PerlSendHeader On
Options +ExecCGI
</Location>
|
|
|
Put CGI script soap.mod_cgi in /Apache/mod_perl/ directory mentioned above:
|
#!perl -w
use SOAP::Transport::HTTP;
SOAP::Transport::HTTP::CGI
-> dispatch_to('/home/soaplite/modules')
-> handle
;
|
|
|
- mod_perl
-
Let's consider mod_perl-based server now. To run it server you'll need to
put SOAP::Apache module (Apache.pm) in any directory from
@INC:
|
package SOAP::Apache;
use SOAP::Transport::HTTP;
my $server = SOAP::Transport::HTTP::Apache
-> dispatch_to('/home/soaplite/modules')
sub handler { $server->handler(@_) }
1;
|
|
|
Then modify your httpd.conf file:
|
<Location /soap>
SetHandler perl-script
PerlHandler SOAP::Apache
</Location>
|
|
|
- mod_soap
-
mod_soap allows you to create a SOAP server by simply configuring the
httpd.conf or .htaccess file.
|
# directory-based access
<Location /mod_soap>
SetHandler perl-script
PerlHandler Apache::SOAP
PerlSetVar dispatch_to "/home/soaplite/modules"
PerlSetVar options "compress_threshold => 10000"
</Location>
# file-based access
<FilesMatch "\.soap$">
SetHandler perl-script
PerlHandler Apache::SOAP
PerlSetVar dispatch_to "/home/soaplite/modules"
PerlSetVar options "compress_threshold => 10000"
</FilesMatch>
|
|
|
Directory-based access turns a directory into SOAP endpoint. For example,
you may point your request to http://localhost/mod_soap (there is no
need to create this directory).
File-based access turns a file with a specified name (or mask) into a SOAP
endpoint. For example, http://localhost/somewhere/endpoint.soap.
Alternatively, you may turn a existing directory into a SOAP server if you put
.htaccess file inside it:
|
SetHandler perl-script
PerlHandler Apache::SOAP
PerlSetVar dispatch_to "/home/soaplite/modules"
PerlSetVar options "compress_threshold => 10000"
|
|
|
It's time now to reuse what has already done and to try and call services
available on the Internet. After all, the most interesting part in SOAP is
operability between systems where communicating parts are
created in different languages, running on different platforms or in
different environments, and providing interfaces with service descriptions
or documentation. XMethods.net can be a perfect starting point
- Name of state based on state's number (in alphabetical order)
-
Frontier implementation has test server that returns the name of a state
based on number you provided. By default, SOAP::Lite generates a SOAPAction
header with the structure of
[URI]#[method]. Frontier, however, expects
SOAPAction to be just the URI, so we have to use on_action to modify it...
In our example we specify on_action(sub { sprintf '"%s"', shift }),
so the resulting SOAPAction will contain only the URI (and don't forget double
quotes there).
|
#!perl -w
use SOAP::Lite;
# Frontier http://www.userland.com/
$s = SOAP::Lite
-> uri('/examples')
-> on_action(sub { sprintf '"%s"', shift })
-> proxy('http://superhonker.userland.com/')
;
print $s->getStateName(SOAP::Data->name(statenum => 25))->result;
|
|
|
You should get the output:
- Whois
-
We will target services with different implementations and this service
is running on Windows platform:
|
#!perl -w
use SOAP::Lite;
# 4s4c (aka Simon's SOAP Server Services For COM) http://www.4s4c.com/
print SOAP::Lite
-> uri('http://www.pocketsoap.com/whois')
-> proxy('http://soap.4s4c.com/whois/soap.asp')
-> whois(SOAP::Data->name('name' => 'yahoo'))
-> result;
|
|
|
Nothing fancy here, 'name' is the name of the field and 'yahoo'
is the value. That should give you the output:
|
The Data in Network Solutions' WHOIS database is provided by Network
Solutions for information purposes, and to assist persons in obtaining
information about or related to a domain name registration record.
Network Solutions does not guarantee its accuracy. By submitting a
WHOIS query, you agree that you will use this Data only for lawful
purposes and that, under no circumstances will you use this Data to:
(1) allow, enable, or otherwise support the transmission of mass
unsolicited, commercial advertising or solicitations via e-mail
(spam); or (2) enable high volume, automated, electronic processes
that apply to Network Solutions (or its systems). Network Solutions
reserves the right to modify these terms at any time. By submitting
this query, you agree to abide by this policy.
Yahoo (YAHOO-DOM) YAHOO.COM
Yahoo Inc. (YAHOO27-DOM) YAHOO.ORG
Yahoo! Inc. (YAHOO4-DOM) YAHOO.NET
To single out one record, look it up with "!xxx", where xxx is the
handle, shown in parenthesis following the name, which comes first.
|
|
|
- Book price based on ISBN
-
In many cases the SOAP interface is just a frontend that requests information,
parses response and formats it and returns according to your request. It may
not be doing that much, but it saves you time on the client side and fixes
this interface, so you don't need to update it every time your service
provider changes format or content.
In addition to that, the major players are moving quickly toward XML;
for example, Google already has XML-based interface for their search
engine. Here is the service that returns book price based on ISBN:
|
#!perl -w
use SOAP::Lite;
# Apache SOAP http://xml.apache.org/soap/ (running on XMethods.net)
$s = SOAP::Lite
-> uri('urn:xmethods-BNPriceCheck')
-> proxy('http://services.xmethods.net/soap/servlet/rpcrouter');
my $isbn = '0596000278'; # Programming Perl, 3rd Edition
print $s->getPrice(SOAP::Data->type(string => $isbn))->result;
|
|
|
Here is the result for 'Programming Perl, 3rd Edition':
Note that we explicitly specified 'string' type, because ISBN looks like
number and will be serialized by default as number, but SOAP server we work
with requires it to be the string.
- Currency Exchange rates
-
This service returns value of 1 unit of country1's currency converted into
country2's unit currency:
|
#!perl -w
use SOAP::Lite;
# GLUE http://www.themindelectric.com/ (running on XMethods.net)
my $s = SOAP::Lite
-> uri('urn:xmethods-CurrencyExchange')
-> proxy('http://services.xmethods.net/soap');
my $r = $s->getRate(SOAP::Data->name(country1 => 'England'),
SOAP::Data->name(country2 => 'Japan'))
->result;
print "Currency rate for England/Japan is $r\n";
|
|
|
Which gives you (as of 2001/03/11):
|
Currency rate for England/Japan is 175.4608
|
|
|
- NASDAQ quotes
-
This service returns delayed stock quote based on stock symbol:
|
#!perl -w
use SOAP::Lite;
# GLUE http://www.themindelectric.com/ (running on XMethods.net)
my $s = SOAP::Lite
-> uri('urn:xmethods-delayed-quotes')
-> proxy('http://services.xmethods.net/soap');
my $symbol = 'AMZN';
my $r = $s->getQuote($symbol)->result;
print "Quote for $symbol symbol is $r\n";
|
|
|
It may (or may not, depending on how Amazon is doing) give you:
|
Quote for AMZN symbol is 12.25
|
|
|
Although support for WSDL 1.1 is limited in SOAP::Lite for now (service
description may work in some cases, but hasn't been extensively
tested), you can access services that don't have complex types in their
description:
|
#!perl -w
use SOAP::Lite;
print SOAP::Lite
-> service('http://www.xmethods.net/sd/StockQuoteService.wsdl')
-> getQuote('MSFT');
|
|
|
If we take a look under the hood we'll find that SOAP::Lite makes the
request for service description, parses it, builds the stub (object that
make available the same methods as remote service) and returns it to you.
As the result, you can run several requests using the same service
description:
|
#!perl -w
use SOAP::Lite;
my $service = SOAP::Lite
-> service('http://www.xmethods.net/sd/StockQuoteService.wsdl');
print 'MSFT + ORCL = ',
$service->getQuote('MSFT') + $service->getQuote('ORCL');
|
|
|
The service description doesn't need to be on the Internet, you can access it
from your local drive also:
This code works in a similar way to the previous example (in OO style), but
loads description and imports all methods, so you can use the function
interface.
And finally, a couple of one-liners for those who like to do something short
and simple (albeit useful and powerful):
|
# following command is splitted for readability
perl "-MSOAP::Lite service=>'http://www.xmethods.net/sd/StockQuoteService.wsdl'"
-le "print getQuote('MSFT')"
|
|
|
Last example (second one-liner) seems to be the shortest SOAP method
invocation.
Though SOAP itself doesn't impose any security mechanisms (unless you'll count
SOAP Security Extensions: Digital Signature specification), the extensibility
of protocol allows you to leverage many security methods that are available
for different protocols, like SSL over HTTP or S/MIME. We'll consider how
SOAP can be used together with SSL, basic authentication, cookie-based
authorization, and access control.
- SSL
-
Let's start with SSL. Surprisingly there is nothing SOAP-specific you
need to do on the server side, and there is only a minor modification on
the client side: just specify
https: instead of http: as the protocol
for your endpoint and everything else will be done for you. Obviously,
endpoint should support this functionality and server should be properly
configured.
|
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/My/Examples',
proxy => 'https://localhost/cgi-bin/soap.cgi',
on_fault => sub { my($soap, $res) = @_;
die ref $res ? $res->faultstring : $soap->transport->status, "\n";
}
;
print getStateName(21);
|
|
|
- Basic authentication
-
The situation gets even more interesting with authentication. Consider this
code that accesses an endpoint that requires authentication.
|
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/My/Examples',
proxy => 'http://services.soaplite.com/auth/examples.cgi',
on_fault => sub { my($soap, $res) = @_;
die ref $res ? $res->faultstring : $soap->transport->status, "\n";
}
;
print getStateName(21);
|
|
|
Keep in mind that the password will be in clear text during the transfer
(not exactly in clear text; it will be base64 encoded, but that's almost
the same) unless the user uses https (ie, authentication doesn't mean
encryption).
The server configuration (for an Apache webserver) with authentication
may look like this (can be specified in .conf or in .htaccess file):
|
AuthUserFile /path/to/users/file/created/with/htpasswd
AuthType Basic
AuthName "SOAP::Lite authentication tests"
require valid-user
|
|
|
If you run example 7.b against this endpoint, you'll probably get error
like this:
|
401 Authorization Required
|
|
|
You may provide required credentials on client side (user soaplite,
and password authtest) overriding function get_basic_credentials()
in class SOAP::Transport::HTTP::Client:
|
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/My/Examples',
proxy => 'http://services.soaplite.com/auth/examples.cgi',
on_fault => sub { my($soap, $res) = @_;
die ref $res ? $res->faultstring : $soap->transport->status, "\n";
}
;
sub SOAP::Transport::HTTP::Client::get_basic_credentials {
return 'soaplite' => 'authtest';
}
print getStateName(21);
|
|
|
That gives you the correct result:
Alternatively you may provide this information with credentials() functions,
but you need to specify host and realm also:
|
#!perl -w
use SOAP::Lite +autodispatch =>
uri => 'http://www.soaplite.com/My/Examples',
proxy => [
'http://services.soaplite.com/auth/examples.cgi',
credentials => [
'services.soaplite.com:80', # host:port
'SOAP::Lite authentication tests', # realm
'soaplite' => 'authtest', # user, password
]
],
on_fault => sub { my($soap, $res) = @_;
die ref $res ? $res->faultdetail : $soap->transport->status, "\n";
}
;
print SOAP->getStateName(21);
|
|
|
Under modern Perl you MAY get the warning about 'deprecated usage of inherited
AUTOLOAD'. To avoid it use the full syntax: SOAP->getStateName(21)
instead of getStateName(21).
The simplest and most convenient way would probably be to provide the user and
password embedded in a URL. Surprisingly, this works:
|
#!perl -w
use SOAP::Lite;
print SOAP::Lite
-> uri('http://www.soaplite.com/My/Examples')
-> proxy('http://soaplite:authtest@services.soaplite.com/auth/examples.cgi')
-> getStateName(21)
-> result;
|
|
|
- Cookie-based authentication
-
Cookie-based authentication also doesn't require a lot of work on
the client side. Usually, it means that you need to provide credentials
in some way, and if everything is OK, the server will return a cookie on
success, then will check it for all subsequent requests.
Using available functionality you may not only
support this behavior on the client side in one session, but even store cookies
in a file and use the same server session for several runs. All you need to
do is:
|
#!perl -w
use SOAP::Lite;
use HTTP::Cookies;
my $soap = SOAP::Lite
-> uri('urn:xmethodsInterop')
-> proxy('http://services.xmethods.net/soap/servlet/rpcrouter',
cookie_jar => HTTP::Cookies->new(ignore_discard => 1));
print $soap->echoString('Hello')->result;
|
|
|
All the magic is in the cookie jar :). You may even add or delete cookies
between calls, but the underlying module does everything you need by default.
Add file => 'filename' option to new() method to save/restore cookies
between sessions. Not much work, huh? Kudos to Gisle Aas on that!
- Ticket-based authentication
-
Ticket-based authentication is a little bit more complex. The logic is similar
to cookie-based authentication, but it is executed on the application level,
instead of at the transport level. The advantage is that it works for any
SOAP transport (not only for HTTP) and gives you a little bit more flexibility.
As a result, you won't get support from webserver and you'll have to do
everything manually. No big deal, right?
First step is ticket generation. We'll build ticket that contains email,
time, and signature.
|
package TicketAuth;
# we will need to manage Header information to get a ticket
@TicketAuth::ISA = qw(SOAP::Server::Parameters);
# ----------------------------------------------------------------------
# private functions
# ----------------------------------------------------------------------
use Digest::MD5 qw(md5);
my $calculateAuthInfo = sub {
return md5(join '', 'something unique for your implementation', @_);
};
my $checkAuthInfo = sub {
my $authInfo = shift;
my $signature = $calculateAuthInfo->(@{$authInfo}{qw(email time)});
die "Authentication information is not valid\n"
if $signature ne $authInfo->{signature};
die "Authentication information is expired\n"
if time() > $authInfo->{time};
return $authInfo->{email};
};
my $makeAuthInfo = sub {
my $email = shift;
my $time = time()+20*60; # signature will be valid for 20 minutes
my $signature = $calculateAuthInfo->($email, $time);
return +{time => $time, email => $email, signature => $signature};
};
# ----------------------------------------------------------------------
# public functions
# ----------------------------------------------------------------------
sub login {
my $self = shift;
pop; # last parameter is envelope, don't count it
die "Wrong parameter(s): login(email, password)\n" unless @_ == 2;
my($email, $password) = @_;
# check credentials, write your own is_valid() function
die "Credentials are wrong\n" unless is_valid($email, $password);
# create and return ticket if everything is ok
return $makeAuthInfo->($email);
}
sub protected {
my $self = shift;
# authInfo is passed inside the header
my $email = $checkAuthInfo->(pop->valueof('//authInfo'));
# do something, user is already authenticated
return;
}
1;
|
|
|
It would be very careless (and insecure) to create calculateAuthInfo()
as a normal, exposed function because then client could invoke it and
generate valid ticket without providing valid credentials (unless you
forbid it in SOAP server configuration, but we'll show another way).
We will create calculateAuthInfo(), checkAuthInfo() and
makeAuthInfo() as 'private' functions, so only other functions inside
the same file may access it. It effectively prevents clients from accessing
them directly.
The login() function returns hash that has email and time inside as well
as an MD5 signature that disallows the user from altering this information.
Since the server used a secret string during signature generation, the user
is not able to tamper with the resulting signature. To access protected
methods, client has to provide the obtained ticket in the header:
|
# login
my $authInfo = login(email => 'password');
# convert it into the Header
$authInfo = SOAP::Header->name(authInfo => $authInfo);
# invoke protected method
protected($authInfo, 'parameters');
|
|
|
This is just a fragment, but it should give you some ideas on how to
implement ticket-based authentication on application level. You MAY even
get the ticket in one place (via HTTP for example) and then access SOAP server
via SMTP providing this ticket (ideally you should use PKI [public key
infrastructure] for that matter).
- Access control
-
Why would you need access control? Imagine that you have a class and
want to give access to it selectively, for example, read access to one
person and read/write access to another person (or list of persons).
At a low level, read and write access means access to specific
functions/methods in class.
You may put this check in at the application level (for example with
ticket-based authentication), or you may split your class into two different
classes and give one person access only to one of them, but it's not
an optimal solution. We consider a different approach, where you create two
different endpoints that refer to the same class on the server side, but
have different access options.
|
use SOAP::Transport::HTTP;
use Protected;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Protected::readonly')
-> handle
;
|
|
|
This endpoint will have access only to readonly() method in Protected
class.
|
use SOAP::Transport::HTTP;
use Protected;
SOAP::Transport::HTTP::CGI
-> dispatch_to('Protected')
-> handle
;
|
|
|
This endpoint will have unrestricted access to all methods/functions in
Protected class. Now you may put it under basic, digest or some other
kind of authentication preventing access to it.
Thus, by combining the capabilities of webserver with the SOAP server you
can create an application that best suites your needs.
Processing of complex data structures isn't different in any aspect
from usual processing in your programming language. General rule is
simple: 'Treat the result of SOAP call as variable of specified type'.
Next example shows service that works with array of structs (strictly
speaking, Perl has no structs. Structs are often emulated with
hashes, and that is exactly what is happening here):
|
#!perl -w
use SOAP::Lite;
my $result = SOAP::Lite
-> uri('urn:xmethodsServicesManager')
-> proxy('http://www.xmethods.net/soap/servlet/rpcrouter')
-> getAllSOAPServices();
if ($result->fault) {
print $result->faultcode, " ", $result->faultstring, "\n";
} else {
# reference to array of structs is returned
my @listings = @{$result->result};
# @listings is the array of structs
foreach my $listing (@listings) {
print "-----------------------------------------\n";
# print description for every listing
foreach my $key (keys %{$listing}) {
print $key, ": ", $listing->{$key} || '', "\n";
}
}
}
|
|
|
Exactly the same thing is true about structs inside of other structs,
list of objects, objects that have lists inside, etc. 'What you return
on server side is what you get on client side, and let me know if you get
something else.'
(Ok, not always. You MAY get a blessed array even
when you return a simple array on the other side and you MAY get a blessed
hash when you return a simple one, but it won't change anything in your code,
just access it as you usually do).
Copyright (C) 2001 Paul Kulchenko. All rights reserved.
Paul Kulchenko (paulclinger@yahoo.com)
Major contributors:
- Nathan Torkington
-
Basically started this work and pushed the whole process.
- Tony Hong
-
Invaluable comments, fixes and input help me keep this material correct,
fresh and simple.
Copyright (C) 2001 Paul Kulchenko |
2001/05/15 11:20:01 |