⚠この記事はブログ移転前のアーカイブです

だいぶ前になりますが,私が企画していたEpic RPG 4という音楽コンピにて作品の受取にダウンロードコード(シリアルコード)を使ったダウンロードページを制作しました.同人音楽でもディスクというモノ以外にスマートな提供方法があればいいなあと思い作りました.(本サービスは参加者向けのみの提供でした.一般頒布向けにコードは発行していません)

この記事では(再現コードですが)PHPを用いてシリアルコードによるダウンロードシステムを作りたいと思います.

シリアルコードによるダウンロードサイトとは

つまりこういうことです.

  • コードを持っている人だけがそのファイルをダウンロードできる.
  • コードは推測・総当りしにくい.
  • URL直叩きによるファイルアクセスは不可

シリアルコードの仕様

シリアルコードの仕様を決定します.使う文字とか長さです.提供先の規模に応じて決定します.

使用する文字

基本的にはアルファベット+数字です.コードの発行量に応じて使う文字の種類を決定します.

  1. アルファベット大文字+小文字+数字
    • 26+26+10 = 62
  2. アルファベット大文字+数字
    • 26+10 = 36
  3. 【オススメ】:1,2から誤読しやすい文字を排除
    • 「I,1」「O,0」「V,U」など

などがありますね.コードの発行量が多い(1万超える)場合「大文字+小文字+数字」を選ぶべきでしょう.ただし,可読性と入力のしやすさが低下してしまいます.

オススメは「アルファベット大文字+数字」から「誤読しやすい文字を排除」する方法です.36文字から30文字に減ってしまいますが可読性が増します.

文字の長さ

文字の長さを決定します.短すぎても長すぎてもダメです.短すぎる場合,コードを持っていない人が総当りで不正にダウンロードできる可能性があるためです.長さにブレがあるような作り方でもいいでしょう.

長さは8~15桁が良いと思います.

使用する文字」セクションで「アルファベット大文字+数字」から「誤読しやすい文字を排除」する方法を選んだ方は 15桁~20桁あると良いでしょう.

ちなみにWindows 7のDSP版ライセンスキーの長さは25桁です

コード出力

ランダムなコードを出力するにはこうします.

cat /dev/urandom | tr -dc 'A-Z0-9' | fold -w 15 | head -1 | sed -e "/[O0I1VU]/d" | uniq

Oや0などが含まれる場合は出力されないので,とりあえず無限に出すスクリプトを書いてみる.無限は得意

while :
  do
    cat /dev/urandom | tr -dc 'A-Z0-9' | fold -w 15 | head -1 | sed -e "/[O0I1VU]/d" | uniq
  done

思う存分吐き出してください foldコマンドで指定した数値が桁数です.

こんな感じになります.

NB2SLF62AANJGXQ
E5AWN96763DX7WE
JJS66PR675CE853
W8Q9EXM6H3ZJ94F
ZY8BGJ52QBQZY9T
WKFLFFYZW25K9N3
2A5D7GMZG8ZCMZM
86MHJQ8FGQ5RRKD
Y8JNHWMNLPG2Q5G
9AJAX38RRY8QPR6
ECF28RNG4A59KEX
......

ダウンロードしてみる

では先ほど発行したコードを使用してみましょう.シナリオとしては

  1. フォームから送信されてきたコードを検証
    1. フォーマットチェック(文字列・長さ・SQLi対策)
    2. 正規コードチェック(データベースから存在するコードか検証)
      • アカウントと紐付ける場合はその検証もここで行う
  2. 正規のコードなら
    1.  HTTPヘッダの作成
    2. ダウンロードさせたいファイルのMIME判別
    3. 指定したパスに存在するファイルをBODYに乗せて送信する.

となります.ダウンロードURLがバレてブラウザから直打ちされても弾くようにつくります.

今回メインになるのは指定したパスに存在するファイルをHTTPに乗せて送信する部分です.シナリオで示したフォーマットチェックや正規コードチェックなどはご自身で記述をお願いします.ファイル名が可変である場合はディレクトリトラバーサル対策を忘れずに.

function download()
{
  $path = '../file.zip';
  if (file_exists($path)) {
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="file.zip"');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: ' . filesize($path));
    $out = fopen('php://output', 'wb');
    $file = fopen($path, 'rb');
    stream_copy_to_stream($file, $out, $len, 0);
  }
}

または

function download()
{
  $path = '../file.zip';
  if (file_exists($path)) {
    header('Content-Type: application/octet-stream');
    header('Content-Disposition: attachment; filename="file.zip"');
    header('Content-Transfer-Encoding: binary');
    header('Content-Length: ' . filesize($path));
    readfile($path);
  }
}

となります.Symfony や laravelなどPHPフレームワークを使用する際は,すでにファイルダウンロード用関数が用意されていることがあります.

laravel 5だと

response()->download( base_path($path));

のようにするだけでヘッダも自動で作ってくれます.

おまけ:reCAPTCHAで総当り対策

コードを持っていないユーザーがでたらめにコードを送信してヒットするまでそれを続ける総当たり攻撃を対策するといいです.完璧ではありませんが,reCAPTCHAによる総当り対策を施すとより健全なダウンロードサイトにすることが出来ます.

「私はロボットではありません」「reCAPTCHA」とは

Googleが開発しており,利用にはGoogleアカウントが必要ですが実装は非常に簡単です.

ウェブサイトにキャプチャを導入する方法【reCAPTCHAの使い方】

上記サイトを参考にcaptchacheck関数を使って正当なアクセスかどうかを判別した上でダウンロードを許可します.

if (checkCaptcha($_POST["g-recaptcha-response"])) {
    $path = '../file.zip';
    if (file_exists($path)) {
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="file.zip"');
        header('Content-Transfer-Encoding: binary');
        header('Content-Length: ' . filesize($path));
        readfile($path);
    }
}

PHPがわからない人向けに簡単に実装できるような仕組みがあるといいですね・・・