For the testing of clients

When developing a client application, it helps to be able to test it.  Sometimes, testing against a real server is enough.  But at other times using a real server will slow you down, either because a real server isn’t available, or because you have more coders than real servers to test against, or because it takes time to set up the server for each run, or simply because you want to test behaviour a real server doesn’t normally display, such as error conditions.

In such cases and many more, you need a mock server.

Enter mock_server.pl.

Usage:

mock_server.pl local_port config_file [log_file]

local_port: the port to listen to. No defaults provided.
config_file: a file containing expected requests and a ‘canned’ response for each one
log_file: name of log file. default: mock_server.log

For example:

mock_server.pl 1080 mock_server.cfg

Mock_server.pl expects each line of the config to have the format:

regexp filename

where:

regexp = a string to search for, wildcards and whitespace OK
filename = file to send if regexp found, no wildcards or whitespace allowed

The first line to match the request is the one that determines the response.
If nothing matches, no response will be sent.

To provide a default response, use a regexp of “.” or “.*”

Leading and trailing whitespace will be ignored.
regexp may contain whitespace or wildcards – if so, it they will be respected.
Filenames must not contain whitespace. If they do, the part up to the last bit of whitespace will be treated as part of the regular expression. (If running on Windows, use “dir /x” to find an alternate name that doesn’t contain whitespaces.)

By way of example, mock_server.cfg might contain:

GET /page_1       mock_server_page_1.html
GET /page_2       mock_server_page_2.html
.                 mock_server_default_page.html

And the request from the browser might look something like

GET /page_1 HTTP/1.1
Host: 127.0.0.1:1083
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:14.0) Gecko/20100101 Firefox/14.0.1
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Referer: http://127.0.0.1:1080

In such a case, mock_server will match on “GET /page_1” and return the contents of mock_server_page_1.html.

Code for Mock_server.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 accepts anything and returns a message,
# The message returned depends on what it was sent, as per mock_server.cfg
#
# #######
# History 
# #######
#
# 2012.08.31 First published version
#
# #####
# Usage
# #####
#
my $usage = "
usage:
  mock_server.pl local_port mock_server.cfg [mock_server.log]

  local_port: the port to listen to
  mock_server.cfg: a file listing requests supported and response to each 

For example:
  mock_server.pl 1080 mock_server.cfg

For the syntax of the config file, see the provided sample mock_server.cfg.
";
# #######################################################################

#
# 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 $local_port_num = shift or die $usage;

# Read config file
my $cfg_file = shift or die $usage;

my @response_files;

open(CFG, $cfg_file) or die "Cannot read $cfg_file: $!\n";
while(my $_=<CFG>){
  next if m/^\s*#/ || m/^\s*$/;
  m/^\s*(.*\S)\s+(\S+)/ or die "Can't parse: $_";
  my $regex=$1;
  my $response_file=$2;
  die "Can't read '$response_file': $!\n" unless -r $response_file;
  push @response_files, [$regex, $response_file];
}
close(CFG);

# 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

my $log_file = shift || "mock_server.log";
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 does the actual work, while the parent goes back to waiting for another connection.
  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;
  }else{
    doit();
    exit;
  }
}

sub doit{
  my ($client_port, $client_iaddr) = sockaddr_in( $client_paddr );
  echo(mydate(),"\nConnection acceapted from ", inet_ntoa($client_iaddr), 
    ":$client_port\n");

  # Build file descriptor list for select call 
  my $rin = "";
  vec($rin, fileno(CLIENT), 1) = 1;

  while( 1 )
  {
    # wait for a request
    my $rout = $rin;
    select( $rout, "", "", undef ) ;
    die unless ( vec($rout,fileno(CLIENT),1) );
    # wait a second, then try to respond (this may or may not be what you want)
    sleep 1;
    while(1){
      my $request="";
      sysread(CLIENT,$request,4000); # Read request, up to 4k at a time (this is enough for many purposes but not all - for eg, soap requests can easily be longer: exercise for the reader. Hint: consider reading repeated until $request =~ m{</soap(?:env)?:Envelope>})
      echo(">>{{{$request}}}\n");
      # 0 length message means connection closed
      if(length($request) == 0){
    echo("Client disconnected\n");
    return;
      }
      # Send the first matching response. If no response matches, ignore the request.
      foreach my $response (@response_files){
    # it would be nice to use a hash instead of an array but order is important here
        my ($regexp, $response_file)=@{$response};
        if($request =~ m/$regexp/){
          print "### matches: '$regexp'->$response_file\n";
      open(RSP, $response_file) or die "Can't read $response_file: $!\n";
      print CLIENT <RSP>;
      close RSP ;
      # Different server behave differently - some close the connection after each response, some wait for another request
      close CLIENT;
      return;
      # last;
    }
      }
    }
  }

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

close CLIENT;

#######
# 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:

Mock_server.pl is a reworked version of tap.pl. The difference is that instead of forwarding messages to/from another server it reads each incoming message and returns a canned response.

To watch what happens:

  1. download mock_server.pl, mock_server.cfg, mock_server_default_page.html, mock_server_page_1.html and mock_server_page_2.html and put them all into the same directory.
  2. stop any existing program running on port 1080
  3. run:
    mock_server.pl 1080 mock_server.cfg
  4. point your browser at http://localhost:1080 and see what happens.
  5. Then try http://localhost:1080/page_1
  6. now telnet to localhost 1080 and type “GET” <return>
  7. Then try “GET /page_1”

In this case, the mock html files were hand-rolled and static. Sometimes that’s sufficient. Sometimes it’s helpful or necessary to generate responses dynamically, for example, in response to attributes in incoming requests. And that, is a topic for another post.

Advertisements