IPから国判定するipinfo.ioもどきのAPIを実装してみた。


概要

ipinfo.ioを使って国判別をしようしてたのですが、APIのリクエスト上限数が気になったので自前で実装してみました。

サンプル

https://tachimachi.hmkt.me/201213_altanative_ipinfo/ip2country.php
サンプル実装のため バチカン、モナコ、ニウエ のみ判別対象にしました。
日本からのアクセスは対象外のためcountryがunknownになります。

ip2country.php
{
    "status": "success",
    "country": "unknown",
    "request_ip": "{** $_SERVER['REMOTE_ADDR'] の値 **}",
    "match_ip": ""
}

GET値でipを指定すると動作チェックができます。

バチカンの場合

ip2country.php?ip=185.17.220.0
{
    "status": "success",
    "country": "va",
    "request_ip": "185.17.220.0",
    "match_ip": "185.17.220.0/22"
}

モナコの場合

ip2country.php?ip=87.238.105.0
{
    "status": "success",
    "country": "mc",
    "request_ip": "87.238.105.0",
    "match_ip": "87.238.104.0/21"
}

ニウエの場合

ip2country.php?ip=49.156.48.9
{
    "status": "success",
    "country": "nu",
    "request_ip": "49.156.48.9",
    "match_ip": "49.156.48.0/22"
}

ディレクトリ構成

altanative_ipinfo
  │
  ├─ ip2country.php ← 判定用スクリプト
  │
  └─ country_data/  ← 国別IPのCSVファイル用ディレクトリ
       ├ va.csv   ← バチカンIP用のCSVファイル
       ├ mc.csv   ← モナコIP用のCSVファイル
       ├ nu.csv   ← ニウエIP用のCSVファイル
       └ …etc

準備

このサイトからプレインテキストで判別対象にしたい国のip割り当て一覧テキストをダウンロード。

バチカン市国の場合はこのページ
https://ipv4.fetus.jp/va

ダウンロードした中身はこんな感じ。

va.txt
#
# [va] バチカン市国 (Holy See (Vatican City State))
#  https://ipv4.fetus.jp/va.txt
#  出力日時: 2020-12-13 12:36:16 JST (2020-12-13 12:36:16 UTC)
#

46.36.200.0/22
185.17.220.0/22
185.152.68.0/22
193.43.102.0/23
193.43.128.0/22
194.50.92.0/24
212.77.0.0/19

これをIPだけのCSVに書き換えて、国別IPのCSVファイル用ディレクトリに配置。

va.csv
46.36.200.0/22
185.17.220.0/22
185.152.68.0/22
193.43.102.0/23
193.43.128.0/22
194.50.92.0/24
212.77.0.0/19

判定用スクリプト

ip2country.php
<?php
header("Content-Type: text/json; charset=utf-8");

// GET値に調査対象のIP指定がない場合は自分のIPを代入する
$request_ip = isset($_GET['ip']) ? $_GET['ip']:$_SERVER['REMOTE_ADDR'];


// IPの書式チェック
if (!preg_match('/^(\d{1,3})(\.\d{1,3}){3}$/', $request_ip)){
  // 不正の場合はエラー
  header('HTTP', true, 400);
  echo json_encode(array(
     "status"=> "error"
    ,"msg" => "illegal IP address"
    ,"request_ip" => $request_ip
  ));
  exit;
}

$match_ip = '';
$country = "unknown"; // 国の初期値は不明

// 国別IPのCSVファイルを配置したディレクトリを指定
$ip_list_files_dir = dirname(__FILE__).'/country_data';

try{

  // 国別IPのCSVファイルのリストを取得
  $ip_list_files = glob($ip_list_files_dir.'/*.csv');

  // 調査対象のIPとサブネットマスクの組み合わせはあらかじめ計算しておく
  $request_ip_long_list = array(); 
  for($mask = 1; $mask <= 32; $mask++){
    $request_ip_long_list[$mask] = ip2long($request_ip) >> (32 - $mask);
  }

  // IPの国マッチング処理を行う
  foreach($ip_list_files as $file){

    // CSVファイル読み込み
    $ip_list_text = file_get_contents($file);

    if($ip_list_text === false){
      continue; // ファイルが読み込めない場合は次へ
    }

    // CSVを1行ごとに配列に分割しループ
    $ip_list = explode("\n", trim($ip_list_text));
    foreach($ip_list as $ip_val){

      // IPアドレス範囲に該当するものかどうかを判断
      list($ip_address, $mask) = explode('/', $ip_val);
      $accept_ip_long = ip2long($ip_address) >> (32 - $mask);

      if ($accept_ip_long == $request_ip_long_list[$mask]) {
        //マッチするIPが見つかった場合
        $country = basename($file, '.csv');
        $match_ip = $ip_val;
        break 2; // 国マッチング処理を抜ける
      }

    }

  }

  // レスポンスを返す
  echo json_encode(array(
     'status' => 'success'
    ,'country' => $country
    ,'request_ip' => $request_ip
    ,'match_ip' => $match_ip
  ));

} catch (Exception $e) {

  // 例外発生の場合はエラー
  header('HTTP', true, 400);
  echo json_encode(array(
      "status"=> "error"
     ,'country' => $country
     ,"msg" => $e->getMessage()
     ,"request_ip" => $request_ip
   ));

}

備考

日本やアジア圏のCSVファイルを入れても軽快に動いてましたが、負荷軽減を考えるならばCookie等で判定結果を一定期間保持させたほうがいいと思います。

あとがき

基本情報で出てくるIPアドレスとサブネットマスクの計算はこういう時に役に立つようですね。
自分は計算苦手なのできっと明日には忘れてると思いますが(笑)

参考記事

IPアドレスが指定した範囲内にあるかどうか判別する
https://qiita.com/ran/items/039706c93a8ff85a011a