English Korean Chinese

RubyでCGI・入門編

今野 滋

文章を受け渡して保存してみる

Web掲示板やブログの編集等、文字情報の受け渡しと保存が必要になります。Webページのフォームに書き込んだ内容を送信して、CGIにて受け取り、それを保存します。データはファイルの形で保存しますが、案件ごとにファイルを作成するよりも、データベースを活用すると便利です。

データを送る

仮に、件名、本文、パスワードの3件を書き込んで送るフォームを作ってみましょう。testpage.htmlの内容を下記に書き換えて、FTP転送しください。
なお、フォームはタグを知らなくても、Nvuを使うと簡単に編集できます。

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
  <meta content="text/html; charset=Shift_JIS" http-equiv="content-type">
  <title>testpage</title>
</head>
<body>
<h1>テストページ</h1>
<p>世界のみなさん、こんにちは。</p>
<form method="POST" action="hello.cgi">
<p>件名:
<br><input type="text" name="kenmei" value="これはテストです" size="72"></p>
<p>パスワード:
<br><input type="password" name="pass" value="" size="20"></p>
<p>本文:
<br><textarea name="honbun" cols="72" rows="3">
こちらに、ご記入願います。
</textarea></p>
<p><input type="submit" name="btn" value="送信します"></p>
</form>
</body></html>

ブラウザにて、表示を確認してください。

データを受け取る

method="GET" の場合は ENV['QUERY_STRING'] を分解・解析して、容易にデータを受け取ることができますが、method="POST" の場合は、簡単ではありません。よって、この面倒な部分をやってくれる道具を読み込んで使ってみましょう。hello.cgi を下記のように書き直して FTP 転送してください。

#!/usr/local/bin/ruby -Ks
require "cgi"
h = CGI.new
puts "Content-type: text/plain;charset=Shift_JIS"
puts
puts "●受け取ったフォームデータです。"
puts
puts "件名:"
puts h['kenmei']
puts
puts "パスワード:"
puts h['pass']
puts
puts "本文"
puts h['honbun']

ブラウザにて、データを送信して動作を確かめてみてください。

データを保存する

送られてきたデータを保存する上で重要な点は、大まかに下記の4点が考えられます。

  1. 保存されたデータは、なるべくオリジナルのデータに復元しやすいこと。蓄積された情報を整理したり再利用したりできるようにするために必要。
  2. セキュリティが考慮されており、悪ガキどもの出来心を誘わないこと。
  3. 読み書きがすみやかに支障無く行われ、「同時書き込み」等へのアクシデント対策がとられていること。
  4. 雷によるサージや、突然の停電や、おちゃめさんがサーバーのコンセントを抜く等の物理的障害にも強いこと。

まずは、ともかくデータを保存してみましょう。先に古典的な方法を紹介します。次にモダンな方法を紹介します。

テキストデータのnameとvalueをタブで区切って保存して、それを新たに読み込んで表示してみます。hello.cgi を以下のように書き換えてFTP転送してください。

#!/usr/local/bin/ruby -Ks
require "cgi"
g = CGI.new # フォームからのデータの受け取り
DataFile = "./datasumple.txt" # データのファイル名
NameList = ['kenmei','pass','honbun'] # 保存するキー(nameの値)のリスト。

# データの保存
f = open( DataFile, "w" )
for key in NameList do
  # データを加工してファイルに書き込み(Win改行⇒Mac改行⇒改ページ⇒UNIX改行⇒隠し文字¥0に置き換え。)
  f.printf "%s¥t%s¥n", key, g[key].gsub(/¥r¥n/,"¥n").gsub(/¥r/,"¥n").gsub(/¥f/,"¥n").gsub(/¥0/,"").gsub(/¥n/,"¥0")
end
f.close

# 先ほど保存したデータの読み出し
h = Hash.new
open( DataFile ).each do |line| # 1行ずつ取り出して、値を line に写し取ります
  key,val = line.split(/¥t/,2)  # 各行最初のタブでkeyとvalに分割します。
  h[key] = val.gsub(/¥0/,"¥n")  # 隠し文字¥0を改行¥nに戻して、ハッシュに格納
end

# Webページに出力
puts "Content-type: text/plain;charset=Shift_JIS"
puts
puts "●保存して読み出したデータです。"
puts
puts "件名:"
puts h['kenmei']
puts
puts "パスワード:"
puts h['pass']
puts
puts "本文"
puts h['honbun']

ブラウザにて動作を確認したら、FTPにて、保存されたファイル(datasumple.txt)の内容も確認してください。

便利な道具を使えばさらに簡単になります。文字列、配列、ハッシュなど、いわゆるオブジェクトと呼ばれるものを、そのままの状態で保存する「PStore」というツールがあるので使ってみましょう。全てのデータをみんなまとめて一つのデータファイルの中に格納できてしまうので、ファイルの配置や管理やらで悩む必要が無くなります。さらに、データの出し入れの効率が上がるから、検索等に要する時間が少なくなるものと思われます。試しに、hello.cgi を以下のように書き換えてFTP転送してください。

#!/usr/local/bin/ruby -Ks
require "cgi"                 # CGIを呼び出す
require "pstore"              # PStoreを呼び出す
DataFile = "./pstoretest.dat" # データのファイル名。拡張子は.datに限らなくてもよい
DataID = Time.now.to_i.to_s   # データの受信ごとに1秒刻みで違う名前にしてみた
g = CGI.new                   # フォームからのデータの受け取り

# データを保存します
dbw = PStore.new DataFile     # データファイルに取手を付ける
dbw.transaction do            # データの読み書きを行う(dbw.transaction do 〜 end)
  dbw[ DataID ] = g    # フォームからのデータを「DataFile」の中に保存
end

# 先ほど保存したデータを取り出します(DataIDを変えれば、古いのも取り出せます)
dbr = PStore.new DataFile
dbr.transaction do
  # 読み出しながら、Webページに出力
  puts "Content-type: text/plain;charset=Shift_JIS"
  puts
  puts "●保存して読み出したデータです。"
  puts
  puts "件名:"
  puts dbr[ DataID ]['kenmei']
  puts
  puts "パスワード:"
  puts dbr[ DataID ]['pass']
  puts
  puts "本文"
  puts dbr[ DataID ]['honbun']
  puts
  puts "●これまでに保存されている受信データの番号は、"
  puts dbr.roots.join(",")
  puts "です。"
end

ブラウザにて動作を確認してください。

もっとシンプル確実に、文字データのみをハッシュのようにして保存できる DBM, GDBM という選択肢もあります。 ⇒DBM,GDBM
DBM を使ってみましょう。 但し、残念ながら手許のWindowsPCでは使えないようです。サーバー内でなら動作します。 下記内容のhello,worldをいろいろ書き換えてFTP転送し、Terminalより実行してみてください。内容が、ファイルtest.dbに保存されて、どんどん増えていきます。

#!ruby -Ks
require 'dbm'
db = DBM.open("test", 0600) # データベースの名前をtestとしました。ファイル test.db に保存されます。
db['hello'] = "world"       # これだけで保存されます。
db.each do |key, value|     # 保存された値を全て呼び出してみます。ハッシュの扱いと同じ。
  print key, "=>", value, "\n" 
end
db.close

応用例:掲示板

PStoreを使った簡単な掲示板です。全く基本的な機能しかありません。(つまり、このままでは実用にはなりません。)

#!/usr/local/bin/ruby -Ks
# 内容がhtmlファイルであることを先に出力しておく。
print "Content-type: text/html;charset=Shift_JIS\n\n"
# クラスライブラリ読み込み
require "cgi"
require "pstore"
# 初期設定
DataFile = "./bbstestdata.dat"
DataID = Time.now.to_i.to_s

# html雛形(あとで「LOGMESSE」をメッセージに置換)
html = <<EOF
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html lang="ja">
<head>
  <meta content="text/html; charset=Shift_JIS" http-equiv="content-type">
  <meta content="text/css" http-equiv="Content-Style-Type">
  <style type="text/css"><!--
  body{color:#028;}
  p{margin:0.2em 1em 0.2em 1em;}
  .hr{border-bottom:dotted 1px silver;}
  .dd{margin-left:3em;}
  --></style>
  <title>TestBBS</title>
</head>
<body>
<h1>テスト掲示板</h1>
<form method="POST" action="#{ ENV['SCRIPT_URL'] }" class="hr">
<p>件名:
<br><input type="text" name="kenmei" value="" size="72"></p>
<p>本文:
<br><textarea name="honbun" cols="72" rows="3">
</textarea></p>
<p><input type="submit" name="btn" value="送信"></p>
</form>
LOGMESSE
</body></html>
EOF

# 各メッセージ雛形
messe = <<EOF
<div class="hr">
<p>件名:KENMEI</p>
<p>本文:</p>
<p class="dd">HONBUN</p>
</div>
EOF

# ▲初期設定ここまで -------------------------------------------------------

# 文章のhtml化メソッド
def texttohtml(str="")
  str = str.gsub(/&/,"&amp;").gsub(/</,"&lt;").gsub(/>/,"&gt;").gsub(/"/,"&quot;")
  str = str.gsub(/\t/,"&nbsp;&nbsp;").gsub(/\f/,"\n").gsub(/\r\n/,"\n").gsub(/\r/,"\n")
  return str
end

# データ入出力と加工
messages = ""
db = PStore.new DataFile
db.transaction do
  # フォームデータ受け取り
  g = CGI.new
  # データの保存
  unless g['kenmei'] == "" or g['honbun'] == "" then
    h = Hash.new
    h['kenmei'] = texttohtml( g['kenmei'] )
    h['honbun'] = texttohtml( g['honbun'] )
    db[ DataID ] = h
  end
  # データの読み出しと改行の処理
  for num in db.roots.sort.reverse do
    kenmei = db[num]['kenmei'].chomp.gsub(/\n/,"")
    honbun = db[num]['honbun'].chomp.gsub(/\n/,"<br>")
    messages += messe.sub(/KENMEI/,kenmei).sub(/HONBUN/,honbun)
  end
end

# メッセージ出力
puts html.sub(/LOGMESSE/,messages)

------▽ ここから先は少し高度な内容になります。読まなくても簡単な掲示板なら作れます ▽------

リレーショナル型データベース

集合論の知識を生かして、表(table)に書いたデータを上手にブロックの組み合わせに分けると、冗長さが省かれれて格段にデータ管理の効率が向上することが知られています。table 同士の関係という意味で、リレーショナルです。この方法を扱うのがSQLと呼ばれるデータベース専門の言語です。

このSQL言語を使って、さらに、多方向から同時に殺到するデータの読み書きや、物理的な突然のトラブルからの復旧に強くなる専門のソフトウエアが開発されています。フリーで入手できるものとして、PostgreSQLMySQL等が有名です。これらのソフトをRubyから操作してやることで、システムの信頼性向上や検索速度の飛躍的なアップが望めます。

よって、掲示板やブログサイトなどのCGIを構築するにあたり、データはリレーショナルデータベース専用のソフトで管理することが理想的です。Ruby に装備されている、PStore はバージョン間の互換性や動作速度に関する不安を抱えているので、できればPostgreSQL等の専用ソフトを使いたいのですが、筆者はまだapサーバーの中にSQL関連のアプリケーションソフトを見いだしていません。どこかにあると思うのですが…

参考:
TECHSCORE - SQL - http://www.techscore.com/tech/sql/
Postgres(Ruby PostgreSQL 拡張モジュール) http://www.postgresql.jp/interfaces/ruby/index-ja.html
MySQL/Ruby http://www.tmtm.org/mysql/ruby/

パスワードは暗号化してから保存する:cryptを使う

掲示板の削除パスや、ブログの管理用パスワードなど、パスワードの管理が必要になる場合があります。 容易には見られない場所に保存しておいたつもりでも、僅かな隙をついて盗み見られてしまう可能性もあります。 そんなときに、パスワードが暗号化して保存してあれば、当面の間の時間稼ぎになります。

暗号化のサンプルを用意しました。下記のスクリプトをRDEの上の窓に貼付けて試してみてください。
又は、適当なファイル名を付けて、FTP転送し、Terminalにて、ruby ファイル名

# パスワードの暗号化
putpass = "hello" # 設定されたパスワード
# saltは、半角英数と./の中からランダムに選んだ2文字(例:i8,jD,9l,...)
salt = [rand(64),rand(64)].pack("C*").tr("\x00-\x3f","A-Za-z0-9./")
savedpass = putpass.crypt(salt) # 暗号化
puts savedpass # この文字列を保存しておく

# パスワード照合
entpass = "hello" # 照合用に入力されたパスワード
if entpass.crypt(savedpass) == savedpass then
  puts "OK"
else
  puts "NG"
end

メソッドを作っておくと便利です

#!ruby -Ks
# 暗号生成メソッド
def ango(passwd)
  srand
  salt = [rand(64),rand(64)].pack("C*").tr("\x00-\x3f","A-Za-z0-9./")
  return passwd.crypt(salt)
end
# 暗号照合メソッド
def syogo(entpass,savedpass)
  entpass.crypt(savedpass) == savedpass
end
# チェックメソッド
def checkpass(passwd)
  if passwd == nil or passwd == "" then
    puts "エラー:パスワードが入力されていません。"; exit 0
  elsif /[^a-zA-Z0-9]/ =~ passwd then
    puts "エラー:入力されたパスワードに不適切な文字が含まれていました。"; exit 0
  end
end
# 実行
STDOUT.sync = true
puts "登録用パスワードを入力してください"
putpass = gets.chomp
checkpass(putpass)
savedpass = ango(putpass)
puts "パスワードが暗号化されました" , savedpass
puts "照合用パスワードを入力してください"
entpass = gets.chomp
if syogo(entpass,savedpass) then
  puts "パスワードは一致しました。"
else
  puts "パスワードは一致しませんでした。"
end

厳密には「暗号化」という呼び方は正しくありません。暗号は鍵のような仕組みを使って解読できなければなりません。cryptを使ったやり方は、いわばデタラメなゴミを混ぜてかき回すことをしているので、「総当たり」以外の手段で復元することは不可能です。cryptは「粉飾」とか「隠蔽」とか言った方が的確かもしれませんが、誰もそうは呼びません。

参考:cryptによる暗号化の情報

参考:あまりお勧めできませんが、「etc」の助けを借りて、ログインパスワードを使うこともできます。マニュアルのcryptに説明があります。 ⇒使用例:passtest.cgi

セキュリティ対策

イタズラ防止のために簡単にできることをいくつか紹介します。

ファイルの保存や読み出しに使える文字、<, > を保存しない。
保存する前に、htmlの「特殊文字」に変換してしまいます。htmlの表示に出力する場合は、そのまま使えます。ついでにHTMLに表示するときに変換する必要のある、"と&も変換してしまいましょう。
str = str.gsub(/&/,"&amp;").gsub(/</,"&lt;").gsub(/>/,"&gt;").gsub(/"/,"&quot;")
元に戻すときには順番に注意してください。&が最後です。('"'は、ダブルクオーテーション"をシングルクオーテーション'ではさんだもの)
str = str.gsub(/&gt;/,">").gsub(/&lt;/,"<").gsub(/&quot;/,'"').gsub(/&amp;/,"&")

データファイルを隠します。見せるつもりのないものを勝手に見られては困ります。少なくともWeb上からは見えなくする必要がありますし、できれば、同じサーバーを利用している他のユーザーからも隠したいところです。

更新日:2005年02月

© 2004-2005 S.Konno, all rights reserved. [ 質問 ] [ TOP ] [ J2J ] [ 目次 ] [<<戻る ] [ 次へ>>]