ソースコードで学ぶWebプログラミング

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