File Coverage

blib/lib/XML/Atom/Server/PSGI.pm
Criterion Covered Total %
statement 16 18 88.8
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 22 24 91.6


line stmt bran cond sub pod time code
1             package XML::Atom::Server::PSGI;
2 1     1   1981 use strict;
  1         5  
  1         89  
3 1     1   1430 use Digest::SHA1 ();
  1         2109  
  1         30  
4 1     1   1554 use MIME::Base64 ();
  1         1244  
  1         27  
5 1     1   2055 use Plack::Request;
  1         112872  
  1         37  
6 1     1   1005 use Scope::Guard ();
  1         432  
  1         21  
7 1     1   430 use XML::Atom::Entry;
  0            
  0            
8             use XML::Atom::Util ();
9              
10             use Class::Accessor::Lite
11             rw => [ qw(callbacks request response xml_parser) ],
12             ;
13              
14             use constant NS_SOAP => 'http://schemas.xmlsoap.org/soap/envelope/';
15             use constant NS_WSSE => 'http://schemas.xmlsoap.org/ws/2002/07/secext';
16             use constant NS_WSU => 'http://schemas.xmlsoap.org/ws/2002/07/utility';
17              
18              
19             our $VERSION = "0.04";
20              
21             sub psgi_app {
22             my $self = shift;
23             return sub {
24             $self->handle_psgi(@_);
25             };
26             }
27              
28             # alias
29             *req = \&request;
30             *res = \&response;
31              
32             sub new {
33             my $klass = shift;
34             my $self = bless {
35             (@_ == 1 && ref($_[0]) eq 'HASH' ? %{$_[0]} : @_),
36             }, $klass;
37             if (! $self->xml_parser()) {
38             $self->xml_parser(XML::Atom::Server::PSGI::XMLParser->new);
39             }
40             if (! $self->callbacks) {
41             $self->callbacks({});
42             }
43             return $self;
44             }
45              
46             sub handle_psgi {
47             my ($self, $env) = @_;
48             my $req = Plack::Request->new($env);
49             my $res = $req->new_response(200);
50             $res->content_type('application/x.atom+xml');
51              
52             $self->request($req);
53             $self->response($res);
54             # Make sure these things are all cleaned up afterwards
55             my $guard = Scope::Guard->new(sub {
56             $self->request(undef);
57             $self->response(undef);
58             });
59              
60             $env->{'xml.atom.server.request_method'} = $req->method;
61              
62             # Process parameters in path_info
63             my $path_info = $req->path_info;
64             my $params = Hash::MultiValue->new;
65             $path_info =~ s/^\///;
66             foreach my $arg (split /\//, $path_info) {
67             my ($k, $v) = split /=/, $arg, 2;
68             $params->add($k, $v);
69             }
70             $env->{'xml.atom.server.request_params'} = $params;
71              
72             if (my $action = $req->header('SOAPAction')) {
73             $env->{'xml.atom.server.is_soap'} = 1;
74             $action =~ s/"//g;
75             my ($method) = $action =~ m!/([^/]+)$!;
76             $env->{'xml.atom.server.request_method'} = $method;
77             }
78              
79             eval {
80             $self->_call('handle_request');
81             if ($self->is_soap) {
82             my $body = $res->body;
83             if (defined $body) {
84             $body =~ s!^(<\?xml.*?\?>)!!;
85             $body = <
86             $1
87            
88             $body
89            
90             SOAP
91             $res->body($body);
92             }
93             }
94             };
95             if (my $E = $@) {
96             # Escape
97             $E =~ s/
98             $E =~ s/>/>/g;
99             $res->code(500);
100             $res->body(<
101            
102             $E
103             EOXML
104             }
105              
106             return $res->finalize;
107             }
108              
109             # for compat
110             sub request_param {
111             shift->request_params->get(@_);
112             }
113              
114             sub request_params {
115             return $_[0]->req->env->{'xml.atom.server.request_params'};
116             }
117              
118             # for compat
119             sub request_content {
120             shift->req->content;
121             }
122              
123             # for compat
124             sub uri {
125             return $_[0]->req->uri;
126             }
127              
128             # for compat
129             sub request_method {
130             return $_[0]->req->env->{'xml.atom.server.request_method'};
131             }
132              
133             # for compat
134             sub request_header {
135             return $_[0]->req->header($_[1]);
136             }
137              
138             # for compat
139             sub response_header {
140             return shift->res->header(@_);
141             }
142              
143             # for compat
144             sub response_content_type {
145             return shift->res->content_type(@_);
146             }
147              
148             # for compat
149             sub response_code {
150             return shift->res->code(@_);
151             }
152              
153             # for compat
154             sub is_soap {
155             return $_[0]->req->env->{'xml.atom.server.is_soap'};
156             }
157              
158             sub _call {
159             my ($self, $name, @args) = @_;
160              
161             my $cb = $self->callbacks->{"on_$name"} ||
162             $self->can($name);
163             if (! $cb) {
164             Carp::croak("no callback nor overridden method $name found");
165             }
166             return $cb->($self, @args);
167             }
168              
169             sub get_auth_info {
170             my $self = shift;
171              
172             my $req = $self->req;
173             my %param; # XXX Hash::MultiValue?
174             if ($self->is_soap) {
175             my $xml = $self->xml_body;
176             my $auth = XML::Atom::Util::first($xml, NS_WSSE, 'UsernameToken');
177             $param{Username} = XML::Atom::Util::textValue($auth, NS_WSSE, 'Username');
178             $param{PasswordDigest} = XML::Atom::Util::textValue($auth, NS_WSSE, 'Password');
179             $param{Nonce} = XML::Atom::Util::textValue($auth, NS_WSSE, 'Nonce');
180             $param{Created} =
181             # Using XML::Atom::Client, Created comes with WSU namespace,
182             # but some the original code in XML::Atom::Server only looks
183             # at WSSE
184             XML::Atom::Util::textValue($auth, NS_WSSE, 'Created') ||
185             XML::Atom::Util::textValue($auth, NS_WSU, 'Created');
186             } else {
187             my $wsse = $req->header('X-WSSE');
188             if (! $wsse) {
189             $self->auth_failure(401, 'X-WSSE authentication required');
190             return;
191             }
192              
193             $wsse =~ s/^(?:WSSE|UsernameToken) //;
194             for my $i (split /,\s*/, $wsse) {
195             my($k, $v) = split /=/, $i, 2;
196             $v =~ s/^"//;
197             $v =~ s/"$//;
198             $param{$k} = $v;
199             }
200             }
201             return \%param;
202             }
203              
204             sub authenticate {
205             my $self = shift;
206             my $auth = $self->get_auth_info;
207             if (! $auth) {
208             return;
209             }
210              
211             for my $f (qw( Username PasswordDigest Nonce Created )) {
212             if (! $auth->{$f}) {
213             $self->auth_failure(400, "X-WSSE requires $f");
214             return;
215             }
216             }
217             my $password = $self->_call('password_for_user', $auth->{Username});
218             if (! defined $password) {
219             $self->auth_failure(403, 'Invalid login');
220             return;
221             }
222              
223             my $expected = MIME::Base64::encode_base64(
224             Digest::SHA1::sha1(
225             MIME::Base64::decode_base64($auth->{Nonce}) .
226             $auth->{Created} .
227             $password
228             ),
229             '');
230             if ($expected ne $auth->{PasswordDigest}) {
231             $self->auth_failure(403, 'Invalid login');
232             return;
233             }
234             return 1;
235             }
236              
237             sub auth_failure {
238             my ($self, $code, $reason) = @_;
239             my $res = $self->res;
240             $res->header('WWW-Authenticate', 'WSSE profile="UsernameToken"');
241             $self->error($code, $reason);
242             }
243              
244             sub error {
245             my ($self, $code, $reason) = @_;
246             my $res = $self->res;
247             $res->code($code);
248             # XXX PSGI doesn't really give us a way to override the
249             # message portion of response status line, so shove it in
250             # X-Reason header
251             $res->header('X-Reason', $reason);
252             }
253              
254             sub xml_body {
255             my $self = shift;
256              
257             my $req = $self->req;
258             my $env = $req->env;
259             my $body = $env->{'xml.server.xml_body'};
260             if (defined $body) {
261             return $body;
262             }
263              
264             $body = $self->xml_parser->parse_string($req->content);
265             if (defined $body) {
266             $env->{'xml.server.xml_body'} = $body;
267             }
268             return $body;
269             }
270              
271             sub atom_body {
272             my $self = shift;
273              
274             my $req = $self->req;
275             my $env = $req->env;
276             my $atom;
277             if ($self->is_soap) {
278             my $xml = $self->xml_body;
279             $atom = XML::Atom::Entry->new(Doc => XML::Atom::Util::first($xml, NS_SOAP, 'Body'));
280             } else {
281             $atom = XML::Atom::Entry->new(Stream => \$req->content);
282             }
283             return $atom;
284             }
285              
286             package
287             XML::Atom::Server::PSGI::XMLParser;
288             use strict;
289             use Class::Accessor::Lite
290             new => 1,
291             rw => [ qw(parser) ]
292             ;
293             BEGIN {
294             if (! XML::Atom::LIBXML) {
295             require XML::XPath;
296             XML::XPath->import;
297             }
298             }
299              
300             sub parse_string {
301             my ($self, $string) = @_;
302              
303             if (XML::Atom::LIBXML) {
304             my $parser = $self->parser;
305             if (! $parser) {
306             $parser = XML::Atom->libxml_parser;
307             $self->parser($parser);
308             }
309             return $parser->parse_string($string);
310             } else {
311             return XML::XPath->new(xml => $string);
312             }
313             }
314              
315             1;
316             __END__