Business-OCV
view release on metacpan or search on metacpan
package OCV;
# Ingenico Online Credit Verification Server interface
######################################################################
#
# UNSW Online Payment System (OPS) Server
# (c) 1999/2000 University of NSW
#
# Written by Benjamin Low <ben@snrc.uow.edu.au>
#
#
# OVERVIEW OF THE OCV "PROTOCOL"
#
# The OCV 'protocol' is generally a command-response message exchange,
# initiated by the client. That is, a request message is sent to the
# server and the server returns an appropriate response message, after
# some delay.
#
# An exception to the command-response sequence is for certain message
# types where a "polled mode" is available. When in "polled mode" the
# server does not send a response and the client must poll the server to
# ascertain the transaction status. Note that this "polled mode" does not
# mean the socket is non-blocking, just the application (i.e. "polled mode"
# simply determines whether the client should expect a response). Whenever
# a response is expected, this client waits for the response up to a
# given timeout period before returning an error to the application. When
# such an error occurs, the communications channel is in an undefined state
# and should be reset (see the OCV::reset method).
#
# The OCV 'protocol' is stateless, that is all messages contain
# sufficient information to fully determine the request. Thus you may
# connect and disconnect to the OCV server at will. For a given client
# (account), some of the required information will be constant (e.g.
# account number, client id). This module is intended to be used as one
# OCV object instance per client/acount: it "wraps" the protocol such
# that the client ID, account number (and transaction ID as required) are
# automatically supplied for each request and the application only need
# supply the 'per-transaction' date (e.g. card details, amount).
#
# The OCV protocol is STREAM based (i.e. TCP/IP). Thus this client
# conceivably may not send nor receive messages atomically - messages
# may be fragmented across reads (and writes). At present, this is not
# accounted for, and will result in an error.
# TODO There is some guarantee regarding minimum block sizes, around 8k
# as I recall. I need to check this. The implications are that under
# high load (when buffers back up), we may start getting incomplete
# messages.
#
# Future Work:
# - determine message fragmentation issue
# - split out networking into a separate object, so objects for
# multiple clients/accounts can share connections to the server
#
# THIS OCV MODULE
#
# This module provides an object-oriented means of transacting with the
# OCV server. An 'OCV' object is used to converse with the server, it
# provides a method corresponding to each of the OCV message types.
# Transaction results (RESPONSEs) are returned as OCV::Message objects.
# The OCV::Message object is simply a blessed array reference, so you can
# use it as you would a simple array ref (e.g. $m->[3] to get the fourth
# element). However, the OCV::Message object provides by-name access to
# its contents so you can say $m->Result without concerning yourself with
# which array element is the Result field.
#
# [There is a small performance penalty for using the OCV::Message
# interface - benchmarking a simplified version of the ::Message object
# shows it takes approx. ~60-70% longer than using a plain array
# (reference) (a bit over half of that seems to be in the method call to
# the constructor). There may also be a memory usage penalty, but I've
# no easy way of checking that... Given that we're talking on the order
# of milliseconds (and perhaps bytes/kb), I've chosen to ignore this
# overhead - internally, all messages are contained in OCV::Message
# objects.]
#
# Transaction messages (i.e. purchase, refund, etc - those messages
# involving the bank) carry all required information in the message. In
# particular, each Transaction message contains the Account number to use,
# which in turn maps within the OCV to a given Merchant ID. This module
# is designed to have one OCV object instance for each 'client', and thus
# the Account number and Client ID are required in the constructor and
# automatically supplied with each message. (They can be overridden per
# request if required).
#
# BTW, most of the terminology used herein is based on that contained in
# the OCV Developer's Specification. Personally, I find some of the terms
# less than clear, however for the sake of consistancy I have followed the
# spec, attempting to clarify any ambiguities in my own terms.
#
# LOGGING
#
# An log of each 'transaction' message exchange with the OCV server is
# written to a file given in the object constructor ("TxnLog" parameter).
# The fields in this log are separated by a configurable character, the
# default is ',' (warning: there is no 'safe' character guaranteed to not be
# contained in an OCV message, so keep an eye out for "extra" fields :-).
# In addition to this transaction log, there is a server debug log ("DebugLog").
# The debug log is used to dump pretty well all the 'raw' data sent and
# receieved to/from the OCV server, plus other odds and sods. Debugging is
# turned off and on via the 'debug' constructor argument and debug() method,
# and is off by default. NOTE that the debug flag also controls general
# program debugging via carp (to recap, the debug log is for OCV interactions,
# STDERR (via carp) is used for general program debugging).
#
# The logreopen() method will reopen the log file/s, which you might
# want to do in response to a signal (also see reset()).
#
# WARNING: when debugging is turned on sensitive data could potentially
# be disclosed (e.g. card data within a purchase transaction message).
# To prevent such data being logged, the logdebug message filters it's
$self->logdebug('Closing connection');
$@ = "no IO object", return undef
unless $self->{'io'};
$self->{'sel'}->remove($self->{'io'}); # remove handle from IO::Select
$@ = "could not close connection: $!", return undef
unless $self->{'io'}->close();
$self->{'disconnected'} = 1;
return 1;
}
sub ping
# try and confirm the server connection is alive
{
my $self = shift;
$@ = "not connected", return undef unless $self->{'io'}->connected;
# there isn't an OCV 'noop' command, use a simple stats request
# - result should be a statistics array, or error
return ($self->statistics(SubCode => STATS_PERMANENT));
}
sub DESTROY
{
my $self = shift;
# sometimes the IO and other 'sub-objects' seem to have been cleaned up
# TODO - figure out why
#warn "$self = \n",
# map {my $s = $self->{$_} || '-'; $s =~ s/[\x00-\x1f\x7f-\xff]/?/g;
# "\t$_ => $s\n"} keys %{$self};
{
local $^W = 0; # ignore IO::Socket warnings
$self->disconnect(@_) if (!$self->{'disconnected'} and
$self->{'io'} and $self->{'io'}->connected);
}
}
sub open { shift-> connect(@_); }
sub close { shift->disconnect(@_); }
sub flush
# try and resynchronise the connection by dumping all pending input
# - probably better to close and (re-)open (see reset method)
{
my $self = shift;
my $buf;
while ($self->{'sel'}->can_read(0) and $self->{'io'}->sysread($buf, 8192))
{
$self->logdebug("flush: discarding [$buf]");
}
"\000"; # true, but "silent" (mainly for the ocv command line util)
}
sub _send
# assumes data is not fragmented
{
my $self = shift;
$@ = "send: not connected", return undef unless $self->{'io'}->connected;
$@ = "send: timeout", return undef
unless $self->{'sel'}->can_write($self->{'timeout'});
# see logdebug() re. logging of sensitive data
$self->logdebug(sprintf("send: %3d [%s]", length($_[0]), $_[0]));
my $r;
eval
{
local $SIG{__WARN__} = 'IGNORE';
local $SIG{ALRM} = sub { die "timeout\n" };
alarm ($self->{'timeout'});
$r = $self->{'io'}->syswrite($_[0], length($_[0]));
alarm (0);
};
chomp ($@), $@ = "send: syswrite: $@", return undef if $@;
$@ = "send: error: $!", return undef unless defined($r);
return $r;
}
sub _recv
# arguments (buf, len): reads len bytes into buf
# assumes data is not fragmented - i.e. if we ask for N bytes, we get N bytes,
# or an error
# - I don't do a dual-read (i.e. read header, extract message length, read
# the rest of the message). I couldn't see the point: once the message
# exchange sequence is messed up, I can no longer trust it.
{
my $self = shift;
$@ = "recv: not connected", return undef unless $self->{'io'}->connected;
$@ = "recv: timeout", return undef
unless $self->{'sel'}->can_read($self->{'timeout'});
my $r;
eval
{
# why do I always feel queasy when it comes to signals under perl :-)
local $SIG{__WARN__} = 'IGNORE';
local $SIG{ALRM} = sub { die "timeout\n" };
alarm ($self->{'timeout'});
$r = $self->{'io'}->sysread($_[0], $_[1]);
alarm (0);
};
chomp ($@), $@ = "recv: sysread: $@", return undef if $@;
$self->logdebug(sprintf("recv: %3d/%3d [%s]", length($_[0]), $_[1],
$_[0]));
$@ = "recv: error: $!", return undef unless defined($r);
# if I listened for something, it's because I was expecting something
# - fail on EOF
#$@ = "recv: end of file", return undef unless length($_[0]);
$@ = "recv: end of file", return undef unless $r;
$@ = "recv: short read: wanted [$_[1]], got [$r]", return undef
unless $r == $_[1];
return $r;
}
sub _message
# send a message, and receive a response if required
# - returns the message response as an OCV::Message object
# - if the Request(Response) message is undef, no data is sent (received)
# - this is useful for "partial" messages (see totals() for example)
# - if no response is required, simply returns true on success
# - consistancy check: if $check is != 0, the ClientID of the recieved
# message is compared against the args ->{ClientID} and any mismatch
# flagged as an error
# - the default is to check all tx/rx sequences, but not to check
# if we're just receiving (i.e. partial receives)
# - returns undef on error
{
my ($self, $args, $mtx, $mrx, $check) = @_;
# consistancy check: nothing to check if we're just receiving
$check = defined($mtx) unless defined $check;
$@ = "unknown Request message [$mtx]", return undef
if (defined($mtx) and not exists $Requests{$mtx});
$@ = "unknown Response message [$mrx]", return undef
( run in 0.916 second using v1.01-cache-2.11-cpan-b16cb0d3907 )