CGI
CGIメモの目次
- ユニーク(一意)な識別番号
- flock()などファイルロックを使わない処理
- CGIからリダイレクトする方法
- コンテンツを圧縮配信する
- CGI経由で拡張子を指定してファイルをダウンロードさせるテクニック
- バイナリのダウンロード処理
- CGIで集計した結果をExcelデータでダウンロードさせる
- シフトJISでCGIを書くときは・・・
- dieをブラウザで出力する
- 生IPの抜き方
- メールによるホームページの更新
- CGIからメールを送信する
- sendmailを使わずにメールを送信する
- おまけ
ユニーク(一意)な識別番号
色々やり方はありますが、私がよく使うのが以下の方法です。
my $id = $^T.$$; print $id;
$^T はプログラムの開始時刻のtime値で、$$はプロセスIDです。この2つの値は単独ではユニークな値とは成りえませんが、組み合わせるとユニークな値になります。
- 同時刻にアクセスしたユーザは、$^T が同じ
- プロセスIDは、ループする
また、IDからアクセス時刻を知りたいなら、"$^T-$$" のように間にハイフンなどを挟んで、splitすればよいでしょう。
flock()などファイルロックを使わない処理
インターネットの仕組み上、同ページへの同時アクセスが容易に想像されるので、掲示板CGIなどでは、同時書き込みを回避するために flock や、独自のロック処理を行ったりします。これはCGIの仕様で、同ファイルに書き込む から必要なので、アクセス毎に別のファイルに書き込むという仕様にしてしまえば、ロック処理は必要なくなります。削除や修正がファイル単位で出来て処理が非常に楽にもなります。
書き込み
- データを格納するフォルダを用意する
- アクセス毎にユニークなファイル名のファイルを作成する
- 作成したファイルに書き込む
#!/usr/bin/perl -w use strict; my $id = "$^T-$$"; my $path = "./datas/$id.txt"; open(WF, '>'. $path) or die; print WF $path.'の書き込み内容です。'; close(WF);
読み込み
- データ格納フォルダ内の全データのファイル名リストを得る
- 必要に応じてファイル名のリストをソート、抽出する
- 抽出されたリストの各ファイル内容を読み込む
foreach(sort glob('./datas/*.txt')){ open(RF, $_) or die; print <RF>, "\n"; close(RF); }
書き込み削除処理
- 指定ファイルを消すだけ
unlink($path) or die; exit;
CGIからリダイレクトする方法
HTMLの<META http-equiv="refresh" content="0;URL=http://hogehoge/">のように、別ページへいきなり飛ばしてやりたい時は、CGIからLocationヘッダを標準出力します。
print "Location: http://hogehoge/\n\n";
コンテンツを圧縮配信する
この機能は、ブラウザ依存の機能です。環境変数 HTTP_ACCEPT_ENCODING に gzip が表示されていれば、そのブラウザで正常にデコード表示することができます。 環境変数一覧の表示サンプルはPWSのページをご覧ください。(といいつつ、その環境変数一覧を圧縮して表示するサンプルを掲載しているという、本末転倒な・・・。)
- コンテンツヘッダに Content-encoding: gzip を加える
- gzip圧縮したHTMLを配信する
という手順で行います。下のサンプルは動的にgzip圧縮を生成して表示させています。なお、gzip はインストールされている必要があります。テストした環境はWindows98+PWS です。
#!/usr/bin/perl -w # gzip_test - HTMLを圧縮出力する use strict; use CGI qw(:standard); local $/; my $TITLE = '環境変数一覧(gzip圧縮出力)'; my $STYLE = <DATA>; print header(-charset=>'Shift_JIS', -'Content-encoding'=> 'gzip'); open(STDOUT,"| c:\\gzip\\gzip.exe -1 -c") or die "gzip起動失敗"; print start_html(-title=>$TITLE, -style=>$STYLE); print h1($TITLE); print table map { Tr[th $_, td $ENV{$_}] } sort keys %ENV; print end_html; exit; __DATA__ body { text-align: center; } table, th, td { border: 1px double navy; } th { background-color: teal; color: white; } td {font-weight: bold; color: gray; } h1 { color: navy; }
補足。gzipのオプションの -1 は速度重視。-9 まであり -9 は圧縮率重視です。
CGI経由で拡張子を指定してファイルをダウンロードさせるテクニック
ダウンロードのログを取得したい場合など、よくCGI経由でダウンロードさせたりしますが、直接CGIにアクセスさせると、保存ダイアログで拡張子が.cgi になったりします。一度ダウンロードしてから拡張子を変更するとかならないようにする為には、さてどうするでしょう?
答えは、環境変数 PATH_INFO を付けて、CGIにアクセスするです。
HTMLからCGIを呼び出すならフォームタグは次のように書きます。
<form action="./cgi-bin/hoge.cgi/foo.lzh" method="post">
例として、foo.lzh というファイル名で保存して欲しい場合です。foo.lzh は実際には存在しないファイルで、hoge.cgiから吐き出した標準出力の保存名ということになります。
バイナリのダウンロード処理
上記のCGI部(hoge.cgiにあたる部分)です。得意の手抜きで書いてなかったのですね(^^;以下はそのダウンロード処理のサンプルです。ダウンロードさせて、ログに書き込むだけの簡単なものです。
#!/usr/bin/perl -w # download.cgi - ダウンロードCGIのサンプル use strict; use CGI qw/:standard/; use CGI::Carp qw/fatalsToBrowser/; my $basedir = '/archives/sample'; my $path = path_info; # バイナリ出力 local $| = 1; print header('application/octet-stream'); open(DL, $basedir.$path) or die("$path の読み込みに失敗!\n"); binmode(DL); binmode(STDOUT); while(read(DL, my $buf, 1024)){ print $buf; } close(DL); # ログを残してみる open(LOG, '>> download.log'); flock(LOG, 2); print LOG "$^T\t$ENV{REMOTE_ADDR}\t$path\n"; close(LOG); exit;
CGIで集計した結果をExcelデータでダウンロードさせる
これは私が結構使ってる技ですが、モジュールとか使わずに簡単にできます。
「TAB区切りのテキストファイルを.xls という拡張子にするだけでExcelファイルとして読みこめる」という原理です。
つまり、集計結果などをTAB区切りのデータにして、application/msexcel というヘッダーで吐き出すだけです。サンプル書きますので参考にしてください。
form.html
<html><body> <form action="xls.cgi/sample.xls"><input type="submit"></form> </body></html>
xls.cgi
#!perl -w use strict; print "content-type: application/msexcel\n\n"; print "1\t2\t3\t4\n"; print "5\t6\t7\t8\n"; print "9\t10\t11\t12\n"; exit;
シフトJISでCGIを書くときは・・・
私はCGIは殆どShift_JISで書きます。EUCは確かに文字化けなど無くよろしいのですが、開発環境がWindowsの場合、少々扱いが面倒です。
Shift_JISでCGIを書くときは、全ての文字リテラルをシングルクオートで囲む とPrint文などで特定コードの文字化けが無くなります。よく文字化け対象文字の前に¥マークをつけて回避したりしますが、止めたほうがいいです。(気づかずに納品してしまったという経験から(^^;)
my $paso = "パソ\ランド"; print "パソ\コン教室「$paso」の受講お申\込み";
こういうものは、以下のようにかきます。
my $paso = 'パソランド'; print 'パソコン教室「' . $paso . '」の受講お申込み';
dieをブラウザで出力する
dieをブラウザに出力するにはfatalsToBrowserをCGI::Carpモジュールの引数に指定します。これだけでブラウザにエラー出力できますが、自分の好きなデザインにしたい場合は、set_messageにエラー出力用の関数を渡します。(HTTPヘッダは要りません)
こうすることでブラウザに致命的エラーが表示され、開発者はデバッグしやすくなるでしょう。反面エンドユーザーにはよろしくないメッセージが表示されるかもしれません。
#!/usr/bin/perl -w # browsedie - dieをブラウザで出力する use strict; use CGI::Carp qw(fatalsToBrowser set_message); BEGIN{ sub print_error { my $text = shift; $text =~ s/\n/<BR>/g; print '<HTML lang="ja"><HEAD>'; print '<META http-equiv="Content-Type" content="text/html; charset=Shift_JIS">'; print '<TITLE>CGI ERROR!!</TITLE>'; print '<STYLE>* {text-align: center}</STYLE></HEAD>'; print '<BODY><H1>CGI ERROR!!</H1><HR><P>'. $text .'</P><HR>'; print '<P><A href="JavaScript:history.back();">[戻る]</A></P>'; print '</BODY></HTML>'; exit; } set_message(\&print_error); } # メイン(エラーのテスト) open(RF) or die $^E; #die "CGIエラーのテストなのです。\nこのエラーはダミーです。\n";
生IPの抜き方
とりあえず Java Appletでソケットを使ったやり方です。AppletからSocketでサーバCGI(ipsrv.cgi)を呼び出し、呼び出し元のログを取ります。このサーバCGIは、単純にREMOTE_ADDRを読み込みログを取るだけです。結局、プロクシを経由しない何者かがサーバCGIにアクセスすることによって生IPを抜くことができます。
今回のサンプルでは、アプレットのHTMLの代わりにcgiを用意しました。(ipcli.cgi)このCGIを見ると、<FORM>タグがありますが、掲示板CGIなどで使う場合の使用例です。hiddenに識別子を仕込み必要なら、生IPログの識別子と照らし合わせを行えばIPを特定できるでしょう。=(送信しても何もおこらないので注意!)= 送信したら抜いたIPを表示するサンプルを加えました。(2003.09.12)
あと、このサンプルを設置したものを見たいという要望がありましたので設置しておきました。もしかしたら期間限定になるかもしれません。抜けるかどうかお試しください。(2003.09.12)
http://tuka.s12.xrea.com/cgi-bin/ipcli.cgi
またサンプルは、HTMLの<PARAM>タグでサーバなど設定できるようにしてあります。コンパイルしなくても試すことができると思います。
成功すると、ip.log に
1061895604-4259,44.33.222.111,hogehoge.net
のように取得されます。
■Download >> namaip.lzh
PickingIP.java
// @author tuka. Created on 2003/08/26 import java.applet.*; import java.net.*; import java.io.*; public class PickingIP extends Applet{ String user; String host; String cgi; public void init(){ user = getParameter("USER"); host = getParameter("HOST"); cgi = getParameter("CGI"); try{ Socket socket = new Socket(host, 80); PrintWriter pw = new PrintWriter(socket.getOutputStream()); pw.print("GET http://" + host + cgi + "?" + user + "\r\n\r\n"); pw.flush(); socket.close(); }catch(Exception e){ } } }
ipsrv.cgi
#!/usr/bin/perl -w # ipsrv.cgi - 生IP抜きのサーバCGI use strict; use Socket; my $logfile = "ip.log"; my $user = $ENV{QUERY_STRING} or exit; my $ip = $ENV{REMOTE_ADDR} or exit; my $host = gethostbyaddr(inet_aton($ip), 2); open(LOG, ">> $logfile") or exit; flock(LOG, 2); print LOG "$user,$ip,$host\n"; close(LOG); exit;
ipcli.cgi
#!/usr/bin/perl -w # ipcli.cgi - 生IP抜きのクライアントCGI use strict; my $host = "tuka.s12.xrea.com"; # サーバ my $cgi = "/cgi-bin/ipsrv.cgi"; # ログを取得するCGI my $user = "$^T-$$"; # セッションIDなど識別子 print "Content-Type: text/html;\n\n"; print <<"HTML"; <html> <head> <title>Applet test</title> </head> <body> <applet code="PickingIP.class" width=0 height=0> <param name=cgi value="$cgi"> <param name=host value="$host"> <param name=user value="$user"> </applet> <form action="dispip.cgi" method="post"> <input type="hidden" name="sid" value="$user"> 名前:<input type="text" name="name"> <input type="submit"> </form> </body> </html> HTML
以下に抜いたIPを表示するサンプルを追加しておきます。掲示板などに組み込む時など参考にしてください。
dispip.cgi
#!/usr/bin/perl -w # dispip.cgi - 抜いたIPを表示するサンプル use strict; use CGI qw/:standard/; my $sid = param('sid'); my $name = escapeHTML(param('name')) || '名無し'; my($ip, $host); if(open(RF, 'ip.log')){ while(<RF>){ my($_sid, $_ip, $_host) = split(','); if($_sid eq $sid){ $ip = $_ip; $host = $_host; chomp($host); last; } } close(RF); } $ip ||= '抜けませんでした!(;_;)'; $host ||= '解りませんでした!(;_;)'; print header(-charset=>'Shift_JIS'); print start_html(); print $name . 'さんのIPは...' . $ip . br; print 'ホストは...' . $host; print end_html(); exit;
メールによるホームページの更新
私はこの技を結構頻繁に使います。メールと連動してホームページを更新する方法で、覚えると色々なプログラムに応用できます。最近だと、あるプロジェクトのホームページ更新履歴をメールで更新できるようなものを作成しました。
考えてみると、メールサーバに届いたメールと連動してプログラムを起動させるのは、結構難しいような気がしますが、実は簡単です。
.forward を使います。.forward は通常、メールを別アドレスに転送するときに使います。転送先のメールアドレスを列記したテキストファイルで、サーバのホームディレクトリに.forward ファイルを置くことで、自動的にメールが転送されます。
実はこの .forward にプログラムへのパイプを記述することで、届いたメールが自動的にプログラムに渡されます。
"| ./public_html/cgi-bin/mailupdt.pl"
のように .forward に記述します。
mailupdt.pl 内では、標準出力に転送されたメールを得ることができます。テストするだけなら以下のような簡単な内容で hoge.html を生成できます。
#!/usr/bin/perl -w # メールをプログラムで受け取るサンプル use strict; use CGI qw/:standard/; open(WF, '> /home/hoge/public_html/hoge.html') or die; print WF start_html; print WF join(br, <STDIN>); print WF end_html; close(WF);
実際には、メールの内容を解析する処理と、パスワードをメール内に記入させたりなど、セキュリティ面の処理も必要です。でないと、誰でもメール送信してHPを更新できてしまうからです。
CGIからメールを送信する
sendmailを使った方法です。単純にsendmailすると日本語のメールヘッダが文字化けしてしまいます。簡単に変換するには、Jcode.pm を使うとよいでしょう。
$subject = jcode($subject)->mime_encode;
Jcodeが利用できない場合は、MIME::Base64を使います。
$subject = '=?ISO-2022-JP?B?' . encode_base64($subject, '') . '?=';
尚、前者のようにJcodeで処理する場合でも内部で、MIME::Base64を呼んでいるので必要です。
サンプルは、メールを送信するCGIのサンプルです。
open(MAIL,"| /usr/lib/sendmail -t");
の部分のパスはサーバの環境にあわせて修正してください。
#!/usr/bin/perl -w # sendmail - 日本語でメール送信テスト use strict; use lib qw/lib/; use CGI qw/-no_xhtml :standard/; use CGI::Carp qw/fatalsToBrowser set_message/; use Jcode; charset('Shift_JIS'); if(param('send')){ my $from = 'SendmailCGIサンプル'; my $to = param('to') or die("宛先を入力してください\n"); my $subject = param('subject') || '無題'; my $message = param('message') || die("本文を入力してください\n"); $from = jcode($from)->mime_encode; $subject = jcode($subject)->mime_encode; sendmail($from, $to, $subject, $message); print header; print start_html(-title=>'メール送信完了', -lang=>'ja'); print 'メールを送信しました'. end_html; }else{ print header; print start_html(-title=>'メール送信テスト', -lang=>'ja'); print start_form; print 'To: ' . textfield('to'), br; print 'Subject: ' . textfield('subject'), br; print 'Message: ' . textarea('message'), br; print submit('send'), end_form, end_html; } sub sendmail { my ($from, $to, $subject, $body) = @_; open(MAIL,"| /usr/lib/sendmail -t"); print MAIL "From: $from\n"; print MAIL "To: $to\n"; print MAIL "Subject: $subject\n"; print MAIL "\n"; print MAIL $body; close(MAIL); }
sendmailを使わずにメールを送信する
Net::SMTPモジュールを使います。SMTPを直接使うので、CGI稼動サーバで sendmail が利用できなくてもメールを送信できます。下記サンプルは日本語用に作ってないので、日本語を扱う場合は上のsendmailのやり方とかを参考にしてください。
#!/usr/bin/perl -w # smtpmail.pl - SMTPを指定してメール送信 use strict; use Net::SMTP; my $SMTP = 'YOUR.SMTP'; my $FROM = 'FROM@DOMAIN'; my $TO = 'TO@DOMAIN'; smtpmail($SMTP, $FROM, $TO, 'TEST', 'SMTP MAIL TEST', 'SMTP MAIL TEST2'); exit(0); sub smtpmail { my($sv, $from, $to, $subject, @body) = @_; my $smtp; $smtp = Net::SMTP->new($sv); $smtp->mail($from); $smtp->to($to); $smtp->data("From: $from\n", "To: $to\n", "Subject: $subject\n\n", join("\n", @body)); $smtp->dataend(); $smtp->quit; }
おまけ
XREAのPATH_INFO
つかのぺの設置している、S12サーバーでは、環境変数 PATH_INFOが一般的に出力されるPATH_INFOと違って、PATH_INFOが2回繰り返されるような感じになります。例えば、以下にアクセスすると、通常は/path/info/dayo がPATH_INFOになりますが、/path/info/dayo/path/info/dayo となってしまうのです。
http://tuka.s12.xrea.com/cgi-bin/env.cgi/path/info/dayo
FreeStyleWiki3.4.2からWikiFarmが導入されましたが、このFarm機能のためにPATH_INFOを使っているため、そのままではエラーが表示されてしまいました。回避する方法はいろいろあると思いますが、私は以下のようにして回避しています。
$path_info =~ s/(.+)\1/$1/;