3 package XML::Atom::Server;
7 use base qw( XML::Atom::ErrorHandler );
8 use MIME::Base64 qw( encode_base64 decode_base64 );
9 use Digest::SHA1 qw( sha1 );
10 use XML::Atom::Util qw( first encode_xml textValue );
13 use constant NS_SOAP => 'http://schemas.xmlsoap.org/soap/envelope/';
14 use constant NS_WSSE => 'http://schemas.xmlsoap.org/ws/2002/07/secext';
15 use constant NS_WSU => 'http://schemas.xmlsoap.org/ws/2002/07/utility';
20 require Apache::Constants;
21 if (lc($r->dir_config('Filter') || '') eq 'on') {
22 $r = $r->filter_register;
24 my $server = $class->new or die $class->errstr;
25 $server->{apache} = $r;
27 return Apache::Constants::OK();
32 my $server = bless { }, $class;
33 $server->init(@_) or return $class->error($server->errstr);
39 $server->{param} = {};
40 unless ($ENV{MOD_PERL}) {
42 $server->{cgi} = CGI->new({});
49 (my $pi = $server->path_info) =~ s!^/!!;
50 my @args = split /\//, $pi;
52 my($k, $v) = split /=/, $arg, 2;
53 $server->request_param($k, $v);
55 if (my $action = $server->request_header('SOAPAction')) {
56 $server->{is_soap} = 1;
58 my($method) = $action =~ m!/([^/]+)$!;
59 $server->request_method($method);
63 defined($out = $server->handle_request) or die $server->errstr;
64 if (defined $out && $server->{is_soap}) {
65 $out =~ s!^(<\?xml.*?\?>)!!;
68 <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
69 <soap:Body>$out</soap:Body>
75 $out = $server->show_error($@);
77 $server->send_http_header;
83 sub password_for_user;
87 $ENV{MOD_PERL} ? $server->{apache}->uri : $server->{cgi}->url;
92 return $server->{__path_info} if exists $server->{__path_info};
95 ## mod_perl often leaves part of the script name (Location)
96 ## in the path info, for some reason. This should remove it.
97 $path_info = $server->{apache}->path_info;
99 my($script_last) = $server->{apache}->location =~ m!/([^/]+)$!;
100 $path_info =~ s!^/$script_last!!;
103 $path_info = $server->{cgi}->path_info;
105 $server->{__path_info} = $path_info;
111 if ($ENV{MOD_PERL}) {
112 return $server->{apache}->header_in($key);
114 ($key = uc($key)) =~ tr/-/_/;
115 return $ENV{'HTTP_' . $key};
122 $server->{request_method} = shift;
123 } elsif (!exists $server->{request_method}) {
124 $server->{request_method} =
125 $ENV{MOD_PERL} ? $server->{apache}->method : $ENV{REQUEST_METHOD};
127 $server->{request_method};
130 sub request_content {
132 unless (exists $server->{request_content}) {
133 if ($ENV{MOD_PERL}) {
134 ## Read from $server->{apache}
135 my $r = $server->{apache};
136 my $len = $server->request_header('Content-length');
137 $r->read($server->{request_content}, $len);
140 my $len = $ENV{CONTENT_LENGTH} || 0;
141 read STDIN, $server->{request_content}, $len;
144 $server->{request_content};
150 $server->{param}{$k} = shift if @_;
151 $server->{param}{$k};
154 sub response_header {
157 if ($ENV{MOD_PERL}) {
158 $server->{apache}->header_out($key, $val);
160 unless ($key =~ /^-/) {
161 ($key = lc($key)) =~ tr/-/_/;
164 $server->{cgi_headers}{$key} = $val;
170 $server->{response_code} = shift if @_;
171 $server->{response_code};
174 sub response_content_type {
176 $server->{response_content_type} = shift if @_;
177 $server->{response_content_type};
180 sub send_http_header {
182 my $type = $server->response_content_type || 'application/x.atom+xml';
183 if ($ENV{MOD_PERL}) {
184 $server->{apache}->status($server->response_code || 200);
185 $server->{apache}->send_http_header($type);
187 $server->{cgi_headers}{-status} = $server->response_code || 200;
188 $server->{cgi_headers}{-type} = $type;
189 print $server->{cgi}->header(%{ $server->{cgi_headers} });
195 if ($ENV{MOD_PERL}) {
196 $server->{apache}->print(@_);
204 my($code, $msg) = @_;
205 $server->response_code($code) if ref($server);
206 return $server->SUPER::error($msg);
212 chomp($err = encode_xml($err));
213 if ($server->{is_soap}) {
214 my $code = $server->response_code;
216 $server->response_code(500);
219 <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
222 <faultcode>$code</faultcode>
223 <faultstring>$err</faultstring>
230 <?xml version="1.0" encoding="utf-8"?>
239 if ($server->{is_soap}) {
240 my $xml = $server->xml_body;
241 my $auth = first($xml, NS_WSSE, 'UsernameToken');
242 $param{Username} = textValue($auth, NS_WSSE, 'Username');
243 $param{PasswordDigest} = textValue($auth, NS_WSSE, 'Password');
244 $param{Nonce} = textValue($auth, NS_WSSE, 'Nonce');
245 $param{Created} = textValue($auth, NS_WSSE, 'Created');
247 my $req = $server->request_header('X-WSSE')
248 or return $server->auth_failure(401, 'X-WSSE authentication required');
249 $req =~ s/^(?:WSSE|UsernameToken) //;
250 for my $i (split /,\s*/, $req) {
251 my($k, $v) = split /=/, $i, 2;
262 my $auth = $server->get_auth_info or return;
263 for my $f (qw( Username PasswordDigest Nonce Created )) {
264 return $server->auth_failure(400, "X-WSSE requires $f")
267 my $password = $server->password_for_user($auth->{Username});
268 defined($password) or return $server->auth_failure(403, 'Invalid login');
269 my $expected = encode_base64(sha1(
270 decode_base64($auth->{Nonce}) . $auth->{Created} . $password
272 return $server->auth_failure(403, 'Invalid login')
273 unless $expected eq $auth->{PasswordDigest};
279 $server->response_header('WWW-Authenticate', 'WSSE profile="UsernameToken"');
280 return $server->error(@_);
285 unless (exists $server->{xml_body}) {
287 my $parser = XML::LibXML->new;
288 $server->{xml_body} =
289 $parser->parse_string($server->request_content);
291 $server->{xml_body} =
292 XML::XPath->new(xml => $server->request_content);
301 if ($server->{is_soap}) {
302 my $xml = $server->xml_body;
303 $atom = XML::Atom::Entry->new(Doc => first($xml, NS_SOAP, 'Body'))
304 or return $server->error(500, XML::Atom::Entry->errstr);
306 $atom = XML::Atom::Entry->new(Stream => \$server->request_content)
307 or return $server->error(500, XML::Atom::Entry->errstr);
317 XML::Atom::Server - A server for the Atom API
322 use base qw( XML::Atom::Server );
325 $server->authenticate or return;
326 my $method = $server->request_method;
327 if ($method eq 'POST') {
328 return $server->new_post;
334 sub password_for_user {
337 $Passwords{$username};
342 my $entry = $server->atom_body or return;
343 ## $entry is an XML::Atom::Entry object.
344 ## ... Save the new entry ...
348 my $server = My::Server->new;
353 I<XML::Atom::Server> provides a base class for Atom API servers. It handles
354 all core server processing, both the SOAP and REST formats of the protocol,
355 and WSSE authentication. It can also run as either a mod_perl handler or as
356 part of a CGI program.
358 It does not provide functions specific to any particular implementation,
359 such as posting an entry, retrieving a list of entries, deleting an entry, etc.
360 Implementations should subclass I<XML::Atom::Server>, overriding the
361 I<handle_request> method, and handle all functions such as this themselves.
365 =head2 Request Handling
367 Subclasses of I<XML::Atom::Server> must override the I<handle_request>
368 method to perform all request processing. The implementation must set all
369 response headers, including the response code and any relevant HTTP headers,
370 and should return a scalar representing the response body to be sent back
377 my $method = $server->request_method;
378 if ($method eq 'POST') {
379 return $server->new_post;
381 ## ... handle GET, PUT, etc
386 my $entry = $server->atom_body or return;
387 my $id = save_this_entry($entry); ## Implementation-specific
388 $server->response_header(Location => $server->uri . '/entry_id=' . $id);
389 $server->response_code(201);
390 $server->response_content_type('application/x.atom+xml');
391 return serialize_entry($entry); ## Implementation-specific
394 =head2 Authentication
396 Servers that require authentication for posting or retrieving entries or
397 feeds should override the I<password_for_user> method. Given a username
398 (from the WSSE header), I<password_for_user> should return that user's
399 password in plaintext. This will then be combined with the nonce and the
400 creation time to generate the digest, which will be compared with the
401 digest sent in the WSSE header. If the supplied username doesn't exist in
402 your user database or alike, just return C<undef>.
406 my %Passwords = ( foo => 'bar' ); ## The password for "foo" is "bar".
407 sub password_for_user {
410 $Passwords{$username};
415 I<XML::Atom::Server> provides a variety of methods to be used by subclasses
416 for retrieving headers, content, and other request information, and for
417 setting the same on the response.
419 =head2 Client Request Parameters
425 Returns the URI of the Atom server implementation.
427 =item * $server->request_method
429 Returns the name of the request method sent to the server from the client
430 (for example, C<GET>, C<POST>, etc). Note that if the client sent the
431 request in a SOAP envelope, the method is obtained from the I<SOAPAction>
434 =item * $server->request_header($header)
436 Retrieves the value of the HTTP request header I<$header>.
438 =item * $server->request_content
440 Returns a scalar containing the contents of a POST or PUT request from the
443 =item * $server->request_param($param)
445 I<XML::Atom::Server> automatically parses the PATH_INFO sent in the request
446 and breaks it up into key-value pairs. This can be used to pass parameters.
447 For example, in the URI
449 http://localhost/atom-server/entry_id=1
451 the I<entry_id> parameter would be set to C<1>.
453 I<request_param> returns the value of the value of the parameter I<$param>.
457 =head2 Setting up the Response
461 =item * $server->response_header($header, $value)
463 Sets the value of the HTTP response header I<$header> to I<$value>.
465 =item * $server->response_code([ $code ])
467 Returns the current response code to be sent back to the client, and if
468 I<$code> is given, sets the response code.
470 =item * $server->response_content_type([ $type ])
472 Returns the current I<Content-Type> header to be sent back to the client, and
473 I<$type> is given, sets the value for that header.
477 =head2 Processing the Request
481 =item * $server->authenticate
483 Attempts to authenticate the request based on the authentication
484 information present in the request (currently just WSSE). This will call
485 the I<password_for_user> method in the subclass to obtain the cleartext
486 password for the username given in the request.
488 =item * $server->atom_body
490 Returns an I<XML::Atom::Entry> object containing the entry sent in the
497 Once you have defined your server subclass, you can set it up either as a
498 CGI program or as a mod_perl handler.
500 A simple CGI program would look something like this:
506 my $server = My::Server->new;
509 A simple mod_perl handler configuration would look something like this:
511 PerlModule My::Server
512 <Location /atom-server>
513 SetHandler perl-script
514 PerlHandler My::Server
517 =head1 ERROR HANDLING
519 If you wish to return an error from I<handle_request>, you can use the
520 built-in I<error> method:
525 return $server->error(500, "Something went wrong");
528 This will be returned to the client with a response code of 500 and an
529 error string of C<Something went wrong>. Errors are automatically
530 serialized into SOAP faults if the incoming request is enclosed in a SOAP
533 =head1 AUTHOR & COPYRIGHT
535 Please see the I<XML::Atom> manpage for author, copyright, and license