Proxy経由でLWP::UserAgentを使うB!

こんにちは!好きな寿司ネタは甘エビのkamipoです。

今日はProxy経由でLWP::UserAgentを使う方法を紹介したいと思います。

クローラやWeb APIなどを扱うモジュールの内部で必ずと言っていいほど使われているHTTPクライアントのLWP::UserAgentですが、世の中には色々な事情でHTTPリクエストするのにProxyを経由しなければいけない環境の人がいるんじゃないかと思います。

まず、LWPとCrypt::SSLeayの最新版をCPANからインストールしておきましょう。

% cpan LWP Crypt::SSLeay

LWP::UserAgentでProxyを指定するには以下のようにします。

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;

my $http_proxy = "http://forward_proxy:8080/";

my $ua = LWP::UserAgent->new;

# proxyメソッドを使う場合
$ua->proxy([qw(http https)], $http_proxy);

# 環境変数から読み込む場合
$ENV{HTTP_PROXY}  = $http_proxy;
$ENV{HTTPS_PROXY} = $http_proxy;

$ua->env_proxy;

my $res = $ua->get(shift);

print $res->dump;

一見するとこれでOKのように見えますが、この方法だとHTTPSのリクエストが失敗するようです。コマンドラインから以下のように実行してみましょう。

% perl proxy_sample1.pl http://http_host/
% perl proxy_sample1.pl https://https_host/

どうでしょう?HTTPSの場合だとステータスラインが

HTTP/1.0 501 Not Implemented

と返ってきたのではないでしょうか。

正解はCrypt::SSLeayのPODに書いてありました。

どうやらapacheのmod_proxy以外でのHTTPSリクエストのProxyは、LWP::UserAgent側でのハンドリングはせず、環境変数にセットしてCrypt::SSLeay側でProxyのハンドリングをしなければいけません。

つまり、HTTPもHTTPSも両方ちゃんと扱うには以下のようにする必要があります。

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent;

# Crypt::SSLeayでは最後に"/"が付いてるとダメ
my $http_proxy = "http://forward_proxy:8080";

my $ua = LWP::UserAgent->new;

# proxyメソッドを使う(HTTPの場合)
$ua->proxy([qw(http)], $http_proxy);

# 環境変数から読み込む(HTTPSの場合)
$ENV{HTTPS_PROXY} = $http_proxy;

$ua->env_proxy;

my $res = $ua->get(shift);

print $res->dump;

もう一度コマンドラインから実行してみましょう。

% perl proxy_sample2.pl http://http_host/
% perl proxy_sample2.pl https://https_host/

今度はちゃんといけましたね!

なぜLWP::UserAgentではHTTPSをProxyできないのか

なんとかProxyすることはできましたが、そもそも同じProxyを経由するのに同じ指定の仕方ができない理由は何か。LWPではだめなのか。

この挙動の原因はLWP::UserAgent::send_requestメソッドの以下のコードにありました。

sub send_request
{
    my($self, $request, $arg, $size) = @_;
    my($method, $url) = ($request->method, $request->uri);
    my $scheme = $url->scheme;

    local($SIG{__DIE__});  # protect against user defined die handlers

    $self->progress("begin", $request);

    my $response = $self->run_handlers("request_send", $request);

    unless ($response) {
        my $protocol;

        {
            # Honor object-specific restrictions by forcing protocol objects
            #  into class LWP::Protocol::nogo.
            my $x;
            if($x = $self->protocols_allowed) {
                if (grep lc($_) eq $scheme, @$x) {
                }
                else {
                    require LWP::Protocol::nogo;
                    $protocol = LWP::Protocol::nogo->new;
                }
            }
            elsif ($x = $self->protocols_forbidden) {
                if(grep lc($_) eq $scheme, @$x) {
                    require LWP::Protocol::nogo;
                    $protocol = LWP::Protocol::nogo->new;
                }
            }
            # else fall thru and create the protocol object normally
        }

        # Locate protocol to use
        my $proxy = $request->{proxy};
        if ($proxy) {
            $scheme = $proxy->scheme;
        }

        unless ($protocol) {
            $protocol = eval { LWP::Protocol::create($scheme, $self) };
            if ($@) {
                ... snip ...
  • HTTPSリクエストでProxyなし
    • $protocolがLWP::Protocol::httpsのインスタンスになる
  • HTTPSリクエストでProxyあり(Proxyの$schemeがHTTP)
    • $protocolがLWP::Protocol::httpのインスタンスになる

つまり、HTTPSリクエストでProxyありのときでも$protocolがLWP::Protocol::httpsのインスタンスになるようにすれば、HTTPリクエストもHTTPSリクエストも扱えそうですね!

そこで以下のコードを書いてみました。モジュール名はいい名前が思いつかなかったので適当です。

package LWP::UserAgent::ProxyConnect;

use strict;
use warnings;
our $VERSION = '0.01';

use LWP::UserAgent ();

{
    my $impclass = LWP::Protocol::implementor('http');

    my $orig = $impclass->can('request');

    my $proxy_method = sub {
        my ($self, $request, $proxy, $arg, $size, $timeout) = @_;

        # $request->url->schemeが'https'で$proxyがあるとき
        if ($request->url->scheme eq 'https' and $proxy) {
            # Crypt::SSLeayのために環境変数をセットして
            local $ENV{HTTPS_PROXY} = $proxy->host_port;

            no warnings 'uninitialized';
            my ($username, $password) = split /:/, $proxy->userinfo;
            local $ENV{HTTPS_PROXY_USERNAME} = $username;
            local $ENV{HTTPS_PROXY_PASSWORD} = $password;
            use warnings 'uninitialized';

            # $selfをLWP::Protocol::httpsのインスタンスにして
            bless $self, LWP::Protocol::implementor('https');

            # $proxyをなしにして元の処理に戻る
            $orig->($self, $request, undef, $arg, $size, $timeout);
        }
        else {
            goto $orig;
        }
    };

    no strict 'refs';
    no warnings 'redefine';
    *{"${impclass}::request"} = $proxy_method;
}

1;

LWP::UserAgentと同じように使えます。

#!/usr/bin/perl
use strict;
use warnings;
use LWP::UserAgent::ProxyConnect;

my $ua = LWP::UserAgent->new;

# 環境変数にセットしていればこれだけで両方ともOK
$ua->env_proxy;

my $res = $ua->get(shift);

print $res->dump;

これでリクエストがHTTPかHTTPSかを気にせずProxyを指定できるようになりました!やったね!

参考

さてさて、JPerl Advent Calendar 2009もいよいよ後半に突入ですね。

明日はvkgtaroさんです!わくてか!