目次


Perl/CGI投票システムを作る

ロックされたDBMファイルとCGIフォームを使用すると、オーバーキルなDBMSがなくてもクライアント・データを保存できる

Comments

ソフトウェアがますます複雑化しているなかで、ソフトウェア・コンポーネントのモジュール性を保つために、システムに付け加えられる層が増えているのは周知の事実です。その主な成果として、このようなシステムは保守が容易になり、安定性が高まるはずですが、このようなテクニックはオーバーキルなだけで、設計過剰なソフトウェアをもたらすことがあります。あるいは、開発者は、単純だが、あまり知られていないテクノロジーを吟味するのではなく、非常に複雑だが、よく知られているテクノロジーを選ぶこともできます。

何しろ、金づちしか手元にない場合は、あらゆる問題が釘のように見えます。

私は最近、ある大学生組織の選挙投票を集計する小さなプログラムの設計を頼まれました。それは、1週間に処理する学生数が500人までであり、その後、結果が集計されて公表されるという限りにおいて、単純なプロジェクトでした。

このプロジェクトに必要なのは、ごく些細なレベルのサービスだったので、外部のデータベースとの間でクエリーをやり取りするほどのことはありませんでした。代わりにスクリプトを使用すれば、直接、データ構造をすばやく読み書きできます。しかし、数ページのスパゲッティ・コードより少しはましなものを作りたいと思いました。導入も容易な、よく考えられた自己充足的な設計にしたいと考えました。

CGIに関する考慮事項:単純対複雑

このプロジェクトに最適と思える言語はPerlでした。一般的なプラットフォームのほとんどでサポートされ、CPANのPerlライブラリー・リポジトリーには多数の便利なライブラリーがあります。

基本アーキテクチャについては、CGI(Common Gateway Interface)は、Webサーバーを拡張してインタラクティブ・コンテンツを提供するために最初に広く普及したアプローチです。開発者はJSP、.NET、mod_perl、PHP、ISAPIなど、より新しい標準の優秀さをよく口にしますし、CGIに欠点があるのも確かです。しかし、このプロジェクトの場合、数百人のユーザーの投票数をカウントするCGIスクリプトは、大規模アプリケーションとは言えません。すべての投票情報をWebサーバーのシステムRAMで容易に保持できるからです。このため、ユーザーがデータ読み書きリクエストを送信するたびに、ルックアップ・テーブル全体をメモリにロードすることができます。

さらに、仮投票、投票の確認、結果の集計という論理的順序は、論理データを3つの異なる物理ファイルに分割するのに適しています。このため、ロックされたファイルを開こうとする試みが最小化されます。

ファイルがロックされていたためにトランザクションが失敗したとしても、実際上の不都合はありません。トランザクションの失敗の原因がネットワーク問題であっても、ファイルがロックされていたためであっても、結果は同じです。ユーザーがもう一度(または、さらにもう一度)クリックすれば、それらの試みのいずれかで投票がカウントされます。ただし、この動作には注意が必要です。別のアプリケーションでは、同時トランザクションを処理できないことが許されないかもしれないからです。

このプロジェクトの場合、CGIの使用には次のような利点があります。

  • 特別なWebサーバー拡張が不要です。
  • (この単純なケースでは)データベース・エンジンが不要です。
  • 段階的な開発が可能です。
  • mod_perlなどのアクセラレーターを使用して、後からアップグレードすることもできます。

ただし、注意してほしいのは、プラットフォームの制約のため、CGIアプリケーション(新しいプロセスを作成するもの)はWin32システムではかなり遅くなります。また、Apache WebサーバーはWindows ® でも性能を発揮しますが、やはり、Linux ™ /UNIX ® によってホストされるアプリケーションとみなされています。 参考文献 に、Win32システム向けの(非IIS)Webサーバー・アプリケーションに関する情報があります。また、NCSA(National Center for Supercomputing Applications)サイトには、オリジナルのCGI仕様の正式な説明があります。

機能設計に関する考慮事項

では、この単純なプロジェクトの主な関心事、すなわち、設計の機能性について考えてみましょう。

アイデアは、次のとおりです。ユーザーに対して表示される最初の画面では、電子メール・アドレスを入力して、Webフォームから候補者を選ぶように求めます。選択の送信は、ローカルで仮投票として記録された後、入力された電子メール・アドレスに確認メールが送信されます。このケースでは、ユーザーの同一性を確定するには、電子メール・アドレスの確認で十分であると考えました。

このため、重複投票という問題が生じます。事実上、ユーザーが複数の電子メール・アドレスを使用して複数回投票するのを防ぐ手段はありませんが、1つの電子メール・アカウントにつき1票しか許されないように投票集計を制限することはできます。この確認メールには、元のCGIスクリプトに戻るリンクが含まれていて、ローカルDBMファイルに保存されたレコードと比較することができます。2つのレコードが一致した場合は、投票テーブルに投票エントリが作成されて、1票とカウントされます。レコードが一致しない場合は、エントリは作成されず、投票確認は行われません。代わりに、新しい確認メールが生成されて、新しい確認レコードがデータベースに作成されます。これにより、電子メール・アドレスに対応する仮投票エントリは上書きされて、事実上、プロセスは始めからやり直しとなります。

レコードが一致した場合は、投票者は仮投票を確定することができます。この時点で、投票者の気が変わった場合は、すぐにWebフォームに戻って、新しい仮投票を入力して、前の仮投票に置き換えることができます。この設計は、かなりセキュアなシステムです。投票するユーザーが、受け入れ可能な電子メール・アカウントを1つしか持っていない限り、ユーザーは2回投票できないことがある程度保証されます。(この点については、後でまた述べます。)

では、システムの詳細を見てみましょう。

詳細:ハッシュ・キー

ハッシュ・キーを使用してPerlで連想配列を作成すると、複雑なデータ構造をその場で開発できます。この機能とバイナリーDBMファイル内のこれらの(任意に複雑な)構造を格納できる機能とを組み合わせることによって、要するに小さなデータベース・システムを作ることができます。このすべてがうまく機能するために足りないコンポーネントは、 MLDBM および MLDBM::Sync モジュールによって提供されます。

MLDBM モジュールを使用すると、複雑なPerlハッシュをほとんどシームレスにローカル・ファイルに格納することができます。 MLDBM::Sync モジュールでは、 $sync->Lock および $sync->ReadLock メソッドを使用して、これらのファイルを安全にロックすることが可能になります。目的の構造をロードまたは保存した後、次に UnLock() メソッドを呼び出すと、I/Oがフラッシュされて、変数がクリアされます。(詳しくは、 MLDBM::Sync モジュールに関するPerlマニュアル「 man 3 MLDBM::Sync 」を参照してください。)

基本的に、論理フローはリスト1に示されているように単純です。

リスト1. 擬似コードによる論理フロー
1  unless( defined( $q->param( $vparm ) )){
2      # Display initial voting stuff here
3      # select a candidate
4      $ballotBox->printForm( $q );
5  } else {
6      # if vote is tallied, do _not_ mail a ballot
7      if( $castBallot->voteIsTallied( $q ) ){
8           print "Your vote has already been recorded"
9      } else {
10          #
11          # vote not tallied yet, check if we have a draft ballot on file
12          # and move the draftBallot into the castBallot object
13          #
14          if( $draftBallot->exactMatch( $q ) ){
15              # cast ballot
16              print $q->h2('Thank you, your vote has been recorded.');
17              # add the vote to the cast ballot db file
18              $castBallot->tallyVote( $q );
19              # sum up all votes
20              $ballotBox->addVotes( $castBallot );
21              $cc_msg->send();
22          } elsif ( $draftBallot->voter_is_okay( $voter_email )){
23              # Send e-mail to allow voter to confirm vote
24              $mime_msg->send()
25          } else {
26              print 'Only University ballots are acceptable';
27          }
28      }
29  }

基本の条件付きフローが決まったら、残っている作業は、このアクション・シーケンスに合うオブジェクトを構築することだけです。すでに述べたように、必要なハッシュ・データ構造は、タイ変数と MLDBM ファイル・ロッキングを使用して取り出され、更新されます。使用されるオブジェクトは、フル装備のオブジェクトよりスマートな構造です。データは、最初の仮投票から最後の集計までが並行して行われるようにオブジェクト間で受け渡されます。

言い換えると、票のリストを使用して DraftBallot を作り、これを使用して CastBallot クラスと BallotBox クラスを作成します。このようにすると、メインの投票CGIプログラムとの連結が最小限で済みます。

ファイルなどの外部リソースに依存するコンストラクターは、(失敗して、予想外の状態で終了することがあるので)うまいやり方ではないと一般に考えられていることは分かっていますが、このケースのコードは、こうすることで非常に理解しやすくなりました。Perlはポインターに頼らないので、この単純さを利用しない理由はありません。

詳細:電子メールのわな

ユーザーがWebサーバーから電子メールを送信できるようにするのは、危なっかしい手です。スパマーが迷惑メールの送信に利用する恐れがあるからです。このような悪用の可能性を少なくするために、スクリプトは電子メールが受け入れ可能なアドレスに送信されているかどうかを常にチェックします。また、 DraftBallot クラスの検証メソッド voter_is_okay() を変更して、受け入れ可能な電子メール・アドレスのリストをチェックするようにすれば、システムをさらに強化できます。事実上、このためには、投票に先立ってユーザーに登録してもらう必要があります。

重複投票を防ぐその他の方法としては、IPアドレスの収集やクライアントでのクッキーの設定などがありますが、私はこれらの方法は使用しませんでした。学内の共有端末を使用する学生が多いはずだからです。

詳細:記名投票

$castBallot->dumpHTMLentrys() メソッドの呼び出しは、誰が誰に投票したかを示す詳細な集計を返します。実際には、この呼び出しをコメントアウトして、選挙が終了したときにはLinuxの at バッチ・コマンドを使用してWebサーバーのシャットダウンをスケジュールします。

サーバーがオフになったら、このセクションをアンコメントして、ローカル・ホスト・アドレスだけを監視するように一時的に設定して、Webサーバーを再起動できます。以前に送信したリンクをクリックすると、完全な結果が表示されます。これは、専用のフリーメール・アカウントに送信されたコピーを通じて収集できます。

この例では、投票が2回集計されないことに注目してください。全員に対して表示される内容は控えめにした方が良いという場合、これらの結果は、短いJavaScript関数を使用して隠されます。たしかに、完全な無記名投票が望ましいと考える人もいますが、クラブの選挙は挙手で行われることが多いので、無記名投票にするほどのことはないでしょう。

このワークフロー方式を考えていたとき、私は、 GET ベースの確認リンクと暗号化されない確認リンクを使用する必要があるので、特定の電子メール・アドレスと既知の確認リンクに基づいてこれらのリンクを読み取り、偽の確定投票を行うのが実に簡単であることが分かりました。これを阻止して、なおかつ、暗号化されないリンクを通じて簡単なデバッグができるようにするために、確認プロセスにもう少しヒネリを加えることにしました。すなわち、それぞれの仮投票に一意識別子を追加することにしました。

この識別子は、実行中のスクリプトのオペレーティング・システム・プロセス識別子(PID)に基づきます。これを乱数と組み合わせて、仮投票を検証するURLを予測しにくくしました。私が心配したのは、悪意の個人が目に見えるURLを解析して、偽の確定票を作成することでした。これは、実行中のPerlインスタンスのPIDとランダムな数字の組み合わせに依存しているので、 mod_perl バージョンに直接変換されないコードの一部です。再利用された mod_perl インスタンスからフォームが生成された場合、PID番号は必ずしも呼び出しごとに変更されるわけではありません。

後から考えると、このリンクを不明瞭にするには、MD5によって生成されたハッシュ値を使用した方がよかったかもしれません。そうすれば、事実上、すべての投票者情報を隠すことができます。この方法には、偽造を困難にし、しかも mod_perl ベースのスクリプトに移植可能という二重のメリットがあります。デメリットは、クライアントとサーバーの間の情報交換を検査してコードをデバッグするのが少し難しくなることです。

詳細:ファイルのレイアウト

インストールには、Webサーバー上に次の3種類のディレクトリーが必要です。

  • ユーザーの送信内容を保存するための書き込み可能なディレクトリー
  • CGIの実行元となる領域
  • 静的データ(CSS、ロゴ画像、詳細説明ファイルなど)を保存できる領域

また、アクセス権を工夫して、WebサーバーからDBMファイルのディレクトリーに書き込めるようにする必要があります。

リスト2は、Webサーバーでの一般的なディレクトリーの作成を示しています。

リスト2. Webサーバー上のディレクトリーのセットアップ
  $ id   uid=500(allan) gid=500(allan) groups=10(wheel),48(apache),500(allan)
    $ sudo mkdir /var/www/db /var/www/javascript/ /var/www/css/
    $ sudo chmod 2775 /var/www/db
    $ sudo chmod 2755 /var/www/javascript/ /var/www/css/
    $ sudo chown apache.apache /var/www/db/

厳密に言うと、cgi-bin(/var/www/cgi-bin)ディレクトリーとDBM(/var/www/db)ディレクトリーは、それぞれスクリプト実行ファイルと投票データを保持するので、絶対不可欠です。リスト1に示されているレイアウトはLinuxに特有のものであり、Webサーバー・プロセスのユーザー名とグループ名はシステムによってさまざまですが、要点は、いくつかのコンポーネントは、Webサーバーから使用できるように、ファイル・システムの正しい領域に置く必要があるということです。サポート・ファイルをそれぞれのディレクトリーにコピーした後、httpd.confなど、Webサーバー構成ファイル内のエイリアスを必ず更新してください。

リスト2で説明されているようにディレクトリーを作成した後、ZIPファイルからファイルをシステム上の類似のサブディレクトリーにコピーします。最も重要なことは、ballotファイル(DraftBallot.pm、BallotBox.pm、およびCastBallot.pm)はcgi-binディレクトリーになければならないということです。必要な非標準Perlモジュールは3つだけです。それらのインストール・プロセスをリスト3に示します(詳しくは、モジュールのREADMEファイルを参照してください)。

リスト3. Perlモジュールのインストール
  $ sudo perl -MCPAN -e 'install MLDBM'
  $ sudo perl -MCPAN -e 'install MLDBM::Sync'
  $ sudo perl -MCPAN -e 'install MIME::Lite'

詳細:静的DNS対動的DNS

このサービスを特定のIPアドレス上のドメインが割り当てられたドメインのサイトからセットアップすることも可能でしたが、動的DNSの方がセキュリティ上の利点があると考えました。通常、静的IPアドレスがなければ、一般のWebからサーバーに到達することはできませんが、動的DNSサービスによって別のトップレベル・ドメイン下に一時的に解決可能なマシン名をセットアップすることができます。こうすると、インターネットにすばやく現れ、すばやく姿を消すことができ、悪者の目に触れる機会を少なくできます。なにより、このサービスは無料です。

8000など、標準以外の高い番号のポートをリスンするようにサーバーを構成した方がよいかもしれない点も注目に値します。多くのISPは、ポート80への着信接続要求をブロックしているからです。その場合、クライアント(投票者)は一般に、学校が用意したWebページなど、既知の静的アドレスからのリンクからしか投票サーバーを参照できません。投票が完了したら、Webアプリケーションを提供しているサーバーをWebから完全に切り離すことができ、シャットダウンや再構成の必要はありません。(別の誰かによって管理されている可能性のある)参照元のページに影響するような脆弱性が生じる恐れはありません。これは、政治的に傷つきやすい環境では特に重要な考慮事項です。(動的DNSサービスの使用の詳細については、 参考文献 を参照してください。)

詳細:GETは有害か?

ブラウザーは、 GET および POST メソッドを使用して参照ページにデータを送信することによって、また、サーバーに渡されるヘッダーに含まれるクッキー情報によって、状態を保つことができます。投票が実在の人物(または少なくともアクティブな電子メール・アカウント)から送信されたことを確認するために、仮投票が電子メール・アドレスに送信されます。さらに、後で参照するためにcc:またはbcc:メッセージを使用することもできます。すでに述べたように、これを実現するための最も簡単な方法は、投票者に HTTP GET 構造のリンクを送信することです。やはり、レコードを更新する GET は無作法だと言う人もいます。しかし、このケースでは、後でリンクをクリックするユーザーは、各候補者の現在の得票数に関する更新を受け取るだけであり、危険はありません。

その他に考えられる改良

このスクリプトを使用するときには、他にもセキュリティに関する考慮事項があります。外部のエンティティーがデータを入力できるプログラムは、バッファー・オーバーフローや制御文字の埋め込みなど、悪意ある活動に対して脆弱です。逆に、ローカルのDBMファイルを読み書きする専用ルーチンの使用には、少なくとも1つの利点があります。それは、アクセスすべきSQLバック・エンドがないときには、SQLインジェクションの恐れがないことです。

着信データをフィルタリングする必要があるので、変数 $CGI::DISABLE_UPLOADS および $CGI::POST_MAX を非常に厳格な値に設定しました。さらに、次のことを推奨します。

  • 予期しない文字の着信変数をすべてひとまとめにして、長さを妥当な限度に切り詰めてください。
  • 多数のランタイム・データがスクリプト内に保持されます。この方法のメリットは、配布してアクセス権を設定しなければならないファイルの数が少なくて済むことです。デメリットは、ユーザーはコードを編集したくないと考えるかもしれないので、コードが分かりにくくなることです。可能な妥協策は、 DATA 擬似ファイル・ハンドルなどでごまかして、スクリプトの最後にデータを押し込むことです。
  • ファイルのロックは非常にトリッキーな問題であり、異論の多い問題でもあります。ファイルの正しいロック方法を述べたガイドラインはどれも、その後に修正が付け加えられているようです。私は、ファイルが開かれている時間をできるだけ短くして、 MLDBM モジュール用に提供されているロッキング・メカニズムを利用しようとしました。
  • Perlモジュールは、CGIとは別の専用のパスに入っているわけではないので、理論的にはcgi-binディレクトリーから実行できます。これらのモジュールを実行ファイルとして設定しないことが推奨されています。
  • PHPは事実上どのLinuxプラットフォームでも使えるので、再実装が必要になった場合には、私はこのスクリプトをPHPに移植することを考えたでしょう。しかし、 MLDBM モジュールに相当するPHPがあるかどうかはわかりません。
  • 投票フォームのレイアウトは、最初の候補者をデフォルトとして表示するので、不公平です。
  • 私はperldocを使用しませんでした。使用すべきでした。

まとめ

このシステムをセットアップする機会を与えられて、単純かつ自己充足的なものにしようと試みる間に、いくつか非常に便利なPerlモジュールを見つけることができました。このように単純なプロジェクトのために、機能を微調整し、機能仕様を作り上げていくプロセスは、有益かつ楽しいことでした。このタイプのシステムを作り上げるときの考慮事項のリストが同様のプロジェクトのお役に立てば幸いです。


ダウンロード可能なリソース


関連トピック

  • PixieによるPerlの永続性管理 」(developerWorks, 2003年3月)は、Perlプロジェクトとリレーショナル・データベースのインターフェースに関して、より柔軟な手法をとっています。
  • セキュアなプログラマー: 競合状態を防ぐ 」(developerWorks, 2004年10月)は、UNIXライクのシステムにおける一般的な競合状態の処理方法として、ロック・ファイルを正しく作る方法やロック・ファイルに代わるものを使う方法、ファイルシステムの扱い方、共有ディレクトリーの扱い方などを解説しています。
  • この記事の ソースコード(15 KBのzipファイル) をダウンロードしてください。
  • Apache Foundation では、Apache Webサーバーの設定方法やセキュアにする方法に関して、幅広い情報を提供しています。
  • The World Wide Web Consortium には、National Center for Supercomputing Applicationsへのレガシーのリンクがあり、また単純で機能的なCGI定義を提供しています。
  • Lincoln Stein's home page では、Perl CGIモジュールの使い方に関して、素晴らしいヒントと、参考資料へのリンクを提供しています。
  • Damien Conwayによる素晴らしいテキスト、 Object Oriented Perl (2000年1月Manning刊)は、Perlでのオブジェクト指向プログラミングの詳細への入門として最高です。
  • あなたは電子的、あるいはインターネット・ベースの投票システムを信頼しますか? Rebecca Mercuriは Electronic Voting の中で、紙による、議論の余地のない投票を提供しない投票システムに関する批判を展開しており、ハッとさせられます。
  • Netscape Cookie Specification は、そこで提供される機能に関して、簡潔に説明しています。
  • Comprehensive Perl Archive Network は、便利なPerlモジュールが、困惑するほど数多くリストアップされています。
  • Dynamic DNS Website は、動的に割り付けられるIPアドレスを持つサーバーに対して、解決可能ドメイン名を提供します。このサービスは、Win32、Mac、UNIXそしてGNU/Linuxのどれでも動作します。
  • O'Reillyから出版されているテキスト、 CGI Programming with Perl (2000年7月O'Reilly刊)は、少し古いですが、CGIアプリケーションを書く場合のヒントやトリック、突き当たりがちな落とし穴などに関する概要を、うまく説明しています。
  • エッセイ、 GET considered harmful; Sometimes は、POSTメソッドとGETメソッドに依存するCGIアプリケーションを書く上での助言を提供しており、非常に便利です。
  • developerWorksのLinuxゾーン には、Linux開発者のための資料が豊富に取り揃えられています。
  • 皆さんの次期Linux開発プロジェクトを、 IBM trial software を使って革新してください。developerWorksから直接ダウンロードすることができます。

コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux, Open source
ArticleID=227096
ArticleTitle=Perl/CGI投票システムを作る
publish-date=05312005