Monitoring communication between a client and a server

Tap.pl is one of the oldest scripts I’ve written that I still use, and I still use it regularly.  Some of the other scripts I will share depend on this one, so it seems like a good place to start.

At its heart, it is incredibly simple: Receive a message, keep a copy and pass it on.

That’s all it does, and that’s all it needs to do to be useful.

Trying to debug a server, or a client? Can’t do it if you can’t see what they’re sending and receiving. A well written process will of course have an option to log everything it sends and receives, but so many programs are not well written. And because this program is so dumb, you can trust it.   It doesn’t have any magic options that treat some messages differently – unless you want it to…

Usage:

tap.pl remote_ip remote_port [local_port [log_file]]

remote_ip, remote_port: the host and port to connect to. No defaults provided.
local_port: the port to listen to. default: remote_port
log_file: name of log file. default: tap.log

Code for Tap.pl:

#! /usr/local/bin/perl -w
#
# (C)opyright Ben Aveling 2012, except for the bits that are taken from the 'Blue Camel book' (See http://c2.com/cgi/wiki?DefinitivePerlBooks)
# This script may be reproduced freely. 
# 
# #################
# General Behaviour
# #################
#
# This program takes a dump copy of an tcp/ip session
#
# It keeps a socket open, and any time that socket is connected to,
# another connection to a hard coded socket is opened.  Any messages
# from either are passed to the other socket, and a copy kept
#
# #######
# History 
# #######
# 2012.08.12 First published version
#
# #####
# Usage
# #####
#
my $usage = "
  tap.pl remote_ip remote_port [local_port [log_file]]

	remote_ip,remote_port: the port to connect to
	local_port: the port to listen to. default: remote_port
	log_file: name of log file. default: tap.log

  eg, to watch what happens between your browser and wordpress, run the following then point your browser at http://localhost:1080
	tap.pl benaveling.wordpress.com 80 1080
  (Hint: possibly less than you might expect)
";
# #######################################################################

#
# initialisation
#

# Tells perl to complain if it sees any code that looks dodgy
use strict;

# Load the libraries we need
require 5.002;
use Socket;
use FileHandle;

# Unbuffer standard output
$|=1;

# We will be spawning child processes. We don't want to create 'zombies' and we don't want to have to 'wait' for our children, so we 'ignore' them.  This next line does that.
$SIG{CLD} = "IGNORE";

# parse parameters

my $remote_ip_address = shift or die $usage;
my $remote_port_num = shift or die $usage;
my $local_port_num = shift or die $usage;
my $log_file = shift || "tap.log";

# open tcp/ip socket - see blue camel book pg 349

my $protocol = getprotobyname('tcp');
socket(LISTEN, PF_INET, SOCK_STREAM, $protocol)
  or die "Can't create socket: $!";
bind(LISTEN, sockaddr_in($local_port_num, INADDR_ANY))
  or die "Can't bind socket: $!";
listen(LISTEN,1)
  or die "Can't listen to socket: $!";

# log file

if( open(LOGFILE, ">>$log_file") )
{
  warn "Logging to $log_file\n";
}
else
{
  warn "Can't open $log_file: $!";
}
binmode(LOGFILE);
select(LOGFILE);$|=1;select(STDOUT);

echo("Waiting\n");

#
# Main Loop
#

my $client_paddr;

# loop forever. when a new connection arrives, spawn a child to handle it then go back to waiting
while(1)
{
  # Accept a new connection - see blue camel book
  $client_paddr = accept(CLIENT, LISTEN);
  select(CLIENT);$|=1;select(STDOUT);
  binmode(CLIENT);
  # call fork to start a new process. Fork is called once, but returns twice, returning different values to the parent and the child. The child process drops out of the while loop.
  last if ! fork();
  # The parent process closes CLIENT, since CLIENT is for the exclusive use of the child. It then goes back to the start of the while(1) loop
  close CLIENT;
}

# from here is on is all the child process

# the child process closes LISTEN because LISTEN is only for the use of the parent process
close LISTEN;

# Report to the user that a new connection has been accepted
my ($client_port, $client_iaddr) = sockaddr_in( $client_paddr );
echo(mydate(),"\nConnection accepted from ", inet_ntoa($client_iaddr), ":$client_port\n");

# Chain to whereever - see blue camel book
socket(SERVER, PF_INET, SOCK_STREAM, $protocol)
  or die "Can't create socket: $!";
my $remote_ip_aton = inet_aton( $remote_ip_address );
my $remote_port_address = sockaddr_in($remote_port_num, $remote_ip_aton )
  or die "Can't get port address: $!";
echo("Connecting to $remote_ip_address\:$remote_port_num\n");
connect(SERVER, $remote_port_address)
  or die "Can't connect to socket: $!";
select(SERVER);$|=1;select(STDOUT);
binmode(SERVER);

echo("Connection made\n");

# use $rin as a 'bit-array' - see blue camel book
my $rin = "";

# Set one bit in $rin for each filehandle we want to listen on
vec($rin, fileno(CLIENT), 1) = 1;
vec($rin, fileno(SERVER), 1) = 1;

while( 1 )
{
  # wait 0.1 seconds, to potentially allow the system to rejoin fragmented messages
  select ( undef, undef, undef, .1 );

  # check for incoming messages from client or server - see blue camel book
  my $rout = $rin;
  select( $rout, "", "", undef ) ;

  # message from client?
  if( vec($rout,fileno(CLIENT),1) )
  {
    # read from CLIENT
    sysread(CLIENT,$_,100000);

    # (0 length message means connection closed)
    if(length($_) == 0)
    { 
      echo("Client disconnected\n");
      close SERVER;
      last ;
    }
    # else, write to SERVER (and log file)
    print LOGFILE (mydate()," >>> {{{", $_, "}}} >>> (",length($_) ," bytes).\n");
    print (mydate()," >>> ",length($_) ," bytes.\n");
    print(SERVER $_) || die;
  }

  # message from server?
  if( vec($rout,fileno(SERVER),1) )
  {
    # Read from SERVER
    sysread(SERVER,$_,100000);

    # (0 length message means connection closed)
    if(length($_) == 0)
    {
      echo("Server disconnected\n");
      close CLIENT;
      last;
    }
    # else, write to CLIENT (and log file)
    print LOGFILE (mydate()," <<< {{{",$_,"}}} <<< (",length($_) ," bytes).\n");
    print (mydate()," <<< ",length($_) ," bytes.\n");
    print(CLIENT $_) || die;
  }
}

echo(mydate(),"Disconnected\n\n");

close CLIENT ;
close SERVER ;

#######
# Subs
#######
sub echo
{
  print @_;
  print LOGFILE @_;
}

sub mydate
{
  my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime();
  # month is returned in the range 0 to 11, where 0=January, 11=December. year is returned in years since 1900.
  return sprintf "%04d/%02d/%02d %02d:%02d:%02d" ,$year+1900,$mon+1,$mday,$hour,$min,$sec;
}

Commentary:

For example, to watch what happens between your browser and wordpress, you could

  1. download tap.pl
  2. run
    tap.pl benaveling.wordpress.com 80 1080
  3. point your browser at http://localhost:1080

What happened? Was it what you expected?  If not, don’t worry. All will be explained in the next post.

Advertisements