ホスト名や IP アドレスによってユーザーの行動を制限する

こんばんは。 この間は東京でも雪が降っていて我が家にも積もりました。 雪がめったに降らない東京に住む僕には一大イベント。 気分はすっかりうきうきしました。 まぁ、そんな中コーディングをしているので雪が降っている意味は、と聞かれたらなんとも言えないのですが。

さて、本題ですが、掲示板とか配信サイトとかではアクセスされたくないユーザーからのアクセスを制限する必要がありますね。 一般のユーザーにはバンバンきて欲しいけど、スパム行為や規約に違反した行動などをする奴は締め出したい。

方法は幾らかあるかと思いますが、お手軽なのがホスト名や IP アドレスのブラックリストを作ってそれにマッチした場合はアクセスを排除する、という方法かと思いまして、それを PHP で実装しましたので公開します。

また、スパム行為をするユーザーはプロキシを通したりして IP アドレスやホストをころころ変えてきたりするので、いちいちすべての IP アドレスを登録していたのでは埒があきません。

そこで、ホストはドメインごとに、 IP アドレス はブロックごとに登録できるようにしました。

はじめに説明しておきますが、 $db は MySQL に接続してクエリを発行したり、その結果をごにょごにょするために使っている俺俺クラスのインスタンスです。 この俺俺クラスは公開する予定はありませが、基本的に最初の foreach を、 DB から返ってきた結果を1行ずつ while でフェッチしている部分だ、と読み替えていただければわかると思います。

<?php
function is_banUser(){
    global $db;
    $hosts = array();
    $addrs = array();
    $db->query('SELECT pattern, type FROM `bbs_banUsers` WHERE is_valid = 1', '', 'get');
    foreach($db->result['get'] as $value){
        if($value['type'] == 'host'){
            for($host=explode('.', strtolower($value['pattern'])), $i=count($host), $root=&$hosts;$i>0;$i--){
                if($i === intval(1)){
                    $root[$host[$i-1]] = TRUE;
                }else{
                    $root = &$root[$host[$i-1]];
                }
            }
        }elseif($value['type'] == 'addr'){
            for($addr=explode('.', $value['pattern']), $i=0, $root=&$addrs;$i&lt;count($addr);$i++){
                if($i === intval(count($addr)-1)){
                    $root[$addr[$i]] = TRUE;
                }else{
                    $root = &$root[$addr[$i]];
                }
            }
        }
    }

    $is_banUser = FALSE;
    $user = gethostbyaddr($_SERVER['REMOTE_ADDR']);
    if($user != $_SERVER['REMOTE_ADDR']){
        for($root=&$hosts,$user=explode('.', strtolower($user)),$i=count($user);$i>0;$i--){
            $root = &$root[$user[$i-1]];
            if(is_array($root)){
                continue;
            }elseif($root === TRUE){
                $is_banUser = TRUE;
                break;
            }else{
                break;
            }
        }
    }
    if(!$is_banUser){
        for($root=&$addrs,$user=explode('.', $_SERVER['REMOTE_ADDR']),$i=0;$i&lt;count($user);$i++){
            $root = &$root[$user[$i]];
            if(is_array($root)){
                continue;
            }elseif($root === TRUE){
                $is_banUser = TRUE;
                break;
            }else{
                break;
            }
        }
    }
    return $is_banUser;
}
?>

以下解説

DB の banUsers テーブルですが、 pattern 、 type 、 is_valid の3つのカラムからなっています。 pattern には弾くユーザーのホスト名ないし IP アドレスを、 type にはその pattern がホスト名なのか IP アドレスなのかを判断するために host または addr のいずれかを、 is_valid にはこのレコードが有効であるかどうかを判断するために1か0を入れます。

コードの前に言ったよにホスト名はドメインごとに、 IP アドレスはブロックごとに指定できるようになっています。 探索はホスト名では後方一致で、 IP アドレスでは前方一致で行っていますので、例えば「 *.hoge.net 」からのアクセスをまとめて弾きたいんだけど、という場合は pattern に hoge.net 、 type に host 、 is_valid に1を指定すれば、 hoge1.hoge.net だろうが、 hoge2.hoge.net だろうが弾けますし、「 192.168.*.* 」からのアクセスをまとめて弾きたければ pattern に 192.168 、 type に addr 、 is_valid に1を指定しておけば 192.168.1.1 だろうが、 192.186.2.254 だろうが弾けます。

この関数を使って、

<?php
if(is_banUsre()){
    print('通常ユーザー');
}else{
    print('アクセス禁止ユーザー');
}
?>

とすれば判断できます。 また、数カ所で判断する場合にこの関数を何度も呼び出していたのでは処理が遅くなってしまうかもしれませんので、

<?php
$is_banUser = is_banUser();
?>

とでもすればいいんじゃないでしょうか。 また、ノードを生成生成する部分と、探索する部分を分けてクラスにするというてもありますが、個人的にそれにはあまり魅力は感じません。 (複数プログラムで同じようにアクセス禁止を使うためにライブラリ化するのであれあクラスでもいいのかな?)また、 MySQL サーバーのレスポンスが悪かったりでアクセス毎にノードを生成するのは気が引ける、という方はノードを生成する部分の処理を分離して、 DB にデータを詰めたときに一度生成して var_export とかでファイルにキャッシュすればよろしいかと。

【おまけ】もともと正規表現でこれを実現しようかな、とも思ったのですが、すべてのレコードを|でつなぐのはあまりにもお粗末に感じたので、効率的な探索のために TLD でまとめたり IP アドレスのブロックでまとめたりした表現を自動生成しようかなと考えたのですが、それだったら木構造を使ったほうが美しいかなと感じこのような処理にした次第であります。