Small issue with IO::Socket server and POST data

1.5k views Asked by At

For some reasons I can only use IO::Socket to build my small http server (not the other modules dedicated to that).

EDIT1: I edited my question, I want to know what I can put instead of the commented line "#last ..."

Here is my script:

use strict;
use IO::Socket;

my $server = IO::Socket::INET->new(LocalPort => 6800,
    Type => SOCK_STREAM,
    Reuse => 1,
    Listen => 10) or die "$@\n";
my $client ;

while ( $client = $server->accept()) {

    my $client_info;
    while(<$client>) {
        #last if /^\r\n$/;
        print "received: '" . $_ . "'\n";
        $client_info .= $_;
    }

    print $client "HTTP/1.0 200 OK\r\n";
    print $client "Content-type: text/html\r\n\r\n";

    print $client '<H1>Hello World(!), from a perl web server</H1>';
    print $client '<br><br>you sent:<br><pre>' . $client_info . '</pre>';

    close($client);
}

Now, when I send a POST request, it (the script) doesn't take into account the last line (the POST data):

wget -qO- --post-data='hello=ok' http://127.0.0.1:6800
<H1>Hello World(!), from a perl web server</H1><br><br>you sent:<br><pre>POST / HTTP/1.1
User-Agent: Wget/1.14 (linux-gnu)
Accept: */*
Host: 127.0.0.1:6800
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 8
</pre>

The script output is:

perl server.pl 
received: 'POST / HTTP/1.1
'
received: 'User-Agent: Wget/1.14 (linux-gnu)
'
received: 'Accept: */*
'
received: 'Host: 127.0.0.1:6800
'
received: 'Connection: Keep-Alive
'
received: 'Content-Type: application/x-www-form-urlencoded
'
received: 'Content-Length: 8
'
1

There are 1 answers

1
amon On

This is to be expected. A POST request looks like

POST / HTTP/1.1
Header: Value

Data=Value

You terminate processing after the end of the header, but the data is in the body!

If you really want to write your own HTTP server, then you should extract the HTTP method from the header. If it is POST, you can look at the value from the Content-length header, and read that number of bytes:

read $client, my $post_data, $content_length;

WRT the updated question:

If you want to build a production HTTP server, you are going to have a bad time. This stuff is difficult. Please read through perlipc which covers the topic of TCP servers. You can then implement a subset of HTTP on top of this.

Also read through the modules on CPAN that implement servers. Even if you cannot compile modules on your system, you may be able to use pure-Perl modules, or may find parts of code that you can reuse. Large parts of CPAN can be used under a GPL license.

If you want to do this, do it right. Write yourself a subroutine that parses a HTTP request. Here is a sketch that doesn't handle encoded fields etc.:

use strict; use warnings; use autodie;

BEGIN { die "Untested code" }

package Local::HTTP::Request {
  sub new {
    my ($class, $method, $path, $version, $header_fields, $content) = @_;
    ...;
  }
  ...; # accessors
  sub new_from_fh {
    my ($class, $fh) = @_;
    local $/ = "\015\102"; # CRLF line endings
    chomp(my $first_line = <$fh>);
    my ($method, $path, $version) = ...; # parse the $first_line

    # this cute little sub parses a single field incl. continuation
    # and returns the next line as well.
    my $parse_a_field = sub {
      chomp(my $line = shift);
      my ($name, $value) = split /:\s+/, $line, 2;
      while(defined(my $nextline = <$fh>)) {
        # handle line continuation
        if ($nextline =~ s/^[ \t]//) {
          chomp $nextline;
          $value .= $nextline;
        } else {
          return $name, $value, $nextline;
        }
      }
    };

    my %fields;
    my $line = <$fh>;
    until ($line eq $/) {
      (my $name, my $value, $line) = $parse_a_field->($line);
      $fields{lc $name} = $value;
    }

    read $fh, my $content, $fields{"content-length"} // 0;

    return $class->new( ... );
  }
}

Then in your accept loop:

 my $request = Local::HTTP::Request->new_from_fh($client);

 print $client "HTTP/1.0 200 OK", "\015\012";
 print $client "Content-type: text/plain", "\015\012";
 print $client "\015\012";
 print $client "Request body:\n";
 print $client $request->content;