Rubyで実装するCGIデコード
CGIとは、Webサーバでプログラムを動作させる仕組みです。CGIプログラムは、Perl製のものが多いですが、PythonやRubyなども動作させることが出来ます。このページでは、CGIでの処理の前提となるURLエンコード(パーセントエンコーディング)を、Rubyでデコードする方法を説明します。
CGIでデコードを行なう理由、HTTPリクエストの概要につきましてはPerlで実装するCGIデコードで説明しています。重複する内容となるため、そちらをご覧下さい。
目次
Rubyでユーザーからの入力を受け取る方法
RubyでGET送信を受け取るには、環境変数ENVを利用し、キーQUERY_STRINGで取得します。環境変数とは、Webサーバが設定した情報の連想配列(ハッシュ)になっています。環境変数には、GET送信かPOST送信かの情報も入っているため、判定処理に利用できます。
POST送信を受け取るには、標準入力から取得します。Rubyでの標準入力はSTDINまたは$stdinで、全取得read、一行取得readline、リストに一行ずつ格納readlinesが使えます。一行ずつ処理する場合はeach_lineが利用できます。
# GET送信のクエリーを取得
q = ENV['QUERY_STRING']
# POST送信されたデータを一行分取得
s = STDIN.readline
Rubyで正規表現を使う方法
受け取った文字列は、決まったルールで変換された状態になっています。これを復元するには、一定の法則で逆処理をすることになります。文字の出現法則で決まった変換が行えるコーディング方法に、正規表現があります。他に変換テーブルを作成し、通す方法がありますが、今回は正規表現でエンコードを行っていきます。
注意点として、Rubyでは正規表現が複数行モードとなっているため、^や$が先頭や末尾の意味では利用できない点があります。先頭は\A、末尾は\zで適合させます。
# Rubyの正規表現による置換
s = s.gsub(/[\d]+/,'') #数値を除去
# 正規表現で判定
if s =~ /\A\d+\z/
# 数値のみ
end
# $を末尾の意味で使用した場合
li = '''1
2
3
'''
li = li.gsub(/$/,'$')
# 結果
1$
2$
3$
$
Rubyのグローバル変数とローカル変数
Rubyには、ブロック内で利用できるローカル変数と、どのブロックでも利用できるグローバル変数があります。グローバル変数は、変数名の先頭に$を付けることで作成できます。
# どこでも使える変数を作る
$_GET = {}
$_POST = {}
$_COOKIE = {}
$_FILES = []
$_FILES だけ複数ファイルアップロード対応のため配列にしています。
RubyでGETリクエストを解析する
リクエストの種類は、環境変数ENVのREQUEST_METHODで確認することが出来ます。GET送信の受け取りデータはURL情報なので、POST送信時も情報が含まれており、処理上必要になることもあるので、常に取得する構成にします。
またクッキーの処理も実装します。GETのURLには、?以降にkey1=value1&key2=value2の状態でデータが並んでいます。クッキーではkey1=value1; key2=value2; となります。「&」繋ぎと「; 」繋ぎの差があります。
# リクエストのメソッド判定
if ENV['REQUEST_METHOD']=='POST'
# ここでPOST用処理
$_GET = decode(ENV['QUERY_STRING'])
$_COOKIE = decode(ENV['HTTP_COOKIE'],'; ')
今回は、クラスとして作成します。Rubyでは、クラス内での自メソッドをレシーバ(self)指定なしで呼び出せます。decode()部分がメソッドになります。
# URLデコード処理
private
def decode(buf,de='&')
# 変数の初期化
h = {}
# 正規表現をコンパイル
# 16進数表現の置換用
r = /%([a-fA-F0-9][a-fA-F0-9])/
# 区切り文字で分離して処理
buf.split(de).each do |s|
(key,val) = s.split('=')
val = '' if val.nil?
# +と半角スペースの置換
key.tr!('+',' ')
# 16進数表現を文字に復元
key.gsub!(r){[$1].pack('H2')}
val.tr!('+',' ')
val.gsub!(r){[$1].pack('H2')}
# ハッシュに保存する処理
setHash(key,val,h)
end
return h
end
置換の正規表現で、置換対象とする対象文字は()で指定できます。()内で適合した文字列は、$1や$2などの連番で指定できます。今回は%と16進数の2文字から元の文字列に変換しています。
# ハッシュに保存する処理
private
def setHash(key,val,h)
# キーがすでにある場合、値を配列に
if h.key?(key)
# 値が配列の場合、最後に追加
if h[key].class==Array
h[key] << val
# 値が文字列の場合、配列に
else
h[key] = [h[key],val]
end
else
h[key] = val
end
end
Rubyでは、関数の引数は参照渡しになっているため、関数で受け取った変数を書き換えると、元の変数も書き換わっています。
URLやフォームからの送信データは、基本的にkey1=value1&key2=value2の構成で渡ってきます。ただし、例えばチェックボックスのように同じ項目を複数選択できる場合、同じkeyが重複して渡ってくる事が有ります。PHPでは、タグのname属性の末尾に[]を付けることで配列化が行えますが、今回は既にデータがある場合に配列化する構成にします。
RubyでPOSTリクエストを解析する
POST送信は、一般的に利用されているもので2つのエンコード方法が存在します。formタグのデフォルトであるapplication/x-www-form-urlencodedと、ファイルをアップロードする場合のmultipart/form-dataです。前者はGET送信と同じエンコード方法で、後者はboundaryと呼ばれる区切り文字で、フォームの各データを区切った状態で送信します。
# リクエストのメソッド判定
if ENV['REQUEST_METHOD']=='POST'
# multipart/form-data を判定
if ENV['CONTENT_TYPE'].match(/multipart/form-data; boundary=(.+)/)
# 区切り文字列のboundaryを取得して渡し
$_POST = multipart($1)
else
# 標準入力から取り出して渡し
$_POST = decode(STDIN.read)
end
end
Rubyでmultipart/form-dataを解析する
ファイルのアップロード時は、フォームタグにenctype="multipart/form-data"を指定して、他のフォーム要素と一緒に、ファイルデータも受け取ることになります。各フォーム要素を区切る文字列として、boundaryが利用されるので、これをフォームデータの終端として利用します。
フォームデータの開始行は、改行のみの行になっています。ただし、フォームデータ内に改行のみの行が存在し得るので、データが空の時のみ開始行と判定します。
# マルチパートデータを処理
private
def multipart(bound)
# 変数の初期化
key,val,file,type = '','','',''
h = {}
f = []
rec = 0
# 正規表現をコンパイル
# 区切り文字判定
r1 = /#{bound}/
# ファイル判定
r2 = /Content-Disposition: form-data; name="([^"]+)"; filename="([^"]+)"/
# 通常のフォーム項目
r3 = /Content-Disposition: form-data; name="([^"]+)"/
# アップロードされたファイルタイプ
r4 = /Content-Type: (.+)/
# データ部の開始判定用
r5 = /Arnz/
# 標準入力から一行ずつ処理
STDIN.each_line do |li|
# 正規表現の文字コードエラー対策
li_utf8 = li.encode('utf-8',:invalid=>:replace)
# 区切り文字があった場合
if li_utf8 =~ r1
# キーがあった場合
if key!=''
# 末尾の改行を除去
val.chomp!
# フォームデータがファイルの場合
if file!=''
# 通常のフォームデータ
else
setHash(key,val,h)
end
key = ''
val = ''
end
rec = 0
# データの結合
elsif rec==1
val << li
# フォームのname属性とアップロードファイル名を取得
elsif li_utf8 =~ r2
key = $1
file = $2
# 通常のフォームデータの場合
elsif li_utf8 =~ r3
key = $1
# アップロードされたファイルタイプ
elsif li_utf8 =~ r4
type = $1.chomp
# データ内容の開始判定
elsif rec==0 && li_utf8 =~ r5
rec = 1
end
end
# ファイル情報を代入
$_FILES = f
return h
end
Rubyで一時ファイルを作成する
Rubyで一時ファイルを作成するには、標準ライブラリを利用できます。標準ライブラリなので、インストール作業は必要なく、requireするだけです。一時ファイルとは、/tmp/ディレクトリに保存されるファイルですが、Rubyでは作成後すぐ、またはGCのタイミングで削除されます。
# 標準ライブラリの読み込み
require 'tempfile'
# フォームデータがファイルの場合
if file!=''
# 自動削除対策
GC.disable
# 一時ファイルを作成
t = Tempfile.open(mode='wb')
# 一時ファイルに書込
t.write(val)
# falseでclose時に削除しない
t.close(real=false)
# 保持用のファイル情報を登録
f << {'name'=>key,'up_name'=>file,'type'=>type,'tmp_name'=>t.path}
file = ''
type = ''
受け取った入力データの利用方法
以上でデコード面は完成しました。ファイルはクラス化して再利用できるようにします。またデコードしたファイルデータは、独自の仕様で保持されているため、保存を簡単に行えるようにメソッド化しておきます。
# 一時ファイルから移動する
public
def move(f,name)
# ファイル指定がおかしいか、ファイル名の指定がない時
if !File.exists?(f['tmp_name']) || name==''
return 0
end
begin
# 一時ファイルを読み込んで新ファイルを作成
File.open(f['tmp_name'],'rb') do |tf|
File.open(name,'wb') do |nf|
nf.write(tf.read)
end
end
File.unlink(f['tmp_name'])
rescue
return 0
end
end
完成したモジュールファイルはこちらで公開しています。
アップロードされたファイルをチェックして、利用できる位置へ移動、保存する処理は、Webアプリごとに実装されるものになります。サンプルとなるコードを掲載します。
# ライブラリの読み込み
require './CgiDecode.rb'
# インスタンス作成、デコードと変数への設定
q = CgiDecode.new
# $_GET,$_POST,$_COOKIE,$_FILESが利用可能
# アップロードされたファイルを順番に処理
$_FILES.each_with_index do |f,i|
ext = File.extname(f['up_name'])
if ext=='.csv'
q.move(f,"#{i}#{ext}")
end
end
最後にCGIでのデコードが出来ているか確認できるコードを掲載します。$_GETや$_POST、$_COOKIEは連想配列なので、name属性をキーとしてvalueを取得できます。
#!/usr/bin/ruby
# coding: utf-8
# ライブラリの読み込み
require './CgiDecode.rb'
# インスタンス作成、デコードと変数への設定
q = CgiDecode.new
# 変数内を確認する関数
def p_r(&b)
s = b.call
a = b.binding.eval(s.to_s)
print "#{s}=" unless s.to_s=='val'
if a.class==Hash
puts '{'
a.each do |key,val|
if val.class==Array
vs = ' "' << key << '" => ['
val.each do |v|
vs << '"' << v << '",'
end
puts vs.chomp(',')<<'],'
else
puts ' "' << key << '" => "' << val << '",'
end
end
puts '}'
elsif a.class==Array
puts '['
a.each do |val|
p_r{:val}
end
puts ']'
else
puts '"'<<a<<'"'
end
end
puts "Content-Type: text/html; charset=utf-8\n\n"
puts '<div style="white-space: pre;">';
p_r{:$_GET}
p_r{:$_POST}
p_r{:$_COOKIE}
p_r{:$_FILES}
puts '</div>';
h = <<'HTML'
<a href="?key1=日本語&キー=値&key1=複数データ">GET確認</a>
<form method="post">
<input type="text" name="email" value="form1@example.com">
<label>
<input type="radio" name="radio1" value="ラジオ">ラジオ
</label>
<label>
<input type="radio" name="radio1" value="ボタン">ボタン
</label>
<label>
<input type="checkbox" name="check1" value="チェック">チェック
</label>
<label>
<input type="checkbox" name="check1" value="ボックス">ボックス
</label>
<textarea name="text">複数行
テキスト</textarea>
<input type="submit" value="送信">
</form>
<form method="post" enctype="multipart/form-data">
<input type="text" name="email" value="form2@example.com">
<label>
<input type="radio" name="radio1" value="ラジオ">ラジオ
</label>
<label>
<input type="radio" name="radio1" value="ボタン">ボタン
</label>
<label>
<input type="checkbox" name="check1" value="チェック">チェック
</label>
<label>
<input type="checkbox" name="check1" value="ボックス">ボックス
</label>
<textarea name="text">複数行
テキスト</textarea>
<input type="file" name="f1">
<input type="file" name="f1">
<input type="file" name="f2">
<input type="submit" value="送信">
</form>
<style>
form {
margin: 50px auto;
width: 400px;
background: #eee;
}
input[type="text"],input[type="submit"],label,textarea {
display: block;
padding: 5px;
margin: 5px 0;
width: 100%;
}
</style>
HTML
print h