crimsonpoint

мысли вслух

понедельник, октября 27, 2008

экономим трафик - прокси на perl

Для тех, кто выходит в интернет через gprs или просто живет не в Москве или Питере и не имеет дешевой безлимитки, короче для всех, кто вынужден экономить трафик.
До вчерашнего дня я выходил в интернет через связку HandyCache + CProxy. HandyCache - это кэширующий прокси, который к тому-же можно настроить на резку банеров или другого нежелательного контента, мне он экономит около 25 процентов трафика. CProxy - это программа-клиент чешского бесплатного сервиса позволяющего уменьшить трафик за счет сжатия (если вам это ни о чем не говорит, сходите сюда). CProxy эконимит мне еще около 70 процентов от того, что пропускает HandyCache.
Беда только в том, что бесплатные сервисы типа CProxy очень тормозные. Лучший вариант - поднять свой собственный жмущий прокси, например ziproxy. Но поскольку я искал быстрое решение и заморачиваться с компиляцией ziproxy мне не хотелось, я решил поискать тоже-самое на perl. Долго искать не пришлось, на первой-же позиции в гугле оказалось то что надо. Правда, сразу у меня скрипт вылетал с ошибкой, но после небольшой доработки все заработало. Теперь наслаждаюсь скоростью по сравнению с CProxy, даже на тормозном firstvds.ru.
Нужно будет еще установить степень сжатия на максимальную и сделать возможным доступ только с определенного списка IP.
#!/usr/bin/perl -Tw
use strict;
$ENV{PATH} = join ":", qw(/usr/ucb /bin /usr/bin);
$|++;

my $VERSION_ID = q$Id: proxy,v 1.21 1998/xx/xx xx:xx:xx merlyn Exp $;
my $VERSION = (qw$Revision: 1.21 $ )[-1];

## Copyright (c) 1996, 1998 by Randal L. Schwartz
## This program is free software; you can redistribute it
## and/or modify it under the same terms as Perl itself.

### debug management
sub prefix {
my $now = localtime;

join "", map { "[$now] [${$}] $_\n" } split /\n/, join "", @_;
}
$SIG{__WARN__} = sub { warn prefix @_ };
$SIG{__DIE__} = sub { die prefix @_ };
&setup_signals();

### logging flags
my $LOG_PROC = 0; # begin/end of processes
my $LOG_TRAN = 0; # begin/end of each transaction
my $LOG_REQ_HEAD = 0; # detailed header of each request
my $LOG_REQ_BODY = 0; # header and body of each request
my $LOG_RES_HEAD = 0; # detailed header of each response
my $LOG_RES_BODY = 0; # header and body of each response

### configuration
my $HOST = 'localhost';
my $PORT = 8080; # pick next available user-port
my $SLAVE_COUNT = 8; # how many slaves to fork
my $MAX_PER_SLAVE = 20; # how many transactions per slave

### main
warn("running version ", $VERSION);

&main();
exit 0;

### subs
sub main { # return void
use HTTP::Daemon;
my %kids;

my $master = HTTP::Daemon->new(LocalPort => $PORT, LocalAddr => $HOST)
or die "Cannot create master: $!";
warn("master is ", $master->url);
## fork the right number of children
for (1..$SLAVE_COUNT) {
$kids{&fork_a_slave($master)} = "slave";
}
{ # forever:
my $pid = wait;
my $was = delete ($kids{$pid}) || "?unknown?";
warn("child $pid ($was) terminated status $?") if $LOG_PROC;
if ($was eq "slave") { # oops, lost a slave
sleep 1; # don't replace it right away (avoid thrash)
$kids{&fork_a_slave($master)} = "slave";
}
} continue { redo }; # semicolon for cperl-mode
}

sub setup_signals { # return void

setpgrp; # I *am* the leader
$SIG{HUP} = $SIG{INT} = $SIG{TERM} = sub {
my $sig = shift;
$SIG{$sig} = 'IGNORE';
kill $sig, 0; # death to all-comers
die "killed by $sig";
};
}

sub fork_a_slave { # return int (pid)
my $master = shift; # HTTP::Daemon

my $pid;
defined ($pid = fork) or die "Cannot fork: $!";
&child_does($master) unless $pid;
$pid;
}

sub child_does { # return void
my $master = shift; # HTTP::Daemon

my $did = 0; # processed count

warn("child started") if $LOG_PROC;
{
flock($master, 2); # LOCK_EX
warn("child has lock") if $LOG_TRAN;
my $slave = $master->accept or die "accept: $!";
warn("child releasing lock") if $LOG_TRAN;
flock($master, 8); # LOCK_UN
my @start_times = (times, time);
$slave->autoflush(1);
warn("connect from ", $slave->peerhost) if $LOG_TRAN;
&handle_one_connection($slave); # closes $slave at right time
if ($LOG_TRAN) {
my @finish_times = (times, time);
for (@finish_times) {
$_ -= shift @start_times; # crude, but effective
}
warn(sprintf "times: %.2f %.2f %.2f %.2f %d\n", @finish_times);
}

} continue { redo if ++$did < $MAX_PER_SLAVE };
warn("child terminating") if $LOG_PROC;
exit 0;
}

sub handle_one_connection { # return void
use HTTP::Request;
my $handle = shift; # HTTP::Daemon::ClientConn

my $request = $handle->get_request;
defined($request) or die "bad request"; # XXX

my $response = &fetch_request($request);
warn("response: <<<\n", $response->headers_as_string, "\n>>>")
if $LOG_RES_HEAD and not $LOG_RES_BODY;
warn("response: <<<\n", $response->as_string, "\n>>>")
if $LOG_RES_BODY;
$handle->send_response($response);
close $handle;
}

sub fetch_request { # return HTTP::Response
use HTTP::Response;
my $request = shift; # HTTP::Request

## XXXX needs policy here
my $url = $request->url;
warn("processing url is $url") if $LOG_TRAN;
&fetch_validated_request($request);
}

BEGIN { # local static block
my $agent; # LWP::UserAgent

sub fetch_validated_request { # return HTTP::Response
my $request = shift; # HTTP::Request

$agent ||= do {
use LWP::UserAgent;
my $agent = LWP::UserAgent->new;
$agent->agent("Mozilla/5.0");
$agent->env_proxy;
$agent;
};

$request->header('accept-encoding'=>'gzip,deflate');

warn("fetch: <<<\n", $request->headers_as_string, "\n>>>")
if $LOG_REQ_HEAD and not $LOG_REQ_BODY;
warn("fetch: <<<\n", $request->as_string, "\n>>>")
if $LOG_REQ_BODY;

my $response = $agent->simple_request($request);

my $content = $response->content;
if ($response->is_success and
$response->content_type =~ /(text|plain|html|javascript)/ and
not ($response->content_encoding || "") =~ /\S/ and
length($content)>100 and
($request->header("accept-encoding") || "") =~ /gzip/) {
require Compress::Zlib;
my $new_content = Compress::Zlib::memGzip($content);
if (defined $new_content) {
$response->content($new_content);
$response->content_length(length $new_content);
$response->content_encoding("gzip");
warn("gzipping content from ".
(length $content)." to ".
(length $new_content)) if $LOG_TRAN;
}
}

$response;
}
}