package DJabberd::IQ; use strict; use base qw(DJabberd::Stanza); use DJabberd::Util qw(exml); use DJabberd::Roster; use DJabberd::Log; our $logger = DJabberd::Log->get_logger(); sub on_recv_from_client { my ($self, $conn) = @_; my $to = $self->to_jid; if (! $to || $conn->vhost->uses_jid($to)) { $self->process($conn); return; } $self->deliver; } my $iq_handler = { 'get-{jabber:iq:roster}query' => \&process_iq_getroster, 'set-{jabber:iq:roster}query' => \&process_iq_setroster, 'get-{jabber:iq:auth}query' => \&process_iq_getauth, 'set-{jabber:iq:auth}query' => \&process_iq_setauth, 'get-{http://jabber.org/protocol/disco#info}query' => \&process_iq_disco_info_query, 'get-{http://jabber.org/protocol/disco#items}query' => \&process_iq_disco_items_query, 'get-{jabber:iq:register}query' => \&process_iq_getregister, 'set-{jabber:iq:register}query' => \&process_iq_setregister, 'set-{djabberd:test}query' => \&process_iq_set_djabberd_test, }; # DO NOT OVERRIDE THIS sub process { my DJabberd::IQ $self = shift; my $conn = shift; # FIXME: handle 'result'/'error' IQs from when we send IQs # out, like in roster pushes # Trillian Jabber 3.1 is stupid and sends a lot of IQs (but non-important ones) # without ids. If we respond to them (also without ids, or with id='', rather), # then Trillian crashes. So let's just ignore them. return unless defined($self->id) && length($self->id); $conn->vhost->run_hook_chain(phase => "c2s-iq", args => [ $self ], fallback => sub { my $sig = $self->signature; my $meth = $iq_handler->{$sig}; unless ($meth) { $self->send_error( qq{}. qq{}. qq{}. qq{This feature is not implemented yet in DJabberd.}. qq{}. qq{} ); $logger->warn("Unknown IQ packet: $sig"); return; } $DJabberd::Stats::counter{"InIQ:$sig"}++; $meth->($conn, $self); }); } sub signature { my $iq = shift; my $fc = $iq->first_element; # FIXME: should signature ever get called on a bogus IQ packet? return $iq->type . "-" . ($fc ? $fc->element : "(BOGUS)"); } sub send_result { my DJabberd::IQ $self = shift; $self->send_reply("result"); } sub send_error { my DJabberd::IQ $self = shift; my $raw = shift || ''; $self->send_reply("error", $self->innards_as_xml . "\n" . $raw); } # caller must send well-formed XML (but we do the wrapping element) sub send_result_raw { my DJabberd::IQ $self = shift; my $raw = shift; return $self->send_reply("result", $raw); } sub send_reply { my DJabberd::IQ $self = shift; my ($type, $raw) = @_; my $conn = $self->{connection} or return; $raw ||= ""; my $id = $self->id; my $bj = $conn->bound_jid; my $from_jid = $self->to; my $to = $bj ? (" to='" . $bj->as_string_exml . "'") : ""; my $from = $from_jid ? (" from='" . $from_jid . "'") : ""; my $xml = qq{$raw}; $conn->xmllog->info($xml); $conn->write(\$xml); } sub process_iq_disco_info_query { my ($conn, $iq) = @_; # Trillian, again, is fucking stupid and crashes on just # about anything its homemade XML parser doesn't like. # so ignore it when it asks for this, just never giving # it a reply. if ($conn->vhost->quirksmode && $iq->id =~ /^trill_/) { return; } # TODO: these can be sent back to another server I believe -- sky # TODO: Here we need to figure out what identities we have and # capabilities we have my $xml; $xml = qq{}; $xml .= qq{}; foreach my $cap ('http://jabber.org/protocol/disco#info', $conn->vhost->features) { $xml .= ""; } $xml .= qq{}; $iq->send_reply('result', $xml); } sub process_iq_disco_items_query { my ($conn, $iq) = @_; my $vhost = $conn->vhost; my $items = $vhost ? $vhost->child_services : {}; my $xml = qq{}. join('', map({ "" } keys %$items)). qq{}; $iq->send_reply('result', $xml); } sub process_iq_getroster { my ($conn, $iq) = @_; my $send_roster = sub { my $roster = shift; $logger->info("Sending roster to conn $conn->{id}"); $iq->send_result_raw($roster->as_xml); # JIDs who want to subscribe to us, since we were offline foreach my $jid (map { $_->jid } grep { $_->subscription->pending_in } $roster->items) { my $subpkt = DJabberd::Presence->make_subscribe(to => $conn->bound_jid, from => $jid); # already in roster as pendin, we've already processed it, # so just deliver it (or queue it) so user can reply with # subscribed/unsubscribed: $conn->note_pend_in_subscription($subpkt); } }; # need to be authenticated to request a roster. my $bj = $conn->bound_jid; unless ($bj) { $iq->send_error( qq{}. qq{}. qq{}. qq{You need to be authenticated before requesting a roster.}. qq{}. qq{} ); return; } # {=getting-roster-on-login} $conn->set_requested_roster(1); $conn->vhost->get_roster($bj, on_success => $send_roster, on_fail => sub { $send_roster->(DJabberd::Roster->new); }); return 1; } sub process_iq_setroster { my ($conn, $iq) = @_; my $item = $iq->query->first_element; unless ($item && $item->element eq "{jabber:iq:roster}item") { $iq->send_error( # TODO make this error proper qq{}. qq{}. qq{}. qq{You need to be authenticated before requesting a roster.}. qq{}. qq{} ); return; } # {=xmpp-ip-7.6-must-ignore-subscription-values} my $subattr = $item->attr('{}subscription') || ""; my $removing = $subattr eq "remove" ? 1 : 0; my $jid = $item->attr("{}jid") or return $iq->send_error( # TODO Yeah, this one too qq{}. qq{}. qq{}. qq{You need to be authenticated before requesting a roster.}. qq{}. qq{} ); my $name = $item->attr("{}name"); # find list of group names to add/update. can ignore # if we're just removing. my @groups; # scalars of names unless ($removing) { foreach my $ele ($item->children_elements) { next unless $ele->element eq "{jabber:iq:roster}group"; push @groups, $ele->first_child; } } my $ritem = DJabberd::RosterItem->new(jid => $jid, name => $name, remove => $removing, groups => \@groups, ); # TODO if ($removing), send unsubscribe/unsubscribed presence # stanzas. See RFC3921 8.6 # {=add-item-to-roster} my $phase = $removing ? "RosterRemoveItem" : "RosterAddUpdateItem"; $conn->vhost->run_hook_chain(phase => $phase, args => [ $conn->bound_jid, $ritem ], methods => { done => sub { my ($self, $ritem_final) = @_; # the RosterRemoveItem isn't required to return the final item $ritem_final = $ritem if $removing; $iq->send_result; $conn->vhost->roster_push($conn->bound_jid, $ritem_final); # TODO: section 8.6: must send a # bunch of presence # unsubscribe/unsubscribed messages }, error => sub { # TODO What sort of error stat is being hit here? $iq->send_error; }, }, fallback => sub { if ($removing) { # NOTE: we used to send an error here, but clients get # out of sync and we need to let them think a delete # happened even if it didn't. $iq->send_result; } else { # TODO ACK, This one as well $iq->send_error; } }); return 1; } sub process_iq_getregister { my ($conn, $iq) = @_; # If the entity is not already registered and the host supports # In-Band Registration, the host MUST inform the entity of the # required registration fields. If the host does not support # In-Band Registration, it MUST return a # error. If the host is redirecting registration requests to some # other medium (e.g., a website), it MAY return an # element only, as shown in the Redirection section of this # document. my $vhost = $conn->vhost; unless ($vhost->allow_inband_registration) { # MUST return a $iq->send_error( qq{}. qq{}. qq{}. qq{In-Band registration is not supported by this server's configuration.}. qq{}. qq{} ); return; } # if authenticated, give them existing login info: if (my $jid = $conn->bound_jid) { my $password = 0 ? "" : ""; # TODO my $username = $jid->node; $iq->send_result_raw(qq{ $username $password }); return; } # not authenticated, ask for their required fields # NOTE: we send_result_raw here, which just writes, so they don't # need to be an available resource (since they're not even authed # yet) for this to work. that's like most things in IQ anyway. $iq->send_result_raw(qq{ Choose a username and password for use with this service. }); } sub process_iq_setregister { my ($conn, $iq) = @_; my $vhost = $conn->vhost; unless ($vhost->allow_inband_registration) { # MUST return a $iq->send_error( qq{}. qq{}. qq{}. qq{In-Band registration is not supported by this server\'s configuration.}. qq{}. qq{} ); return; } my $bjid = $conn->bound_jid; # remove (cancel) support my $item = $iq->query->first_element; if ($item && $item->element eq "{jabber:iq:register}remove") { if ($bjid) { my $rosterwipe = sub { $vhost->run_hook_chain(phase => "RosterWipe", args => [ $bjid ], methods => { done => sub { $iq->send_result; $conn->stream_error("not-authorized"); }, }); }; $vhost->run_hook_chain(phase => "UnregisterJID", args => [ username => $bjid->node, conn => $conn ], methods => { deleted => sub { $rosterwipe->(); }, notfound => sub { warn "notfound.\n"; return $iq->send_error; }, error => sub { return $iq->send_error; }, }); $iq->send_result; } else { $iq->send_error; # TODO: } return; } my $query = $iq->query or die; my @children = $query->children; my $get = sub { my $lname = shift; foreach my $c (@children) { next unless ref $c && $c->element eq "{jabber:iq:register}$lname"; my $text = $c->first_child; return undef if ref $text; return $text; } return undef; }; my $username = $get->("username"); my $password = $get->("password"); return $iq->send_error unless $username =~ /^\w+$/; return $iq->send_error if $bjid && $bjid->node ne $username; # create the account $vhost->run_hook_chain(phase => "RegisterJID", args => [ username => $username, conn => $conn, password => $password ], methods => { saved => sub { return $iq->send_result; }, conflict => sub { my $epass = exml($password); return $iq->send_error(qq{ $username $epass }); }, error => sub { return $iq->send_error; }, }); } sub process_iq_getauth { my ($conn, $iq) = @_; # brad # force SSL by not letting them login if ($conn->vhost->requires_ssl && ! $conn->ssl) { $conn->stream_error("policy-violation", "Local policy requires use of SSL before authentication."); return; } my $username = ""; my $child = $iq->query->first_element; if ($child && $child->element eq "{jabber:iq:auth}username") { $username = $child->first_child; die "Element in username field?" if ref $username; } # FIXME: use nodeprep or whatever, not \w+ $username = '' unless $username =~ /^\w+$/; my $type = ($conn->vhost->are_hooks("GetPassword") || $conn->vhost->are_hooks("CheckDigest")) ? "" : ""; $iq->send_result_raw("$username$type"); return 1; } sub process_iq_setauth { my ($conn, $iq) = @_; # bradworkab2459dc7506d56247e2dc684f6e3b0a5951a808 my $id = $iq->id; my $query = $iq->query or die; my @children = $query->children; my $get = sub { my $lname = shift; foreach my $c (@children) { next unless ref $c && $c->element eq "{jabber:iq:auth}$lname"; my $text = $c->first_child; return undef if ref $text; return $text; } return undef; }; my $username = $get->("username"); my $resource = $get->("resource"); my $password = $get->("password"); my $digest = $get->("digest"); return unless $username =~ /^\w+$/; my $vhost = $conn->vhost; my $reject = sub { $DJabberd::Stats::counter{'auth_failure'}++; $iq->send_reply("error", qq{}); return 1; }; my $accept = sub { my $cb = shift; my $authjid = shift; # create default JID unless (defined $authjid) { my $sname = $vhost->name; $authjid = "$username\@$sname"; } # register my $jid = DJabberd::JID->new("$authjid/$resource"); unless ($jid) { $reject->(); return; } my $regcb = DJabberd::Callback->new({ registered => sub { $conn->set_bound_jid($jid); $DJabberd::Stats::counter{'auth_success'}++; $iq->send_result; }, error => sub { $iq->send_error; }, _post_fire => sub { $conn = undef; $iq = undef; }, }); $vhost->register_jid($jid, $conn, $regcb); }; my $can_get_password = $vhost->are_hooks("GetPassword"); if ($can_get_password) { $vhost->run_hook_chain(phase => "GetPassword", args => [ username => $username, conn => $conn ], methods => { set => sub { my (undef, $good_password) = @_; if ($password && $password eq $good_password) { $accept->(); } elsif ($digest) { my $good_dig = lc(Digest::SHA1::sha1_hex($conn->{stream_id} . $good_password)); if ($good_dig eq $digest) { $accept->(); } else { $reject->(); } } else { $reject->(); } }, }, fallback => $reject); } elsif ($vhost->are_hooks("CheckDigest")) { $vhost->run_hook_chain(phase => "CheckDigest", args => [ username => $username, conn => $conn, digest => $digest, resource => $resource ], methods => { accept => $accept, reject => $reject, }); } else { $vhost->run_hook_chain(phase => "CheckCleartext", args => [ username => $username, conn => $conn, password => $password ], methods => { accept => $accept, reject => $reject, }); } return 1; # signal that we've handled it } sub process_iq_set_djabberd_test { my ($conn, $iq) = @_; # some command my $id = $iq->id; unless ($ENV{DJABBERD_TEST_COMMANDS}) { $iq->send_error; return; } my $query = $iq->query or die; my $command = $query->first_child; if ($command eq "write error") { $conn->set_writer_func(sub { my ($bref, $to_write, $offset) = @_; $conn->close; return 0; }); $iq->send_result_raw(""); return; } $iq->send_result_raw(""); } sub id { return $_[0]->attr("{}id"); } sub type { return $_[0]->attr("{}type"); } sub query { my $self = shift; my $child = $self->first_element or return; my $ele = $child->element or return; return undef unless $child->element =~ /\}query$/; return $child; } sub deliver_when_unavailable { my $self = shift; return $self->type eq "result" || $self->type eq "error"; } sub make_response { my ($self) = @_; my $response = $self->SUPER::make_response(); $response->attrs->{"{}type"} = "result"; return $response; } 1;