package Volity::GameRecord; ############################################################################ # LICENSE INFORMATION - PLEASE READ ############################################################################ # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ############################################################################ =head1 NAME Volity::GameRecord - Information about a completed Volity game. =head1 SYNOPSIS use Volity::GameRecord; =head1 DESCRIPTION An object of this class represents an information record about. In practical terms, it's an abstraction of the information that a game referee sends to its Volity network's bookkeeper once a game has finished. Through the methods this class provides, it's easy for a parlor to build and sign this RPC call, and just as easy for the receiving bookkeeper. Note that, since RPC is the neutral middle ground, a Frivolity game parlor (a.k.a. a Perl object of a Volity::Game::Server subclass) can pass a Volity::GameRecord object to any Volity bookkeeper, regardless of its platform. Similarly, A Volity::Bookkeeper object can knit a Volity::Gamerecord object out of any game parlor's RPC request, whether or not that parlor runs Frivolity. (In reality, this latter situation will probably be quite common.) =head1 USAGE Game module designers don't have to worry about using this module at all. Records are automatically created by Volity::Referee objects when a game wraps up, and incoming records are automatically parsed by the bookkeeper. Your main responsibility with game record handling involves the winners list, available through the C method of your C subclass and manipulatable through the methods described in L. The following documentation is really here for completeness' sake, but those wishing to modify the Frivolity referee or bookkeeper behavior might find it interesting. =cut use warnings; use strict; use URI; use DateTime; use DateTime::Format::W3CDTF; use base qw( Volity ); use fields qw( id signature winners start_time end_time game_uri_object game_name parlor finished seats ); # Set up package variables for GPG config. our ( $gpg_bin, $gpg_secretkey, $gpg_passphrase ); ######################## # Special Constructors (Class methods) ######################## =head1 METHODS =head2 Class methods (constructors) =over =item new_from_hashref($hashref) Creates a new object based on the given hash reference, typically one that has been freshly translated from an RPC EstructE argument. For the opposite functionality, see C under L<"Object Methods">. =back =head2 Object accessors All these are simple accessors which return the named object attribute. If an argument is passed in, then the attribute's value is first set to that argument. In the case of lists, either an array or an array reference is returned, depending upon context. This module inherits from Class::Accessor, so all the tips and tricks detailed in L apply here as well. =over =item id =item signature =item winners =item start_time =item end_time =item game_uri =item game_name =item parlor =item finished =back =cut ###################### # Object methods ###################### =head2 Object methods =over =cut ######################## # Special Accessors ######################## # Most accessors are automatically defined by Class::Accessors::Fields. sub game_uri { my $self = shift; # We store URI-class objects, and return stringy-dings. # You can pass in either URI objects or strings. if ( exists( $_[0] ) ) { if ( defined( ref( $_[0] ) ) and ref( $_[0] ) eq 'URI' ) { $self->game_uri_object(@_); } elsif ( not( ref( $_[0] ) ) ) { my $uri = URI->new( $_[0] ); unless ( defined($uri) ) { $self->logger->warn ( "The game_uri method thinks that this doesn't look like a URI: $_[0]" ); return; } $self->game_uri_object($uri); } else { $self->logger->warn ( "You must call game_uri() with either a URI string, or a URI-class object." ); return; } } return $self->game_uri_object->as_string if defined( $self->game_uri_object ); } ############################## # Security methods ############################## # These methods all deal with the attached signature somehow. # confirm_record_owner: Make sure that the stored copy of this record agrees # with what this record asserts is its parlor, and that the record's signature # is valid. This is a necessary step before performing an SQL UPDATE on this # record's DB entry, lest stupid/evil parlors stomp other parlors' records. sub confirm_record_owner { my $self = shift; unless ( $self->id ) { $self->logger->warn( "This record has no ID, and thus no owner at all. You shouldn't have called confirm_record_owner on it!" ); return 0; } return $self->verify_signature; } =item sign IGenerates a signature for this record, and attaches it. The signature is based on a specific subset of the record's information, which both sender and receiver agree upon. Refer to the Volity protocol documentation for more information on this procedure. =cut # sign: generate a signature based on the serialized version of this record, # and sign the sucker. sub sign { my $self = shift; my $serialized = $self->serialize; unless ($serialized) { $self->logger->warn( "Not signing, because I couldn't get a good serialization of this reciord." ); return; } unless ( $self->check_gpg_attributes ) { $self->logger->warn( "The sign() method was called on a game record, but the GPG attrubutes aren't properly set on the Volity::GameRecord class." ); return; } # XXX Very hacky, but good enough for now. my $filename = "/tmp/volity_record_$$"; unless ( open( SERIALIZED, ">$filename" ) ) { $self->expire("Can't write to $filename: $!"); } print SERIALIZED $serialized; close(SERIALIZED) or $self->expire("Could not close $filename: $!"); my $out_filename = "/tmp/volity_signature_$$"; my $gpg_command = sprintf( "%s --no-tty --default-key %s -sba --passphrase-fd 0 --yes --output $out_filename $filename", $gpg_bin, $gpg_secretkey ); open( GPG, "|$gpg_command" ) or $self->expire( "Can't open a pipe into the gpg command: $!\nCommand was: $gpg_command" ); print GPG $gpg_passphrase . "\n"; close(GPG) or $self->expire("Couldn't close gpg command pipe: $!"); open( SIG, $out_filename ) or $self->expire("Can't read $out_filename: $!"); # local $/ = undef; my $signature = ; my $signature = ''; while () { $signature .= $_ } close(SIG) or $self->expire("Can't close $out_filename: $!"); # Clean up our messy mess... foreach ( $filename, $out_filename ) { unlink($_) or $self->expire("Couldn't unlink $_: $!"); } # Finally, attach the signature to the object. # XXX HACK, to get around apparent bug where P::F::X::N strips newlines $signature =~ s/\n/==NEWLINE==/g; $self->signature($signature); return $signature; } =item verify Verifies that the record is signed, and that said signature is valid. Returns truth if everything looks OK, and falsehood otherwise. =cut sub verify { my $self = shift; unless ( defined($gpg_bin) ) { $self->logger->warn( "Can't verify the record, because the path to the GPG binary isn't set!" ); return; } unless ( defined( $self->signature ) ) { $self->logger->warn( "Can't verify the record, because there doesn't appear to be a signature attached to this record!!" ); return; } my $serialized = $self->serialize; unless ( defined($serialized) ) { $self->logger->warn( "Can't verify this record, since it won't serialize."); return; } # XXX Very hacky, but good enough for now. my $serialized_filename = "/tmp/volity_record_$$"; open( SERIALIZED, ">$serialized_filename" ) or $self->expire("Can't write to $serialized_filename: $!"); print SERIALIZED $serialized; close(SERIALIZED) or $self->expire("Could not close $serialized_filename: $!"); my $signature_filename = "/tmp/volity_signature_$$"; open( SIGNATURE, ">$signature_filename" ) or $self->expire("Can't write to $signature_filename: $!"); print SIGNATURE $self->signature; close(SIGNATURE) or $self->expire("Could not close $signature_filename: $!"); my $gpg_command = $gpg_bin . " --verify $signature_filename $serialized_filename"; my $result = system($gpg_command); # Clean up my messy mess. foreach ( $signature_filename, $serialized_filename ) { unlink($_) or $self->expire("Can't unlink $_: $!"); } if ($result) { return 0; } else { return 1; } } sub check_gpg_attributes { my $self = shift; foreach ( $gpg_bin, $gpg_secretkey, $gpg_passphrase, ) { unless ( defined($_) ) { $self->logger->warn( "You can't perform GPG actions unless you set all three package variables on Volity::Gamerecord: \$gpg_bin, \$gpg_secretkey and \$gpg_passphrase." ); return 0; } } return 1; } =item unsign Removes the signature from the record, if it has one. =cut # unsign: toss out the key. Just a hey-why-not synonym. sub unsign { my $self = shift; return $self->signature(undef); } # serialize: return a string that represents a signable (and, after sending, # verifyable version of this record. Fails if the record lacks certain # information. # XXX For now, it just returns the end_time timestamp!! It will be more # complex when the Volity standard for this is made. sub serialize { my $self = shift; if ( defined( $self->end_time ) ) { return $self->end_time; } else { $self->logger->warn( "This record lacks the information needed to serialize it!"); return; } } ############################## # Data verification methods ############################## sub set { my $self = shift; my ( $field, @values ) = @_; # XXX This needs to do more JID-checking. if ( $field eq 'quitters' ) { foreach (@values) { $_ = $self->massage_jid($_); } } # We will massage time fields to make sure that they are in W3C format, # and just to be nice we'll set them to GMT. elsif ( $field eq 'start_time' or $field eq 'end_time' ) { $values[0] = $self->massage_time( $values[0] ); } return $self->SUPER::set( $field, @values ); } sub massage_jid { my $self = shift; my ($jid) = @_; if ( $jid =~ /^([\w-]+@[\w-]+(?:\.[\w-]+)*)(?:\/([\w-]+))?/ ) { my ( $main_jid, $resource ) = ( $1, $2 ); return $main_jid; } else { $self->logger->warn("This does not look like a valid JID: $jid"); } } sub massage_time { my $self = shift; my ($time) = @_; my $formatter = DateTime::Format::W3CDTF->new; # Cure possible MySQLization by inserting a 'T'. Instant W3C format! $time =~ s/^(\d\d\d\d-\d\d-\d\d) (\d\d:\d\d:\d\d)$/$1T$2/; my $dt; # DateTime object eval { $dt = $formatter->parse_datetime($time); }; if ($@) { $self->logger->warn( "I can't parse this timestamp: $time\nThat was not valid W3C time format." ); return; } # Set the time zone to GMT, and return the resulting string. $dt->set_time_zone('GMT'); my $massaged_time = $formatter->format_datetime($dt); return $massaged_time; } ######################### # RPC param prep ######################### =item render_as_hashref Returns an unblessed hash reference describing the game record. It just so happens that this hash reference is in the very same format that the Volity C RPC request requires as its EsructE argument. Fancy that! =cut sub render_as_hashref { my $self = shift; my $hashref = {}; # First, directly copy some fields from the object into the hashref... foreach ( qw( id winners start_time end_time parlor signature finished seats ) ) { $$hashref{$_} = $self->{$_} if defined( $self->$_ ); } # ...then define some others based on the results of method calls. foreach ( qw( game_uri ) ) { $$hashref{$_} = $self->$_ if defined( $self->$_ ); } return $hashref; } # This here's a class method... sub new_from_hashref { my $class = shift; my ($hashref) = @_; # XXX HACK, to get around apparent bug where P::F::X::N strips newlines if ($$hashref{signature}) { $$hashref{signature} =~ s/==NEWLINE==/\n/g; } return $class->new($hashref); } =back =head1 AUTHOR Jason McIntosh =head1 COPYRIGHT Copyright (c) 2003-2006 by Jason McIntosh. =cut 1;