# # cheap RVP server implementation. # # Bolting this onto your Apache server: # PerlSwitches -I/path/to/RVP/module # # PerlHeaderParserHandler RVP # # # You also require a mysql database; the schemas required are at the # end of this script. # # Main TODO item: # * implement ACL controls. # most of this works pretty well # * polling would be good package RVP; use strict; use warnings; use DBI; use DBD::mysql; use Data::Dumper; use XML::Simple; use XML::LibXML; use URI; use LWP::UserAgent; # hmm # I HATE MOD_PERL "COMPATIBILITY" BEGIN { eval <<"EOC"; use Apache2::RequestRec (); use Apache2::RequestIO (); use Apache2::RequestUtil (); use Apache2::ServerUtil (); use Apache2::ServerRec (); use Apache2::Process (); use Apache2::Const; use Apache2::Log; use APR::Table (); use Apache2::Reload; use APR::Brigade (); use APR::Bucket (); use Apache2::Connection; use Apache2::Filter; use Apache2::Const -compile => qw(MODE_READBYTES); use APR::Const -compile => qw(SUCCESS BLOCK_READ); package Apache; sub OK { Apache2::Const::OK } sub DECLINED { Apache2::Const::DECLINED } sub MODE_READBYTES { Apache2::Const::MODE_READBYTES } package APR; sub BLOCK_READ { APR::Const::BLOCK_READ } package main; EOC if ( $@ ) { eval <<"EOC2"; use Apache::RequestRec (); use Apache::RequestIO (); use Apache::RequestUtil (); use Apache::ServerUtil (); use Apache::ServerRec (); use Apache::Process (); use Apache::Const; use Apache::Log; use APR::Table (); use Apache::Reload; use Apache::Const -compile => qw(MODE_READBYTES); use APR::Const -compile => qw(SUCCESS BLOCK_READ); use APR::Brigade (); use APR::Bucket (); use Apache::Connection; use Apache::Filter; EOC2 if ( $@ ) { die $@; } } } # ACL /instmsg/aliases/ HTTP/1.1 # NOTIFY /instmsg/aliases/ HTTP/1.1 # PROPFIND /instmsg/aliases/ HTTP/1.1 # PROPPATCH /instmsg/aliases/ HTTP/1.1 # PROPPATCH /instmsg/local//instmsg/aliases/ HTTP/1.1 # SUBSCRIBE /instmsg/aliases/ HTTP/1.1 # SUBSCRIBE /instmsg/aliases/ HTTP/1.1 # SUBSCRIPTIONS /instmsg/aliases/ HTTP/1.1 # UNSUBSCRIBE /instmsg/local//instmsg/aliases/ HTTP/1.1 my %methods = ( ACL => \&handle_acl, NOTIFY => \&handle_notify, PROPFIND => \&handle_propfind, PROPPATCH => \&handle_proppatch, SUBSCRIBE => \&handle_subscribe, SUBSCRIPTIONS => \&handle_subscriptions, UNSUBSCRIBE => \&handle_unsubscribe, # undocumented. yay. POLL => \&handle_poll, GET => \&handle_get, HEAD => \&handle_501, POST => \&handle_501, PUT => \&handle_501, LOCK => \&handle_501, UNLOCK => \&handle_501, OPTIONS => \&handle_501, COPY => \&handle_405, MOVE => \&handle_405, ); # DBI::apache automatically turns this lot into shared connections. sub getdbh { # this env setting keeps getting lost. $ENV{DBI_DSN} = "dbi:mysql:database=rvp:host=db;user=rvp;password=rvp"; my $dbh = DBI->connect( "dbi:mysql:database=rvp;host=db", "rvp", "rvp" ); expire_stuff( $dbh ); return $dbh; } sub ungetdbh { my $dbh = shift; if ( !defined( $dbh )) { print STDERR "disconnecting undef\n"; return; } $dbh->disconnect(); } # # clean out any old subscriptions/views # sub expire_stuff { my $dbh = $_[0]||getdbh(); # fixme: if we expire someone out of sight, send notifies $dbh->do( "DELETE from views WHERE expires < ?", undef, time()); $dbh->do( "DELETE from subscriptions WHERE expires < ?", undef, time()); if ( defined( $_[0] )) { ungetdbh( $dbh ); } } sub handler { my $r = shift; # handler code comes here my $method = $r->method; if ( grep /^$method$/, keys %methods ) { $r->server->method_register( $r->method ); $r->handler( "perl-script" ); $r->push_handlers( PerlResponseHandler => $methods{$r->method} ); return Apache::OK; } else { return Apache::DECLINED; } } # # Handle ACL get/set # sub handle_acl { my $r = shift; my $content = content( $r ); my $principal = $r->headers_in->get( 'RVP-From-Principal' ); my $whom = $r->uri; my $hostname = my_fully_qualified_hostname( $r ); my %principals; # canonicalise $whom $whom = 'http://' . $hostname . $whom; my $dbh = getdbh(); # this happens regardless of which operation we're doing but we # shouldn't be doing it twice FIXME my $sth = $dbh->prepare( "SELECT * FROM acls WHERE principal=? ORDER BY apply_to" ); my $rv = $sth->execute( $whom ); if ( defined( $rv )) { while ( my $hash = $sth->fetchrow_hashref ) { $principals{$hash->{apply_to}} = { credentials => [], grant => [], deny => [] }; for my $a ( "assertion", "digest", "ntlm" ) { if ( $hash->{$a}||0 ) { push @{$principals{$hash->{apply_to}}->{credentials}}, $a; } } for my $a ( "list", "read", "write", "send_to", "receive_from", "readacl", "writeacl", "presence", "subscriptions", "subscribe_others" ) { if ( $hash->{$a}||0 ) { # yes, this is silly. damned constants. my $b = $a; $b =~ s/_/-/g; push @{$principals{$hash->{apply_to}}->{grant}}, $b; } } } } $sth->finish(); if ( !$content ) { # retrieve ACLs } else { # set ACLs eval { my $ref = XMLin( $content, KeepRoot => 1, ForceArray => 1, KeyAttr => [] ); for my $ace ( @{$ref->{"a:rvpacl"}->[0]->{"a:acl"}->[0]->{"a:ace"}}) { my $recip = $ace->{'a:principal'}->[0]->{'a:rvp-principal'}->[0] if defined( $ace->{'a:principal'}->[0]->{'a:rvp-principal'}); if ( !defined( $recip )) { if ( $ace->{'a:principal'}->[0]->{'a:allprincipals'}) { $recip = "allprincipals"; } else { $r->log_error( Dumper( $ace )); next; } } $dbh->do( "REPLACE INTO acls(principal, apply_to) VALUES(?,?)", undef, $whom, $recip ); for my $cred ( keys %{$ace->{'a:principal'}->[0]->{'a:credentials'}->[0]}) { $cred =~ s/^a://; $dbh->do( "UPDATE acls SET $cred=1 WHERE principal=? AND apply_to=?", undef, $whom, $recip ); } my $count = 0; for my $grant ( keys %{$ace->{'a:grant'}->[0]}) { $grant =~ s/^a://; $grant =~ s/-/_/g; $dbh->do( "UPDATE acls SET \`$grant\`=1 WHERE principal=? AND apply_to=?", undef, $whom, $recip ); $count++; } for my $deny ( keys %{$ace->{'a:deny'}->[0]}) { $deny =~ s/^a://; $deny =~ s/-/_/g; $dbh->do( "UPDATE acls SET \`$deny\`=0 WHERE principal=? AND apply_to=?", undef, $whom, $recip ); $count++; } # if no acls are set, delete the entire entry if ( $count == 0 ) { $dbh->do( "DELETE FROM acls WHERE principal=? AND apply_to=?", undef, $whom, $recip ); } } }; if ( $@ ) { $r->log_error( "ACL failed: $@" ); } # requery %principals = (); my $sth = $dbh->prepare( "SELECT * FROM acls WHERE principal=? ORDER BY apply_to" ); my $rv = $sth->execute( $whom ); if ( defined( $rv )) { while ( my $hash = $sth->fetchrow_hashref ) { $principals{$hash->{apply_to}} = { credentials => [], grant => [], deny => [] }; for my $a ( "assertion", "digest", "ntlm" ) { if ( $hash->{$a}||0 ) { push @{$principals{$hash->{apply_to}}->{credentials}}, $a; } } for my $a ( "list", "read", "write", "send_to", "receive_from", "readacl", "writeacl", "presence", "subscriptions", "subscribe_others" ) { if ( $hash->{$a}||0 ) { # yes, this is silly. damned constants. my $b = $a; $b =~ s/_/-/g; push @{$principals{$hash->{apply_to}}->{grant}}, $b; } } } } $sth->finish(); } ungetdbh( $dbh ); # build the response if ( !keys %principals ) { # make sure we can access our own ACL stuff, at least $principals{$principal} = { credentials => [ # 'assertion', 'digest', 'ntlm' ], grant => [ "list", "read", "write", "send-to", "receive-from", "readacl", "writeacl", "presence", "subscriptions", "subscribe-others", ], deny => [ ], }; # wtf quoting? seems like 'read' and 'write' cause parse # problems unless quoted. $dbh->do( 'INSERT INTO acls(principal,apply_to,assertion,digest,ntlm,list,`read`,`write`,send_to,receive_from,readacl,writeacl,presence,subscriptions,subscribe_others) values(?,?,0,1,1,1,1,1,1,1,1,1,1,1,1)', undef, $principal, $principal ); # add a default principal, should probably be configurable FIXME $principals{allprincipals} = { credentials => [ # 'assertion', 'digest', 'ntlm' ], grant => [ 'list', 'read', 'send-to', 'presence', ], deny => [ ], }; $dbh->do( 'INSERT INTO acls(principal,apply_to,assertion,digest,ntlm,list,`read`,send_to,presence) VALUES(?,?,0,1,1,1,1,1,1)', undef, $principal, "allprincipals" ); } my $out = ""; $out = ""; $out .=<<"EOF"; none EOF for my $principal ( keys %principals ) { $out .= ""; $out .= ""; if ( $principal eq "allprincipals" ) { $out .= ""; } else { $out .= ""; $out .= $principal; $out .= ""; } $out .= ""; for my $cred ( @{$principals{$principal}->{credentials}}) { $out .= ""; } $out .= ""; $out .= ""; $out .= ""; for my $grant ( @{$principals{$principal}->{grant}} ) { $out .= ""; } $out .= ""; $out .= ""; for my $deny ( @{$principals{$principal}->{deny}} ) { $out .= ""; } $out .= ""; $out .= ""; } $out .= ""; $r->content_type( "text/xml" ); $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); $r->headers_out->set( "Content-Length" => length( $out )); $r->print( $out ); return Apache::OK; } # # handle notifications # sub handle_notify { my $r = shift; my $headers_in = $r->headers_in; my $content = content($r); my $to = $r->uri; my $from = $r->headers_in->get( "RVP-From-Principal" ); my $acktype = $r->headers_in->get( "RVP-Ack-Type" ); my $hopcount = $r->headers_in->get( "RVP-Hop-Count" ); my $rc = 200; my $hostname = my_fully_qualified_hostname( $r ); if ( !defined( $from )) { $r->status_line( "400 No principal specified in request" ); $r->log_error( "no principal specified in notify" ); return 400; } my $dbh = getdbh(); # see if the intended user is online my $sth = $dbh->prepare( "SELECT url FROM subscriptions WHERE principal=? AND (subscribee IS NULL OR subscribee = '')" ); $sth->execute( "http://" . $hostname . $to ); while ( my $hash = $sth->fetchrow_hashref ) { my $callback = $hash->{url}; if ( defined( $callback )) { $rc = send_notify( $callback, $from, $to, $content, $r ); } else { $rc = 404; } if ( $rc == 404 or $rc == 500 ) { if ( defined( $callback )) { $r->log_error( "$to is not reachable, deleting callback URL" ); $dbh->do( "DELETE FROM subscriptions WHERE principal=? AND url=?", undef, "http://" . $hostname . $to, $callback ); } else { $r->log_error( "no callback defined for $to" ); } } elsif ( $rc != 200 ) { $r->log_error( "$to return code $rc" ); } } $sth->finish(); ungetdbh( $dbh ); $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); $r->content_type('text/html'); my $principal = new URI( $from ); $principal = sprintf( "http://%s%s", $principal->host, $to ); # xxx port my $response; $r->status( $rc ); if ( $rc == 200 ) { $response =<<"EOF"; Successful

Success 200 (Successful)


Notify on node $principal succeeded
EOF } else { $response =<<"EOF"; Error

Error $rc


Notify on node $principal failed
EOF } $r->headers_out->set( "Content-Length" => length( $response )); $r->print( $response ); return $rc; } # # request a property # sub handle_propfind { my $r = shift; my $content = content( $r ); if ( !( $content||"" )) { $r->log_error( "no content in propfind" ); return 400; } my $url = $r->headers_in->get( 'RVP-From-Principal' ); my ( $whom ) = $r->uri =~ m|.*/aliases/(.*)|; eval { my $ref = XMLin( $content, KeepRoot => 1, ForceArray => 1, KeyAttr => [] ); my $props = $ref->{"d:propfind"}->[0]->{"d:prop"}; my @found; for my $prop ( @{$props} ) { if ((keys %{$prop})[0] eq "d:displayname" ) { push @found, { "d:displayname" => [ $whom ], }; } } my $status = "HTTP/1.1 200 Successful"; my $out = multistatus( $url, { 'd:prop' => \@found, "d:status" => [ $status ]} ); $r->content_type( "text/xml" ); $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); $r->headers_out->set( "Content-Length" => length( $out )); $r->print( $out ); }; if ( $@ ) { $r->log_error( $@ ); $r->status_line( "500 $@" ); return 500; } $r->status( 207 ); return Apache::OK; } # # Handle property change # sub handle_proppatch { my $r = shift; my $hostname = $r->server->server_hostname; my $content = content( $r ); my $principal = $r->headers_in->get( 'RVP-From-Principal' ); my $whom = $r->uri; my $subscr = $r->headers_in->get( 'Subscription-Id' ); if ( !defined( $principal )) { $r->log_error( "no principal set in proppatch" ); return 400; } if ( !$content ) { $r->log_error( "no content in proppatch" ); return 400; } my $dbh = getdbh(); my $props; my $state; eval { # This dies on invalid input; our handler will catch that and # signal a 500 error. my $ref = XMLin( $content, KeepRoot => 1, ForceArray => 1, KeyAttr => [] ); # XXX may be more than one prop to set! $props = $ref->{"d:propertyupdate"}->[0]->{"d:set"}->[0]; for my $prop ( @{$ref->{"d:propertyupdate"}->[0]->{"d:set"}} ) { # XXX # Parse props here eval { if ( defined( $prop->{"d:prop"}->[0]->{"r:state"}->[0]->{"r:leased-value"}->[0]->{"r:value"} )) { $state = (keys %{$prop->{"d:prop"}->[0]->{"r:state"}->[0]->{"r:leased-value"}->[0]->{"r:value"}->[0]})[0]; } $state ||= ( keys %{$prop->{"d:prop"}->[0]->{"r:state"}->[0]->{"r:leased-value"}->[0]->{"r:default-value"}->[0]})[0]; }; $r->log_error( "propatch: $@" ) if $@; my $viewid = 0; my $timeout = 0; eval { $viewid = $prop->{"d:prop"}->[0]->{"r:state"}->[0]->{"r:view-id"}->[ 0 ]; }; $r->log_error( "proppatch: $@" ) if $@; eval { $timeout = $prop->{"d:prop"}->[0]->{"r:state"}->[0]->{"r:leased-value"}->[0]->{"d:timeout"}->[0]; $timeout ||= 14400; $timeout += time; }; $r->log_error( "proppatch: $@" ) if $@; # urgh $r->log_error( "no state found, defaulting to offline" ) unless $state; $state ||= "r:offline"; $state =~ s/^r://; $r->log_error( "setting $principal to $state" ); if ( !$viewid ) { $dbh->do( "INSERT INTO views(principal,expires,state) VALUES(?,?,?)", undef, $principal, $timeout, $state ); my $s = $dbh->prepare( "SELECT viewid FROM views WHERE principal=? AND expires=?" ); my $r = $s->execute( $principal, $timeout ); ( $viewid ) = $s->fetchrow_array(); } else { $dbh->do( "UPDATE views SET expires=?, state=? WHERE viewid=?", undef, $timeout, $state, $viewid ); } # XXX don't set this unless we ARE successful, plus if # there are multiple props it should probably get set # multiple times... my $status = "HTTP/1.1 200 Successful"; # FIXME XXX HONK $prop->{"d:prop"}->[0]->{"r:state"}->[0]->{"r:view-id"} = [ $viewid ]; $prop->{"d:status"} = [ $status ]; } my $out = multistatus( $principal, $props ); $r->content_type( "text/xml" ); $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); $r->headers_out->set( "Content-Length" => length( $out )); $r->print( $out ); }; if ( $@ ) { $r->log_error( $@ ); $r->status_line( "500 $@" ); ungetdbh( $dbh ); return 500; } # notify anyone who's subscribed to me my $sth = $dbh->prepare( "SELECT DISTINCT principal FROM subscriptions WHERE subscribee=?" ); my $sth2 = $dbh->prepare( "SELECT url FROM subscriptions WHERE principal=? AND (subscribee IS NULL OR subscribee = '')" ); $sth->execute( $principal ); while ( my $hash = $sth->fetchrow_hashref ) { next unless $hash->{principal}; $sth2->execute( $hash->{principal}); while ( my $hash2 = $sth2->fetchrow_hashref ) { next unless $hash2->{url}; notify_propchange( $principal, $hash->{principal}, $hash2->{url}, $state, $r ); } $sth2->finish(); } $sth->finish(); ungetdbh( $dbh ); $r->status( 207 ); return Apache::OK; } # # Handle a subscription request # sub handle_subscribe { my $r = shift; my $headers_in = $r->headers_in; my $content = content($r); my $type = $r->headers_in->get( 'Notification-Type' ); my $uri = $r->uri; my $principal = $r->headers_in->get( "RVP-From-Principal" ); my $hostname = my_fully_qualified_hostname( $r ); my $subscr = $r->headers_in->get( "Subscription-Id" ); my $callback = $r->headers_in->get( "Call-Back" ); my $lifetime = $r->headers_in->get( "Subscription-Lifetime" )||14400; if ( $headers_in->get( 'Host' ) ne $hostname ) { $r->log_error( "redirecting to $hostname" ); $r->status_line( "302 Object Moved" ); $r->headers_out->set( "Location" => "http://" . $hostname . $uri ); return 302; } my $expires = $lifetime + time; my $dbh = getdbh(); if ( defined( $subscr )) { my $sth = $dbh->prepare( "SELECT principal,url,subscribee FROM subscriptions WHERE subscription=?" ); $sth->execute( $subscr ); while ( my $hash = $sth->fetchrow_hashref()) { if ( !($hash->{subscribee}||0) ) { if ( "http://" . $hostname . $uri ne $hash->{principal} ) { $r->status_line( "412 Precondition failed" ); $r->log_error( "Tried to subscribe myself with the wrong URI: wanted " . $hash->{principal} . ", got http://" . $hostname . $uri ); ungetdbh( $dbh ); return 412; } $type = "pragma/notify"; $principal = $hash->{principal}; $callback = $hash->{url}; last; } else { $type = "update/propchange"; $principal = $hash->{principal}; $callback = $hash->{url}; last; } } } if ( !defined( $principal )) { $r->status_line( "400 No principal specified in request" ); $r->log_error( "no principal specified in subscribe" ); ungetdbh( $dbh ); return 400; } if ( !defined( $type )) { $r->status_line( "400 Subscription type not specified" ); $r->log_error( "no type specified in subscribe for $principal" ); ungetdbh( $dbh ); return 400; } if ( $callback =~ /:0$ /) { $r->status_line( "400 Invalid callback port" ); $r->log_error( "Invalid port specified in callback" ); return 400; } # we don't actually use this at the moment my $authuser = $r->user(); my $subid; if ( defined( $subscr )) { $subid = $subscr; } else { $dbh->do( "INSERT INTO subscriptions(principal,url,subscribee,expires) VALUES(?,?,?,?)", undef, $principal, "", "", $expires ); $subid = $dbh->last_insert_id( undef, undef, undef, undef ); $subid ||= $dbh->{mysql_insertid}; # oh the hatred if ( !defined( $subid )) { my $s = $dbh->prepare( "SELECT subscription FROM subscriptions WHERE principal=? AND url='' AND expires=?" ); my $res = $s->execute( $principal, $expires ); ( $subid ) = $s->fetchrow_array(); } } if ( !defined( $subid )) { $r->log_error( "failed to generate a subid" ); return 500; } $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); $r->headers_out->set( "Subscription-Id" => $subid ); $r->headers_out->set( "Subscription-Lifetime" => $lifetime ); my ( $user ) = $uri =~ m|/([^/]+)$|; my ( $href ) = $principal =~ m|(http://[^/]+)/|; $href .= "/instmsg/aliases/$user"; # SUBSCRIBE handles both buddy lists and logins via the # Notification-Type header. # This is a login if ( $type eq "pragma/notify" ) { if ( $dbh->do( "SELECT subscription FROM subscriptions WHERE subscription=?", undef, $subid )) { $dbh->do( "UPDATE subscriptions SET principal=?, url=?, subscribee=?, expires=? WHERE subscription=?", undef, $principal, $callback, "", $expires, $subid ); } else { $dbh->do( "INSERT INTO subscriptions( subscription, principal, url, subscribee, expires ) VALUES( ?,?,?,?,? )", undef, $subid, $principal, $callback, "", $expires ); } $r->content_type( "text/html" ); my $response =<<"EOF"; Successful

Success 200 (Successful)


SUBSCRIBE on node $principal succeeded
EOF $r->headers_out->set( "Content-Length" => length( $response )); $r->print( $response ); $r->status( 200 ); # And this is a buddy-list request } elsif ( $type eq "update/propchange" ) { # do we know the user? since we're authing against a PDC, yes # we do. We don't know much about the user, though. my $state = "r:offline"; # hurrah! my $userinfo = `/usr/bin/wbinfo -i $user 2>/dev/null`; my ( @bits ) = split( ':', $userinfo ); my $displayname = $user; if ( @bits ) { $displayname = $bits[4]; } my $email = $user . '@' . my_fully_qualified_hostname( $r ); my $sth = $dbh->prepare( "SELECT state FROM views WHERE principal=?" ); $sth->execute( "http://" . $hostname . $uri ); while ( my $hash = $sth->fetchrow_hashref()) { # attempt to consolidate more than one state: any state # other than offline trumps offline. if ( $state eq "r:offline" ) { $state = "r:" . $hash->{state}; } else { # any online trumps any other state if ( $state ne "r:online" ) { $state = $hash->{state}; } } } $sth->finish(); my $props = { "d:prop" => [ { "r:state" => [ $state ], }, { "d:displayname" => [ $displayname ], }, { "r:email" => [ $email ], }, ], "d:status" => [ "HTTP/1.1 200 Successful" ], }; if ( $dbh->do( "SELECT subscription FROM subscriptions WHERE subscription=?", undef, $subid )) { $dbh->do( "UPDATE subscriptions SET principal=?, url=?, subscribee=?, expires=? WHERE subscription=?", undef, $principal, $callback||"", "http://" . $hostname . $uri, $expires, $subid ); } else { $dbh->do( "INSERT INTO subscriptions(subscription, principal, url, subscribee, expires ) VALUES( ?,?,?,?,? )", undef, $subid, $principal, $callback||"", "http://" . $hostname . $uri, $expires ); } if ( !defined( $subscr )) { my $response = multistatus( $href, $props ); $r->content_type( "text/xml" ); $r->headers_out->set( "Content-Length" => length( $response )); $r->print( $response ); $r->status( 207 ); } else { $r->content_type( "text/html" ); my $response = "

Dude!

"; $r->headers_out->set( "Content-Length" => length( $response )); $r->print( $response ); $r->status( 200 ); } } else { $r->status_line( "400 Unknown subscribe type $type" ); $r->log_error( "unknown subscription type $type in subscribe" ); $r->status( 400 ); } ungetdbh( $dbh ); return Apache::OK; } sub handle_poll { my $r = shift; my $content = content( $r ); $r->log_error( "POLL received, data:" ); $r->log_error( $content ); return Apache::OK; } sub handle_subscriptions { my $r = shift; my $hostname = $r->server->server_hostname; my $user = $r->headers_in->get( 'RVP-From-Principal' ); my ( $whom ) = $r->uri =~ m|.*/aliases/(.*)|; my $type = $r->headers_in->get( 'Notification-Type' ); return 400 unless $type; return 400 unless $type =~ m{(update/propchange|pragma/notify)};#}; my $dbh = getdbh(); my $sth = $dbh->prepare( "SELECT subscription,subscribee,expires FROM subscriptions WHERE principal=?" ); $sth->execute( $whom ); my @subs; while ( my $hash = $sth->fetchrow_array()) { if ( $type eq "update/propchange" ) { next unless ( $hash->{subscribee}||0 ); } else { next if ( $hash->{subscribee}||0 ); } my $timeout = $hash->{expires} - time; my $id = $hash->{subscription}; my $sub = $hash->{subscribee}; push @subs, <<"EOF"; $id$sub$sub$timeout EOF } ungetdbh( $dbh ); my $subs = join( '', @subs ); my $out = <<"EOF"; $subs EOF $out =~ s/\n//; # grr $out = '' . "\n" . $out; $r->content_type( "text/xml" ); $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); $r->headers_out->set( "Content-Length" => length( $out )); $r->print( $out ); return Apache::OK; } # # Handle unsubscribe request. # sub handle_unsubscribe { my $r = shift; my $uri = $r->uri; my $principal = $r->headers_in->get( "Principal" ); my $subid = $r->headers_in->get( "Subscription-Id" ); my $hostname = $r->server->server_hostname; # It appears that there is never any content for this. if ( defined( $principal )) { $r->log_error( "$principal unsubs from $uri" ); } else { if ( !defined( $subid )) { $r->log_error( "uh. Can't unsubscribe without a principal or subid" ); return 400; } my $dbh = getdbh(); $dbh->do( "DELETE FROM subscriptions WHERE subscription=?", undef, $subid ); ungetdbh( $dbh ); } $r->headers_out->set( "RVP-Notifications-Version" => 0.2 ); return Apache::OK; } sub handle_405 { return 405; } sub handle_501 { return 501; } # wholly stolen from mod_perl examples use constant IOBUFSIZE => 8192; sub content { my $r = shift; my $bb = APR::Brigade->new($r->pool, $r->connection->bucket_alloc); my $data = ''; my $seen_eos = 0; do { $r->input_filters->get_brigade($bb, Apache::MODE_READBYTES, APR::BLOCK_READ, IOBUFSIZE); for (my $b = $bb->first; $b; $b = $bb->next($b)) { if ($b->is_eos) { $seen_eos++; last; } if ($b->read(my $buf)) { $data .= $buf; } $b->remove; # optimization to reuse memory } } while (!$seen_eos); $bb->destroy; return $data; } # # Return a multistatus response with a href of URL and properties PROPS # sub multistatus { my $url = shift; my $props = shift; # Dear Microsoft, you suck. Love, Waider. # # to elaborate: # You can't simply use XMLout to generate the reply here, because # the MSN client is sensitive to (at the very least) the ordering # of subattributes in the r:state property. Thus this giant mess # to force the 'correct' order. my $out =<<"LEADIN"; $url LEADIN chomp( $out ); # lose the trailing newline my $status; # Handle the props if ( defined( $props )) { for my $prop ( @{$props->{"d:prop"}} ) { $out .= ""; my $propname = ( keys %{$prop} )[0]; $out .= "<$propname>"; my @propvals = @{$prop->{$propname}}; # and now for the hatred if ( $propname eq "d:displayname" or $propname eq "r:email" or $propname eq "r:mobile-state" or $propname eq "r:mobile-description" ) { $out .= $propvals[0]; } elsif ( $propname eq "r:state" ) { if ( ref $propvals[0] ne "HASH" ) { $out .= "<"; $out .= $propvals[0]; $out .= "/>"; } else { for my $attr ( 'r:leased-value', 'r:view-id' ) { next unless defined( $propvals[0]->{$attr} ); $out .= "<$attr>"; if ( $attr eq 'r:view-id' ) { $out .= $propvals[0]->{$attr}->[0]; } else { for my $subattr ( 'r:value', 'r:default-value', 'd:timeout' ) { my $subattr_ref = $propvals[0]->{$attr}->[0]->{$subattr}; next unless defined $subattr_ref; next unless ref $subattr_ref eq "ARRAY"; next unless defined $subattr_ref->[0]; my $sa_hash = $subattr_ref->[0]; # next unless ref $sa_hash eq "HASH"; # next unless keys %{$sa_hash}; $out .= "<$subattr>"; if ( $subattr eq "d:timeout" ) { $out .= ( $propvals[0]->{$attr}->[0]->{$subattr}->[0] || 0 ); } else { if ( ref $sa_hash eq "HASH" and keys %{$sa_hash} ) { $out .= "<" . ( keys %{$propvals[0]->{$attr}->[0]->{$subattr}->[0]})[0] . "/>"; } } $out .= ""; } } $out .= ""; } } } else { print STDERR "ERROR: don't know how to format $propname\n"; } $out .= ""; $out .= ""; } $status = $props->{"d:status"}->[0]; } else { $status = "HTTP/1.1 500 WTF"; } # trailer $out .=<<"LEADOUT"; $status LEADOUT $out; } # Tell someone about a propchange sub notify_propchange { my ( $from, $to, $dest, $state, $r ) = @_; my $hostname = my_fully_qualified_hostname( $r ); my $ua = new LWP::UserAgent(); my $req = new HTTP::Request( "NOTIFY" => $dest ); my $uri = new URI( $dest ); $req->protocol( "HTTP/1.1" ); $req->header( "RVP-Notifications-Version" => "0.2" ); $req->header( "Host" => $uri->host ); $req->header( "Content-Type" => "text/xml" ); $req->header( "RVP-From-Principal" => $hostname ); # xxx this is dubious my $properties = "<$state/>"; # seems reasonably correct... $req->header( "RVP-Hop-Count" => "2" ); my $msg =<<"MSG"; $from$to$properties MSG $req->header( "Content-Length" => length( $msg )); $req->content( $msg ); my $response = $ua->request( $req ); # this is a bit crunky. if ( !$response->is_success) { # $r->log_error( "sent:" ); # $r->log_error( $req->as_string ); # $r->log_error( "recv:" ); # $r->log_error( $response->as_string ); if ( $response->as_string =~ /Client-Warning: internal response/is ) { return 0; } else { return 1; } } return 1; } # # have to send session in to this as a param because it's called with # the session lock held! # sub send_notify { my ( $dest, $principal, $uri, $msg, $r ) = @_; my $u = new URI( $dest ); my $header = ""; my $ua = new LWP::UserAgent(); my $req = new HTTP::Request( "NOTIFY" => $dest ); $uri ||= "/"; $req->uri( "rvp://" . $u->host . ":" . $u->port . $uri ); $req->protocol( "HTTP/1.1" ); $req->header( "RVP-Notifications-Version" => "0.2" ); $req->header( "Content-Type" => "text/xml" ); $req->header( "Host" => $u->host ); $req->header( "Content-Length" => length( $msg )); $req->content( $msg ); my $response = $ua->request( $req ); # this is a bit crunky. if ( !$response->is_success) { $r->log_error( "sent:" ); $r->log_error( $req->as_string ); $r->log_error( "recv" ); $r->log_error( $response->as_string ); # fake a response, because a 500 will cause the Gaim plugin to # blow up right now return 404; } return $response->code; } # # convert a principal to an email address # sub email_from_principal { my $principal = shift; if ( $principal =~ /local/ ) { $principal =~ s{^http://.*local/([^/]+)/.*/([^/]+)$}{$2\@$1}; } else { $principal =~ s{^http://([^/]+)/.*/([^/]+)$}{$2\@$1}; } $principal; } # # diagnostic interface # sub handle_get { my $r = shift; my %foo; my $session = \%foo; $r->content_type( "text/html" ); if ( $r->uri =~ /rvp.png$/ ) { my $img = "/usr/share/pixmaps/gaim/status/default/msn.png"; if ( open( IMG, "<$img" )) { local $/ = undef; binmode( IMG ); my $pic = ; $r->content_type( "image/png" ); $r->headers_out->set( "Content-Length", length( $pic )); $r->print( $pic ); close( IMG ); } return 200; } my $page =<<"HEAD"; RVP status page HEAD eval { $page .= ""; $page .= "\n"; for my $user ( keys %{$session->{users}}) { $page .= "\n"; } $page .= "
Usersubs
"; $page .= email_from_principal( $user ); $page .= ""; $page .= ""; $page .= ""; for my $sub ( keys %{$session->{users}->{$user}->{subscriptions}}) { $page .= ""; $page .= ""; $page .= ""; $page .= ""; $page .= ""; $page .= ""; } $page .= "
subidsubcallbackexpiry
"; $page .= $session->{users}->{$user}->{subscriptions}->{$sub}->{subid}; $page .= "$sub"; $page .= $session->{users}->{$user}->{subscriptions}->{$sub}->{callback}||"-"; $page .= ""; $page .= $session->{users}->{$user}->{subscriptions}->{$sub}->{expires}||"-"; $page .= "

"; $page .= "global callback @ " . $session->{users}->{$user}->{callback} if $session->{users}->{$user}->{callback}; $page .= "
"; $page .= "state: " . $session->{users}->{$user}->{state} if $session->{users}->{$user}->{state}; $page .= "
"; }; if ( $@ ) { $page .= "

$@

"; } $page .= "
" . Dumper( $session ) . "
"; $page .= ""; $r->print( $page ); $r->status( 200 ); return Apache::OK; } sub my_fully_qualified_hostname { my $r = shift; my $name = $r->server->server_hostname(); $name; } # This is a horrible bunch of code to allow me to piggyback on LWP. package LWP::Protocol::rvp; use vars qw( @ISA ); use LWP::Protocol::http; @ISA = qw(LWP::Protocol::http); sub request { my($self, $request, @rest ) = @_; my $uri = $request->uri(); $uri =~ s/^rvp/http/; $request->uri( $uri ); return $self->SUPER::request( $request, @rest ); } sub _fixup_header { my ( $self, $h, $url, $proxy ) = @_; my $return = $self->SUPER::_fixup_header( $h, $url, $proxy ); # make sure there's no port in the Host header $h->init_header( 'Host' => $url->host ); $h->init_header( 'RVP-Notifications-Version' => 0.2 ); $return; } # Yumping yimminy. package LWP::Protocol::rvp::SocketMethods; use vars qw(@ISA); @ISA = qw(LWP::Protocol::http::SocketMethods Net::HTTP); package LWP::Protocol::rvp::Socket; use vars qw(@ISA); @ISA = qw(LWP::Protocol::http::SocketMethods Net::HTTP); # need to make the URL relative sub format_request { my ( $self, $method, $uri, @rest ) = @_; $uri =~ s|rvp://[^/]+||; $self->SUPER::format_request( $method, $uri, @rest ); } package URI::rvp; use URI::http; use vars qw(@ISA); @ISA = qw(URI::http); 1; __DATA__ Database Schema: you should be able to feed this straight to mysql to set up the database. No prepopulation is required. MySQL 4 required, evidently. -- MySQL dump 10.9 -- -- Host: localhost Database: rvp -- ------------------------------------------------------ -- Server version 4.1.16 /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; /*!40101 SET NAMES utf8 */; /*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; -- -- Current Database: `rvp` -- CREATE DATABASE /*!32312 IF NOT EXISTS*/ `rvp` /*!40100 DEFAULT CHARACTER SET latin1 */; USE `rvp`; -- -- Table structure for table `acls` -- DROP TABLE IF EXISTS `acls`; CREATE TABLE `acls` ( `principal` text NOT NULL, `apply_to` text NOT NULL, `assertion` tinyint(1) NOT NULL default '0', `digest` tinyint(1) NOT NULL default '0', `ntlm` tinyint(1) NOT NULL default '0', `list` tinyint(1) NOT NULL default '0', `read` tinyint(1) NOT NULL default '0', `write` tinyint(1) NOT NULL default '0', `send_to` tinyint(1) NOT NULL default '0', `receive_from` tinyint(1) NOT NULL default '0', `readacl` tinyint(1) NOT NULL default '0', `writeacl` tinyint(1) NOT NULL default '0', `presence` tinyint(1) NOT NULL default '0', `subscriptions` tinyint(1) NOT NULL default '0', `subscribe_others` tinyint(1) NOT NULL default '0', PRIMARY KEY (`principal`(512),`apply_to`(512)) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -- Table structure for table `subscriptions` -- DROP TABLE IF EXISTS `subscriptions`; CREATE TABLE `subscriptions` ( `subscription` int(11) NOT NULL auto_increment, `principal` text character set latin1 NOT NULL, `url` text character set latin1 NOT NULL, `subscribee` text NOT NULL, `expires` int(11) NOT NULL default '0', PRIMARY KEY (`subscription`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='new RVP table'; -- -- Table structure for table `views` -- DROP TABLE IF EXISTS `views`; CREATE TABLE `views` ( `viewid` int(11) NOT NULL auto_increment, `principal` text NOT NULL, `expires` int(11) NOT NULL default '0', `state` varchar(20) NOT NULL default '', PRIMARY KEY (`viewid`) ) ENGINE=InnoDB DEFAULT CHARSET=latin1; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; /*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */; /*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;