AMF-Connection

 view release on metacpan or  search on metacpan

README  view on Meta::CPAN

AMF-Connection
==============

A simple module to write AMF [1] clients to invoke remote services as used by Flex/AIR RIAs.

The module includes basic support for synchronous HTTP/S based RPC request-response access, where 
the client sends a request to the server to be processed and the server returns a response to the client
containing the processing outcome. Data is sent back and forth in AMF binary format (AMFChannel). Other access patterns
such as pub/sub and channels transport are out of scope of this inital release.

[1] http://en.wikipedia.org/wiki/Action_Message_Format

See public github repo http://github.com/areggiori/AMF-Connection

INSTALLATION

To install this module type the following:

   perl Makefile.PL

examples/amfclient.pl  view on Meta::CPAN

  {
    no strict 'refs';

    # blessed hash object to JSON object
    map
      {
        my $amf_class = $_;
        my $foo = $amf_class."::TO_JSON";

        # unbless object
        *$foo = sub {
            my $f = $_[0];

            #process_amf_object ($f, $amf_class);

            +{ %{$f} };
          }
      } (
          # add your own remote service classes here - or use an SWFDecompiler

          'flex.messaging.messages.AcknowledgeMessage'
        );

    # blessed hash object to JSON array
    map
      {
        my $foo = $_."::TO_JSON";
        # unbless
        *$foo = sub {
            $_[0]->{'externalizedData'};
          }
      } (
          'flex.messaging.io.ArrayCollection'
        );
  }

my $endpoint = 'http://swxformat.org/php/amf.php';
my $service = 'Twitter';
my $method = 'search';

examples/get-brightcove-videos-metadata.pl  view on Meta::CPAN

  {
    no strict 'refs';

    # blessed hash object to JSON object
    map
      {
        my $amf_class = $_;
        my $foo = $amf_class."::TO_JSON";

        # unbless object
        *$foo = sub {
            my $f = $_[0];

            #process_amf_object ($f, $amf_class);

            +{ %{$f} };
          }
      } (
          # add your own remote service classes here - or use an SWFDecompiler

          'com.brightcove.templating.SecondaryContentDTO',

examples/get-brightcove-videos-metadata.pl  view on Meta::CPAN

	  'com.brightcove.utils.BrightcoveDateDTO',
	  'com.brightcove.catalog.TagDTO',
	  'com.brightcove.catalog.VideoCuePointDTO'
        );

    # blessed hash object to JSON array
    map
      {
        my $foo = $_."::TO_JSON";
        # unbless
        *$foo = sub {
            $_[0]->{'externalizedData'};
          }
      } (
          'flex.messaging.io.ArrayCollection'
        );
  }

my $endpoint = 'http://c.brightcove.com/services/amfgateway';
my $service = 'com.brightcove.templating.TemplatingFacade';
my $method = 'getContentForTemplateInstance';

lib/AMF/Connection.pm  view on Meta::CPAN

$HASUUID = ($@) ? 0 : 1 ;
}

our $HAS_LWP_PROTOCOL_SOCKS;
{
local $@;
eval { require LWP::Protocol::socks };
$HAS_LWP_PROTOCOL_SOCKS = ($@) ? 0 : 1 ;
}

sub new {
	my ($proto, $endpoint) = @_;
        my $class = ref($proto) || $proto;

	my $self = {
		'endpoint' => $endpoint,
		'headers' => [],
		'http_headers' => {},
		'http_cookie_jar' => new HTTP::Cookies(),
		'response_counter' => 0,
		'encoding' => 0, # default is AMF0 encoding

lib/AMF/Connection.pm  view on Meta::CPAN

	$self->{'ua'}->cookie_jar( $self->{'http_cookie_jar'} );

        return bless($self, $class);
	};

# plus add paramters, referer, user agent, authentication/credentials ( see also SecureAMFChannel stuff ), 
# plus timezone on retunred dates to pass to de-serializer - see AMF3 spec saying "it is suggested that time zone be queried independnetly as needed" - unelss local DateTime default to right locale!

# we pass the string, and let Storable::AMF to parse the options into a scalar - see Input/OutputStream and Storable::AMF0 documentation

sub setInputAMFOptions {
	my ($class, $options) = @_;

	$class->{'input_amf_options'} = $options;
	};

sub setOutputAMFOptions {
	my ($class, $options) = @_;

	$class->{'output_amf_options'} = $options;
	};

# useful when input and output options are the same
sub setAMFOptions {
	my ($class, $options) = @_;

	$class->setInputAMFOptions ($options);
	$class->setOutputAMFOptions ($options);
	};

sub getInputAMFOptions {
	my ($class) = @_;

	return $class->{'input_amf_options'};
	};

sub getOutputAMFOptions {
	my ($class) = @_;

	return $class->{'output_amf_options'};
	};

sub setEndpoint {
	my ($class, $endpoint) = @_;

	$class->{'endpoint'} = $endpoint;
	};

sub getEndpoint {
	my ($class) = @_;

	return $class->{'endpoint'};
	};

sub setHTTPProxy {
	my ($class, $proxy) = @_;

	if(	($proxy =~ m!^socks://(.*?):(\d+)!) &&
		(!$HAS_LWP_PROTOCOL_SOCKS) ) {
		croak "LWP::Protocol::socks is required for SOCKS support";
		};

	$class->{'http_proxy'} = $proxy;

	$class->{'ua'}->proxy( [qw(http https)] => $class->{'http_proxy'} );
	};

sub getHTTPProxy {
	my ($class) = @_;

	return $class->{'http_proxy'};
	};

sub setEncoding {
	my ($class, $encoding) = @_;

	croak "Unsupported AMF encoding $encoding"
		unless( $encoding==0 or $encoding==3 );

	$class->{'encoding'} = $encoding;
	};

sub getEncoding {
	my ($class) = @_;

	return $class->{'encoding'};
	};

sub addHeader {
	my ($class, $header, $value, $required) = @_;

	if( ref($header) ) {
		croak "Not a valid header $header"
			unless( $header->isa("AMF::Connection::MessageHeader") );
	} else {
		$header = new AMF::Connection::MessageHeader( $header, $value, ($required==1) ? 1 : 0  );
		};

	push @{ $class->{'headers'} }, $header;
	};

sub addHTTPHeader {
	my ($class, $name, $value) = @_;

	$class->{'http_headers'}->{ $name } = $value ;
	};

sub setUserAgent {
	my ($class, $ua) = @_;

	croak "Not a valid User-Agent $ua"
		unless( ref($ua) and $ua->isa("LWP::UserAgent") and $ua->can("post") );

	# the passed UA might have a different agent and cookie jar settings
	$class->{'ua'} = $ua;

	# make sure we set the proxy if was already set
	# NOTE - we do not re-check SOCKS support due we assume the setHTTPProxy() was called earlier
	$class->{'ua'}->proxy( [qw(http https)] => $class->{'http_proxy'} )
		if( exists $class->{'http_proxy'} and defined $class->{'http_proxy'} );

	# copy/pass over cookies too
	$class->{'ua'}->cookie_jar( $class->{'http_cookie_jar'} );
	};

sub setHTTPCookieJar {
	my ($class, $cookie_jar) = @_;

	croak "Not a valid cookies jar $cookie_jar"
		unless( ref($cookie_jar) and $cookie_jar->isa("HTTP::Cookies") );

	# TODO - copy/pass over the current cookies (in-memory by default) if any set
	$class->{'http_cookie_jar'}->scan( sub { $cookie_jar->set_cookie( @_ ); } );

	$class->{'http_cookie_jar'} = $cookie_jar;

	# tell user agent to use new cookie jar
        $class->{'ua'}->cookie_jar( $class->{'http_cookie_jar'} );
	};

sub getHTTPCookieJar {
        my ($class) = @_;
		
	return $class->{'http_cookie_jar'};
	};

# send "flex.messaging.messages.RemotingMessage"

sub call {
	my ($class, $operation, $arguments, $destination) = @_;

	my @call = $class->callBatch ({ "operation" => $operation,
					"arguments" => $arguments,
					"destination" => $destination });

	return (wantarray) ? @call : $call[0];
	};

sub callBatch {
	my ($class, @batch) = @_;

	my $request = new AMF::Connection::Message;
	$request->setEncoding( $class->{'encoding'} );

	# add AMF any request headers
	map { $request->addHeader( $_ ); } @{ $class->{'headers'} };

	# TODO - prepare HTTP/S request headers based on AMF headers received/set if any - and credentials

lib/AMF/Connection.pm  view on Meta::CPAN

	$class->_process_response_headers( $response );

	my @all = @{ $response->getBodies() };

	# we make sure the main response is always returned first
	return (wantarray) ? @all : $all[0];
	};

# TODO
#
# sub command { } - to send "flex.messaging.messages.CommandMessage" instead
#

sub setCredentials {
	my ($class, $username, $password) = @_;

	$class->addHeader( 'Credentials', { 'userid' => $username,'password' => $password }, 0 );
	};


sub _process_response_headers {
	my ($class,$message) = @_;

	foreach my $header (@{ $message->getHeaders()}) {
		if($header->getName eq 'ReplaceGatewayUrl') { # another way used by server to keep cookies-less sessions
			$class->setEndpoint( $header->getValue )
				unless( ref($header->getValue) );
		} elsif($header->getName eq 'AppendToGatewayUrl') { # generally used for cokies-less sessions E.g. ';jsessionid=99226346ED3FF5296D08146B02ECCA28'
			$class->{'append_to_endpoint'} = $header->getValue
				unless( ref($header->getValue) );
			};
		};
	};

# just an hack to avoid rewrite class mapping local-to-remote and viceversa and make Storable::AMF happy
sub _brew_flex_remoting_message {
	my ($class,$source,$operation,$headers,$body,$destination) = @_;

	return bless( {
		'clientId' => _generateID(),
                'destination' => $destination,
                'messageId' => _generateID(),
                'timestamp' => time() . '00',
                'timeToLive' => 0,
                'headers' => ($headers) ? $headers : {},
                'body' => $body,
                'correlationId' => undef,
                'operation' => $operation,
		'source' => $source # for backwards compatibility - google for it!
                 }, 'flex.messaging.messages.RemotingMessage' );
        };

sub _generateID {
        my $uniqueid;

        if($HASUUID) {
                eval {
                        my $ug = new Data::UUID;
                        $uniqueid = $ug->to_string( $ug->create() );
                        };
        } elsif ($HASMD5) {
                eval {
                        $uniqueid = substr(Digest::MD5::md5_hex(Digest::MD5::md5_hex(time(). {}. rand(). $$)), 0, 32);

lib/AMF/Connection.pm  view on Meta::CPAN

  my @response = $client->callBatch ( { "operation" => $service.$method", "arguments" => \@params }, ... );

=head1 DESCRIPTION

I was looking for a simple Perl module to automate data extraction from an existing Flash+Flex/AMS application, and I could not find a decent client implementation. So, this module was born based on available online documentation.

This module has been inspired to SabreAMF PHP implementation of AMF client libraries.

AMF::Connection is meant to provide a simple AMF library to write client applications for invocation of remote services as used by most flex/AIR RIAs. 

The module includes basic support for synchronous HTTP/S based RPC request-response access, where the client sends a request to the server to be processed and the server returns a response to the client containing the processing outcome. Data is sent...

AMF0 and AMF3 support is provided using the Storable::AMF module. While HTTP/S requestes to the AMF endpoint are carried out using the LWP::UserAgent module. The requests are sent using the HTTP POST method as AMF0 encoded data by default. AMF3 encod...

If encoding is set to AMF3 the Flex Messaging framework is used on returned responses content (I.e. objects casted to "flex.messaging.messages.AcknowledgeMessage" and "flex.messaging.messages.ErrorMessage" are returned).

Simple batch requests and responses is provided also.

See the sample usage synopsis above to start using the module.

=head1 DATE TYPE SUPPORT

lib/AMF/Connection/InputStream.pm  view on Meta::CPAN

eval "use Storable::AMF3 0.84";
if ($@)
  {
    $storable_with_options = 0;
  }
else
  {
    $storable_with_options = 1;
  }

sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;

	my ($stream, $storable_amf_options) = @_;

	croak "Input stream must be a valid string"
		if(ref($stream));
	
	my $self = {
		'stream' => $stream,

lib/AMF/Connection/InputStream.pm  view on Meta::CPAN

	    if ($Storable::AMF::VERSION < 0.84)
	      {
	        croak "Storable::AMF 0.84 or newer needed to set stream options\n";
	      }
	    $self->{'options'} = Storable::AMF::parse_option ($storable_amf_options);
	  }

	return bless($self, $class);
	};

sub readBuffer {
	my ($class, $length) = @_;

	croak "Buffer underrun at position: ". $class->{'cursor'} . ". Trying to fetch ". $length . " bytes from buffer total length ".length($class->{'stream'})
		if($length + $class->{'cursor'} > length($class->{'stream'}));

        my $data = substr($class->{'stream'},$class->{'cursor'},$length);
	$class->{'cursor'}+=$length;
	
	return $data;
	};

sub readByte {
	my ($class) = @_;

	return ord($class->readBuffer(1));
	};

sub readInt {
	my ($class) = @_;

	my $block = $class->readBuffer(2);
	my @int = unpack("n",$block);

	return $int[0];
	};

sub readDouble {
	my ($class) = @_;

	my $double = $class->readBuffer(8);

	my @testEndian = unpack("C*",pack("S*",256));
        my $bigEndian = !$testEndian[1]==1;
        $double = reverse($double)
                if($bigEndian);
        my @double = unpack("d",$double);

        return $double[0];
	};

sub readLong {
	my ($class) = @_;

	my $block = $class->readBuffer(4);
	my @long = unpack("N",$block);

        return $long[0];
	};

# deparse out the next avail AMF entity
# TODO - make sure ref counts are reset/preserved between calls in the scope of the same InputStream - study Storable::AMF API
sub readAMFData {
	my ($class) = @_;

	my $type = $class->readByte();

	# Storable::AMF will take care of deparsing the right AMF format
	$class->{'cursor'}--;

	local $@ = undef;

        my ($obj, $len);

lib/AMF/Connection/Message.pm  view on Meta::CPAN


use strict;
use Carp;

use AMF::Connection::OutputStream;
use AMF::Connection::InputStream;

use AMF::Connection::MessageBody;
use AMF::Connection::MessageHeader;

sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;
	
	my $self = {
		'encoding' => 0, # default is AMF0 encoding
		'bodies' => [],
		'headers' => []
		};

	return bless($self, $class);
	};

sub serialize {
	my ($class, $stream) = @_;

	croak "Stream $stream is not a valid output stream"
		unless(ref($stream) and $stream->isa("AMF::Connection::OutputStream"));

	# we default to AMF0 encoding
	$stream->writeByte(0x00);
	$stream->writeByte($class->getEncoding());

	$stream->writeInt(scalar(@{$class->{'headers'}}));

lib/AMF/Connection/Message.pm  view on Meta::CPAN

		my $response = $body->getResponse();
		$stream->writeInt(length($response));
		$stream->writeBuffer($response);

		$stream->writeLong(-1);
		$stream->writeAMFData( $class->getEncoding(), $body->getData() );
		};

	}; 

sub deserialize {
	my ($class, $stream) = @_;

	$class->{'headers'} = [];
	$class->{'bodies'} = [];

        $stream->readByte();

        my $sent_encoding = $stream->readByte();
	# need to make AMF1 returned encoding the same as AMF0 - see more about the bug at http://balazs.sebesteny.com/footprints-in-blazeds/
        $class->setEncoding( ( $sent_encoding!=0 and $sent_encoding!=3 ) ? 0 : $sent_encoding );

lib/AMF/Connection/Message.pm  view on Meta::CPAN

		$body->setResponse( $stream->readBuffer($strLen) );

		# TODO - make sure we deal properly with avm+ object marker stuff here - and have message containing multiple encodings
                $stream->readLong();
		$body->setData( $stream->readAMFData() ); # we deparse the next read value out

                $class->addBody( $body );
		};
	}; 

sub addBody {
	my ($class, $body) = @_;

	croak "Body $body is not a valid message body"
		unless(ref($body) and $body->isa("AMF::Connection::MessageBody"));

	push @{ $class->{'bodies'} }, $body;
	};

sub addHeader {
	my ($class, $header) = @_;

	croak "Header $header is not a valid message header"
		unless(ref($header) and $header->isa("AMF::Connection::MessageHeader"));

	push @{ $class->{'headers'} }, $header;
	};

sub getHeaders {
	my ($class) = @_;

	return $class->{'headers'};
	};

sub getBodies {
	my ($class) = @_;

	return $class->{'bodies'};
	};

sub setEncoding {
        my ($class, $encoding) = @_;

	croak "Unsupported AMF encoding $encoding"
		unless( $encoding==0 or $encoding==3 );

        $class->{'encoding'} = $encoding;
        };

sub getEncoding {
        my ($class) = @_;

        return $class->{'encoding'};
        };


1;
__END__

=head1 NAME

lib/AMF/Connection/MessageBody.pm  view on Meta::CPAN

package AMF::Connection::MessageBody;

use strict;
use Carp;

sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my ($target,$response,$data) = @_;
	
	my $self = {
		'target' => $target,
		'response' => $response,
		'data' => $data # we might want to have some kind of mapper between remote objects and local / user registered ones
		};

	return bless($self, $class);
	};

sub setTarget {
	my ($class, $target) = @_;

	$class->{'target'} = $target;
	};

sub getTarget {
	my ($class) = @_;

	return $class->{'target'};
	};

sub setResponse {
	my ($class, $response) = @_;

	$class->{'response'} = $response;
	};

sub getResponse {
	my ($class) = @_;

	return $class->{'response'};
	};

sub setData {
	my ($class, $data) = @_;

	$class->{'data'} = $data;
	};

sub getData {
	my ($class) = @_;

	return $class->{'data'};
	};

# HTTP::Response-ish methods ...

sub is_error {
	my ($class) = @_;

	return ($class->{'target'} =~ m|onStatus|) ? 1 : 0 ;
	};

sub is_success {
	my ($class) = @_;

	return ($class->{'target'} =~ m|onResult|) ? 1 : 0 ;
	};

sub is_debug {
	my ($class) = @_;

	return ($class->{'target'} =~ m|onDebug|) ? 1 : 0 ;
	};

1;
__END__

=head1 NAME

lib/AMF/Connection/MessageHeader.pm  view on Meta::CPAN

package AMF::Connection::MessageHeader;

use strict;
use Carp;

sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;
	my ($name, $value, $required) = @_;
	
	my $self = {};

	$self->{'name'} = $name;

	$self->{'value'} = $value;

	$self->{'required'} = ($required=~m/(1|yes)/) ? 1 : 0;

	return bless($self, $class);
	};

sub isRequired {
	my ($class) = @_;

	return $class->{'required'};
	};

sub getName {
	my ($class) = @_;

	return $class->{'name'};
	};

sub getValue {
	my ($class) = @_;

	return $class->{'value'};
	};

sub setName {
	my ($class, $name) = @_;

	$class->{'name'} = $name;
	};

sub setValue {
	my ($class, $value) = @_;

	$class->{'value'} = $value;
	};

sub setRequired {
	my ($class, $required) = @_;

	$class->{'required'} = ($required) ? 1 : 0 ;
	};


1;
__END__

=head1 NAME

lib/AMF/Connection/OutputStream.pm  view on Meta::CPAN

eval "use Storable::AMF3 0.84";
if ($@)
  {
    $storable_with_options = 0;
  }
else
  {
    $storable_with_options = 1;
  }

sub new {
	my $proto = shift;
	my $class = ref($proto) || $proto;

 	my ($storable_amf_options) = @_;
	
	my $self = {
		'stream' => ''
		};

	if (defined $storable_amf_options)

lib/AMF/Connection/OutputStream.pm  view on Meta::CPAN

            if ($Storable::AMF::VERSION < 0.84)
              {
                croak "Storable::AMF 0.84 or newer needed to set stream options\n";
              }
            $self->{'options'} = Storable::AMF::parse_option ($storable_amf_options);
          }

	return bless($self, $class);
	};

sub writeBuffer {
	my ($class, $str) = @_;

	$class->{'stream'}.=$str;
	};

sub writeByte {
	my ($class, $byte) = @_;

	$class->{'stream'}.=pack("c",$byte);
	};

sub writeInt {
	my ($class, $int) = @_;

	$class->{'stream'}.=pack("n",$int);
	};

sub writeDouble {
	my ($class, $double) = @_;

        my $bin = pack("d",$double);
        my @testEndian = unpack("C*",pack("S*",256));
        my $bigEndian = !$testEndian[1]==1;
        $bin = reverse($bin)
                if ($bigEndian);

	$class->{'stream'}.=$bin;
	};

sub writeLong {
	my ($class, $long) = @_;

	$class->{'stream'}.=pack("N",$long);
	};

sub getStreamData {
	my ($class) = @_;
	
	return $class->{'stream'};
	};

# wrtie an AMF entity
sub writeAMFData {
        my ($class,$encoding,$data) = @_;

	local $@ = undef;

	my $bytes;
        if($encoding == 3 ) {
		if ($storable_with_options  == 0
		    || not defined $class->{'options'})
                  {
		    $bytes = Storable::AMF3::freeze($data);



( run in 0.320 second using v1.01-cache-2.11-cpan-4d50c553e7e )