1package DJabberd::IQ; 2use strict; 3use base qw(DJabberd::Stanza); 4use DJabberd::Util qw(exml); 5use DJabberd::Roster; 6use Digest::SHA1; 7 8use DJabberd::Log; 9our $logger = DJabberd::Log->get_logger(); 10 11sub on_recv_from_client { 12 my ($self, $conn) = @_; 13 14 my $to = $self->to_jid; 15 if (! $to || $conn->vhost->uses_jid($to)) { 16 $self->process($conn); 17 return; 18 } 19 20 $self->deliver; 21} 22 23my $iq_handler = { 24 'get-{jabber:iq:roster}query' => \&process_iq_getroster, 25 'set-{jabber:iq:roster}query' => \&process_iq_setroster, 26 'get-{jabber:iq:auth}query' => \&process_iq_getauth, 27 'set-{jabber:iq:auth}query' => \&process_iq_setauth, 28 'set-{urn:ietf:params:xml:ns:xmpp-session}session' => \&process_iq_session, 29 'set-{urn:ietf:params:xml:ns:xmpp-bind}bind' => \&process_iq_bind, 30 'get-{http://jabber.org/protocol/disco#info}query' => \&process_iq_disco_info_query, 31 'get-{http://jabber.org/protocol/disco#items}query' => \&process_iq_disco_items_query, 32 'get-{jabber:iq:register}query' => \&process_iq_getregister, 33 'set-{jabber:iq:register}query' => \&process_iq_setregister, 34 'set-{djabberd:test}query' => \&process_iq_set_djabberd_test, 35}; 36 37# DO NOT OVERRIDE THIS 38sub process { 39 my DJabberd::IQ $self = shift; 40 my $conn = shift; 41 42 # FIXME: handle 'result'/'error' IQs from when we send IQs 43 # out, like in roster pushes 44 45 # Trillian Jabber 3.1 is stupid and sends a lot of IQs (but non-important ones) 46 # without ids. If we respond to them (also without ids, or with id='', rather), 47 # then Trillian crashes. So let's just ignore them. 48 return unless defined($self->id) && length($self->id); 49 50 $conn->vhost->run_hook_chain(phase => "c2s-iq", 51 args => [ $self ], 52 fallback => sub { 53 my $sig = $self->signature; 54 my $meth = $iq_handler->{$sig}; 55 unless ($meth) { 56 $self->send_error( 57 qq{<error type='cancel'>}. 58 qq{<feature-not-implemented xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}. 59 qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}. 60 qq{This feature is not implemented yet in DJabberd.}. 61 qq{</text>}. 62 qq{</error>} 63 ); 64 $logger->warn("Unknown IQ packet: $sig"); 65 return; 66 } 67 68 $DJabberd::Stats::counter{"InIQ:$sig"}++; 69 $meth->($conn, $self); 70 }); 71} 72 73sub signature { 74 my $iq = shift; 75 my $fc = $iq->first_element; 76 # FIXME: should signature ever get called on a bogus IQ packet? 77 return $iq->type . "-" . ($fc ? $fc->element : "(BOGUS)"); 78} 79 80sub send_result { 81 my DJabberd::IQ $self = shift; 82 $self->send_reply("result"); 83} 84 85sub send_error { 86 my DJabberd::IQ $self = shift; 87 my $raw = shift || ''; 88 $self->send_reply("error", $self->innards_as_xml . "\n" . $raw); 89} 90 91# caller must send well-formed XML (but we do the wrapping element) 92sub send_result_raw { 93 my DJabberd::IQ $self = shift; 94 my $raw = shift; 95 return $self->send_reply("result", $raw); 96} 97 98sub send_reply { 99 my DJabberd::IQ $self = shift; 100 my ($type, $raw) = @_; 101 102 my $conn = $self->{connection} 103 or return; 104 105 $raw ||= ""; 106 my $id = $self->id; 107 my $bj = $conn->bound_jid; 108 my $from_jid = $self->to; 109 my $to = $bj ? (" to='" . $bj->as_string_exml . "'") : ""; 110 my $from = $from_jid ? (" from='" . $from_jid . "'") : ""; 111 my $xml = qq{<iq$to$from type='$type' id='$id'>$raw</iq>}; 112 $conn->xmllog->info($xml); 113 $conn->write(\$xml); 114} 115 116sub process_iq_disco_info_query { 117 my ($conn, $iq) = @_; 118 119 # Trillian, again, is fucking stupid and crashes on just 120 # about anything its homemade XML parser doesn't like. 121 # so ignore it when it asks for this, just never giving 122 # it a reply. 123 if ($conn->vhost->quirksmode && $iq->id =~ /^trill_/) { 124 return; 125 } 126 127 # TODO: these can be sent back to another server I believe -- sky 128 129 # TODO: Here we need to figure out what identities we have and 130 # capabilities we have 131 my $xml; 132 $xml = qq{<query xmlns='http://jabber.org/protocol/disco#info'>}; 133 $xml .= qq{<identity category='server' type='im' name='djabberd'/>}; 134 135 foreach my $cap ('http://jabber.org/protocol/disco#info', 136 $conn->vhost->features) 137 { 138 $xml .= "<feature var='$cap'/>"; 139 } 140 $xml .= qq{</query>}; 141 142 $iq->send_reply('result', $xml); 143} 144 145sub process_iq_disco_items_query { 146 my ($conn, $iq) = @_; 147 148 my $vhost = $conn->vhost; 149 150 my $items = $vhost ? $vhost->child_services : {}; 151 152 my $xml = qq{<query xmlns='http://jabber.org/protocol/disco#items'>}. 153 join('', map({ "<item jid='".exml($_)."' name='".exml($items->{$_})."' />" } keys %$items)). 154 qq{</query>}; 155 156 $iq->send_reply('result', $xml); 157} 158 159sub process_iq_getroster { 160 my ($conn, $iq) = @_; 161 162 my $send_roster = sub { 163 my $roster = shift; 164 $logger->info("Sending roster to conn $conn->{id}"); 165 $iq->send_result_raw($roster->as_xml); 166 167 # JIDs who want to subscribe to us, since we were offline 168 foreach my $jid (map { $_->jid } 169 grep { $_->subscription->pending_in } 170 $roster->items) { 171 my $subpkt = DJabberd::Presence->make_subscribe(to => $conn->bound_jid, 172 from => $jid); 173 # already in roster as pendin, we've already processed it, 174 # so just deliver it (or queue it) so user can reply with 175 # subscribed/unsubscribed: 176 $conn->note_pend_in_subscription($subpkt); 177 } 178 }; 179 180 # need to be authenticated to request a roster. 181 my $bj = $conn->bound_jid; 182 unless ($bj) { 183 $iq->send_error( 184 qq{<error type='auth'>}. 185 qq{<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}. 186 qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}. 187 qq{You need to be authenticated before requesting a roster.}. 188 qq{</text>}. 189 qq{</error>} 190 ); 191 return; 192 } 193 194 # {=getting-roster-on-login} 195 $conn->set_requested_roster(1); 196 197 $conn->vhost->get_roster($bj, 198 on_success => $send_roster, 199 on_fail => sub { 200 $send_roster->(DJabberd::Roster->new); 201 }); 202 203 return 1; 204} 205 206sub process_iq_setroster { 207 my ($conn, $iq) = @_; 208 209 my $item = $iq->query->first_element; 210 unless ($item && $item->element eq "{jabber:iq:roster}item") { 211 $iq->send_error( # TODO make this error proper 212 qq{<error type='error-type'>}. 213 qq{<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}. 214 qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='langcode'>}. 215 qq{You need to be authenticated before requesting a roster.}. 216 qq{</text>}. 217 qq{</error>} 218 ); 219 return; 220 } 221 222 # {=xmpp-ip-7.6-must-ignore-subscription-values} 223 my $subattr = $item->attr('{}subscription') || ""; 224 my $removing = $subattr eq "remove" ? 1 : 0; 225 226 my $jid = $item->attr("{}jid") 227 or return $iq->send_error( # TODO Yeah, this one too 228 qq{<error type='error-type'>}. 229 qq{<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}. 230 qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='langcode'>}. 231 qq{You need to be authenticated before requesting a roster.}. 232 qq{</text>}. 233 qq{</error>} 234 ); 235 236 my $name = $item->attr("{}name"); 237 238 # find list of group names to add/update. can ignore 239 # if we're just removing. 240 my @groups; # scalars of names 241 unless ($removing) { 242 foreach my $ele ($item->children_elements) { 243 next unless $ele->element eq "{jabber:iq:roster}group"; 244 push @groups, $ele->first_child; 245 } 246 } 247 248 my $ritem = DJabberd::RosterItem->new(jid => $jid, 249 name => $name, 250 remove => $removing, 251 groups => \@groups, 252 ); 253 254 # TODO if ($removing), send unsubscribe/unsubscribed presence 255 # stanzas. See RFC3921 8.6 256 257 # {=add-item-to-roster} 258 my $phase = $removing ? "RosterRemoveItem" : "RosterAddUpdateItem"; 259 $conn->vhost->run_hook_chain(phase => $phase, 260 args => [ $conn->bound_jid, $ritem ], 261 methods => { 262 done => sub { 263 my ($self, $ritem_final) = @_; 264 265 # the RosterRemoveItem isn't required to return the final item 266 $ritem_final = $ritem if $removing; 267 268 $iq->send_result; 269 $conn->vhost->roster_push($conn->bound_jid, $ritem_final); 270 271 # TODO: section 8.6: must send a 272 # bunch of presence 273 # unsubscribe/unsubscribed messages 274 }, 275 error => sub { # TODO What sort of error stat is being hit here? 276 $iq->send_error; 277 }, 278 }, 279 fallback => sub { 280 if ($removing) { 281 # NOTE: we used to send an error here, but clients get 282 # out of sync and we need to let them think a delete 283 # happened even if it didn't. 284 $iq->send_result; 285 } else { # TODO ACK, This one as well 286 $iq->send_error; 287 } 288 }); 289 290 return 1; 291} 292 293sub process_iq_getregister { 294 my ($conn, $iq) = @_; 295 296 # If the entity is not already registered and the host supports 297 # In-Band Registration, the host MUST inform the entity of the 298 # required registration fields. If the host does not support 299 # In-Band Registration, it MUST return a <service-unavailable/> 300 # error. If the host is redirecting registration requests to some 301 # other medium (e.g., a website), it MAY return an <instructions/> 302 # element only, as shown in the Redirection section of this 303 # document. 304 my $vhost = $conn->vhost; 305 unless ($vhost->allow_inband_registration) { 306 # MUST return a <service-unavailable/> 307 $iq->send_error( 308 qq{<error type='cancel' code='503'>}. 309 qq{<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}. 310 qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}. 311 qq{In-Band registration is not supported by this server's configuration.}. 312 qq{</text>}. 313 qq{</error>} 314 ); 315 return; 316 } 317 318 # if authenticated, give them existing login info: 319 if (my $jid = $conn->bound_jid) { 320 321 my $password = 0 ? "<password></password>" : ""; # TODO 322 my $username = $jid->node; 323 $iq->send_result_raw(qq{<query xmlns='jabber:iq:register'> 324 <registered/> 325 <username>$username</username> 326 $password 327 </query>}); 328 return; 329 } 330 331 # not authenticated, ask for their required fields 332 # NOTE: we send_result_raw here, which just writes, so they don't 333 # need to be an available resource (since they're not even authed 334 # yet) for this to work. that's like most things in IQ anyway. 335 $iq->send_result_raw(qq{<query xmlns='jabber:iq:register'> 336 <instructions> 337 Choose a username and password for use with this service. 338 </instructions> 339 <username/> 340 <password/> 341 </query>}); 342} 343 344sub process_iq_setregister { 345 my ($conn, $iq) = @_; 346 347 my $vhost = $conn->vhost; 348 unless ($vhost->allow_inband_registration) { 349 # MUST return a <service-unavailable/> 350 $iq->send_error( 351 qq{<error type='cancel'>}. 352 qq{<service-unavailable xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>}. 353 qq{<text xmlns='urn:ietf:params:xml:ns:xmpp-stanzas' xml:lang='en'>}. 354 qq{In-Band registration is not supported by this server\'s configuration.}. 355 qq{</text>}. 356 qq{</error>} 357 ); 358 return; 359 } 360 361 my $bjid = $conn->bound_jid; 362 363 # remove (cancel) support 364 my $item = $iq->query->first_element; 365 if ($item && $item->element eq "{jabber:iq:register}remove") { 366 if ($bjid) { 367 my $rosterwipe = sub { 368 $vhost->run_hook_chain(phase => "RosterWipe", 369 args => [ $bjid ], 370 methods => { 371 done => sub { 372 $iq->send_result; 373 $conn->stream_error("not-authorized"); 374 }, 375 }); 376 }; 377 378 $vhost->run_hook_chain(phase => "UnregisterJID", 379 args => [ username => $bjid->node, conn => $conn ], 380 methods => { 381 deleted => sub { 382 $rosterwipe->(); 383 }, 384 notfound => sub { 385 warn "notfound.\n"; 386 return $iq->send_error; 387 }, 388 error => sub { 389 return $iq->send_error; 390 }, 391 }); 392 393 $iq->send_result; 394 } else { 395 $iq->send_error; # TODO: <forbidden/> 396 } 397 return; 398 } 399 400 my $query = $iq->query 401 or die; 402 my @children = $query->children; 403 my $get = sub { 404 my $lname = shift; 405 foreach my $c (@children) { 406 next unless ref $c && $c->element eq "{jabber:iq:register}$lname"; 407 my $text = $c->first_child; 408 return undef if ref $text; 409 return $text; 410 } 411 return undef; 412 }; 413 414 my $username = $get->("username"); 415 my $password = $get->("password"); 416 return $iq->send_error unless $username =~ /^\w+$/; 417 return $iq->send_error if $bjid && $bjid->node ne $username; 418 419 # create the account 420 $vhost->run_hook_chain(phase => "RegisterJID", 421 args => [ username => $username, conn => $conn, password => $password ], 422 methods => { 423 saved => sub { 424 return $iq->send_result; 425 }, 426 conflict => sub { 427 my $epass = exml($password); 428 return $iq->send_error(qq{ 429 <query xmlns='jabber:iq:register'> 430 <username>$username</username> 431 <password>$epass</password> 432 </query> 433 <error code='409' type='cancel'> 434 <conflict xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> 435 </error> 436 }); 437 }, 438 error => sub { 439 return $iq->send_error; 440 }, 441 }); 442 443} 444 445 446sub process_iq_getauth { 447 my ($conn, $iq) = @_; 448 # <iq type='get' id='gaimf46fbc1e'><query xmlns='jabber:iq:auth'><username>brad</username></query></iq> 449 450 # force SSL by not letting them login 451 if ($conn->vhost->requires_ssl && ! $conn->ssl) { 452 $conn->stream_error("policy-violation", "Local policy requires use of SSL before authentication."); 453 return; 454 } 455 456 my $username = ""; 457 my $child = $iq->query->first_element; 458 if ($child && $child->element eq "{jabber:iq:auth}username") { 459 $username = $child->first_child; 460 die "Element in username field?" if ref $username; 461 } 462 463 # FIXME: use nodeprep or whatever, not \w+ 464 $username = '' unless $username =~ /^\w+$/; 465 466 my $type = ($conn->vhost->are_hooks("GetPassword") || 467 $conn->vhost->are_hooks("CheckDigest")) ? "<digest/>" : "<password/>"; 468 469 $iq->send_result_raw("<query xmlns='jabber:iq:auth'><username>$username</username>$type<resource/></query>"); 470 return 1; 471} 472 473sub process_iq_setauth { 474 my ($conn, $iq) = @_; 475 # <iq type='set' id='gaimbb822399'><query xmlns='jabber:iq:auth'><username>brad</username><resource>work</resource><digest>ab2459dc7506d56247e2dc684f6e3b0a5951a808</digest></query></iq> 476 my $id = $iq->id; 477 478 my $query = $iq->query 479 or die; 480 my @children = $query->children; 481 482 my $get = sub { 483 my $lname = shift; 484 foreach my $c (@children) { 485 next unless ref $c && $c->element eq "{jabber:iq:auth}$lname"; 486 my $text = $c->first_child; 487 return undef if ref $text; 488 return $text; 489 } 490 return undef; 491 }; 492 493 my $username = $get->("username"); 494 my $resource = $get->("resource"); 495 my $password = $get->("password"); 496 my $digest = $get->("digest"); 497 498 # "Both the username and the resource are REQUIRED for client 499 # authentication" Section 3.1 of XEP 0078 500 return unless $username && $username =~ /^\w+$/; 501 return unless $resource; 502 503 my $vhost = $conn->vhost; 504 505 my $reject = sub { 506 $DJabberd::Stats::counter{'auth_failure'}++; 507 $iq->send_reply("error", qq{<error code='401' type='auth'><not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/></error>}); 508 return 1; 509 }; 510 511 512 my $accept = sub { 513 my $cb = shift; 514 my $authjid = shift; 515 516 # create default JID 517 unless (defined $authjid) { 518 my $sname = $vhost->name; 519 $authjid = "$username\@$sname"; 520 } 521 522 # register 523 my $jid = DJabberd::JID->new("$authjid"); 524 525 unless ($jid) { 526 $reject->(); 527 return; 528 } 529 530 my $regcb = DJabberd::Callback->new({ 531 registered => sub { 532 (undef, my $fulljid) = @_; 533 $conn->set_bound_jid($fulljid); 534 $DJabberd::Stats::counter{'auth_success'}++; 535 $iq->send_result; 536 }, 537 error => sub { 538 $iq->send_error; 539 }, 540 _post_fire => sub { 541 $conn = undef; 542 $iq = undef; 543 }, 544 }); 545 546 $vhost->register_jid($jid, $resource, $conn, $regcb); 547 }; 548 549 550 # XXX FIXME 551 # If the client ignores your wishes get a digest or password 552 # We should throw an error indicating so 553 # Currently we will just return authentication denied -- artur 554 555 if ($vhost->are_hooks("GetPassword")) { 556 $vhost->run_hook_chain(phase => "GetPassword", 557 args => [ username => $username, conn => $conn ], 558 methods => { 559 set => sub { 560 my (undef, $good_password) = @_; 561 if ($password && $password eq $good_password) { 562 $accept->(); 563 } elsif ($digest) { 564 my $good_dig = lc(Digest::SHA1::sha1_hex($conn->{stream_id} . $good_password)); 565 if ($good_dig eq $digest) { 566 $accept->(); 567 } else { 568 $reject->(); 569 } 570 } else { 571 $reject->(); 572 } 573 }, 574 }, 575 fallback => $reject); 576 } elsif ($vhost->are_hooks("CheckDigest")) { 577 $vhost->run_hook_chain(phase => "CheckDigest", 578 args => [ username => $username, conn => $conn, digest => $digest, resource => $resource ], 579 methods => { 580 accept => $accept, 581 reject => $reject, 582 }); 583 } else { 584 $vhost->run_hook_chain(phase => "CheckCleartext", 585 args => [ username => $username, conn => $conn, password => $password ], 586 methods => { 587 accept => $accept, 588 reject => $reject, 589 }); 590 } 591 592 return 1; # signal that we've handled it 593} 594 595## sessions have been deprecated, see appendix E of: 596## http://xmpp.org/internet-drafts/draft-saintandre-rfc3921bis-07.html 597## BUT, we have to advertise session support since, libpurple REQUIRES it 598## (sigh) 599sub process_iq_session { 600 my ($conn, $iq) = @_; 601 602 my $from = $iq->from; 603 my $id = $iq->id; 604 605 my $xml = qq{<iq from='$from' type='result' id='$id'/>}; 606 $conn->xmllog->info($xml); 607 $conn->write(\$xml); 608} 609 610sub process_iq_bind { 611 my ($conn, $iq) = @_; 612 613 # <iq type='set' id='purple88621b5d'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><resource>yann</resource></bind></iq> 614 my $id = $iq->id; 615 616 my $query = $iq->bind 617 or die; 618 619 my $bindns = 'urn:ietf:params:xml:ns:xmpp-bind'; 620 my @children = $query->children; 621 622 my $get = sub { 623 my $lname = shift; 624 foreach my $c (@children) { 625 next unless ref $c && $c->element eq "{$bindns}$lname"; 626 my $text = $c->first_child; 627 return undef if ref $text; 628 return $text; 629 } 630 return undef; 631 }; 632 633 my $resource = $get->("resource") || DJabberd::JID->rand_resource; 634 635 my $vhost = $conn->vhost; 636 637 my $reject = sub { 638 my $xml = <<EOX; 639<iq id='$id' type='error'> 640 <error type='modify'> 641 <bad-request xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> 642 </error> 643</iq> 644EOX 645 $conn->xmllog->info($xml); 646 $conn->write(\$xml); 647 return 1; 648 }; 649 650 ## rfc3920 §8.4.2.2 651 my $cancel = sub { 652 my $reason = shift || "no reason"; 653 my $xml = <<EOX; 654<iq id='$id' type='error'> 655 <error type='cancel'> 656 <not-allowed 657 xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/> 658 </error> 659 </iq> 660EOX 661 $conn->log->error("Reject bind request: $reason"); 662 $conn->xmllog->info($xml); 663 $conn->write(\$xml); 664 return 1; 665 }; 666 667 my $sasl = $conn->sasl 668 or return $cancel->("no sasl"); 669 670 my $authjid = $conn->sasl->authenticated_jid 671 or return $cancel->("no authenticated_jid"); 672 673 # register 674 my $jid = DJabberd::JID->new($authjid); 675 676 unless ($jid) { 677 $reject->(); 678 return; 679 } 680 681 my $regcb = DJabberd::Callback->new({ 682 registered => sub { 683 (undef, my $fulljid) = @_; 684 $conn->set_bound_jid($fulljid); 685 $DJabberd::Stats::counter{'auth_success'}++; 686 my $xml = <<EOX; 687<iq id='$id' type='result'> 688 <bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'> 689 <jid>$fulljid</jid> 690 </bind> 691</iq> 692EOX 693 $conn->xmllog->info($xml); 694 $conn->write(\$xml); 695 }, 696 error => sub { 697 $reject->(); 698 }, 699 _post_fire => sub { 700 $conn = undef; 701 $iq = undef; 702 }, 703 }); 704 705 $vhost->register_jid($jid, $resource, $conn, $regcb); 706 return 1; 707} 708 709sub process_iq_set_djabberd_test { 710 my ($conn, $iq) = @_; 711 # <iq type='set' id='foo'><query xmlns='djabberd:test'>some command</query></iq> 712 my $id = $iq->id; 713 714 unless ($ENV{DJABBERD_TEST_COMMANDS}) { 715 $iq->send_error; 716 return; 717 } 718 719 my $query = $iq->query 720 or die; 721 my $command = $query->first_child; 722 723 if ($command eq "write error") { 724 $conn->set_writer_func(sub { 725 my ($bref, $to_write, $offset) = @_; 726 $conn->close; 727 return 0; 728 }); 729 $iq->send_result_raw("<wont_get_to_you_anyway/>"); 730 return; 731 } 732 733 $iq->send_result_raw("<unknown-command/>"); 734} 735 736sub id { 737 return $_[0]->attr("{}id"); 738} 739 740sub type { 741 return $_[0]->attr("{}type"); 742} 743 744sub from { 745 return $_[0]->attr("{}from"); 746} 747 748sub query { 749 my $self = shift; 750 my $child = $self->first_element 751 or return; 752 my $ele = $child->element 753 or return; 754 return undef unless $child->element =~ /\}query$/; 755 return $child; 756} 757 758sub bind { 759 my $self = shift; 760 my $child = $self->first_element 761 or return; 762 my $ele = $child->element 763 or return; 764 return unless $child->element =~ /\}bind$/; 765 return $child; 766} 767 768sub deliver_when_unavailable { 769 my $self = shift; 770 return $self->type eq "result" || 771 $self->type eq "error"; 772} 773 774sub make_response { 775 my ($self) = @_; 776 777 my $response = $self->SUPER::make_response(); 778 $response->attrs->{"{}type"} = "result"; 779 return $response; 780} 781 7821; 783