Search code examples
perlasynchronoushttp-proxymojolicious

Perl: An asynchronous http proxy via mojolicious


I made a simple http proxy, it's work fine, but not fast, because in the function handle_request, I use

my $tx = $ua->start( Mojo::Transaction::HTTP->new(req=>$request) );

to do the request, it's blocking.

I try to use a callback like:

$ua->start( Mojo::Transaction::HTTP->new(req=>$request) )=>sub{ ... }

to make it's non-blocking, and then, got a mistake:

'error' => { 'message' => 'Premature connection close'}

I guess that's because function handle_request return immediately, it does not wait the callback to be finished. If I use semaphore to wait the callback, that's mean it's blocking again.

#!/usr/bin/perl
use strict;
use warnings;
use utf8;
use Mojo::IOLoop::Server;
use Mojo::UserAgent;
use Mojo::Message::Response;
use Mojo::Message::Request;
use Mojo::Transaction::HTTP;
use Data::Dumper;

binmode STDOUT, ":encoding(UTF-8)";

my %buffer;

Mojo::IOLoop->server( {port => 3128} => sub {
    my ($loop, $stream, $client) = @_;

    $stream->on(
        read => sub {
            my ($stream, $chunk) = @_;

            my $buffer = $buffer{$client}{read_buffer} .= $chunk;

            if ($buffer =~ /^GET\s+|POST\s+|HEAD\s+(.*)\r\n\r\n$/i) {
                $buffer{$client}{read_buffer} = '';
                &handle_request($client,$stream,$buffer);
            }

            elsif ($buffer =~ /^CONNECT\s+(.*)\r\n\r\n$/i) {
                $buffer{$client}{read_buffer} = '';
                &handle_connect($stream,$buffer);
            }

            elsif($buffer{$client}{connection})
            {
                $buffer{$client}{read_buffer} = '';
                Mojo::IOLoop->stream($buffer{$client}{connection})->write($chunk);
            }

            if(length($buffer)>= 20 *1024 * 1024) {
                delete $buffer{$client};
                Mojo::IOLoop->remove($client);
                return;
            }
        });
});

sub handle_request{

    my($client,$stream,$chunk) = @_;

    my $request = Mojo::Message::Request->new;
    $request = $request->parse($chunk);

    my $ua = Mojo::UserAgent->new;
    my $tx = $ua->start( Mojo::Transaction::HTTP->new(req=>$request) );

    $stream->write( $tx->res->to_string );
}

sub handle_connect{
    my ($stream, $chunk) = @_;
    my $request = Mojo::Message::Request->new;
    my $ua = Mojo::UserAgent->new;

    $request = $request->parse($chunk);

    print Dumper($request);
}

Mojo::IOLoop->start;

Hope to get some suggestions .


Solution

  • You have 2 problem:

    1. You try to call nonblocking variant of $ua->start when your code have blocking style. Function handle_request must have callback as parameter. If you have chain of callback then the best way to implement it is to use Mojo::IOLoop::Delay.
    2. When you create variable $ua in non-blocking style in sub handle_request then your variable is destoyed by garbage collector because first execute exit of sub handle_request and $ua destroyed, because it is local variable and then get answer from $ua. So you get Premature connection close. You need to save instance of $ua elsewhere to prevent such error.

    Upd.
    I write bad variant of http/https proxy which work only via CONNECT method and have bug with not full first http message.

    Upd.
    I add another example of http/https proxy which correctly read first http message and work not only via CONNECT method.

    Upd.
    Oh, author of the Mojo wrote example of https proxy