Push public IM aliases as XMPP roster names and vCard full names.
[thrasher.git] / perl / lib / Thrasher / Component.pm
blobf7d423bd55e172a4ebd6c4aea29899768a3324aa
1 package Thrasher::Component;
2 use strict;
3 use warnings;
5 =pod
7 =head1 NAME
9 Thrasher::Component - tie together XML stream processing and component
10 state into one module to handle a single "component"
12 =head1 DESCRIPTION
14 Thrasher::Component implements a XEP-0100 compliant component, with
15 hooks for adding further stuff into the component in a defined
16 way. The component is primarily "private" and documented in comments,
17 but this POD will document two things: The interface for the
18 Thrasher::Protocol implementations, and the hooks provided
19 for extending the base XEP-0100 protocol.
21 =cut
23 # General overview of this file: Some support code for the component
24 # is written in the front, then after the
25 ###### PROTOCOL SUPPORT
26 # comment below, we'll be implementing the component protocol
27 # by following along the specification at
28 # http://www.xmpp.org/extensions/xep-0114.html
29 # which will be referenced by section number.
31 # This file should only implement the bare minimum protocol stuff;
32 # additional capabilities should live elsewhere, just to avoid this
33 # file getting pointlessly large, and to avoid mixing extensions
34 # in with the XEP-0100 stuff.
36 # Right now this only permits one protocol per connection, but
37 # I'm trying to structure this for multi-protocol support in the
38 # future, per the later XEP on components.
40 use Thrasher::Log qw(:all);
41 use Thrasher::XMPPStreamOut;
42 use Thrasher::XMPPStreamIn qw(:all);
43 use Thrasher::Constants qw(:all);
44 use Thrasher::Plugin qw(:all);
45 use Thrasher::XML qw(:all);
46 use Thrasher::ConnectionManager qw(:all);
47 use Thrasher::XHTML_IM_Normalize qw(xhtml_and_text text);
48 use Thrasher::Callbacks qw(:all);
50 use Thrasher::Plugin::Basic;
51 use Encode;
53 use Carp qw(confess longmess);
54 use Data::Dumper;
56 use Digest::SHA1 qw(sha1_hex);
58 use base 'Exporter';
60 our @EXPORT_OK = qw(feature
61 has_subtags strip_resource no_match);
62 our %EXPORT_TAGS = (all => \@EXPORT_OK);
64 my $DEBUG = $Thrasher::DEBUG;
66 # This indicates whether or not to use the connection manager to
67 # prevent flooding and to tell whether or not the remote service is
68 # having trouble. This should generally only be turned off for
69 # debugging purposes, but it could be useful in other scenarios.
70 our $USE_CONNECTION_MANAGER = 1;
72 # For testing purposes, this allows use to simply tell the component
73 # whether it is getting directly connected or not, so we can
74 # test the XML output it is supposed to generate. In real execution
75 # this should never be set.
76 our $WILL_BE_DIRECTLY_CONNECTED = undef;
78 # Hmm, this isn't good, but ProxyFileTransfer needs it. When/if
79 # we ever want to run multiple components out of one protocol we
80 # need to fix this.
81 our $COMPONENT;
83 # This manages the input from and the output to the component, but is
84 # not responsible for managing the socket; it receives XML text and
85 # is expected to output XML text. This, again, allows us to trivially
86 # test this component in a unit test environment without having to
87 # connect to a real XMPP server.
89 # Text is received by this object by calling "->text_in". Text out
90 # is sent out along the closure received during object construction,
91 # converted through Thrasher::XMPPStreamOut into text. A
92 # method ->xml_out accepts XML for output, but should be internal-only.
94 # used to make unique IDs
95 our $id = 1;
96 sub get_id {
97 return "id" . ($id++);
100 # States change mostly during connection. This changes which
101 # functions are passed the incoming XML events.
102 my $states = {
103 'disconnected' => sub { },
104 connecting => \&xml_in_connecting,
105 handshaking => \&xml_in_handshaking,
106 connected => \&xml_in_connected
109 # Out-of-band hook:
110 our $UNREGISTER_CALLBACK = undef;
112 sub new {
113 my $class = shift;
114 my $self = {};
115 bless $self, $class;
117 $self->{protocol} = shift;
118 if (!UNIVERSAL::isa($self->{protocol}, 'Thrasher::Protocol')) {
119 die "The first argument to the component needs to be "
120 ."a Thrasher::Protocol instance.";
123 my $text_output_closure = shift;
124 if (ref($text_output_closure) ne 'CODE') {
125 die "Creating a component requires a closure for the output"
126 ." of XML text.";
128 $self->{output} = $text_output_closure;
130 $self->setup_streams;
132 # Need info for the stream connect
133 $self->{secret} = shift;
134 $self->{component_name} = shift;
136 $self->{state} = 'disconnected';
137 $self->{xml_buf} = [];
139 # This holds a jid => {registration_info => $registration_info,
140 # error_xml => $xml_tag} hash. If we get a login request, but
141 # we've already tried that registration info and the remote
142 # service told us it's bad, we don't re-try it. This is
143 # deliberately held in transient memory storage.
144 $self->{authentication_errors} = {};
146 $COMPONENT = $self;
148 return $self;
151 sub setup_streams {
152 my $self = shift;
154 my $out_stream = new Thrasher::XMPPStreamOut($self->{output});
155 $self->{out_stream} = $out_stream;
157 my $in_stream = Thrasher::XMPPStreamIn::get_parser();
158 $self->{in_stream} = $in_stream;
161 # The usual xml_out used for most traffic, will buffer the
162 # XML if we're not currently connected.
163 sub xml_out {
164 my $self = shift;
166 if ($self->{state} ne 'connected') {
167 push @{$self->{xml_buf}}, @_;
168 return;
170 $self->{out_stream}->output(@_);
173 # xml_out used by connection routines, to force out the
174 # necessary handshaking.
175 sub force_xml_out {
176 my $self = shift;
177 $self->{out_stream}->output(@_);
180 sub output_initial_stream_tag {
181 my $self = shift;
183 my $initial_stream =
184 [[$NS_STREAM, 'stream'],
185 {"{$NS_COMPONENT}to" => $self->{component_name}},
186 []];
187 $self->set_state('connecting');
188 # This is a direct call to output_tag_and_children so we can
189 # pass in the $is_root_element value, which this needs.
190 $self->{out_stream}->output_tag_and_children($initial_stream, 1);
193 # Once we know we're connected, probe everybody in our list
194 sub initialize_connection {
195 my $self = shift;
197 if ($self->{initialized}) {
198 log("Already initialized connection.");
199 return;
202 $self->{initialized} = 1;
204 log("Initializing connection");
206 my $backend = $self->{protocol}->{backend};
207 my $all_jids = $backend->all_jids;
209 for my $jid (@$all_jids) {
210 $self->send_presence_xml($jid, 'probe');
213 # Send a discovery request at the server, which we
214 # then recurse down one level to get the info for
215 # those items.
216 # But only do it if we have a SERVER NAME.
217 if ($Thrasher::SERVER_NAME) {
218 $self->iq_query
219 ([[$NS_COMPONENT, 'iq'],
220 {to => $Thrasher::SERVER_NAME,
221 from => $self->{component_name},
222 type => 'get'},
223 [[[$NS_DISCO_ITEMS, 'query'], {}, []]]],
224 sub {
225 my $component = shift;
226 my $iq_params = shift;
227 my $iq_packet = shift;
229 if ($iq_params->{type} eq 'error') {
230 # FIXME: Schedule a timeout to try again.
231 log("Server discovery failed, this may cause "
232 ."odd, random problems.");
233 failed("server_discovery_items");
234 return;
237 my $items =
238 extract_disco_items($iq_params->{query});
239 my %items_hash = map { $_ => 1} @$items;
241 my $item_count = scalar(@$items);
243 # For each item, fire off an info request
244 for my $item (@$items) {
245 if ($item eq $self->{component_name}) {
246 $item_count--;
247 next;
250 $self->iq_query
251 ([[$NS_COMPONENT, 'iq'],
252 {to => $item,
253 from => $self->{component_name},
254 type => 'get'},
255 [[[$NS_DISCO_INFO, 'query'], {}, []]]],
256 sub {
257 my $component = shift;
258 my $iq_params = shift;
259 my $iq_packet = shift;
260 debug("Disco info got $iq_params->{type} from $item"
261 . " with $item_count remaining.");
263 if ($iq_params->{type} ne 'error') {
264 my ($identities, $features) =
265 extract_disco_info($iq_params->{query});
266 $Thrasher::SERVER_INFO->{$item} =
267 [$identities, $features];
268 } else {
269 # Server was configured to return this
270 # existed, but it doesn't seem to.
271 # Hopefully the proxy service isn't
272 # transient
273 delete $Thrasher::SERVER_INFO->{$item};
276 $item_count--;
278 if ($item_count == 0) {
279 succeeded("server_discovery_items");
282 # Ensure server_discovery_items callbacks fire
283 # eventually even when some component doesn't reply.
284 # That would be one evil component...
285 my $no_reply_check_timeout = sub {
286 # after 30s...
287 if ($item_count == scalar(@{$items})) {
288 # ...not a single component responded?!!
289 failed('server_discovery_items');
291 elsif ($item_count > 0) {
292 # ...got some responses, but not all.
293 # Give what we have to success callbacks--if
294 # we luck out the missing component(s) aren't
295 # the ones they need.
296 succeeded('server_discovery_items');
298 return 0; # never repeat.
300 $self->{'event_loop'}->schedule($no_reply_check_timeout,
301 30000);
307 sub set_state {
308 my ($self, $state) = @_;
309 $self->{state} = $state;
311 if ($state eq 'connected') {
312 log("State set to 'connected'");
313 $self->initialize_connection;
317 sub xml_in {
318 my $self = shift;
319 my $xml = shift;
321 my $state_xml_func = $states->{$self->{state}};
322 if (!defined($state_xml_func)) {
323 die "Receiving xml, but I don't have a handler for "
324 ."state '" . $self->{state} . "', how odd! (1)";
327 my $parsed = $self->{in_stream}->parse($xml);
328 for my $message (@$parsed) {
329 $state_xml_func->($self, $message);
331 # State may change after processing a message
332 $state_xml_func = $states->{$self->{state}};
333 if (!defined($state_xml_func)) {
334 die "Receiving xml, but I don't have a handler for "
335 ."state '" . $self->{state} . "', how odd! (2)";
342 ######
343 ## State handlers; mostly for connection, as once we're connected
344 ## this component is basicly in a steady state. (The individual
345 ## connections are more complicated, but not this.)
346 ######
348 my $STREAM = [$NS_STREAM, 'stream'];
349 my $HANDSHAKE = [$NS_COMPONENT, 'handshake'];
351 # In this state, we've sent the original <stream:stream ...> tag,
352 # and we're expecting the stream tag from the server
353 sub xml_in_connecting {
354 my $self = shift;
355 my $xml_message = shift;
357 eval {
358 multi_extract(
359 $xml_message,
361 # Expected case - stream returned from the server.
362 # Annoyingly, we can't really check for stream errors
363 # at this level, since the stream tag is exactly the
364 # same for success and failure, EXCEPT that we get
365 # an additional error tag upon failure.
366 [$STREAM, {from => save('host'), id => save('stream_id')}] =>
367 sub {
368 # Server is on the track we expect, send out the
369 # handshake
370 my $params = shift;
371 my $handshake = lc(sha1_hex($params->{stream_id}
372 .$self->{secret}));
373 $self->set_state('handshaking');
375 $self->force_xml_out([$HANDSHAKE, {}, [$handshake]]);
378 if ($@) {
379 log("Error in stream tag? Reconnecting:\n$@\n");
380 $self->reconnect_stream();
384 sub xml_in_handshaking {
385 my $self = shift;
386 my $xml_message = shift;
388 # If this passes, we're connected.
389 eval {
390 multi_extract($xml_message,
391 [[$NS_COMPONENT, 'handshake'], {}, []] =>
392 sub {
393 $self->set_state('connected');
394 callbacks('connected', $self);
397 [[$NS_STREAM, 'error'], {}, save('text')] =>
398 sub {
399 my $params = shift;
400 die "Stream error after handshake. Server said: $params->{text}";
403 if ($@) {
404 log("Handshake error; reconnecting:\n$@\n");
405 $self->reconnect_stream();
409 # In this state, we are connected, and are receiving arbitrary
410 # packets from arbitrary users.
411 sub xml_in_connected {
412 my $self = shift;
413 my $xml_message = shift;
415 # Route the XML message according to the nature of the message.
416 multi_extract($xml_message,
418 # IQ messages
419 [[$NS_COMPONENT, 'iq'],
420 {type => save("type"),
421 from => save("from"),
422 to => save("to"),
423 id => save("id"),
424 "{$NS_XML}lang" => save("language", 1)
426 # Save first child under "query" whether or not
427 # that's the actual tag name (e.g. "si").
428 save_match('query', [undef, undef, undef], 1)] =>
429 sub {
430 my $iq_params = shift;
432 # Get and set handlers
433 return $self->handle_iq($iq_params,
434 $xml_message);
437 [[$NS_COMPONENT, 'presence'],
438 undef, undef] =>
439 sub {
440 callbacks('presence_in',
441 $self,
442 sub { $self->handle_presence($_[0]) },
443 $xml_message);
446 [[$NS_COMPONENT, 'message'], {
447 to => save('to'),
448 from => save('from'),
449 type => save('type', 1),
452 save_match('chatstate',
453 [[$NS_CHATSTATES, undef], undef, undef],
455 save_match('body',
456 [[undef, 'body'], undef, undef],
458 ]] =>
459 sub {
460 my $message_params = shift;
461 $message_params->{'type'} ||= 'chat';
462 if ($message_params->{'chatstate'}) {
463 $message_params->{'chatstate'}
464 = $message_params->{'chatstate'}->[0]->[1];
466 $self->handle_message($message_params->{to},
467 $message_params->{from},
468 $message_params->{body},
469 $message_params->{'type'},
470 $message_params->{'chatstate'});
473 # Stream error
474 [[$NS_STREAM, 'error']] =>
475 sub {
476 my $children = $xml_message->[2];
477 my $first_tag;
478 for my $child (@$children) {
479 if (ref($child) eq 'ARRAY') {
480 $first_tag = $child;
481 last;
485 if (!$first_tag) {
486 # No error tag? Shouldn't happen. Panic!
487 $self->terminate;
488 return;
491 my $tag_name = $first_tag->[0]->[1];
492 # If it's a "not well formed" error,
493 # we can try to reconnect. If it's anything
494 # else, panic.
495 if ($tag_name eq 'xml-not-well-formed') {
496 $self->reconnect_stream;
497 } else {
498 $self->terminate;
502 # Default handler - complain about the unknown
503 # packet, but otherwise ignore it.
504 undef() =>
505 sub {
506 log "Unexpected packet: " . Dumper($xml_message);
511 # $IQ_CALLBACKS{"${jid_without_resource}-${id}"} => \&callback;
513 # The request/response ID (generated in iq_query()) includes the bare
514 # JID in case another user tries to inject a forged response.
515 our %IQ_CALLBACKS;
517 sub handle_iq {
518 my $self = shift;
519 my $iq_params = shift;
520 my $iq_packet = shift;
522 my $id = $iq_packet->[1]->{'{}id'};
524 my $request_id = strip_resource($iq_params->{'from'}) . '-' . $id;
525 my $callback = $IQ_CALLBACKS{$request_id};
526 if ($callback
527 # Must not mistake unrelated requests using the same ID
528 # scheme for the expected response (e.g. two Thrasher
529 # instances doing server disco at the same time).
530 && $iq_params->{type} =~ /^(?:result|error)$/) {
531 local $@;
532 eval {
533 $callback->($self, $iq_params, $iq_packet);
535 log "IQ callback error: $@" if ($@);
536 delete($IQ_CALLBACKS{$request_id});
537 return;
540 if (! $iq_params->{query}
541 || @{$iq_params->{query}} == 0
542 || @{$iq_params->{query}->[0]} == 0) {
543 # Unused and causes interesting issues when replying
544 # especially if autovivification occurs.
545 log('Skipping childless IQ: ' . Dumper($iq_packet));
546 return;
549 my $query_ns = $iq_params->{query}->[0]->[0];
550 my $query_type = $iq_packet->[1]->{'{}type'};
551 my $target = 'client';
552 if (!defined($iq_params->{to}) ||
553 $iq_params->{to} eq $self->{component_name}) {
554 $target = 'component';
556 my $func = method_for_iq_namespace($target, $query_type, $query_ns);
558 # Allow ourselves to suppress the error for some namespaces.
559 if ($func && $func eq 'ignore') {
560 $self->iq_error($iq_params, 'service_unavailable');
561 return;
564 if (!defined($func)) {
565 log "Unexpected IQ query: " . Dumper($iq_params,
566 $target, $query_type,
567 $query_ns);
568 if ($query_type ne 'error') {
569 # Prevent loop with error response to error generating an error....
570 $self->iq_error($iq_params, 'service_unavailable');
572 return;
575 return $func->($self, $iq_params, $iq_packet);
578 sub iq_query {
579 my $self = shift;
580 # Everything but the ID
581 my $iq_packet = shift;
582 my $callback = shift;
584 my $id = get_id;
585 $iq_packet->[1]->{id} = $id;
587 if ($callback) {
588 # get_id() never repeats within a Thrasher instance so the only way
589 # we can get a duplicate ID in responses from the same bare JID
590 # is if the user sends two from different resources. Oh, well.
591 my $to = $iq_packet->[1]->{'to'}
592 || $iq_packet->[1]->{'{}to'}
593 || '';
594 my $request_id = strip_resource($to) . '-' . $id;
595 $IQ_CALLBACKS{$request_id} = $callback;
598 $self->xml_out($iq_packet);
602 ###### PROTOCOL SUPPORT
605 sub send_presence_xml {
606 my $self = shift;
607 my $target_jid = shift;
608 my $presence_type = shift;
609 my $from_jid = shift || $self->{component_name};
610 my $show = shift;
611 my $status = shift;
612 my $extra = shift;
614 # target_jid can be unset when the presence tag is coming
615 # from the transport itself
616 if ($target_jid) {
617 my $session = $self->session_for($target_jid);
618 if ($session && $session->{status} eq 'disconnecting') {
619 # Don't send presence info for connections we're
620 # currently disconnecting.
621 log("Bypassing a presence from $from_jid because disconnecting");
622 return;
627 no warnings 'uninitialized';
628 if ($target_jid eq $from_jid &&
629 $target_jid eq $self->{component_name}) {
630 log("Attempt to send presence to self: " . longmess);
633 if ($target_jid =~ /$self->{component_name}$/ &&
634 $from_jid =~ /$self->{component_name}$/) {
635 log("Attempting to send presence to self: "
636 . Dumper($target_jid, $from_jid) .
637 "\n" . longmess);
641 my @children;
642 if ($show) {
643 push @children, [[$NS_COMPONENT, 'show'], {}, [$show]];
645 if ($status) {
646 push @children, [[$NS_COMPONENT, 'status'], {}, [$status]];
648 if ($extra) {
649 push @children, @$extra;
652 my $presence_out_tag = [[$NS_COMPONENT, 'presence'],
653 {($presence_type ? (type => $presence_type) : ()),
654 from => $from_jid,
655 ($target_jid ? (to =>
656 strip_resource($target_jid)) : ())},
657 \@children];
659 callbacks('presence_out',
660 $self,
661 sub { $self->xml_out($_[0]) },
662 $presence_out_tag);
665 sub session_for {
666 my $self = shift;
667 my $session_for = shift;
668 $session_for = strip_resource($session_for);
669 return $self->{sessions}->{$session_for};
672 sub set_session_for {
673 my ($self, $jid, $session) = @_;
674 $jid = strip_resource($jid);
676 $self->{'sessions'}->{$jid} = $session;
679 # Welcome to the ugliest function in all of Thrasher!
680 sub handle_presence {
681 my $self = shift;
682 my $presence_tag = shift;
684 my ($element, $atts, $children) = @$presence_tag;
686 for my $att qw(to from) {
687 if (!$atts->{"{}$att"}) {
688 log "Presence received with no '$att'; ignored.";
689 return;
693 my $type = $atts->{'{}type'};
695 if ($type && $type eq 'error') {
696 log("Got a presence error.");
697 return;
700 # Section 4.1.1 #10 - our request accepted
701 # FIXME: What if the request is rejected?
702 if (defined($type) &&
703 ($type eq 'subscribed' || $type eq 'unsubscribed') &&
704 (!defined($atts->{'{}to'}) ||
705 $atts->{'{}to'} eq $self->{component_name})) {
706 return;
709 # Section 4.1.1 # 11
710 if (defined($type) &&
711 $type eq 'subscribe' &&
712 $atts->{'{}to'} eq $self->{component_name}) {
713 # Section 4.1.1 #12
714 # Hey, sure, buddy, no problem
715 # FIXME: There ought to be something about registration here.
716 $self->xml_out([[$NS_COMPONENT, 'presence'],
717 {type => 'subscribed',
718 from => $self->{component_name},
719 to => $atts->{'{}from'}},
720 []]);
721 return;
724 if (defined($type) &&
725 $type eq 'unsubscribe' &&
726 $atts->{'{}to'} eq $self->{component_name}) {
727 # Section 4.3.1 #5
728 # FIXME: Unregister?
729 $self->xml_out([[$NS_COMPONENT, 'presence'],
730 {type => 'unsubscribed',
731 from => $self->{component_name},
732 to => $atts->{'{}from'}},
733 []]);
734 return;
737 # Everything above here is there because it can be
738 # done without a session; below this, a session
739 # is required
741 my $from = strip_resource($atts->{'{}from'});
742 my $session = $self->session_for($from);
744 if (!defined($session) && $atts->{'{}type'}) {
745 if ($atts->{'{}type'} && $atts->{'{}type'} eq 'probe') {
746 # Not authorized.
747 $self->send_presence_xml($atts->{'{}from'},
748 'unavailable');
749 return;
752 my $registration_info =
753 $self->{protocol}->{backend}->registered($from);
755 if (!defined($registration_info)) {
756 if ($atts->{'{}from'} =~ /$self->{component_name}$/) {
757 # Don't reply to what is effectively ourself.
758 return;
761 if ($atts->{'{}to'} ne $self->{component_name}) {
762 # If this was a directed presence and it wasn't
763 # directly for the transport, eat it.
764 return;
767 $self->xml_out([[$NS_COMPONENT, 'presence'],
768 {from => $self->{component_name},
769 to => $atts->{'{}from'},
770 type => 'error'},
771 [error_tag('registration_required')]]);
772 return;
773 } else {
774 # A presence tag has been sent other than to log in,
775 # such as to subscribe, but the user is not currently
776 # logged in. If they are unsubscribing, go ahead
777 # and say they are unsubscribed. Otherwise, this
778 # is an error
779 if ($atts->{'{}type'} eq 'unsubscribe') {
780 $self->xml_out
781 ([[$NS_COMPONENT, 'presence'],
782 {from => $atts->{'{}to'},
783 to => $atts->{'{}from'},
784 type => 'unsubscribed'}, []]);
785 } else {
786 return;
788 # This gets sent out after logging off; if I can
789 # work out a way to distinguish that case vs.
790 # other cases where this would be called for, we can
791 # put it back.
792 $self->xml_out([[$NS_COMPONENT, 'presence'],
793 {from => $self->{component_name},
794 to => $atts->{'{}from'},
795 type => 'error'},
796 [error_tag('not_authorized')]]);
798 return;
802 if (!defined($type)) {
803 if (!defined($session)) {
804 $self->login($atts->{'{}from'}, $presence_tag);
805 if ($atts->{'{}to'} ne $self->{component_name}) {
808 } else {
809 $self->echo_presence($session, $presence_tag);
811 return;
814 if ($type eq 'subscribe') {
815 $session->subscribe($atts->{'{}to'});
816 return;
819 if ($type eq 'unsubscribe') {
820 $session->unsubscribe($atts->{'{}to'});
821 return;
824 if ($type eq 'subscribed' || $type eq 'unsubscribed') {
825 my $protocol = $self->{protocol};
826 my $legacy_name =
827 $self->xmpp_name_to_legacy($atts->{'{}from'},
828 $atts->{'{}to'});
829 if (!defined($legacy_name)) {
830 log "No legacy name for " . $atts->{'{}to'};
831 return;
833 $protocol->$type($session, $self, $legacy_name);
835 return;
838 # FIXME: This conforms to the specification, but I think
839 # we ought to track which resources are online and only
840 # disconnect if the user has no resources online.
841 if ($type eq 'unavailable') {
842 if (!$atts->{'{}to'} ||
843 $atts->{'{}to'} eq $self->{component_name}) {
844 $self->logout($session);
845 return;
846 } else {
847 # Maybe we should just skip this?
848 log ("Presence unavailable not handled properly: "
849 .Dumper($atts));
850 return;
854 if (defined($session)) {
855 $self->echo_presence($session, $presence_tag);
856 return;
859 if ($type eq 'probe') {
860 # We know who you are.
861 $self->send_presence_xml($atts->{'{}from'}, '');
862 return;
865 # This shouldn't be able to happen, all bases should be
866 # covered above.
867 log "Received unexpected presence packet with no "
868 . "associated session: \n" . Dumper($presence_tag);
871 # Echos the presence of the user back out to the protocol, be
872 # it a general update or a targetted update.
873 sub echo_presence {
874 my $self = shift;
875 my $session = shift;
876 my $presence_tag = shift;
878 my $type = $presence_tag->[1]->{'{}type'};
879 my $show;
880 my $status;
881 for my $child (@{$presence_tag->[2]}) {
882 if (ref($child) &&
883 $child->[0]->[1] eq 'show') {
884 $show = join '', @{$child->[2]};
886 if (ref($child) &&
887 $child->[0]->[1] eq 'status') {
888 $status = join '', @{$child->[2]};
892 my $to = $presence_tag->[1]->{'{}to'};
893 if ($to eq $self->{component_name}) {
894 $self->{protocol}->user_presence_update
895 ($session, $type, $show, $status);
896 } else {
897 my $target_user =
898 $self->xmpp_name_to_legacy(strip_resource($presence_tag->[1]->{'{}from'}),
899 $to);
900 if ($target_user) {
901 $self->{protocol}->user_targeted_presence_update
902 ($session, $type, $show, $status, $target_user);
903 } else {
904 log "Sent targetted presence to user "
905 .$presence_tag->[1]->{'{}from'} . ", but I have no such user.";
910 sub login {
911 my $self = shift;
912 my $full_jid = shift;
913 my $original_presence_tag = shift;
914 my $jid = strip_resource($full_jid);
916 # Already queued a past login attempt. Tell that attempt to use
917 # the current full JID and don't enqueue another one.
918 if ($self->{'connection_queued'}->{$jid}) {
919 $self->{'connection_queued'}->{$jid} = $full_jid;
920 $self->send_connection_queued($jid);
921 return;
924 my $registration_info = $self->{protocol}->{backend}->registered($jid);
926 if (my $error = $self->{authentication_errors}->{$jid}) {
927 my $bad_registration_info = $error->{registration_info};
928 if (compare_hashref($bad_registration_info,
929 $registration_info)) {
930 # It looks like this only happens when users ask for it,
931 # so dump out the XML.
932 $self->xml_out($error->{error_xml});
934 # Note there is one case this doesn't cover well; the user
935 # entered the wrong password, gets it labelled as bad,
936 # then actually CHANGES THE PASSWORD on the remote service
937 # to match this password. I'll worry when that happens,
938 # I guess, because right now the wrongness of pounding on
939 # the remote service outweighs that chance.
940 log("Discarding login attempt by $jid, because the "
941 ."same registration info has already been labelled "
942 ."as bad by the remote service.");
943 return;
946 # It's a new registration, so try again. But first...
947 delete $self->{authentication_errors}->{$jid};
950 my $login_handler = sub {
951 # Result from session can be:
952 # * ref (implies its the Session object)
953 # * error string
954 # * undef if there was an error and the Protocol is handling it.
955 my ($session_or_error, $error_is_local_only) = @_;
957 # Failed login - Section 4.4.2
958 if (!ref(my $error = $session_or_error)) {
959 # Protocol can pass an error here to have XML generated or
960 # roll its own.
961 if ($error) {
962 my $packet = [[$NS_COMPONENT, 'presence'],
963 {to => $full_jid,
964 from => $self->{component_name},
965 type => 'error'},
966 [error_tag($error)]];
967 $self->xml_out($packet);
968 if ($error eq 'not_acceptable') {
969 # Credential issue
970 $self->{authentication_errors}->{$jid} = {
971 registration_info => $registration_info,
972 error_xml => $packet,
976 if ($USE_CONNECTION_MANAGER) {
977 Thrasher::ConnectionManager::connection_failure(
978 $error_is_local_only,
981 return;
984 # Success! Paranoia:
985 delete $self->{authentication_errors}->{$jid};
987 $self->send_presence_xml($full_jid, '');
989 my $session = $session_or_error;
990 # In case protocol didn't already associate the session.
991 $self->set_session_for($jid, $session);
992 $self->{protocol}->initial_login($session);
993 if (defined($original_presence_tag)) {
994 # If this presence is intended for the transport, use it
995 # as the initial status for all transport contacts. Or, it
996 # may be targeted at a specific transport contact.
997 $self->echo_presence($session, $original_presence_tag);
1000 if ($USE_CONNECTION_MANAGER) {
1001 Thrasher::ConnectionManager::connection_success();
1005 if (!defined($registration_info)) {
1006 # FIXME: Determine if this happens and when; be sure
1007 # to check the possibility of us losing the registration
1008 # info while the user still thinks they are registered.
1009 log "$jid sent us available presence but has no "
1010 ."registration";
1011 return;
1014 # Verify that we have all required components
1015 my @required_items = $self->{protocol}->registration_items;
1016 for my $item (@required_items) {
1017 if (!defined($registration_info->{$item})) {
1018 log("Registration item $item missing for $jid! Ack! Panic!");
1019 $self->{protocol}->{backend}->remove($jid);
1020 $self->xml_out([[$NS_COMPONENT, 'presence'],
1021 {from => $self->{component_name},
1022 to => $full_jid,
1023 type => 'error'},
1024 [error_tag('registration_required')]]);
1025 return;
1029 my $protocol_login = sub {
1030 if ($self->session_for($jid)) {
1031 # Racing? Can't call ->login() with a session already defined.
1032 log("login($full_jid) reached protocol_login"
1033 . " but already has a session. WHAT IS GOING ON?\n");
1034 # Multiple active sessions for the same JID ends in tears.
1035 return;
1038 # If the connect was queued, a more current resource may have
1039 # been stored since this closure was created.
1040 my $last_full_jid = delete($self->{'connection_queued'}->{$jid})
1041 || $full_jid;
1042 $self->{protocol}->login($login_handler,
1043 $registration_info,
1044 $last_full_jid,
1045 $self);
1047 $self->{'connection_queued'}->{$jid} = $full_jid;
1049 if (!$USE_CONNECTION_MANAGER) {
1050 $protocol_login->();
1051 } else {
1052 my $immediate_connection = request_connect($protocol_login);
1053 if (defined($WILL_BE_DIRECTLY_CONNECTED)) {
1054 $immediate_connection = $WILL_BE_DIRECTLY_CONNECTED;
1057 # In the event that an immediate connection is made, the rest
1058 # of the code already takes care of the presence tags.
1059 if (!$immediate_connection) {
1060 $self->send_connection_queued($jid);
1065 # Fires our extended "connection queued" presence tag at the given bare JID.
1066 sub send_connection_queued {
1067 my ($self, $jid) = @_;
1069 my $thrasher_presence =
1070 [[[$NS_THRASHER_PRESENCE, 'connection-queued'], {}, []]];
1071 $self->send_presence_xml($jid,
1072 'unavailable',
1073 undef,
1074 undef,
1075 'connection queued',
1076 $thrasher_presence);
1079 sub logout {
1080 my $self = shift;
1081 my $session = shift;
1082 my $extra = shift;
1083 my $logout_status_message = shift;
1085 # Accept JIDs for the session
1086 if (!ref($session)) {
1087 $session = $self->session_for($session);
1090 if ($session->{status} =~ /disconnecting/) {
1091 log("Already logging out $session->{jid}, but got another "
1092 ."request to log out.");
1093 return;
1096 log("Logging out $session->{jid}");
1098 $session->{status} = 'disconnecting before presence';
1100 my $logout_handler = sub {
1101 # One way or another, logging off is successful.
1102 # Send logout packets; this should show everybody who
1103 # isn't offline as offline.
1105 if (!defined($session)) {
1106 # FIXME: This shouldn't happen.
1107 # Sequence to trigger:
1108 # * Register.
1109 # * kill transport, whack database.
1110 # * come online still subscribed, go offline.
1111 # * this is reached.
1112 # As you can guess, the "whack database" step is frankly
1113 # more hostility than we can really plan for.
1114 return;
1115 confess "Made it to logout handler without session.";
1118 $session->logout($self);
1120 my $roster = $self->{protocol}->{backend}->get_roster
1121 ($session->{jid});
1123 my @on_roster = map {
1124 $self->{protocol}->{backend}->legacy_name_to_jid($session->{jid},
1125 $_,
1126 $self->{component_name})
1127 } keys %$roster;
1128 for my $roster_entry (@on_roster) {
1129 $self->send_presence_xml($session->{jid}, 'unavailable',
1130 $roster_entry);
1133 if ($extra && ref($extra) ne 'CODE') {
1134 log("Got 'extra' that isn't code: " . longmess);
1135 } elsif ($extra) {
1136 $extra->();
1139 $self->send_presence_xml($session->{jid}, 'unavailable',
1140 undef, undef, $logout_status_message);
1142 $session->{status} = 'disconnecting';
1144 delete $self->{sessions}->{$session->{jid}};
1146 log("session disconnected for $session->{jid}");
1149 # Do we also need to show all transport users as offline,
1150 # or does something in the server take care of that.
1151 $self->{protocol}->logout($session, $logout_handler);
1154 sub handle_message {
1155 my $self = shift;
1156 my $to = shift;
1157 my $from = shift;
1158 my $body_xml = shift;
1159 my $type = shift;
1160 my $chatstate = shift;
1162 if (defined($type) && $type eq 'error') {
1163 log("Got an error message from a user.");
1164 return;
1167 my $session = $self->session_for($from);
1169 # FIXME: There can be a race condition where the error sub is
1170 # called after the user disconnects. We shouldn't send this then.
1171 my $error_handler = sub {
1172 my $error = shift;
1173 my $message = [[$NS_COMPONENT, 'message'],
1174 {to => $from,
1175 from => $to,
1176 type => 'error'},
1178 # $body_xml, # FIXME - better to send this?
1179 error_tag($error)]];
1180 $self->xml_out($message);
1183 my $error_message = [[$NS_COMPONENT, 'message'],
1184 {to => $from,
1185 from => $to,
1186 type => 'error'},
1187 [error_tag('registration_required'),
1188 $body_xml]];
1189 if (!$session) {
1190 my $registration_info = $self->registration_info($from);
1192 if (!defined($registration_info)) {
1193 $self->xml_out($error_message);
1194 return;
1197 # If we get here, the user has registered, and is sending
1198 # a message, but they are apparently not actually logged
1199 # in, perhaps because they deliberately logged off. I'm
1200 # choosing to allow them to log in this way.
1201 # FIXME: Hey, actually do that. For now you get an error.
1202 $self->send_error_message
1203 ($from, "You must be logged in to send messages "
1204 ."to the remote service users.", 'service_unavailable',
1205 $to);
1206 return;
1208 } elsif (!$session->is_registered) {
1209 $self->xml_out($error_message);
1210 return;
1213 my $converted_to = $self->xmpp_name_to_legacy($session->{jid},
1214 strip_resource($to));
1216 # Tie successful call to the protocol to the successful extraction
1217 # of the message from the input
1218 if ($body_xml) {
1219 eval {
1220 my $body = extract([undef, undef,
1221 save_sub("text", \&text_extractor)],
1222 $body_xml);
1224 my $body_text = join '', @{$body->{text} || []};
1226 $self->{protocol}->send_message($session,
1227 $converted_to,
1228 $body_text,
1229 $type,
1230 $error_handler);
1232 if ($@) {
1233 log("Error in extracting message from "
1234 . Dumper($body_xml) . ":\n" . $@);
1238 if ($chatstate) {
1239 eval {
1240 $self->{protocol}->outgoing_chatstate($session,
1241 $converted_to,
1242 $chatstate);
1244 if ($@) {
1245 log("Error in outgoing_chatstate:\n$@");
1249 if (! ($body_xml || $chatstate)) {
1250 log('Message without usable child.');
1251 return;
1255 sub registration_info {
1256 my $self = shift;
1257 my $jid = shift;
1258 my $stripped_jid = strip_resource($jid);
1259 return $self->{protocol}->{backend}->registered($stripped_jid);
1262 sub legacy_name_to_xmpp {
1263 my $self = shift;
1264 my $user_jid = strip_resource(shift());
1265 my $legacy_name = shift;
1266 my $lang = shift || 'en';
1268 # FIXME: XMPP is correct
1269 # FIXME: Lang on the user
1270 return $self->{protocol}->{backend}->legacy_name_to_jid
1271 ($user_jid, $legacy_name, $self->{component_name}, $lang);
1274 sub xmpp_name_to_legacy {
1275 my $self = shift;
1276 my $user_jid = strip_resource(shift());
1277 my $target_jid = strip_resource(shift());
1279 return $self->{protocol}->{backend}->jid_to_legacy_name
1280 ($user_jid, $target_jid);
1283 =pod
1285 =head1 PROTOCOL INTERFACE
1287 The protocol interface is intended to sheild Thrasher::Protocol
1288 implementers from potential changes to the Component interface.
1289 If you, as a Thrasher::Protocol implementer ever feel compelled
1290 to reach into the ::Component to do anything not accessible from
1291 this interface, please let us know so we can give you a more
1292 official path.
1294 The officially-implemented methods are:
1296 =over 4
1298 =item *
1299 C<add_contact>($jid, $legacy_user_name): This will send out
1300 the correct <presence> tag to attempt to add the $legacy_user_name
1301 to the given $jid. This corresponds with section 5.1 in the XEP.
1303 You should be able to retrieve the $jid out of the information you
1304 stored in the ::Session, and you should send in the $legacy_user_name
1305 as the raw username from the service; ::Component will take care of
1306 mapping it as appropriate, in accordance with the name translation
1307 protocols.
1309 =cut
1311 sub add_contact {
1312 my $self = shift;
1313 my $jid = shift;
1314 my $legacy_user_name = shift;
1316 my $legacy_jid = $self->legacy_name_to_xmpp
1317 ($jid, $legacy_user_name);
1319 $self->send_presence_xml($jid, 'subscribe', $legacy_jid);
1322 =pod
1324 =item *
1325 C<send_presence>($jid, $legacy_user_name, $type, $show):
1326 Send the given presence for the given legacy_user_name.
1327 The ::Protocol implementation will need to convert the status
1328 into an XMPP-status and give us the "type" and "show".
1330 =cut
1332 sub send_presence {
1333 my $self = shift;
1334 my $jid = shift;
1335 my $legacy_user_name = shift;
1336 my $type = shift;
1337 my $show = shift;
1338 my $status = shift;
1340 my $from_jid = $self->legacy_name_to_xmpp($jid, $legacy_user_name);
1342 if ($status) {
1343 $status = text($status);
1346 my $session = $self->session_for($jid);
1347 $session->{component}->{presence}->{strip_resource($jid)}->{strip_resource($from_jid)} =
1348 [$type, $show, $status];
1350 $self->send_presence_xml($jid, $type, $from_jid, $show, $status);
1353 =pod
1355 =item *
1356 C<delete_contact>($jid, $legacy_user_name): This will send out
1357 the necessary packets to indicate that a user has unsubscribed.
1359 =cut
1361 sub delete_contact {
1362 my $self = shift;
1363 my $jid = shift;
1364 my $legacy_user_name = shift;
1366 my $legacy_jid = $self->legacy_name_to_xmpp($jid, $legacy_user_name);
1368 $self->send_presence_xml($jid, 'unsubscribe', $legacy_jid);
1369 $self->send_presence_xml($jid, 'unsubscribed', $legacy_jid);
1370 # FIXME: Example 50 says this should be to the JID w/
1371 # the resource
1372 $self->send_presence_xml($jid, 'unavailable', $legacy_jid);
1375 =pod
1377 =item *
1378 C<send_message>($jid_from, $jid_to, $message, $extra): Sends a message
1379 from the given jid to the given jid. $extra is a hash containing extra
1380 parametrs, which include:
1382 =over 4
1384 =item *
1386 C<$is_xhtml_ish>: If false, sends the UTF-8 encoded $message to the
1387 target $jid_to.
1389 If it is true, it will process the XHTML-ish message into an
1390 XHTML and a plain text string, and send the XHTML-ish message
1391 as an XHTML-IM message in complaince with XEP-0071. Note that
1392 there is a normalization step, so you don't need to sweat
1393 whether it is proper XHTML; this does a decent job of turning
1394 dreck into XHTML.
1396 =item *
1398 C<$nick>: If set to a true string, will broadcast the nick conforming
1399 to XEP-0172. Note that according to the XEP, nickname should be
1400 broadcast only once per connection per (legacy) user, and it
1401 is your responsibility to ensure this, not this method's.
1403 =item *
1405 C<$type>: If set, will set the type of the message to the given
1406 XMPP type.
1408 =back
1410 =cut
1412 sub send_message {
1413 my $self = shift;
1414 my $jid_from = shift;
1415 my $jid_to = shift;
1416 my $message = shift;
1417 my $extra = shift;
1419 my $type = $extra->{type} || 'chat';
1420 my $is_xhtml_ish = $extra->{is_xhtml_ish};
1421 my $nick = $extra->{nick};
1422 my $extra_children = $extra->{children} || [];
1424 if ($nick) {
1425 $nick = [[[$NS_NICK, 'nick'], {}, [$nick]]];
1426 } else {
1427 $nick = [];
1430 if ($jid_from =~ / / ||
1431 $jid_to =~ / /) {
1432 log("Trying to send/receive message from a JID with "
1433 ."a space in it: from: $jid_from to: $jid_to "
1434 ."\n" . longmess);
1435 return;
1438 if (!$is_xhtml_ish) {
1439 $self->xml_out([[$NS_COMPONENT, 'message'],
1440 {from => $jid_from,
1441 to => $jid_to,
1442 type => $type},
1443 [[[$NS_COMPONENT, 'body'],
1445 [$message]],
1446 @$nick, @$extra_children]]);
1447 } else {
1448 my ($xhtml, $text) = xhtml_and_text($message);
1450 # Omit the XHTML-IM body if it turned out to be the same as
1451 # the text.
1452 my @xhtml_part;
1453 if ($xhtml ne $text) {
1454 # XMPPStreamOut outputs a ref to a scalar as the scalar
1455 # without passing it through the normal escapeHTML() step.
1456 # The HTML $message may have &escape; sequences, which
1457 # xhtml_and_text passes through unaltered, so we need to
1458 # not re-escape even for the plain text body.
1459 @xhtml_part = [[$NS_XHTML_IM, 'html'],
1461 [[[$NS_XHTML, 'body'], {}, [\$xhtml]]]]
1464 $self->xml_out([[$NS_COMPONENT, 'message'],
1465 {from => $jid_from,
1466 to => $jid_to,
1467 type => $type},
1468 [[[$NS_COMPONENT, 'body'],
1469 {}, [\$text]],
1470 @$nick, @$extra_children,
1471 @xhtml_part]]);
1475 =pod
1477 =item *
1479 C<send_error_message>($jid, $error_msg): Sends an error message
1480 to the user, coming from the transport.
1482 In my experience, this should be limited, because this gets very
1483 annoying very quickly. As the method name implies, reserve it
1484 for errors.
1486 You're responsible for providing the errors. The $session for a user
1487 may have their language available to you in $session->get_lang,
1488 but it depends on their XMPP client (and how carefully we picked the
1489 language out of the stream).
1491 =cut
1493 sub send_error_message {
1494 my $self = shift;
1495 my $target_jid = shift;
1496 my $error_message = shift;
1497 my $error_type = shift;
1498 my $from = shift || $self->{component_name};
1500 my $error_body = [];
1502 if ($error_type) {
1503 push @$error_body, error_tag($error_type);
1506 $self->send_message($from, $target_jid,
1507 $error_message,
1508 {type => 'error', children => $error_body});
1511 =pod
1513 =item *
1515 C<set_roster_name>($jid, $legacy_jid, $name): Sets $jid's
1516 roster entry to $legacy_jid to have the given nickname,
1517 if $jid's client advertises support for XEP-0144, by
1518 sending a modify request.
1520 =cut
1522 sub set_roster_name {
1523 my $self = shift;
1524 my $jid = shift;
1525 my $legacy_jid = shift;
1526 my $name = shift;
1527 my $force = shift;
1529 my $session = $self->session_for($jid);
1531 my $send_iq = sub {
1532 my $iq = [[$NS_COMPONENT, 'iq'],
1533 {from => $self->{component_name},
1534 to => $jid,
1535 type => 'set'},
1536 [[[$NS_ROSTER_EXCHANGE, 'x'], {},
1537 [[[$NS_ROSTER_EXCHANGE, 'item'],
1538 {action => 'modify',
1539 jid => $legacy_jid,
1540 name => $name}, []
1541 ]]]]];
1542 $self->iq_query($iq);
1545 if ($force) {
1546 $send_iq->();
1547 } else {
1548 $session->do_if_feature($NS_ROSTER_EXCHANGE,
1549 $send_iq);
1552 callbacks('set_roster_name',
1553 $session->{'full_jid'},
1554 undef,
1555 $legacy_jid,
1556 $name);
1559 =pod
1561 =back
1563 =cut
1565 # For some reason, we can no longer continue. Send all presence
1566 # closing, terminate the connection, and terminate the mainloop.
1567 sub terminate {
1568 my $self = shift;
1569 my %args = @_;
1571 $args{reason} ||= 'Internal error';
1573 if ($self->{I_AM_TERMINATING}) {
1574 return;
1577 log("Component terminating");
1578 $self->{I_AM_TERMINATING} = 1;
1580 my $protocol = $self->{protocol};
1581 my $sessions = $self->{sessions};
1583 # If we are terminating because the DB lost the connection,
1584 # we no longer know enough to actually log people off. If
1585 # we are terminating due to a signal, or most other reasons,
1586 # we can log people off cleanly.
1587 if (!$args{no_db}) {
1588 for my $session (values %$sessions) {
1589 log("Terminating connection for $session->{jid}");
1590 $self->logout($session, undef, $args{reason});
1594 # And terminate the event loop, which is currently
1595 # hard-coded
1596 $self->{event_loop}->quit;
1599 # This is for when the XMPP server stream simply disappears.
1600 # This is probably because the server has crashed or gone down.
1601 # In this case, we want the full terminate routine since it
1602 # probably implies all users have been disconnected.
1603 # Unfortunately, we can't know this, but it's the best guess.
1604 sub lost_connection {
1605 my $self = shift;
1607 $self->terminate;
1610 # This is for when we have screwed up and borked our stream.
1611 # If this ever triggers, it is almost certainly a bug in
1612 # Thrasher, but let's at least try to recover. We may lose
1613 # some messages from the server in the meantime.
1614 # FIXME: We ought to have a configuration setting for whether
1615 # we try this recovery or just give up, because if you're
1616 # using component load balancing, this will really screw your
1617 # users up.
1618 sub reconnect_stream {
1619 my $self = shift;
1621 log("Attempting to reconnect stream.");
1623 # This causes any events that may be generated by the protocol
1624 # side while we are reconnecting to be buffered.
1625 $self->set_state('disconnected');
1627 # By the time this is getting called, the socket is entirely gone.
1628 log("Closing socket");
1629 $self->{thrasher_socket}->close();
1631 local $@;
1632 eval { $self->{thrasher_socket}->connect(); };
1633 if ($@) {
1634 # We can't seem to connect to the server. This should
1635 # never happen, so just panic.
1636 log("Connection to server could not be re-established.");
1637 $self->terminate;
1638 return;
1641 log("Connection to server re-established. Handshaking.");
1643 $self->setup_streams;
1644 $self->{thrasher_socket}->establish_fd_watch;
1646 # Re-begin connection process
1647 $self->output_initial_stream_tag;
1650 sub socket_in_closure {
1651 my $self = shift;
1652 my $socket = shift;
1654 my $closure = sub {
1655 my $got_data = 0;
1656 while (1) {
1657 my $val = eval { $socket->read(); };
1658 if ($@) {
1659 log("$@");
1660 $self->lost_connection();
1661 return 0;
1663 elsif (! defined($val)) {
1664 last;
1666 else {
1667 $got_data = 1;
1668 debug("IN: $val");
1669 eval {
1670 $self->xml_in($val);
1672 if ($@) {
1673 # Terminate immediately after an unhandled error.
1674 # Ugly, but better than leaving protocol-side online
1675 # but component-side unreachable from the XMPP server
1676 # because only the FD watch has gone.
1677 log("Fatal error handling XML input:\n$@\n");
1678 $self->terminate();
1679 return 0;
1683 if (!$got_data) {
1684 log "Connection to XMPP server lost.";
1685 $self->lost_connection();
1686 return 0;
1688 return 1;
1691 return $closure;
1694 sub compare_hashref {
1695 my $a = shift;
1696 my $b = shift;
1698 if (scalar(keys %$a) != scalar(keys %$b)) {
1699 return 0;
1702 while (my ($key, $value) = each %$a) {
1703 if ($b->{$key} ne $value) {
1704 return 0;
1708 return 1;