Web Analytics

Technically Impossible

Lets look at the weak link in your statement. Anything "Technically Impossible" basically means we haven't figured out how yet.

Geolocation API抜き、タイムゾーンでアクセス元の国を判定するJavaScript

今年の2月から、不審なアクセスに悩まされていた*1。このブログのアクセス数は、毎日200~300程度なのだが、この不審アクセスだけで1000~2000にも達するのだ。
Google Analyticsなど、主要なアクセス集計サービスでは、いわゆるボットによるアクセスを自動的に排除するのだが、一部のサービスは、そのことごとくを検知していたのだった。

スクレイピングが目的ならば、一通りのページを取得すれば、あとは日々の更新分を確認するだけで済むのだから、これほどのアクセスを繰り返す必要はない。日々の更新を確認するにしても、毎日1000回以上もアクセスする必要もない。

たとえ不審アクセスによる実害がないとしても、そのことごとくを検知してしまう集計サービスから、毎日該当アクセスを除外する手間は避けられない。何より、心理的に不快なのだ。
このようなアクセスを遮断するのは、通常インフラ側の役割だ。しかし、はてなブログのインフラに手を加えることはできず、クライアント側で可能な対応を考えた結果、思いついたのがタイムゾーンによるアクセス制御だった。

イデアIPアドレスではなく、タイムゾーンに基づくアクセス制限

この不審アクセスには、次の特徴があった。

Referrer 無し
Direct
アクセス元 シンガポール
中国
OS 不明
Android 5
ブラウザ 不明

最近は、Torのように、参照のたびにアクセス元を変更する技術もあるが、不審アクセスは、そのような技術を採用していない様子だ。あるサービスは、それをシンガポールからの通信と認識し、別のサービスは中国からのものと認識していた。まずはアクセス元となる国を制限対象とするのが良い、と考えた。

以下に示すように、接続元のIPアドレスから、発信元となる国を特定できるwebサービスが存在する。それらは有料であったり、無料であっても呼び出し回数に制限が設けられていたり、あまり積極的に採用し難いハードルもある。

📂Geolocation API
www.abstractapi.com
ipstack.com
ipapi.co
ipinfo.io

OSやブラウザ名のようなUserAgent情報は容易に詐称できるため、今回のような事例には適さない。
そこで思いついたのが、クライアント側のタイムゾーンで国を特定し、アクセスを制限することだった。そして、同様のことを考え、すでに実行している人がいることも分かった。

コードの説明

stackoverflow.com
codepen.io

以上で紹介されているコードでは、関数getCountryにて、次のようなレコードを含む2つの連想配列を生成している。

関数名 レコード例
countries JP: "Japan",
timezones Japan: {a: "Asia/Tokyo", r: 1 },

通常、次のコードを実行すると「Asia/Tokyo」のような文字列が取得できる。

Intl.DateTimeFormat().resolvedOptions().timeZone

つまりクライアント側のタイムゾーン情報と、以上の2つの連想配列を照らし合わせて、国を推定することができる。
「推定」としたのは、例えば、日本で生活しているからと言って、必ずしもPCやスマートフォンタイムゾーンを日本に設定しているとは限らないからだ。この投稿では、そのような場合は例外として、考慮外としている。

特定国からのアクセスをリダイレクトするコード

クライアント側のタイムゾーンに基づいて確認した国によって、あらかじめ定められた接続先へリダイレクトするコードを考える。例えば、次のような具合だ。
日本からのアクセス(タイムゾーンが日本)と見なせば、日本のNHK Newsへリダイレクトするが、それ以外からのアクセスならばNHK WORLDへリダイレクトするコードだ。

const REDIRECT_DESTINATION_JP = "https://www3.nhk.or.jp/news/"
const REDIRECT_DESTINATION_OTHER = "https://www3.nhk.or.jp/nhkworld/"
const TARGET_COUNTRIES = ["Japan"]
let userCountry = getCountry();

if (TARGET_COUNTRIES.includes(userCountry)){
  window.location.replace(REDIRECT_DESTINATION_JP);
}
else{
  window.location.replace(REDIRECT_DESTINATION_OTHER);
}


もし「TARGET_COUNTRIES」をブラックリスト的に扱えば、望まぬ国からのボット的なアクセスをリダイレクトさせる用途にも転用できる。

const REDIRECT_DESTINATION = "https://chinadigitaltimes.net/space/CDS%E4%B8%93%E9%A1%B5%EF%BC%9A%E6%95%8F%E6%84%9F%E8%AF%8D%E5%BA%93"
const PROHIBITED_COUNTRIES = ["China", "Singapore"]
let userCountry = getCountry();

if (PROHIBITED_COUNTRIES.includes(userCountry)){
  window.location.replace(REDIRECT_DESTINATION);
}
else{
  //window.location.replace(REDIRECT_DESTINATION_OTHER);
}

余談

scriptタグ

developer.mozilla.org

スクリプトをHTMLファイルへ直接記載せず、scriptタグに指定したファイルから呼び出す場合、その属性指定に注意を要する。

async HTMLファイルとJSファイルのパースが同時並行で実施される。
JSファイルが実行可能になり次第、実行する。
defer HTMLファイルのパース完了を待って、JSファイルを実行する。
redirect

www.w3schools.com

リダイレクト先を指定するに際し、2通りの方法がある。分岐となるページにアクセスさせ、目的のページへ誘導する(リダイレクトさせる)意味では、"window.location.replace"が望ましい。

window.location.replace("http://www.w3schools.com");
  • 現在のURLをリダイレクト先のURLへ置き換える→分岐ページが履歴に残らない
  • バックしても、リダイレクト・ページに戻れない
window.location.href = "http://www.w3schools.com";
  • リダイレクト先のURLへジャンプさせる→履歴が残る
  • バックすると、リダイレクト・ページに戻る