File Coverage

blib/lib/Jabber/RPC/HTTPgate.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             # Jabber-RPC: J-RPC/HTTP Gateway
2             # (experimental!)
3             # (c) DJ Adams 2001
4              
5             # $Id: HTTPgate.pm,v 1.1.1.1 2001/10/14 20:47:51 dj Exp $
6              
7             =head1 NAME
8              
9             Jabber::RPC::HTTPgate - An HTTP gateway for Jabber-RPC / XML-RPC
10              
11             =head1 SYNOPSIS
12              
13             use Jabber::RPC::HTTPgate;
14              
15             my $gw = new Jabber::RPC::HTTPgate(
16             server => 'myserver.org:5701',
17             identauth => 'jrpchttp.localhost:secret',
18             httpcomp => 'http',
19             );
20              
21             $gw->start;
22              
23             =head1 DESCRIPTION
24              
25             Jabber::RPC::HTTPgate is an experimental gateway that provides
26             a conduit service between 'traditional' (HTTP-transported)
27             XML-RPC encoded requests/responses and Jabber-RPC (XML-RPC
28             encoded requests/responses transported over Jabber).
29              
30             The idea is that you can start a gateway, that connects as
31             a component to the backbone of a Jabber server, and it proxies
32             Jabber-RPC to HTTP-based XML-RPC endpoints, and vice versa.
33             That means that your Jabber-RPC client can not only make XML-RPC
34             encoded calls to a Jabber-RPC endpoint but also to a 'traditional'
35             HTTP-based XML-RPC endpoint. And it also means that your
36             'traditional' HTTP-based XML-RPC client can make XML-RPC encoded
37             calls to a Jabber-RPC endpoint.
38              
39             =head2 Jabber -> HTTP
40              
41             When you create and start up a gateway, it listens for Jabber-RPC
42             calls, just like a normal Jabber-RPC responder. On receipt of such a
43             call, the gateway creates an HTTP request and sends this
44             request on to the HTTP-based XML-RPC endpoint. The response
45             received back from this HTTP call is relayed back to the original
46             Jabber-RPC requester.
47              
48             While a Jabber-RPC endpoint address is a Jabber ID (JID), an
49             traditional XML-RPC endpoint address is a URL. So all the Jabber-RPC
50             client needs to do is specify the URL in the I part of
51             the gateway's endpoint JID.
52              
53             =head2 HTTP -> Jabber
54              
55             As well as listening for Jabber-RPC calls, a gateway will also
56             service incoming HTTP requests that can be made to the HTTP
57             component that this gateway uses. The HTTP component (called
58             simply 'http') can be downloaded from the normal Jabber software
59             repository.
60              
61             On receipt of an HTTP request (passed to it by the HTTP component),
62             the gateway creates a Jabber-RPC request containing the XML-RPC
63             encoded payload, and sends it on to the Jabber-RPC responder
64             endpoint. This endpoint is identified (via a JID) by the I
65             part of the URL used in the call by the traditional client.
66              
67             =head2 Diagram
68              
69             Here's what it all looks like:
70              
71              
72             +---+ 2-----------+ 3-----------+
73             Jabber backbone -----> l | http |<-- HTTP -->| HTTP |
74             l l===| component | | responder |
75             5-----------+ l l +-> |<- HTTP -+ | |
76             | JabberRPC | l l | +-----------+ | +-----------+
77             | responder |===l l | |
78             |(component)|<---+ l +---+ | 4-----------+
79             +-----------+ l| l | | | HTTP |
80             l| 3-v---------+ +->| requester |
81             6-----------+ l+----->| HTTPgate | | |
82             | JabberRPC | l| l===| component | +-----------+
83             | responder | l| +--->| |
84             | (client) | l| |l +-----------+
85             +-----------+ l| |l
86             ^ l| |l
87             | l| |l
88             v l| |l
89             7-----------+ l| |l
90             | JSM |<---+ |l
91             | component |===l |l
92             | |<-----+l
93             +-----------+ l
94             ^ l l
95             | : :
96             v +---+
97             8-----------+
98             | JabberRPC |
99             | requester |
100             | (client) |
101             +-----------+
102              
103             The diagram shows all the possible components in the Jabber-RPC and
104             traditional HTTP-based XML-RPC world. Each box is numbered. Here are
105             the descriptions:
106              
107             =over 4
108              
109             =item 1 HTTPgate component
110              
111             This is an instance of this module (HTTPgate.pm) and serves as a
112             gateway between HTTP-based and Jabber-based XML-RPC requests and
113             responses. On the one side it uses an http component (see #2) to
114             make and respond to HTTP calls, and on the other side it accepts
115             and generates packets containing XML-RPC encoded payloads.
116              
117             =item 2 http component
118              
119             The HTTPgate component uses this http component to make and receive
120             HTTP calls. HTTP calls and responses are routed between the HTTPgate
121             component and this component via s. You need an http
122             component like this for HTTPgate to work; download the code from
123             http://download.jabber.org.
124              
125             =item 3 HTTP responder
126              
127             This represents a web server on which an XML-RPC responder is
128             present.
129              
130             =item 4 HTTP requester
131              
132             This represents a traditional HTTP-based XML-RPC requester.
133              
134             =item 5 Jabber-RPC responder (component)
135              
136             This is a Jabber-RPC responder that has been attached to the
137             Jabber backbone as a component. It responds to
138             XML-RPC encoded requests carried in packets.
139              
140             =item 6 Jabber-RPC responder (client)
141              
142             This is a Jabber-RPC responder that is connected to Jabber
143             via the JSM (Jabber Session Manager) as a client. It also responds
144             to XML-RPC encoded requests carried in packets.
145              
146             =item 7 JSM component
147              
148             This doesn't have anything to do with Jabber-RPC per se, it's
149             just that Jabber clients connect via the JSM.
150              
151             =item 8 Jabber-RPC requester
152              
153             This is a Jabber-RPC requester, in the form of a Jabber client,
154             connected via the JSM.
155              
156             =head2 Some examples
157              
158             =over 4
159              
160             =item 1 Jabber-RPC client makes request directed to an HTTP-based
161             responder.
162              
163             The Jabber-RPC requester (#8) connects to Jabber via the JSM and
164             creates an XML-RPC encoded request and stores it as the query payload
165             of an IQ packet. The namespace qualifying the query payload is
166             jabber:iq:rpc. Normally, if the request were to to go a Jabber-RPC
167             responder, the JID of that responder (e.g. jrpc.localhost/jrpc-server)
168             would be specified in the 'to' attribute of the IQ packet. But
169             in this case, we want to send the request to an HTTP responder (#3),
170             so we go through the gateway - the HTTPgate component (#3).
171              
172             The address of the HTTP responder is a URL, e.g.:
173              
174             http://localhost:8000/RPC2
175              
176             So we need to specify this URL somewhere - and we specify it in the
177             I part of the HTTPgate component's JID. So if the HTTPgate
178             component's basic JID is jrpchttp.localhost, then we specify
179              
180             jrpchttp.localhost/http://localhost:8000/RPC2
181              
182             as the target JID.
183              
184             =item 2 HTTP-based requester makes request directed to a Jabber-RPC
185             responder.
186              
187             The HTTP requester (#4) formulates an XML-RPC encoded request and sends
188             it to the http component (#2). What's the basic URL of the http
189             component? Well, you specify a port in the component instance definition
190             in the jabber.xml configuration file, like this:
191              
192            
193             ./http/http.so
194            
195            
196            
197            
198             60
199             http-dns
200            
201            
202              
203             See the README in the http component tarball for more info.
204              
205             So the basic URL of the http component is e.g.:
206              
207             http://localhost:5281
208              
209             While we need to specify a URL when we call a HTTP-based XML-RPC
210             responder from Jabber, this time we need to specify a JID when calling
211             a Jabber-based XML-RPC responder from HTTP. What we do is extend the
212             URL by specifying JID as the I, like this:
213              
214             http://localhost:5281/jrpc@localhost/jrpc-server
215              
216             In this example, the Jabber-RPC responder is connected to Jabber as
217             a client, not a component - you can tell this from the JID by the
218             existence of an @ sign (user@hostname - users (their sessions) are
219             managed by the JSM).
220              
221             =back
222              
223             =head2 Setting it all up
224              
225             You need three components to get this working in both directions;
226             the HTTPgate component itself, the http component, and a helper DNS
227             resolver component for when the http component wants to make outgoing
228             HTTP requests. Setting up the latter two components are described in
229             the http component's README. (The helper DNS resolver is identified
230             in the example above by the tag).
231              
232             In the HTTPgate's instantiation call, e.g.:
233              
234             my $gw = new Jabber::RPC::HTTPgate(
235             server => 'localhost:5701',
236             identauth => 'jrpchttp.localhost:secret',
237             httpcomp => 'http',
238             );
239              
240             there are three arguments required.
241              
242             =over 4
243              
244             =item server
245              
246             This argument specifies the host and port to which the
247             HTTPgate component will connect. You will need a corresponding
248             component instance definition in your jabber.xml configuration
249             file that looks like this:
250              
251            
252            
253             127.0.0.1
254             secret
255             5701
256            
257            
258              
259             =item identauth
260              
261             This is the identity and secret for the component, separated by
262             a colon. The identity refers to the value of the 'id' attribute in
263             the component instance definition, and the secret refers to the
264             value of the tag.
265              
266             =item httpcomp
267              
268             This is used to specify the name of the http component, and refers
269             to the value of the 'id' attribute in the http component's instance
270             definition in jabber.xml - see earlier for an example of this.
271              
272             =back
273              
274             =head1 VERSION
275              
276             early
277              
278             =head1 AUTHOR
279              
280             DJ Adams
281              
282             =head1 SEE ALSO
283              
284             Jabber::RPC, Jabber::Connection
285              
286             =cut
287              
288             package Jabber::RPC::HTTPgate;
289              
290 1     1   816 use strict;
  1         2  
  1         32  
291 1     1   914 use URI;
  1         8232  
  1         31  
292 1     1   1520 use Jabber::Connection;
  0            
  0            
293             use Jabber::NS qw(:all);
294              
295             use vars qw($VERSION);
296              
297             $VERSION = '0.01';
298              
299             sub new {
300              
301             my $class = shift;
302             my %args = @_;
303             my $self = {};
304            
305             $self->{server} = $args{server};
306             $self->{httpcomp} = $args{httpcomp};
307             ($self->{id}, $self->{pass}) = split(':', $args{identauth});
308              
309             $self->{iq_requests} = { id => 1 };
310              
311             $self->{c} = new Jabber::Connection(
312             server => $self->{server},
313             localname => $self->{id},
314             ns => NS_ACCEPT,
315             log => 1,
316             );
317              
318             $self->{c}->register_handler('iq', sub { $self->_reflect(@_) } );
319             $self->{c}->register_handler('iq', sub { $self->_relay_result(@_) } );
320             $self->{c}->register_handler('route', sub { $self->_handle_http(@_) } );
321              
322             $self->{nf} = new Jabber::NodeFactory;
323              
324             $self->{c}->connect or die "Oops: ".$self->{c}->lastError;
325             $self->{c}->auth($self->{pass});
326              
327             bless $self => $class;
328             return $self;
329              
330             }
331              
332             sub start {
333              
334             my $self = shift;
335             $self->{c}->start;
336              
337             }
338              
339              
340              
341             sub _reflect {
342              
343             my $self = shift;
344             my $node = shift;
345              
346             # Ignore irrelevant packets. What we want is
347             # an IQ-set with a jabber:iq:rpc qualified NS
348             return unless $node->attr('type') eq IQ_SET
349             and my $query = $node->getTag('query', NS_RPC);
350              
351             my $request = $query->getTag('methodCall')->toStr;
352             # my $request = $node->getTag('query', NS_RPC)->getTag('methodCall')->toStr;
353              
354             # We need to create a route packet to the http component
355             # that looks like this:
356             #
357             #
358             #
359             #
360             # the payload
361             #
362             #
363             #
364              
365             # Store the request addresses
366             # (we want to refer to this in handle_response())
367             $self->{iq_requests}->{++$self->{iq_requests}->{id}} = {
368             'from' => $node->attr('from'),
369             'to' => $node->attr('to'),
370             'id' => $node->attr('id'),
371             };
372              
373             my $route = $self->{nf}->newNode('route');
374             $route->attr('type', 'request');
375             $route->attr('to', $self->{httpcomp});
376             $route->attr('from', join('@', $self->{iq_requests}->{id}, $self->{id}));
377              
378             # Resource should contain the URL
379             my (undef, undef, $r) = _parseJID($node->attr('to'));
380              
381             # Split up URL
382             my $uri = new URI($r);
383              
384             my $http = $route->insertTag('http');
385             $http->attr('type', 'post');
386             $http->attr('to', $uri->host);
387             $http->attr('port', $uri->port) if $uri->port;
388             $http->attr('path', $uri->path) if $uri->path;
389              
390             # Insert the payload
391             $http->insertTag('body')->rawdata($request);
392              
393             # Send it along to the http component
394             $self->{c}->send($route);
395              
396             }
397              
398              
399             sub _relay_result {
400              
401             my $self = shift;
402              
403             # Relaying the result of a JRPC IQ, via the http
404             # component. We need to generate a routed packet
405             # that looks like this:
406             #
407             #
408             #
409             #
410             # my content
411             #
412             #
413              
414             my $node = shift;
415              
416             # Ignore irrelevant packets. What we
417             # want is an IQ result with a NS_JRPC-qualified
418             return unless ($node->attr('type') eq IQ_RESULT
419             or $node->attr('type') eq IQ_ERROR)
420             and my $query = $node->getTag('query', NS_RPC);
421            
422             # The resource on the 'to' is the http id
423             # to send back to
424             (undef, undef, my $httpid) = _parseJID($node->attr('to'));
425              
426             # Create route to send HTTP result back
427             my $route = $self->{nf}->newNode('route');
428             $route->attr('type', 'result');
429             $route->attr('to', $httpid);
430             $route->attr('from', $self->{id});
431              
432             # Add http element
433             my $http = $route->insertTag('http');
434              
435             # We need to reflect any IQ-related error
436             # (e.g. 502 Internal Timeout) - how ...?
437             # As the HTTP status? Hmmm.
438             if ($node->attr('type') eq IQ_ERROR) {
439             my $error = $node->getTag('error');
440             $http->attr('status', join(' ', $error->attr('code'), $error->data));
441             }
442             else {
443             $http->attr('status', '200 OK');
444             }
445              
446             # Copy the payload from the IQ
447             my $payload = $query->getTag('')->toStr; # usually methodResponse but will
448             # be methodCall when type='error'
449              
450             # Add some headers
451             my $header;
452             $header = $http->insertTag('head');
453             $header->attr('Content-type', 'text/xml');
454             $header = $http->insertTag('head');
455             $header->attr('Content-length', length($payload));
456              
457             $http->insertTag('body')->rawdata($payload);
458              
459             $self->{c}->send($route);
460              
461             }
462              
463              
464             sub _handle_http {
465              
466             my $self = shift;
467             my $node = shift;
468              
469             # Ignore everything except from the http component
470             my (undef, $sending_host, undef) = _parseJID($node->attr('from'));
471             return unless $sending_host eq $self->{httpcomp};
472              
473             # Either it's a request or a result:
474             # A request: an incoming HTTP call to be reflected
475             # to a Jabber-component/client based responder
476             # A result: the result of our making an HTTP call in
477             # a Jabber-to-HTTP reflection
478              
479             if ($node->attr('type') eq 'request') { $self->_handle_http_request($node) }
480             if ($node->attr('type') eq 'result') { $self->_handle_http_result($node) }
481              
482             return;
483              
484             }
485              
486             sub _handle_http_request {
487              
488             my $self = shift;
489             my $node = shift;
490              
491             # Request will look like this:
492             #
493             #
494             #
495             # foo
496             #
497             #
498              
499             my $http = $node->getTag('http');
500              
501             # Ignore non-POST requests
502             return unless $http->attr('type') eq 'post';
503              
504             # Get path - this is the JID of the Jabber-RPC responder
505             my $jid = $http->attr('path');
506             $jid =~ s|^\/||; # must remove initial slash
507            
508             # Get the payload
509             my $payload = $http->getTag('body')->getTag('methodCall')->toStr;
510              
511             # Construct the request IQ
512             my $iq = $self->{nf}->newNode('iq');
513             $iq->attr('type', IQ_SET);
514             $iq->attr('from', join('/', $self->{id}, $node->attr('from')));
515             $iq->attr('to', $jid);
516             $iq->insertTag('query', NS_RPC)->rawdata($payload);
517              
518             $self->{c}->send($iq);
519              
520             }
521              
522              
523             sub _handle_http_result {
524              
525             my $self = shift;
526             my $node = shift;
527              
528             # Set up response
529             my $response = $self->{nf}->newNode('iq');
530              
531             # Get the key for our request hash - it's in
532             # the user part of the JID
533             my ($u, undef, undef) = _parseJID($node->attr('to'));
534              
535             # Address the response
536             $response->attr('to', $self->{iq_requests}->{$u}->{from});
537             $response->attr('from', $self->{iq_requests}->{$u}->{to});
538              
539             # Respond with the id from the request
540             $response->attr('id', $self->{iq_requests}->{$u}->{id});
541            
542             # Reflect the type (result|error) from the route
543             # into the returning IQ
544             $response->attr('type', $node->attr('type'));
545             if ($response->attr('type') eq IQ_ERROR) {
546             my $error = $response->insertTag('error');
547             $error->attr('code', '502');
548             $error->data('Remote Server Error');
549             }
550              
551             # Transfer the payload from the result to our IQ
552             # (If it's an error, the HTTP component returns the
553             # original payload. This is nice.)
554             my $query = $response->insertTag('query', NS_RPC);
555             my $payload = $node->getTag('http')->getTag('body')->getTag('');
556             $query->rawdata($payload->toStr);
557              
558             $self->{c}->send($response);
559              
560             }
561              
562              
563             sub _parseJID {
564              
565             my $jid = shift;
566             my ($user, $host, $resource, $rest);
567              
568             # Is there a username part? (an @ before any /)
569             if ($jid =~ m[^[^\/]+@]) {
570             ($user, $rest) = split("@", $jid, 2);
571             }
572             else {
573             $rest = $jid;
574             }
575              
576             ($host, $resource) = split('/', $rest, 2);
577              
578             return ($user, $host, $resource);
579              
580             }
581              
582              
583             1;