Moai+Easter Advanced Manual
ご案内 Moaiエンジン CustomBoyエンジン HowToコンパイル Moai CGI Developers Raraku言語
RarakuでCGI入門 C言語でCGI mkfgenリファレンス
はじめに
この記事はMoai Ver2.2以前にあったC言語によるCGIプログラムの作成講座です。 Moai Ver2.3からはRaraku言語が導入されたため、この記事の内容はもはやあまり意味のないものですが、 元々の記事をベースに整理しなおしてリニューアルしたものを一応ここに残しておきます。
尚、この記事は完全にC言語に習熟している方向けです(C言語に詳しくない方は「RarakuでCGI入門」の方をご覧下さい)。 またこの記事で紹介しているサンプルコードのライセンスはNYSLとします。
参考: 敢えてCでやる意味
世の中他に便利な言語が腐るほどある中、なぜC言語かというと、
- ネット上には C ベースの各種基本ライブラリが大量に溢れかえっており、必要ならそれを利用するのも(他の言語からより)容易い。
- C++よりもCの方が動的リンクライブラリとの相性がよい。
- 出来上がるバイナリは(動的リンクライブラリを除けば)単独で実行できる(別途インタプリタを必要としない)。
- 記述されたこと以上の余計なことをあまりしない。
- Moai(正確にはlibRano)が提供するAPIがC言語で用意されているため、これらの機能と協調動作するのに親和性が良い。
- C言語は言語仕様が(多分これからも)変わらないため、一度作ったコードはずっと使える。
4については、「余計なことはせんでいい」というポリシーの方には良いポイントです。 CGIが内部でどのように呼び出されどのような仕組みでサーバとデータのやり取りが行われるかをなるべく包み隠さず学習するのにも最適です。 これはまさに処理の透明性を謳うMoaiのポリシーにも合致するのではないでしょうか?
6については、言語のバージョンを気にする方については良いポイントです。 昨今の言語は(言語にもよりますが)ときに後方互換性が大きく捨てられることがあります。 つまりバージョンが上がると古いバージョン向けのコードが使えなくなるといったことです。 それでもAPIやライブラリの変更だけならコンパイルエラー表示がまだまともになる可能性もあるのでコードの修正作業は容易かもしれませんが、 言語仕様そのものが変わっていた場合、エラー表示(と何よりプログラマ自身)はおそらく混乱するでしょうからその修正は厳しいものとなるでしょう。
また逆に新しく書き起こすとき、対応するバージョンを最新バージョンのみではなく、すこし古いバージョンでも動作するように考慮しようとすれば、 場合によっては「今書こうとする記法」は言語のどのバージョンから対応するものかを調べる必要もでてくるでしょう。 その点、いわゆるC言語(ここでは一般的なC90を考え、C99やC++等は置いときます)では、 言語仕様という観点ではガチガチに固まっており、そのような心配が(多分これからも)ありません。 ただC言語でも標準以外のライブラリを使っていれば、ライブラリのバージョンが上がったり新しいものに変えたりすることで、修正作業は起こり得る話ではあります。
Close
目次
- Hello World
- Hello 環境変数
- Hello Query String
- Hello Moai AuthenticKey
- Hello POST
- Transfer-Encoding: chunked転送モードとsleep
- mkfgen
- libZnkとlibRanoの使用
- 画像のPOST
- テンプレートHTMLを使って表示する.
- 時間のかかる処理の途中経過を表示する
- おわりに
Hello World
処理系依存処理への対策
まずは Moai CGI で単純にメッセージを出力してみます。
尚、この記事における青いリンクは各サンプルをビルドしたバイナリファイルへのリンクとなっておりますが、
Ver2.3のMoaiからはこのバイナリファイルは付属しないため、初期状態ではこれは実行できません。
このバイナリファイルを作成するには、ソースコードznk_project/cgi_developers/cgi_c内でMakefileによりビルドを行う必要があります。
その上でそのバイナリファイルをMoaiインストールディレクトリへインストールし、
なおかつMoaiを起動した状態でのみこのリンクが実行可能です。
「RarakuでCGI入門」でも同様のサンプルがありますので(そちらはただちに実行可能です)、 実行した結果をてっとり早く確認したければそちらで確認してもらった方がいいかもしれません。
「RarakuでCGI入門」でも同様のサンプルがありますので(そちらはただちに実行可能です)、 実行した結果をてっとり早く確認したければそちらで確認してもらった方がいいかもしれません。
Execute : cgi_c/hello.cgi Download View source code (129 lines)
#include <stddef.h>
#if _MSC_VER >= 1400
# pragma warning(disable: 4996)
#endif
/***
* This is a macro to determine the Target OS, and the name is in the form of TARGET_*.
*/
#if defined(_WIN32) || defined(_WIN64) || defined(_MSC_VER) || \
defined(__MINGW32__) || defined(__MINGW64__) || \
defined(__BORLANDC__) || defined(__DMC__)
/* Windows API */
# define TARGET_WINDOWS 1
#elif defined(__unix__) || defined(__linux__) || defined(__CYGWIN__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
/* POSIX UNIX + X11 and its compatible systems. */
# define TARGET_UNIX 1
#else
# error "Not supported OS"
#endif
#if defined(TARGET_WINDOWS)
/* windows API */
# include <windows.h> /* for Sleep */
# include <io.h> /* for setmode */
# include <fcntl.h> /* for setmode */
#else
/* Implementation using UNIX (Posix to be precise). */
# include <unistd.h> /* for usleep */
#endif
#include <stdio.h>
#if defined(TARGET_WINDOWS)
static FILE* Internal_getStdFP( int no )
{
switch( no ){
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default:
break;
}
return NULL;
}
#endif
static void
CGIUtil_Internal_setMode( int no, int is_binary_mode )
{
#if defined(TARGET_WINDOWS)
/* Before changing the mode, we need to make sure that fflush is executed first. */
fflush( (FILE*)Internal_getStdFP( no ) );
# if defined(__BORLANDC__)
setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# else
_setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# endif
#endif
}
int main( void )
{
/***
* Disables the automatic conversion of line feeds on Windows.
* First argument : 1 means the standard output.
* Second argument : 1 means conversion to binary mode.
*/
{
static const int is_binary_mode = 1;
CGIUtil_Internal_setMode( 1, is_binary_mode );
}
/***
* Output HTTP Header for CGI
* In the HTTP header, the line feed code should always be written as \r\n.
*/
/***
* If you follow the general specification of CGI, this Content-Type specification is required.
* However, if it is limited to Moai CGI, it will work without this Content-Type: specification
* because this specification is already made in the HTTP header sent from the moai server side.
* (See znk_project/src/libMoai/Moai_cgi.c for details.)
*/
printf( "Content-Type: text/html\r\n" );
/***
* In the case of CGI, you most likely do not want the browser to cache the content.
* To tell the browser to do this, add the following two lines.
*/
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
/***
* This newline marks the end of the HTTP header section and is required.
*/
printf( "\r\n" );
/***
* Output HTML for CGI
* ( From here on, the newline code is no longer important, but we'll use \n for now. )
*/
printf( "<!DOCTYPE html>\n" ); /* for HTML5 */
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<meta charset=\"utf-8\" />\n" );
printf( "<meta http-equiv=\"Content-Type\" content=\"text/html\" />\n" );
printf( "<meta http-equiv=\"Content-Script-Type\" content=\"text/javascript\" />\n" );
printf( "<meta http-equiv=\"Content-Style-Type\" content=\"text/css\" />\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "Hello World on <b>Moai CGI</b>.\n" );
printf( "</body></html>\n" );
fflush( stdout );
return 0;
}
Closeそれではソースコードの説明に入りましょう。
このチュートリアルにおけるサンプルは全て一貫して、Windows/Linuxともに動作するように作成してあります。 とはいえ、Windows独自の処理、Linux独自の処理といったものも当然ありますので、 TARGET_WINDOWSマクロが1にセットされているならばWindows用の処理を、TARGET_UNIXマクロが1にセットされているならばLinux用の処理を それぞれ書き分けるといったことをします。
ただしこのチュートリアルではMacOSは考慮しませんのでご了承ください。
そのため、まずはTARGET_WINDOWSとTARGET_UNIXの値を決定しなければなりませんが、 これを行っているのが以下になります。
#include <stddef.h>
#if _MSC_VER >= 1400
# pragma warning(disable: 4996)
#endif
/***
* This is a macro to determine the Target OS, and the name is in the form of TARGET_*.
*/
#if defined(_WIN32) || defined(_WIN64) || defined(_MSC_VER) || \
defined(__MINGW32__) || defined(__MINGW64__) || \
defined(__BORLANDC__) || defined(__DMC__)
/* Windows API */
# define TARGET_WINDOWS 1
#elif defined(__unix__) || defined(__linux__) || defined(__CYGWIN__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
/* POSIX UNIX + X11 and its compatible systems. */
# define TARGET_UNIX 1
#else
# error "Not supported OS"
#endif
上記では、基本的に今使っているコンパイラ(VC,MinGW,Cygwin,Linux GCC等)は何か、 そしてWindowsとLinuxのいずれの環境向けにコンパイルしようとしているのかをプリプロセッサの機能で自動判定しています。 たとえば _MSC_VER というマクロが定義されていればVCだと判断できますし、Windows向けのコンパイルであるといったこともわかります。 __MINGW32__ または __MINGW64__ というマクロが定義されていればMinGWであり、これもまたWindows向けであると判断できます。 一方それ以外のシステムでは現在ではほとんどUNIX互換システム向けとみなすことができます(ここではCygwinもUNIXエミュレータとしての役割を重視してUNIXとみなします)。 上記を駆使して最終的にWindows向けならばTARGET_WINDOWSを1に、UNIX向けならばTARGET_UNIXを1に定義します。
その他若干の調整として、pragma warningを使用してVCの明らかに無駄な警告(今回は警告C4996)の表示を抑制しています。
HTTPヘッダの出力
CGIを作成する場合、まず最初にHTTPヘッダをprintfにより出力する必要があります。 そしてHTTPヘッダにおける改行は \r\n で記述しなければならない仕様となっています。 ところがC言語で改行を出力する場合、処理系依存の独特の問題が生じます。
Linuxにおいては、そのまま \r\n と記述すれば何も問題ないのですが、 一方Windowsのデフォルトでは printfで \n を一つ標準出力するとこれが \r\n へと内部で自動的に変換されてしまいます。 つまりWindowsにおいては \r\n と標準出力するとこのままでは実質は \r\r\n となってしまいます。 かと言って \n で記述すれば今度はこの自動変換が行われないLinuxで問題になります。
CGI開発においてはWindowsにおけるこの自動変換は邪魔でしかありませんので、これを無効化します。 このサンプルではWindowsにおけるこの自動変換を無効にするため、以下のような関数CGIUtil_Internal_setModeを作成しています。
#if defined(TARGET_WINDOWS)
static FILE* Internal_getStdFP( int no )
{
switch( no ){
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default:
break;
}
return NULL;
}
#endif
static void
CGIUtil_Internal_setMode( int no, int is_binary_mode )
{
#if defined(TARGET_WINDOWS)
/* Before changing the mode, we need to make sure that fflush is executed first. */
fflush( (FILE*)Internal_getStdFP( no ) );
# if defined(__BORLANDC__)
setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# else
_setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# endif
#endif
}
CGIUtil_Internal_setModeの第1引数(no)はファイルディスクリプタを意味し、この値が 0 である場合は標準入力を、1 である場合は標準出力を意味します。 第2引数(is_binary_mode)が true の場合、バイナリモードへの変換を意味します。 バイナリモードに設定することでWindowsにおける余計な自動変換を無効にできます。 ただしこのときfflush関数をあらかじめ実行しておく必要があります。 一方、UNIX環境の場合、最初からこれが無効であるため何もする必要はありません。 つまりこの場合はCGIUtil_Internal_setMode 関数は実質空っぽになります。
Windows環境のコンパイラの場合、大抵_setmodeという標準的なシステム関数が用意されていますが、 この関数は最終的にこれを呼び出すwrapperです。
Borland製のCコンパイラでは _setmodeではなくsetmodeという名前の関数が用意されているため、__BORLANDC__マクロで切り分けも行っています。
これでようやくHTTPヘッダを標準出力する準備が整いました。 CGIプログラムでは、標準出力を行うとそれがそのままHTMLとしてブラウザに渡される仕組みとなっていますが、 このときまず最初に以下のようなHTTPヘッダの出力をしておく必要があります。
/***
* If you follow the general specification of CGI, this Content-Type specification is required.
* However, if it is limited to Moai CGI, it will work without this Content-Type: specification
* because this specification is already made in the HTTP header sent from the moai server side.
* (See znk_project/src/libMoai/Moai_cgi.c for details.)
*/
printf( "Content-Type: text/html\r\n" );
/***
* In the case of CGI, you most likely do not want the browser to cache the content.
* To tell the browser to do this, add the following two lines.
*/
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
/***
* This newline marks the end of the HTTP header section and is required.
*/
printf( "\r\n" );
繰り返しますがHTTPヘッダの部分の改行コードは\r\nとしなければなりませんので注意しましょう。 またHTTPヘッダでは最終行にも\r\nを一つ指定する必要があります。 最後のprintfはそれを標準出力しているものです。
参考: HTTPヘッダにおけるcharsetを指定すべきか?
古いHTMLの仕様(HTML4以前)では headタグ内に<meta http-equiv="Content-Type" content="text/html; charset=Shift_JIS" />
といったような記述で文字コードの指定を行っていました。
現在でも上記のような記述は実質的に可能ではありますが、<meta charset="utf-8">といったようにHTML5流に記述するのが最近の主流でしょう。
一方、(HTMLではなく)HTTPヘッダにおける Content-Type: の指定でもcharsetが指定可能です。 しかしながらHTTPヘッダ側にcharsetは指定しない方がいいでしょう。 HTTPヘッダ側に記述してしまうと、(ブラウザにもよるでしょうが、少なくともFirefoxとChromeにおいて)HTML側でのmetaタグでのcharset指定が無視されてしまうからです。
とはいえ現代では、ほとんどのケースでcharsetの値は"utf-8" 固定で問題ないため、 utf-8ならばHTTPヘッダ側/HTML側のいずれに記述しても問題が発生することは少ないでしょう。 ただ稀に、Shift_JISとutf-8の両方を切り分けて使いたいような用途(例えば未だにShift_JISが使われている古めの掲示板に対応したアプリなどはまさにそうですが)において、 HTML側で明確に文字コードのモードを確認でき、また柔軟に変更できることが重要なケースもあります。
Close
一方、(HTMLではなく)HTTPヘッダにおける Content-Type: の指定でもcharsetが指定可能です。 しかしながらHTTPヘッダ側にcharsetは指定しない方がいいでしょう。 HTTPヘッダ側に記述してしまうと、(ブラウザにもよるでしょうが、少なくともFirefoxとChromeにおいて)HTML側でのmetaタグでのcharset指定が無視されてしまうからです。
とはいえ現代では、ほとんどのケースでcharsetの値は"utf-8" 固定で問題ないため、 utf-8ならばHTTPヘッダ側/HTML側のいずれに記述しても問題が発生することは少ないでしょう。 ただ稀に、Shift_JISとutf-8の両方を切り分けて使いたいような用途(例えば未だにShift_JISが使われている古めの掲示板に対応したアプリなどはまさにそうですが)において、 HTML側で明確に文字コードのモードを確認でき、また柔軟に変更できることが重要なケースもあります。
Close
HTML本体の出力
HTTPヘッダの出力が終ったら、HTMLの本体をprintfで出力します。
ソースコード内でこれに該当する部分を以下に示します.
/***
* Output HTML for CGI
* ( From here on, the newline code is no longer important, but we'll use \n for now. )
*/
printf( "<!DOCTYPE html>\n" ); /* for HTML5 */
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<meta charset=\"utf-8\" />\n" );
printf( "<meta http-equiv=\"Content-Type\" content=\"text/html\" />\n" );
printf( "<meta http-equiv=\"Content-Script-Type\" content=\"text/javascript\" />\n" );
printf( "<meta http-equiv=\"Content-Style-Type\" content=\"text/css\" />\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "Hello World on <b>Moai CGI</b>.\n" );
printf( "</body></html>\n" );
fflush( stdout );
ここからはHTTPヘッダではありませんので改行コードは適当でよいですが、上記では一応\nとして記述しています。 上記では一番最後にfflush関数を呼び出し、標準出力が確実に行われるようにしてあります。
参考: HTML4
これから新しくCGIプログラムを作成するなら、何か特別な理由がない限りHTML5にすべきでしょう。
私はどちらかというと古い環境も重視するタイプの人間ですが、それでもさすがにもはやHTML5でよいと思います。
しかしそれでもどうしてもHTML4に厳密に対応する必要があるならば、 上記の printf( "<!DOCTYPE html>\n" ); の部分を以下のように記述する必要があります。
またHTML4においては、上記の printf( "<meta charset=\"utf-8\" />\n" ); の部分を以下のように記述する必要があります。
ただ、現実的にはほとんどのブラウザでは、上記の記述がなくてもHTMLとしてなんらかの解釈をして動作はするとは思います。
Close
しかしそれでもどうしてもHTML4に厳密に対応する必要があるならば、 上記の printf( "<!DOCTYPE html>\n" ); の部分を以下のように記述する必要があります。
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">\n" );
またHTML4においては、上記の printf( "<meta charset=\"utf-8\" />\n" ); の部分を以下のように記述する必要があります。
printf( "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />" );
ただ、現実的にはほとんどのブラウザでは、上記の記述がなくてもHTMLとしてなんらかの解釈をして動作はするとは思います。
Close
ビルド方法(はじめに)
C言語での開発の場合、C言語によるプログラミングそのものよりもビルドのための準備を整えるほうが大変です。 そのためこの記事でもそれに説明に多くを裂かざるを得ません。
この記事は、あなたが既にC言語コンパイラをインストールされていることを前提として話を進めますが、 インストールされていない方は、例えばMinGWのインストール方法であれば HowToコンパイル で説明してありますのでよろしければ参考にしてください。 使用するC言語コンパイラは基本的になんでもよいです。 そして以下で色々書きましたが、要するに実行バイナリ(hello.cgi)さえ生成できればどんな手段を用いてもOKです。
hello.cgiの実体はWindowsの場合はexe形式、Linuxの場合はELF形式(gccでは特に指定しなければ通常はa.outという名前として出力されるもの)です。 ただ、拡張子を.exeにしてしまうとLinuxとの統一性がとれず、また特にCGIの場合URLで指定するのに拡張子としてcgiが使われるので、 ここでもhello.cgiと拡張子をcgiとして実行バイナリを作成することにします。
ビルド方法(gccの場合)
それでは各コンパイラ毎に手順を見ていきましょう。 gccの場合、おそらく次のようにコマンドを実行すれば、コンパイルできるでしょう。
gcc -c hello.c
gcc -o hello.cgi hello.o
1番目のコマンドでは、オプション-cを指定してoファイル(hello.o)を作ります(oファイルのことを普通はオブジェクトファイルと呼びます。 2番目のコマンドでは、作成したオブジェクトファイル(今回はhello.o一つですが、oファイルを複数作った場合はそれをスペース区切りで後ろに並べます)を連結して(この処理をリンクと呼びますが)、最終的なhello.cgiを作るといった流れです。 オプション-oは出力する実行バイナリの名前を指定するためのものです。
実はこの程度の処理なら1つのコマンドで書くこともできますが、
オブジェクトファイル生成フェーズとリンクフェーズの2段階に分けた方が教育的かつ応用が効くのでここでもそうしてあります。
一旦hello.cgiが出来上がってしまえば、oファイルはもはや不要なので邪魔ならば削除しても結構です。
次に出来上がった hello.cgi を Webサーバの所定の場所へコピーします。 Moaiに関していえば、moaiのインストールディレクトリ内の cgis/cgi_developers/cgi_c へコピーします。
ただしLinuxの場合、もしかするとhello.cgiに実行権限(パーミッション)を与えておく必要があるかもしれません。 これは以下のようにchmodコマンドを実行することで行います。
chmod 755 hello.cgi
hello.cgiを所定の場所へコピーしたら、ブラウザから対応するURLへアクセスすればこのhello.cgiを実行することができます。 例えばMoaiを自分のPC上で起動した場合ですと、ブラウザから http://127.0.0.1:8124/cgis/cgi_developers/cgi_c/hello.cgi にアクセスします。 一見ネットワークを介したアクセスに見えますが、今回のケースでは127.0.0.1へのアクセスであるため、実際は自分自身のPCだけですべてが完結しています (このような処理をLOOPBACKと呼びます)。
ビルド方法(VCの場合)
VCの場合、(環境変数INCLUDE,LIB,PATHの設定が適切なら)おそらく次のようにコマンドを実行すれば、コンパイルできるでしょう.
cl -c hello.c
link /MACHINE:AMD64 /OUT:hello.cgi hello.obj
hello.cgiが作成されるまでの流れはgccの場合と同じです。 clとありますが、(IDE環境をご使用の方はあまりなじみがないかもしれませんが)これはVCのコンパイラの実体です。 またlinkとありますが、これはVCのリンカの実体です。
clの場合、オプション-cを指定することにより途中でobjファイルを生成しますが、これはオブジェクトファイルと呼ばれます。 オプション/OUTは出力する実行バイナリの名前を指定するためのものです。 上記は64bit用バイナリを作成するための指定ですが、32bit用のバイナリを作りたい場合は、オプション/MACHINE:AMD64の替わりに/MACHINE:X86を指定するとよいでしょう.
環境変数INCLUDE,LIB,PATHの設定の仕方がわからない場合、znk_projectが用意しているsetenv_vc.batファイルを試してみることもできます (VC2017までならばこれで動作すると思われます)。 すなわち、コマンドプロンプトを開き、cgi_developers/cgi_cディレクトリへ移動し、以下のように実行します。
..\..\mkfsys\bat_utils\setenv_vc.bat
cl -c hello.c
link /MACHINE:AMD64 /OUT:hello.cgi hello.obj
setenv_vc.batの実行は最初に1回行うだけで結構です。 2回目のコンパイルからはこれを実行する必要はありません。 コマンドプロンプトを閉じるまではその効果が続くからです (ただし効果は永続するわけではないので、新しいコマンドプロンプトを開いた場合は、また実行する必要があります)。
setenv_vc.batは、お使いのPCにVCがインストールされていることを自動的に検出し、
なおかつ環境変数INCLUDE,LIB,PATHを自動的に設定します。
ビルド方法(Makefileの使用)
znk_project/cgi_developers/cgi_c 内にあるMakefileを流用するという手もあります。 例えばlinux上でgccによるコンパイルを行う場合は以下のようになります。 これならたった1行でhello.cgiの作成までは済みます (直接コンパイラを実行する場合と比べ、タイピング量はそう減ってはおりませんが、Tabキーによる補完機能を使えばこっちのが少し楽でしょう)。
make -f Makefile_linux.mak
さらに、Moaiがインストールされたディレクトリがznk_projectディレクトリの直下にあれば、 おそらく以下を実行すれば、hello.cgiをそこへ自動的にコピーしてくれると思います(うまくいかない可能性もあります)。
make -f Makefile_linux.mak install
VCの場合、nmakeコマンドなるものが使えます。 setenv_vc.batがうまくいきさえすれば、次のようにすることでコンパイルできると思います。
..\..\mkfsys\bat_utils\setenv_vc.bat
nmake -f Makefile_vc.mak
Windows環境限定ですが、auto_triggerというツールを使う手もあります (これは我々が開発したツールですが、私も普段はこれを使っています)。
auto_trigger は特にWindows上でC言語またはRaraku言語をビルドするのを簡単にするためのツールです。
Moai本体やEasterを始めとしたCGIアプリケーションはすべて auto_trigger によるビルドが可能になっています。
勿論、上記のようにこれを使わずにコマンドプロンプトを自分で開いてmakeコマンドを叩けばビルドできるのですが、
特にWindows上ではこれを使った方が圧倒的に楽だと思います。
ソースコードと同じディレクトリに auto_trigger.batというファイルがありますのでそれをクリックして実行してください.
古き良き時代を彷彿とさせるコマンドラインベースでのメニュー選択UIが表示されるので、 「3. switch debug-mode」を何回か選んでDEBUG_MODEを「optimize」にし、 「4. switch mkfid」を何回か選んでMKF_IDを「vc」にし、 「5. switch machine」を何回か選んでMACHINEを「x86」あるいは「x64」にし、 最後に「0. make」または「1. make install」を選びましょう。
ここで「選ぶ」というのは、一番先頭にある数字のキーを押せばよいです。
またはその項目までカーソルキーで移動してEnterキーを押してもよいです。
auto_triggerは、内部でsetenv_vc.batを自動的に呼び出します。 従って、これが失敗する場合、auto_trigger.batも失敗することになることに注意してください。
MinGWの場合は、環境変数 ZNK_MINGW32_DIR に MinGWのトップディレクトリのパスを正しく指定すれば (つまり $ZNK_MINGW32_DIR/bin/gcc.exe が実行できるように設定すれば)、 auto_triggerを使うことができます。
「0. make」を選んだ場合は、このディレクトリ内にあるすべてのCファイルについて、必要なコンパイルが行われます。 このときout_dirディレクトリが自動的に作成され、その中にコンパイル結果(拡張子がcgiとなっているファイル)が作成されます。 「1. make install」を選んだ場合はまずcgiファイルがコンパイルによって作成された後、 さらにsrcディレクトリより一つ上の階層にmoaiインストール用のディレクトリが作成され、そこにcgiファイルがインストールされます。
目次に戻る
Hello 環境変数
CGIで使われる環境変数
今回は CGI で使われる環境変数の内容をテスト出力してみます。
Execute : cgi_c/evar1.cgi Download View source code(evar1.c) (91 lines)
#include "cgi_util.h"
#include <stdio.h>
#include <string.h>
static const char*
rejectHtmlTag( const char* cstr )
{
/***
* I'll take the minimum XSS measures.
* In fact, the HTML tag disabling process should be taken more seriously.
* However, in order to simplify the process as much as possible for the sake of the sample,
* I've restricted the process by returning NULL and rejecting any HTML tags that may be included.
*/
if( cstr && ( strchr( cstr, '<' ) || strchr( cstr, '>' ) ) ){
return NULL;
}
return cstr;
}
static void
show_result( const CGIEVar* evar )
{
/***
* Output HTTP Header for CGI
*/
printf( "Content-type: text/html; charset=Shift_JIS\r\n" );
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
printf( "\r\n" );
/***
* Output HTML for CGI
*/
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" );
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<META http-equiv=\"Content-type\" content=\"text/html; charset=Shift_JIS\">\n" );
printf( "<META http-equiv=\"Content-Script-Type\" content=\"text/javascript\">\n" );
printf( "<META http-equiv=\"Content-Style-Type\" content=\"text/css\">\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "Moai CGI Enviroment Variables:<br>\n" );
printf( "<pre>\n" );
printf( "server_name=[%s]\n", rejectHtmlTag(evar->server_name_) );
printf( "server_port=[%s]\n", rejectHtmlTag(evar->server_port_) );
printf( "content_type=[%s]\n", rejectHtmlTag(evar->content_type_) );
printf( "content_length=[%s]\n", rejectHtmlTag(evar->content_length_) );
printf( "remote_addr=[%s]\n", rejectHtmlTag(evar->remote_addr_) );
printf( "remote_host=[%s]\n", rejectHtmlTag(evar->remote_host_) );
printf( "remote_port=[%s]\n", rejectHtmlTag(evar->remote_port_) );
printf( "request_method=[%s]\n", rejectHtmlTag(evar->request_method_) );
printf( "query_string=[%s]\n", rejectHtmlTag(evar->query_string_) );
printf( "http_cookie=[%s]\n", rejectHtmlTag(evar->http_cookie_) );
printf( "http_user_agent=[%s]\n", rejectHtmlTag(evar->http_user_agent_) );
printf( "http_accept=[%s]\n", rejectHtmlTag(evar->http_accept_) );
printf( "</pre>" );
printf( "</body></html>\n" );
fflush( stdout );
}
int main( void )
{
/***
* Get the environment variables in CGI.
*/
CGIEVar* evar = CGIEVar_create();
/***
* Disables the automatic conversion of \n => \r\n in standard output(1) on Windows.
*/
{
static const int is_binary_mode = 1;
CGIUtil_Internal_setMode( 1, is_binary_mode );
}
/***
* This is the output of the contents of the environment variable "evar".
* Because of the length of the description, this is a subroutine.
*/
show_result( evar );
/***
* CGIEVar* needs to be released after use.
*/
CGIEVar_destroy( evar );
return 0;
}
Close
#include "cgi_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if _MSC_VER >= 1400
# pragma warning(disable: 4996)
#endif
/***
* Target OSを判定するマクロ.
* TARGET_* という形式の名前となる.
*/
#if defined(_WIN32) || defined(_WIN64) || defined(_MSC_VER) || \
defined(__MINGW32__) || defined(__MINGW64__) || \
defined(__BORLANDC__) || defined(__DMC__)
/* Windows API */
# define TARGET_WINDOWS 1
#elif defined(__unix__) || defined(__linux__) || defined(__CYGWIN__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
/* POSIX UNIX + X11 およびその互換システム */
# define TARGET_UNIX 1
#else
# error "Not supported OS"
#endif
#if defined(TARGET_WINDOWS)
/* windows API */
# include <windows.h> /* for Sleep */
# include <io.h> /* for setmode */
# include <fcntl.h> /* for setmode */
#else
/* UNIXというかPosixを使った実装 */
# include <unistd.h> /* for usleep */
#endif
#if defined(TARGET_WINDOWS)
#endif
#define M_MIN(x,y) ( (x)<(y) ? (x) : (y) )
static void
copyStr_safely( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
if( buf_size ){
const size_t cpy_size = M_MIN( cstr_leng, buf_size-1 );
memmove( buf, cstr, cpy_size );
buf[ cpy_size ] = '\0';
}
}
static size_t
S_strnlen( const char* buf, size_t buf_size )
{
char* p = (char*)memchr( buf, '\0', buf_size );
return ( p == NULL ) ? buf_size : (size_t)(p - buf);
}
static char*
concatEx( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
size_t dst_len;
if( buf_size == 0 || buf == NULL ){ return NULL; }
dst_len = S_strnlen( buf, buf_size );
if( dst_len == buf_size ){ return NULL; }
if( cstr_leng < buf_size - dst_len ){
memmove( buf + dst_len, cstr, cstr_leng );
buf += ( dst_len + cstr_leng );
*buf = '\0';
return buf;
}
/* trancate over parts */
memmove( buf + dst_len, cstr, buf_size - 1 - dst_len );
buf += buf_size - 1;
*buf = '\0';
return buf;
}
#if defined(TARGET_WINDOWS)
static FILE* Internal_getStdFP( int no )
{
switch( no ){
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default:
break;
}
return NULL;
}
#endif
void
CGIUtil_Internal_setMode( int no, int is_binary_mode )
{
#if defined(TARGET_WINDOWS)
/* モード変更に先立ってまず確実にfflushしておく必要がある */
fflush( (FILE*)Internal_getStdFP( no ) );
# if defined(__BORLANDC__)
setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# else
_setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# endif
#endif
}
void
CGIUtil_sleep( size_t msec )
{
#if defined(TARGET_WINDOWS)
Sleep( (DWORD)msec );
#else
usleep( msec * 1000 ); /* 指定はマイクロ秒 */
#endif
}
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length )
{
int chr = 0;
size_t count = 0;
if( stdin_bfr_size == 0 ){
return 0;
}
while( count < content_length && count < stdin_bfr_size-1 ){
/* 渡されたデータを標準入力より取得する */
chr = fgetc( stdin );
/* データ終了のチェック */
if( chr == EOF || chr == (int)content_length ){
/* content_lengthに満たないうちに EOFが出現 */
break;
} else if( count < stdin_bfr_size ){
stdin_bfr[ count ] = (unsigned char)chr;
++count;
}
}
/***
* count は最大でも stdin_bfr_size-1
* すなわち最大でもstdin_bfrの最後を指し示す.
* よって以下でstdin_bfrから確実にはみ出すことなくnull終端できる.
*/
stdin_bfr[ count ] = '\0';
return count;
}
static char*
EnvVar_get( const char* varname )
{
/***
* getenvが返すポインタはプログラマが予期しない形で非常に無効化しやすく危険である.
* このポインタが示す内容は、putenvやsetenvの呼び出しによってメモリが書き換えられ、
* ポインタが無効化される可能性がある. しかしそれだけではない. getenv関数はその
* 名前とは裏腹に環境変数が存在するメモリ領域を書き換える場合がある. よって、
* 次の単なるgetenvの呼び出しによってすらメモリが書き換えられ、このポインタが
* 無効化される可能性がある. このことは一見なんともないような次のようなコードが
* 完全に不適合なコードであることを示す.
*
* const char* var_TMP = getenv( "TMP" );
* const char* var_TEMP = getenv( "TEMP" ); // <= この時点でvar_TMPは無効化する恐れがある!
*
* 従ってユーザは、これが指す内容を直ちに別バッファへコピーすべきである.
* しかしこれでも万全ではない. getenvはスレッドセーフでもないため、上記の例で
* 別バッファへコピーするようにしたとしても、コピーが完了する前に他のスレッドにおいて、
* putenvやgetenvが呼び出されることで、var_TMPが突然無効化され、コピーに失敗するシナリオも
* 有り得る.
*
* よって、さらにこれを防ぐには、ここでGlobalMutexにより lock/unlockを掛ける必要がある.
* しかしこのcgi_utilは入門向けに用意されたユーティリティなのでそこまではやらない.
* 本格的なものについてはlibZnkのZnk_envarに実装されているので、そちらを使用していただきたい.
*/
const char* unsafe_ptr = NULL;
char* ans = NULL;
/* GlobalMutex_lock(); マルチスレッドの場合なら必要 */
unsafe_ptr = getenv( varname );
if( unsafe_ptr ){
ans = strdup( unsafe_ptr );
} else {
ans = strdup( "" );
}
/* GlobalMutex_unlock(); マルチスレッドの場合なら必要 */
return ans;
}
static void
EnvVar_free( char* val )
{
if( val ){
free( val );
}
}
CGIEVar*
CGIEVar_create( void )
{
CGIEVar* evar = malloc( sizeof( struct CGIEVar_tag ) );
evar->server_name_ = EnvVar_get( "SERVER_NAME" );
evar->server_port_ = EnvVar_get( "SERVER_PORT" );
evar->content_type_ = EnvVar_get( "CONTENT_TYPE" );
evar->content_length_ = EnvVar_get( "CONTENT_LENGTH" );
evar->remote_addr_ = EnvVar_get( "REMOTE_ADDR" );
evar->remote_host_ = EnvVar_get( "REMOTE_HOST" );
evar->remote_port_ = EnvVar_get( "REMOTE_PORT" );
evar->request_method_ = EnvVar_get( "REQUEST_METHOD" );
evar->query_string_ = EnvVar_get( "QUERY_STRING" );
evar->http_cookie_ = EnvVar_get( "HTTP_COOKIE" );
evar->http_user_agent_= EnvVar_get( "HTTP_USER_AGENT" );
evar->http_accept_ = EnvVar_get( "HTTP_ACCEPT" );
return evar;
}
void
CGIEVar_destroy( CGIEVar* evar )
{
if( evar ){
EnvVar_free( evar->server_name_ );
EnvVar_free( evar->server_port_ );
EnvVar_free( evar->content_type_ );
EnvVar_free( evar->content_length_ );
EnvVar_free( evar->remote_addr_ );
EnvVar_free( evar->remote_host_ );
EnvVar_free( evar->remote_port_ );
EnvVar_free( evar->request_method_ );
EnvVar_free( evar->query_string_ );
EnvVar_free( evar->http_cookie_ );
EnvVar_free( evar->http_user_agent_ );
EnvVar_free( evar->http_accept_ );
free( evar );
}
}
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size )
{
/***
* まず & で区切られたquery_idx番目のtokenを求める.
* そのようなtokenの開始位置をbegin,
* その終了位置(tokenの最後の文字の次の位置)をendとする.
*/
const char* begin = query_string;
const char* end = strchr( begin, '&' );
while( query_idx ){
if( end == NULL ){
/* Error : 要求されたquery_idx番目のtokenは存在しない */
return 0;
}
begin = end+1; /* &の次 */
end = strchr( begin, '&' );
--query_idx;
}
if( end == NULL ){
/***
* 要求されたquery_idx番目のtokenは最後にある.
* endをquery_stringの終端文字のある位置へ補正.
*/
end = query_string + strlen( query_string );
}
/***
* 次に query_idx番目のtoken内の = 位置を探索.
* これより前がkey, 後がvalの値であるから、
* これをそれぞれのバッファにコピーする.
*/
{
const char* eq_pos = strchr( begin, '=' );
if( eq_pos && eq_pos < end ){
copyStr_safely( key, key_size, begin, eq_pos-begin );
copyStr_safely( val, val_size, eq_pos+1, end-(eq_pos+1) );
} else {
/* Error : [ begin, end ) 内に '=' が存在しない */
return 0;
}
}
return 1;
}
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir )
{
static const char* filename = "authentic_key.dat";
char path[ 256 ] = "";
FILE* fp;
copyStr_safely( path, sizeof(path), serv_dir, strlen(serv_dir) );
concatEx( path, sizeof(path), filename, strlen(filename) );
fp = fopen( path, "rb" );
if( fp ){
fgets( authentic_key, authentic_key_size, fp );
fclose( fp );
return 1;
} else {
copyStr_safely( authentic_key, authentic_key_size, "0123456789ABCDEF", 16 );
}
return 0;
}
Close
#ifndef INCLUDE_GUARD__cgi_util_h__
#define INCLUDE_GUARD__cgi_util_h__
#include <stddef.h>
/***
* 標準入出力に関するバイナリモードの変更.
*/
void
CGIUtil_Internal_setMode( int no, int is_binary_mode );
/***
* msecミリ秒sleepします.
*/
void
CGIUtil_sleep( size_t msec );
/***
* @brief
* 標準入力からのデータをstdin_bfrへと格納する.
* stdin_bfrには最低でも stdin_bfr_size バイト以上のメモリが確保されていなければならない.
*
* @return
* 実際に取得されたバイト数を返す.
* すなわちこれがcontent_lengthと同じ場合はすべて取得できており、
* そうでなければ、content_length が stdin_bfr_size より大きいか、
* さもなければ予期せぬEOFが発生した可能性がある.
*/
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length );
/***
* CGIで扱う環境変数.
*/
typedef struct CGIEVar_tag {
char* server_name_;
char* server_port_;
char* content_type_;
char* content_length_;
char* remote_addr_;
char* remote_host_;
char* remote_port_;
char* request_method_;
char* query_string_;
char* http_cookie_;
char* http_user_agent_;
char* http_accept_;
} CGIEVar;
CGIEVar*
CGIEVar_create( void );
void
CGIEVar_destroy( CGIEVar* evar );
/***
* @brief
* CGIで扱うQuery String.
*
* Query String が指定されている場合はそれを取得する.
* 指定されていない場合、この関数は空文字を返す.
* Query String が指定されていない場合、evar->query_string_ は NULLとなっている.
*
* @return
* 成功ならば1、失敗ならば0を返す.
*/
//#define CGIUtil_getQueryString( evar ) ( (evar)->query_string_ ? (evar)->query_string_ : "" )
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size );
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir );
#endif /* INCLUDE_GUARD */
CloseCGIプログラムでは環境変数を介して、クライアントやサーバに関するさまざまな情報を取得します。 CGIに関する重要な環境変数としては、QUERY_STRING, CONTENT_TYPE, HTTP_COOKIE, SERVER_NAME, SERVER_PORT などがあります。 ところが実際Windowsで「システムのプロパティ」から「環境変数」のダイアログを開いてみてもこんな環境変数どこにも定義されてません。 Linuxでも同じで、envコマンドを叩いてもそれらしき環境変数はどこにも見当たりません。 どういうことでしょう?
からくりはCGIを呼び出すWebサーバにあります。 WebサーバはCGIスクリプトの実行要求を受け取ると、CGI用のプロセスを一つ立ち上げます。 そしてそれに先立ち、クライアントやサーバに関する環境変数をそのプロセス内だけから参照できる形で一時的にセットしているのです。 そのため、OSでは定義されているはずのない環境変数がCGIスクリプト内だけからは定義されているように見えることになります。
グローバル変数/ローカル変数という言葉をご存知の方ならば、
OSで定義されている環境変数はグローバル変数に、CGIの環境変数はローカル変数に相当するというように
(その関係性を比喩的に)考えてもよいかもしれません。
C言語ではgetenv関数を使って環境変数を参照することができるのですが、 次のセクションにおいて詳しく述べますがこの関数には色々と注意点があるため、我々はこれを直接使うべきではありません。 では実際にこれらを取得する方法ですが、cgi_util.c と cgi_util.h にgetenvを安全に使うためのwrapperを定義しておきましたので、 これを使うことにします。
cgi_util が提供する関数 CGIEVar_create によってMoai CGIが提供する全ての環境変数を即座に取得できます。 このセクションでは一旦この関数の使い方だけ把握しておきましょう(CGIEVar_createの中身については次のセクションで解説します)。 ソースコード内でこれを使用している部分を以下に示します。
/***
* Get the environment variables in CGI.
*/
CGIEVar* evar = CGIEVar_create();
/***
* Disables the automatic conversion of \n => \r\n in standard output(1) on Windows.
*/
{
static const int is_binary_mode = 1;
CGIUtil_Internal_setMode( 1, is_binary_mode );
}
/***
* This is the output of the contents of the environment variable "evar".
* Because of the length of the description, this is a subroutine.
*/
show_result( evar );
/***
* CGIEVar* needs to be released after use.
*/
CGIEVar_destroy( evar );
上記では、まずCGIEVar_create関数によりCGIEVar構造体を生成します。 これを使った後、最後にCGIEVar_destroy( evar )を呼び出し、これを破棄するといった流れです。
CGIEVar構造体は以下のように定義されており、 各環境変数の値を文字列として保持します。
typedef struct CGIEVar_tag {
char* server_name_;
char* server_port_;
char* content_type_;
char* content_length_;
char* remote_addr_;
char* remote_host_;
char* remote_port_;
char* request_method_;
char* query_string_;
char* http_cookie_;
char* http_user_agent_;
char* http_accept_;
} CGIEVar;
例えば、環境変数QUERY_STRINGの値を知りたければ、上記の構造体におけるquery_string_メンバの値を調べます。 尚、これらのメンバは読み込み専用であり、従って外から代入してはいけません。 これらはCGIEVar_create関数内で動的に確保(strdup)されたものであり、 またCGIEVar_destroy内でfreeされるからです。
目次に戻る
Hello Query String
Query Stringについて
今回は CGI で使われる環境変数のうち、QUERY_STRINGについてのお話です。
Execute : cgi_c/query_string1.cgi Download View source code(query_string1.c) (96 lines)
#include "cgi_util.h"
#include <stdio.h>
#include <string.h>
static const char*
rejectHtmlTag( const char* cstr )
{
/***
* 一応最低限のXSS対策はしておく.
* 本来はもっと真面目に無効化処理を行うべきですが、これは所詮サンプルなので極力簡単化するため、
* HTMLタグを含む可能性がある時点でNULLを返して弾くという猛烈な制限をかけて対処しています.
*/
if( cstr && ( strchr( cstr, '<' ) || strchr( cstr, '>' ) ) ){
return NULL;
}
return cstr;
}
static void
show_result( const char* query_string )
{
/***
* Output HTTP Header for CGI
*/
printf( "Content-type: text/html; charset=Shift_JIS\r\n" );
/* ブラウザにキャッシュさせない。 */
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
printf( "\r\n" );
/***
* Output HTML for CGI
*/
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" );
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<META http-equiv=\"Content-type\" content=\"text/html; charset=Shift_JIS\">\n" );
printf( "<META http-equiv=\"Content-Script-Type\" content=\"text/javascript\">\n" );
printf( "<META http-equiv=\"Content-Style-Type\" content=\"text/css\">\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "<pre>\n" );
printf( "Query String Tokens: \n" );
{
size_t i = 0;
char key[ 256 ] = "";
char val[ 256 ] = "";
for( i=0; i<64; ++i ){
int result = CGIUtil_getQueryStringToken( query_string, i,
key, sizeof(key),
val, sizeof(val) );
if( result == 0 ){
break;
}
printf( "%s = [%s]\n", rejectHtmlTag(key), rejectHtmlTag(val) );
}
}
printf( "</pre>" );
printf( "</body></html>\n" );
fflush( stdout );
}
int main( void )
{
CGIEVar* evar = CGIEVar_create();
const char* query_string = evar->query_string_;
{
static const int is_binary_mode = 1;
/***
* Windowsにおける標準入力(0)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 0, is_binary_mode );
/***
* Windowsにおける標準出力(1)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 1, is_binary_mode );
}
/***
* query_string の内容は以下のように & で区切られる変数指定の連続となっているはずです.
*
* name1=val1&name2=val2&name3=val3&...
*
* これを & 文字に関して分割(split)して、結果を出力します.
* 長くなるため、サブルーチン化します.
*/
show_result( query_string );
CGIEVar_destroy( evar );
return 0;
}
Close
#include "cgi_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if _MSC_VER >= 1400
# pragma warning(disable: 4996)
#endif
/***
* Target OSを判定するマクロ.
* TARGET_* という形式の名前となる.
*/
#if defined(_WIN32) || defined(_WIN64) || defined(_MSC_VER) || \
defined(__MINGW32__) || defined(__MINGW64__) || \
defined(__BORLANDC__) || defined(__DMC__)
/* Windows API */
# define TARGET_WINDOWS 1
#elif defined(__unix__) || defined(__linux__) || defined(__CYGWIN__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
/* POSIX UNIX + X11 およびその互換システム */
# define TARGET_UNIX 1
#else
# error "Not supported OS"
#endif
#if defined(TARGET_WINDOWS)
/* windows API */
# include <windows.h> /* for Sleep */
# include <io.h> /* for setmode */
# include <fcntl.h> /* for setmode */
#else
/* UNIXというかPosixを使った実装 */
# include <unistd.h> /* for usleep */
#endif
#if defined(TARGET_WINDOWS)
#endif
#define M_MIN(x,y) ( (x)<(y) ? (x) : (y) )
static void
copyStr_safely( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
if( buf_size ){
const size_t cpy_size = M_MIN( cstr_leng, buf_size-1 );
memmove( buf, cstr, cpy_size );
buf[ cpy_size ] = '\0';
}
}
static size_t
S_strnlen( const char* buf, size_t buf_size )
{
char* p = (char*)memchr( buf, '\0', buf_size );
return ( p == NULL ) ? buf_size : (size_t)(p - buf);
}
static char*
concatEx( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
size_t dst_len;
if( buf_size == 0 || buf == NULL ){ return NULL; }
dst_len = S_strnlen( buf, buf_size );
if( dst_len == buf_size ){ return NULL; }
if( cstr_leng < buf_size - dst_len ){
memmove( buf + dst_len, cstr, cstr_leng );
buf += ( dst_len + cstr_leng );
*buf = '\0';
return buf;
}
/* trancate over parts */
memmove( buf + dst_len, cstr, buf_size - 1 - dst_len );
buf += buf_size - 1;
*buf = '\0';
return buf;
}
#if defined(TARGET_WINDOWS)
static FILE* Internal_getStdFP( int no )
{
switch( no ){
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default:
break;
}
return NULL;
}
#endif
void
CGIUtil_Internal_setMode( int no, int is_binary_mode )
{
#if defined(TARGET_WINDOWS)
/* モード変更に先立ってまず確実にfflushしておく必要がある */
fflush( (FILE*)Internal_getStdFP( no ) );
# if defined(__BORLANDC__)
setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# else
_setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# endif
#endif
}
void
CGIUtil_sleep( size_t msec )
{
#if defined(TARGET_WINDOWS)
Sleep( (DWORD)msec );
#else
usleep( msec * 1000 ); /* 指定はマイクロ秒 */
#endif
}
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length )
{
int chr = 0;
size_t count = 0;
if( stdin_bfr_size == 0 ){
return 0;
}
while( count < content_length && count < stdin_bfr_size-1 ){
/* 渡されたデータを標準入力より取得する */
chr = fgetc( stdin );
/* データ終了のチェック */
if( chr == EOF || chr == (int)content_length ){
/* content_lengthに満たないうちに EOFが出現 */
break;
} else if( count < stdin_bfr_size ){
stdin_bfr[ count ] = (unsigned char)chr;
++count;
}
}
/***
* count は最大でも stdin_bfr_size-1
* すなわち最大でもstdin_bfrの最後を指し示す.
* よって以下でstdin_bfrから確実にはみ出すことなくnull終端できる.
*/
stdin_bfr[ count ] = '\0';
return count;
}
static char*
EnvVar_get( const char* varname )
{
/***
* getenvが返すポインタはプログラマが予期しない形で非常に無効化しやすく危険である.
* このポインタが示す内容は、putenvやsetenvの呼び出しによってメモリが書き換えられ、
* ポインタが無効化される可能性がある. しかしそれだけではない. getenv関数はその
* 名前とは裏腹に環境変数が存在するメモリ領域を書き換える場合がある. よって、
* 次の単なるgetenvの呼び出しによってすらメモリが書き換えられ、このポインタが
* 無効化される可能性がある. このことは一見なんともないような次のようなコードが
* 完全に不適合なコードであることを示す.
*
* const char* var_TMP = getenv( "TMP" );
* const char* var_TEMP = getenv( "TEMP" ); // <= この時点でvar_TMPは無効化する恐れがある!
*
* 従ってユーザは、これが指す内容を直ちに別バッファへコピーすべきである.
* しかしこれでも万全ではない. getenvはスレッドセーフでもないため、上記の例で
* 別バッファへコピーするようにしたとしても、コピーが完了する前に他のスレッドにおいて、
* putenvやgetenvが呼び出されることで、var_TMPが突然無効化され、コピーに失敗するシナリオも
* 有り得る.
*
* よって、さらにこれを防ぐには、ここでGlobalMutexにより lock/unlockを掛ける必要がある.
* しかしこのcgi_utilは入門向けに用意されたユーティリティなのでそこまではやらない.
* 本格的なものについてはlibZnkのZnk_envarに実装されているので、そちらを使用していただきたい.
*/
const char* unsafe_ptr = NULL;
char* ans = NULL;
/* GlobalMutex_lock(); マルチスレッドの場合なら必要 */
unsafe_ptr = getenv( varname );
if( unsafe_ptr ){
ans = strdup( unsafe_ptr );
} else {
ans = strdup( "" );
}
/* GlobalMutex_unlock(); マルチスレッドの場合なら必要 */
return ans;
}
static void
EnvVar_free( char* val )
{
if( val ){
free( val );
}
}
CGIEVar*
CGIEVar_create( void )
{
CGIEVar* evar = malloc( sizeof( struct CGIEVar_tag ) );
evar->server_name_ = EnvVar_get( "SERVER_NAME" );
evar->server_port_ = EnvVar_get( "SERVER_PORT" );
evar->content_type_ = EnvVar_get( "CONTENT_TYPE" );
evar->content_length_ = EnvVar_get( "CONTENT_LENGTH" );
evar->remote_addr_ = EnvVar_get( "REMOTE_ADDR" );
evar->remote_host_ = EnvVar_get( "REMOTE_HOST" );
evar->remote_port_ = EnvVar_get( "REMOTE_PORT" );
evar->request_method_ = EnvVar_get( "REQUEST_METHOD" );
evar->query_string_ = EnvVar_get( "QUERY_STRING" );
evar->http_cookie_ = EnvVar_get( "HTTP_COOKIE" );
evar->http_user_agent_= EnvVar_get( "HTTP_USER_AGENT" );
evar->http_accept_ = EnvVar_get( "HTTP_ACCEPT" );
return evar;
}
void
CGIEVar_destroy( CGIEVar* evar )
{
if( evar ){
EnvVar_free( evar->server_name_ );
EnvVar_free( evar->server_port_ );
EnvVar_free( evar->content_type_ );
EnvVar_free( evar->content_length_ );
EnvVar_free( evar->remote_addr_ );
EnvVar_free( evar->remote_host_ );
EnvVar_free( evar->remote_port_ );
EnvVar_free( evar->request_method_ );
EnvVar_free( evar->query_string_ );
EnvVar_free( evar->http_cookie_ );
EnvVar_free( evar->http_user_agent_ );
EnvVar_free( evar->http_accept_ );
free( evar );
}
}
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size )
{
/***
* まず & で区切られたquery_idx番目のtokenを求める.
* そのようなtokenの開始位置をbegin,
* その終了位置(tokenの最後の文字の次の位置)をendとする.
*/
const char* begin = query_string;
const char* end = strchr( begin, '&' );
while( query_idx ){
if( end == NULL ){
/* Error : 要求されたquery_idx番目のtokenは存在しない */
return 0;
}
begin = end+1; /* &の次 */
end = strchr( begin, '&' );
--query_idx;
}
if( end == NULL ){
/***
* 要求されたquery_idx番目のtokenは最後にある.
* endをquery_stringの終端文字のある位置へ補正.
*/
end = query_string + strlen( query_string );
}
/***
* 次に query_idx番目のtoken内の = 位置を探索.
* これより前がkey, 後がvalの値であるから、
* これをそれぞれのバッファにコピーする.
*/
{
const char* eq_pos = strchr( begin, '=' );
if( eq_pos && eq_pos < end ){
copyStr_safely( key, key_size, begin, eq_pos-begin );
copyStr_safely( val, val_size, eq_pos+1, end-(eq_pos+1) );
} else {
/* Error : [ begin, end ) 内に '=' が存在しない */
return 0;
}
}
return 1;
}
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir )
{
static const char* filename = "authentic_key.dat";
char path[ 256 ] = "";
FILE* fp;
copyStr_safely( path, sizeof(path), serv_dir, strlen(serv_dir) );
concatEx( path, sizeof(path), filename, strlen(filename) );
fp = fopen( path, "rb" );
if( fp ){
fgets( authentic_key, authentic_key_size, fp );
fclose( fp );
return 1;
} else {
copyStr_safely( authentic_key, authentic_key_size, "0123456789ABCDEF", 16 );
}
return 0;
}
Close
#ifndef INCLUDE_GUARD__cgi_util_h__
#define INCLUDE_GUARD__cgi_util_h__
#include <stddef.h>
/***
* 標準入出力に関するバイナリモードの変更.
*/
void
CGIUtil_Internal_setMode( int no, int is_binary_mode );
/***
* msecミリ秒sleepします.
*/
void
CGIUtil_sleep( size_t msec );
/***
* @brief
* 標準入力からのデータをstdin_bfrへと格納する.
* stdin_bfrには最低でも stdin_bfr_size バイト以上のメモリが確保されていなければならない.
*
* @return
* 実際に取得されたバイト数を返す.
* すなわちこれがcontent_lengthと同じ場合はすべて取得できており、
* そうでなければ、content_length が stdin_bfr_size より大きいか、
* さもなければ予期せぬEOFが発生した可能性がある.
*/
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length );
/***
* CGIで扱う環境変数.
*/
typedef struct CGIEVar_tag {
char* server_name_;
char* server_port_;
char* content_type_;
char* content_length_;
char* remote_addr_;
char* remote_host_;
char* remote_port_;
char* request_method_;
char* query_string_;
char* http_cookie_;
char* http_user_agent_;
char* http_accept_;
} CGIEVar;
CGIEVar*
CGIEVar_create( void );
void
CGIEVar_destroy( CGIEVar* evar );
/***
* @brief
* CGIで扱うQuery String.
*
* Query String が指定されている場合はそれを取得する.
* 指定されていない場合、この関数は空文字を返す.
* Query String が指定されていない場合、evar->query_string_ は NULLとなっている.
*
* @return
* 成功ならば1、失敗ならば0を返す.
*/
//#define CGIUtil_getQueryString( evar ) ( (evar)->query_string_ ? (evar)->query_string_ : "" )
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size );
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir );
#endif /* INCLUDE_GUARD */
CloseそもそもQuery Stringとは何でしょうか? URLの後ろの方に「?」文字があるのをご覧になったことがあるかもしれません。 その「?」文字よりさらに後ろにある文字列の部分をQuery Stringと呼びます。
Query Stringは name=val という形式のデータの羅列となっており、これが複数ある場合はさらに & 文字でこれらを区切ります。 つまり一般に、name1=val1&name2=val2&name3=val3 … という形式になっています。 通常はnameの部分になんらかの意味があってわかりやすい名前をつけます。 そして = 文字に続けてそれに対する値を付加することで、CGIプログラムに対する引数を指定する形になります。
Query Stringの内容をCGIプログラム側で獲得するにはどうすればよいでしょうか? そのために環境変数QUERY_STRINGを参照します。 我々は一つ前のセクションでCGIEVar_create関数を使いCGIEVar構造体を作成しました。
次の処理として、この query_string の解析を行います。 そのためには cgi_util 内で定義している CGIUtil_getQueryStringToken を使います。 この関数の定義部を以下に抜粋します。
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size )
{
/***
* まず & で区切られたquery_idx番目のtokenを求める.
* そのようなtokenの開始位置をbegin,
* その終了位置(tokenの最後の文字の次の位置)をendとする.
*/
const char* begin = query_string;
const char* end = strchr( begin, '&' );
while( query_idx ){
if( end == NULL ){
/* Error : 要求されたquery_idx番目のtokenは存在しない */
return 0;
}
begin = end+1; /* &の次 */
end = strchr( begin, '&' );
--query_idx;
}
if( end == NULL ){
/***
* 要求されたquery_idx番目のtokenは最後にある.
* endをquery_stringの終端文字のある位置へ補正.
*/
end = query_string + strlen( query_string );
}
/***
* 次に query_idx番目のtoken内の = 位置を探索.
* これより前がkey, 後がvalの値であるから、
* これをそれぞれのバッファにコピーする.
*/
{
const char* eq_pos = strchr( begin, '=' );
if( eq_pos && eq_pos < end ){
copyStr_safely( key, key_size, begin, eq_pos-begin );
copyStr_safely( val, val_size, eq_pos+1, end-(eq_pos+1) );
} else {
/* Error : [ begin, end ) 内に '=' が存在しない */
return 0;
}
}
return 1;
}
この関数は、第1引数(query_string)を & 文字で分割し、それぞれの「key=val」という形式の塊(Token)において、そのkeyとvalにあたる部分を取得します。 この関数の第2引数(query_idx)では、それらの塊(Token)の何番目を取得したいかを指定します。 第3引数(key)、第4引数(key_size)では最終的に取得すべきキーの情報を格納するためのバッファへのポインタとそのサイズ、 第5引数(val)、第6引数(val_size)では最終的に取得された値の情報を格納するためのバッファへのポインタとそのサイズを 指定することになります。 特に第3引数から第6引数については、この関数を呼び出している部分を見た方がわかりやすいかもしれません。 以下をご覧下さい。
{
size_t i = 0;
char key[ 256 ] = "";
char val[ 256 ] = "";
for( i=0; i<64; ++i ){
int result = CGIUtil_getQueryStringToken( query_string, i,
key, sizeof(key),
val, sizeof(val) );
if( result == 0 ){
break;
}
printf( "%s = [%s]\n", rejectHtmlTag(key), rejectHtmlTag(val) );
}
}
上記ではchar型のバッファkeyとvalをsizeof演算子を交えてこの関数に与えている様子がわかると思います。 また、各々取得したkeyとvalの内容をprintfで表示しています(この表示が今回のプログラムの目的となります)。
尚、CGIUtil_getQueryStringToken関数自体は、指定できるquery_idxに上限はありませんが、
上記の例では簡単のため最大64個までのTokenを取得する形にしてあります。
またrejectHtmlTagは、keyとval内に「<」や「>」が含まれる場合はNULLを返す関数です。
本来はこれを適切にエスケープしたものを返すべきですが、ここでは一旦このような関数でお茶を濁しています。
getenvの危険性
一つ前のセクションでCGIEVar_create関数を紹介しましたが、これの中身についての解説がまだでした。 ここではそれについて説明します。
少し話が脱線しますが C言語で環境変数を取得するには getenv を使います。 ですが前のセクションでも少し述べましたが、我々はこの関数を直接使うべきではありません。 安易に使うと大変危険なためです。 使うのはこの関数の特性を十分理解した上で、環境変数の取得/設定を行うラッパーを自作するような仕方のない場合に限定すべきです。 簡単に理由を説明しましょう。
例えば2つの環境変数QUERY_STRINGとHTTP_COOKIEの値を取得したいからといって以下のようなコードを記述してはいけません!
char* query_string = getenv( "QUERY_STRING" );
char* http_cookie = getenv( "HTTP_COOKIE" ); /* NG! */
printf( "query_string=[%s] http_cookie=[%s]",
rejectHtmlTag(query_string), rejectHtmlTag(http_cookie) );
パッと見では何の問題もなさそうに見えるかもしれませんが、実はgetenvを不用意に連続させていることが問題です。 その根拠は The C Standard, 7.22.4.6, paragraph 4 [ISO/IEC 9899:2011] において getenv に関する次のような記述に拠ります。
The getenv function returns a pointer to a string associated with the matched list member.
The string pointed to shall not be modified by the program but may be overwritten by a subsequent call to the getenv function.
つまり上の「NG!」の時点のgetenv呼び出しで早くもquery_stringの示すメモリ領域は無効化している恐れがあります。 これ以降でquery_stringにアクセスすると未定義の動作を引き起こすことでしょう。 従ってgetenvで取得した値は、即座に別の文字列バッファにでもコピーしておかなければなりません。 以下にstrdupを使った修正例を示しましょう. あらかじめEnvVar_get関数を下請けとして定義しておきます。
char* EnvVar_get( const char* varname ){
char* tmp = getenv( varname );
return tmp ? strdup( tmp ) : strdup( "" );
}
char* query_string = EnvVar_get( "QUERY_STRING" );
char* http_cookie = EnvVar_get( "HTTP_COOKIE" );
printf( "query_string=[%s] http_cookie=[%s]",
rejectHtmlTag(query_string), rejectHtmlTag(http_cookie) );
free( query_string ); /* strdupでは解放が必要です */
free( http_cookie ); /* strdupでは解放が必要です */
上記ではEnvVar_getの内部でgetenvを呼び出し、そのgetenvの戻り値を一旦tmp(ヒープ領域)へとコピーしています。 この(tmpの)領域は(freeをしない限りは)無効化することはないので、EnvVar_getはこれを戻り値として返します。 ただしgetenvは(指定した名前の環境変数が定義されていない場合)NULLを返すこともあるのでそのための配慮は必要です。 EnvVar_getではこれがNULLの場合は空文字列に強制的に変換しています。 マルチスレッドの場合にも万全を期すなら、EnvVar_get内の開始と終わりにおいてGlobalMutexなどでlock/unlockを掛ける必要があります。
ちなみにCGIプログラミングにおいてはあまり関係ありませんが、環境変数を設定する関数にputenvというものもあります。
この関数も同様に(むしろgetenv以上に)深刻な問題があります。
libZnk が提供する関数 ZnkEnvVar_get と ZnkEnvVar_set はgetenvとputenvに関するこの種の問題を解消したものです。
現在のfreeの仕様ではその引数にNULLを与えた場合は何もしないようになっていますが、
直接freeを呼び出すのが気になる方は、if文で事前にNULLのチェックし、NULLの場合はfreeしないようにしてもよいでしょう。
ではCGIEVar_createについて話を戻しましょう。 この関数の定義は以下のようになっています。
CGIEVar*
CGIEVar_create( void )
{
CGIEVar* evar = malloc( sizeof( struct CGIEVar_tag ) );
evar->server_name_ = EnvVar_get( "SERVER_NAME" );
evar->server_port_ = EnvVar_get( "SERVER_PORT" );
evar->content_type_ = EnvVar_get( "CONTENT_TYPE" );
evar->content_length_ = EnvVar_get( "CONTENT_LENGTH" );
evar->remote_addr_ = EnvVar_get( "REMOTE_ADDR" );
evar->remote_host_ = EnvVar_get( "REMOTE_HOST" );
evar->remote_port_ = EnvVar_get( "REMOTE_PORT" );
evar->request_method_ = EnvVar_get( "REQUEST_METHOD" );
evar->query_string_ = EnvVar_get( "QUERY_STRING" );
evar->http_cookie_ = EnvVar_get( "HTTP_COOKIE" );
evar->http_user_agent_= EnvVar_get( "HTTP_USER_AGENT" );
evar->http_accept_ = EnvVar_get( "HTTP_ACCEPT" );
return evar;
}
ご覧のようにほとんどEnvVar_getの呼び出しの連続なのでEnvVar_get関数がわかってしまえば 上記についての説明は不要でしょう。
ついでにCGIEVar_destroyの定義についても以下に示しておきます。
void
CGIEVar_destroy( CGIEVar* evar )
{
if( evar ){
EnvVar_free( evar->server_name_ );
EnvVar_free( evar->server_port_ );
EnvVar_free( evar->content_type_ );
EnvVar_free( evar->content_length_ );
EnvVar_free( evar->remote_addr_ );
EnvVar_free( evar->remote_host_ );
EnvVar_free( evar->remote_port_ );
EnvVar_free( evar->request_method_ );
EnvVar_free( evar->query_string_ );
EnvVar_free( evar->http_cookie_ );
EnvVar_free( evar->http_user_agent_ );
EnvVar_free( evar->http_accept_ );
free( evar );
}
}
EnvVar_freeはfreeのwrapperです。 従って、ここで行われている処理は、CGIEVar_create内でmallocやstrdupで確保していたものを、 単に解放しているだけです。
目次に戻る
Hello Moai AuthenticKey
Moai_AuthenticKeyについて
このセクションでは Moai_AuthenticKey について説明します。
Execute : cgi_c/moai_auth.cgi Download View source code(moai_auth.c) (75 lines)
#include "cgi_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char*
rejectHtmlTag( const char* cstr )
{
/***
* 一応最低限のXSS対策はしておく.
* 本来はもっと真面目に無効化処理を行うべきですが、これは所詮サンプルなので極力簡単化するため、
* HTMLタグを含む可能性がある時点でNULLを返して弾くという猛烈な制限をかけて対処しています.
*/
if( cstr && ( strchr( cstr, '<' ) || strchr( cstr, '>' ) ) ){
return NULL;
}
return cstr;
}
static void
show_result( CGIEVar* evar )
{
char auth_key[ 32 ] = "";
char* serv_dir = "../../../"; /* moai top dir */
CGIUtil_loadAuthenticKey( auth_key, sizeof(auth_key), serv_dir );
/***
* Output HTTP Header for CGI
*/
printf( "Content-type: text/html; charset=Shift_JIS\r\n" );
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
printf( "\r\n" );
/***
* Output HTML for CGI
*/
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" );
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<META http-equiv=\"Content-type\" content=\"text/html; charset=Shift_JIS\">\n" );
printf( "<META http-equiv=\"Content-Script-Type\" content=\"text/javascript\">\n" );
printf( "<META http-equiv=\"Content-Style-Type\" content=\"text/css\">\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "Moai_AuthenticKey=[%s].\n", rejectHtmlTag(auth_key) );
printf( "</body></html>\n" );
fflush( stdout );
}
int main( void )
{
CGIEVar* evar = CGIEVar_create();
{
static const int is_binary_mode = 1;
/***
* Windowsにおける標準入力(0)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 0, is_binary_mode );
/***
* Windowsにおける標準出力(1)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 1, is_binary_mode );
}
show_result( evar );
CGIEVar_destroy( evar );
return 0;
}
Close
#include "cgi_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if _MSC_VER >= 1400
# pragma warning(disable: 4996)
#endif
/***
* Target OSを判定するマクロ.
* TARGET_* という形式の名前となる.
*/
#if defined(_WIN32) || defined(_WIN64) || defined(_MSC_VER) || \
defined(__MINGW32__) || defined(__MINGW64__) || \
defined(__BORLANDC__) || defined(__DMC__)
/* Windows API */
# define TARGET_WINDOWS 1
#elif defined(__unix__) || defined(__linux__) || defined(__CYGWIN__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
/* POSIX UNIX + X11 およびその互換システム */
# define TARGET_UNIX 1
#else
# error "Not supported OS"
#endif
#if defined(TARGET_WINDOWS)
/* windows API */
# include <windows.h> /* for Sleep */
# include <io.h> /* for setmode */
# include <fcntl.h> /* for setmode */
#else
/* UNIXというかPosixを使った実装 */
# include <unistd.h> /* for usleep */
#endif
#if defined(TARGET_WINDOWS)
#endif
#define M_MIN(x,y) ( (x)<(y) ? (x) : (y) )
static void
copyStr_safely( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
if( buf_size ){
const size_t cpy_size = M_MIN( cstr_leng, buf_size-1 );
memmove( buf, cstr, cpy_size );
buf[ cpy_size ] = '\0';
}
}
static size_t
S_strnlen( const char* buf, size_t buf_size )
{
char* p = (char*)memchr( buf, '\0', buf_size );
return ( p == NULL ) ? buf_size : (size_t)(p - buf);
}
static char*
concatEx( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
size_t dst_len;
if( buf_size == 0 || buf == NULL ){ return NULL; }
dst_len = S_strnlen( buf, buf_size );
if( dst_len == buf_size ){ return NULL; }
if( cstr_leng < buf_size - dst_len ){
memmove( buf + dst_len, cstr, cstr_leng );
buf += ( dst_len + cstr_leng );
*buf = '\0';
return buf;
}
/* trancate over parts */
memmove( buf + dst_len, cstr, buf_size - 1 - dst_len );
buf += buf_size - 1;
*buf = '\0';
return buf;
}
#if defined(TARGET_WINDOWS)
static FILE* Internal_getStdFP( int no )
{
switch( no ){
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default:
break;
}
return NULL;
}
#endif
void
CGIUtil_Internal_setMode( int no, int is_binary_mode )
{
#if defined(TARGET_WINDOWS)
/* モード変更に先立ってまず確実にfflushしておく必要がある */
fflush( (FILE*)Internal_getStdFP( no ) );
# if defined(__BORLANDC__)
setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# else
_setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# endif
#endif
}
void
CGIUtil_sleep( size_t msec )
{
#if defined(TARGET_WINDOWS)
Sleep( (DWORD)msec );
#else
usleep( msec * 1000 ); /* 指定はマイクロ秒 */
#endif
}
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length )
{
int chr = 0;
size_t count = 0;
if( stdin_bfr_size == 0 ){
return 0;
}
while( count < content_length && count < stdin_bfr_size-1 ){
/* 渡されたデータを標準入力より取得する */
chr = fgetc( stdin );
/* データ終了のチェック */
if( chr == EOF || chr == (int)content_length ){
/* content_lengthに満たないうちに EOFが出現 */
break;
} else if( count < stdin_bfr_size ){
stdin_bfr[ count ] = (unsigned char)chr;
++count;
}
}
/***
* count は最大でも stdin_bfr_size-1
* すなわち最大でもstdin_bfrの最後を指し示す.
* よって以下でstdin_bfrから確実にはみ出すことなくnull終端できる.
*/
stdin_bfr[ count ] = '\0';
return count;
}
static char*
EnvVar_get( const char* varname )
{
/***
* getenvが返すポインタはプログラマが予期しない形で非常に無効化しやすく危険である.
* このポインタが示す内容は、putenvやsetenvの呼び出しによってメモリが書き換えられ、
* ポインタが無効化される可能性がある. しかしそれだけではない. getenv関数はその
* 名前とは裏腹に環境変数が存在するメモリ領域を書き換える場合がある. よって、
* 次の単なるgetenvの呼び出しによってすらメモリが書き換えられ、このポインタが
* 無効化される可能性がある. このことは一見なんともないような次のようなコードが
* 完全に不適合なコードであることを示す.
*
* const char* var_TMP = getenv( "TMP" );
* const char* var_TEMP = getenv( "TEMP" ); // <= この時点でvar_TMPは無効化する恐れがある!
*
* 従ってユーザは、これが指す内容を直ちに別バッファへコピーすべきである.
* しかしこれでも万全ではない. getenvはスレッドセーフでもないため、上記の例で
* 別バッファへコピーするようにしたとしても、コピーが完了する前に他のスレッドにおいて、
* putenvやgetenvが呼び出されることで、var_TMPが突然無効化され、コピーに失敗するシナリオも
* 有り得る.
*
* よって、さらにこれを防ぐには、ここでGlobalMutexにより lock/unlockを掛ける必要がある.
* しかしこのcgi_utilは入門向けに用意されたユーティリティなのでそこまではやらない.
* 本格的なものについてはlibZnkのZnk_envarに実装されているので、そちらを使用していただきたい.
*/
const char* unsafe_ptr = NULL;
char* ans = NULL;
/* GlobalMutex_lock(); マルチスレッドの場合なら必要 */
unsafe_ptr = getenv( varname );
if( unsafe_ptr ){
ans = strdup( unsafe_ptr );
} else {
ans = strdup( "" );
}
/* GlobalMutex_unlock(); マルチスレッドの場合なら必要 */
return ans;
}
static void
EnvVar_free( char* val )
{
if( val ){
free( val );
}
}
CGIEVar*
CGIEVar_create( void )
{
CGIEVar* evar = malloc( sizeof( struct CGIEVar_tag ) );
evar->server_name_ = EnvVar_get( "SERVER_NAME" );
evar->server_port_ = EnvVar_get( "SERVER_PORT" );
evar->content_type_ = EnvVar_get( "CONTENT_TYPE" );
evar->content_length_ = EnvVar_get( "CONTENT_LENGTH" );
evar->remote_addr_ = EnvVar_get( "REMOTE_ADDR" );
evar->remote_host_ = EnvVar_get( "REMOTE_HOST" );
evar->remote_port_ = EnvVar_get( "REMOTE_PORT" );
evar->request_method_ = EnvVar_get( "REQUEST_METHOD" );
evar->query_string_ = EnvVar_get( "QUERY_STRING" );
evar->http_cookie_ = EnvVar_get( "HTTP_COOKIE" );
evar->http_user_agent_= EnvVar_get( "HTTP_USER_AGENT" );
evar->http_accept_ = EnvVar_get( "HTTP_ACCEPT" );
return evar;
}
void
CGIEVar_destroy( CGIEVar* evar )
{
if( evar ){
EnvVar_free( evar->server_name_ );
EnvVar_free( evar->server_port_ );
EnvVar_free( evar->content_type_ );
EnvVar_free( evar->content_length_ );
EnvVar_free( evar->remote_addr_ );
EnvVar_free( evar->remote_host_ );
EnvVar_free( evar->remote_port_ );
EnvVar_free( evar->request_method_ );
EnvVar_free( evar->query_string_ );
EnvVar_free( evar->http_cookie_ );
EnvVar_free( evar->http_user_agent_ );
EnvVar_free( evar->http_accept_ );
free( evar );
}
}
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size )
{
/***
* まず & で区切られたquery_idx番目のtokenを求める.
* そのようなtokenの開始位置をbegin,
* その終了位置(tokenの最後の文字の次の位置)をendとする.
*/
const char* begin = query_string;
const char* end = strchr( begin, '&' );
while( query_idx ){
if( end == NULL ){
/* Error : 要求されたquery_idx番目のtokenは存在しない */
return 0;
}
begin = end+1; /* &の次 */
end = strchr( begin, '&' );
--query_idx;
}
if( end == NULL ){
/***
* 要求されたquery_idx番目のtokenは最後にある.
* endをquery_stringの終端文字のある位置へ補正.
*/
end = query_string + strlen( query_string );
}
/***
* 次に query_idx番目のtoken内の = 位置を探索.
* これより前がkey, 後がvalの値であるから、
* これをそれぞれのバッファにコピーする.
*/
{
const char* eq_pos = strchr( begin, '=' );
if( eq_pos && eq_pos < end ){
copyStr_safely( key, key_size, begin, eq_pos-begin );
copyStr_safely( val, val_size, eq_pos+1, end-(eq_pos+1) );
} else {
/* Error : [ begin, end ) 内に '=' が存在しない */
return 0;
}
}
return 1;
}
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir )
{
static const char* filename = "authentic_key.dat";
char path[ 256 ] = "";
FILE* fp;
copyStr_safely( path, sizeof(path), serv_dir, strlen(serv_dir) );
concatEx( path, sizeof(path), filename, strlen(filename) );
fp = fopen( path, "rb" );
if( fp ){
fgets( authentic_key, authentic_key_size, fp );
fclose( fp );
return 1;
} else {
copyStr_safely( authentic_key, authentic_key_size, "0123456789ABCDEF", 16 );
}
return 0;
}
Close
#ifndef INCLUDE_GUARD__cgi_util_h__
#define INCLUDE_GUARD__cgi_util_h__
#include <stddef.h>
/***
* 標準入出力に関するバイナリモードの変更.
*/
void
CGIUtil_Internal_setMode( int no, int is_binary_mode );
/***
* msecミリ秒sleepします.
*/
void
CGIUtil_sleep( size_t msec );
/***
* @brief
* 標準入力からのデータをstdin_bfrへと格納する.
* stdin_bfrには最低でも stdin_bfr_size バイト以上のメモリが確保されていなければならない.
*
* @return
* 実際に取得されたバイト数を返す.
* すなわちこれがcontent_lengthと同じ場合はすべて取得できており、
* そうでなければ、content_length が stdin_bfr_size より大きいか、
* さもなければ予期せぬEOFが発生した可能性がある.
*/
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length );
/***
* CGIで扱う環境変数.
*/
typedef struct CGIEVar_tag {
char* server_name_;
char* server_port_;
char* content_type_;
char* content_length_;
char* remote_addr_;
char* remote_host_;
char* remote_port_;
char* request_method_;
char* query_string_;
char* http_cookie_;
char* http_user_agent_;
char* http_accept_;
} CGIEVar;
CGIEVar*
CGIEVar_create( void );
void
CGIEVar_destroy( CGIEVar* evar );
/***
* @brief
* CGIで扱うQuery String.
*
* Query String が指定されている場合はそれを取得する.
* 指定されていない場合、この関数は空文字を返す.
* Query String が指定されていない場合、evar->query_string_ は NULLとなっている.
*
* @return
* 成功ならば1、失敗ならば0を返す.
*/
//#define CGIUtil_getQueryString( evar ) ( (evar)->query_string_ ? (evar)->query_string_ : "" )
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size );
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir );
#endif /* INCLUDE_GUARD */
CloseMoai_AuthenticKeyとは、今現在起動しているMoaiが固有で割り当てている認証キーで、16桁の16進数となります。 この次のセクションではPOSTについて学びますが、その前段階としてこれを取り挙げるのは、 単にPOSTするよりこのような認証キーを付加してPOSTした方がより安全性が高まるためです。
POSTを行うCGIプログラムでは、若干安全性に関する配慮が必要です。 この記事はネット上に公開されているものですから、(非常に確率は低いとは思いますが)悪意あるWebサイトの作成者がこの記事を読み、 Moai CGIの仕様を把握した上で、何を思ったか「http://localhost:8124/cgis/cgi_developers/cgi_c/your_cgi.cgi」になんらかの攻撃をしかけるような javascriptを自分のサイトに仕込み、かつ非常に運悪くあなたがそのサイトにアクセスするといったシナリオも一応想定はできます。 これはそのような状況になった場合でも確実にそれを防御するための仕組みです。
参考: XhrDMZ
通常、Moai/Easterはhttp://localhost:8124にアクセスして使用しますが、
このURLの「8124」の部分が「8125」になる場合があります。
http://localhost:8125のことをMoaiではXhrDMZと呼びます。
XhrDMZに隔離されたjavascriptは、(少なくともjavascriptによって機械的には)このMoai認証キーの値を知ることができません。 これを利用して、あまり安全ではないよくわからないjavascriptはXhrDMZへ置き、 一方、安全であることがわかっていて重要な処理を行うCGIなどをMoai WebServer上で それぞれ分離して実行させれば互いに干渉する範囲を小さくでき、セキュリティはより強固なものになります。
Moai CGIを開発する上ではXhrDMZについてこれ以上のことを知っておく必要はありませんが、 XhrDMZについてさらに知りたい方はこちらを参照してください。
Close
XhrDMZに隔離されたjavascriptは、(少なくともjavascriptによって機械的には)このMoai認証キーの値を知ることができません。 これを利用して、あまり安全ではないよくわからないjavascriptはXhrDMZへ置き、 一方、安全であることがわかっていて重要な処理を行うCGIなどをMoai WebServer上で それぞれ分離して実行させれば互いに干渉する範囲を小さくでき、セキュリティはより強固なものになります。
Moai CGIを開発する上ではXhrDMZについてこれ以上のことを知っておく必要はありませんが、 XhrDMZについてさらに知りたい方はこちらを参照してください。
Close
CGIへのPOSTはJavascriptから自動的に行うこともできます。 この記事はJavascriptの講座ではありませんが、そのメカニズムを理解するために、 一旦 XMLHttpRequest について説明しましょう。 例えばJavascript上から、以下のようなコードで指定したサイトへ自動的にPOSTすることができます。
function checkPost( filename ){
var data = {
"emal":"sage"
,"com": "Test"
};
var xhr = newXhr();
xhr.open( "POST", "http://127.0.0.1:8124/cgis/cgi_developers/" + filename );
xhr.onreadystatechange = function() {
if( xhr.readyState == 4 ){
if( typeof xhr.status != "undefined" ){
if( xhr.status == 200 ){
var tgt = document.getElementById( "result" );
var text = xhr.responseText;
tgt.innerHTML = text;
} else {
var tgt = document.getElementById( "result" );
var text = "status code = " + xhr.status;
tgt.innerHTML = text;
}
}
}
};
xhr.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
xhr.send( EncodeHTMLForm( data ) );
}
上記の data の部分がこれまでに学んだpost_varsにあたるもので、 このコードはこれを xhr.open の第2引数で指定したサイトへPOSTするものになります。 またこのコードによるPOSTは、それがPOSTされるサーバとは別の位置にあるJavascriptからも行うことができます (ただしすぐ後で述べますが、そのような位置にあるJavascriptからの実行の場合、上記の「xhr.responseText」の値は取得不能です)。
さて、POSTされるサーバ側でこのようなJavascriptでの投稿を拒否したければ、 これがJavascriptからの投稿か否かを判別する情報が必要となり、そのための情報がMoai_AuthenticKeyとなります。 Moai_AuthenticKeyはPOSTされるサーバ側からユーザに向けて発行されます。 POST時にこのキーもいっしょに含めて投稿すれば、このキーが指定されていないか、その値が異なる場合は無効な投稿とみなし、 その投稿を棄却することができます。
しかしこのMoai_AuthenticKeyの値をJavascript上から自動的に取得される恐れはないでしょうか? 実はPOSTされるサーバとは別のURLに置かれたJavascriptからの実行の場合、 このMoai_AuthenticKeyをJavascript上から取得することはできません。 というのは、Javascript上でサーバから発行されたHTMLの内容を取得するには、上記の「xhr.responseText」の値を調べる必要がありますが、 POSTされるサーバとは別のURLに置かれたJavascriptからの実行の場合、xhr.responseTextの値を取得することはできないためです。 よってJavascript上から自動的にMoai_AuthenticKeyを取得し、それを元に自動的にPOSTするといったようなことは、 (POSTされるサーバとは別のURLのJavascriptからは)不可能となります。
通常、作成したcgiファイルはmoaiのインストールディレクトリ内の cgis/cgi_developers/cgi_c へコピーしますが、
この記事の旧バージョンでは、これをcgis/cgi_developers/protected へコピーするように説明していました。
この場合開くべきURLは、例えばhello.cgiの場合であれば http://localhost:8124/cgis/cgi_developers/protected/hello.cgi のようになります。
これを実行させるといきなり次のような画面が表示されるはずです。
これはMoai CGIのセキュリティー機構が働いたために表示されます。 とりあえず画面の指示に従って「Please retry this」を押せば、「Hello World on Moai CGI」と表示されると思います。
最初に開いたURLは http://localhost:8124/cgis/cgi_developers/protected/hello.cgi でした。 しかし「Please retry this」ボタンを押した後では、URLが変化しています。 http://localhost:8124 と /cgis の間に、/authentic/16桁の16進数列というパターンが挿入された形となっています。 この16桁の16進数列の部分をMoai認証キー(Moai_AuthenticKey)と呼びます。
protectedに置くことで悪意ある外部javascriptからこのCGIへ機械的なPOSTが行われるのを確実に防止する効果が期待できます。 ただ毎回上記のような表示が出てるのもやはり煩わしいですし使い勝手に影響がでます。 そこでこのセクションでは、Moai_AuthenticKeyをMoaiディレクトリから直接取得して認証する方法を紹介します。
これはMoai CGIのセキュリティー機構が働いたために表示されます。 とりあえず画面の指示に従って「Please retry this」を押せば、「Hello World on Moai CGI」と表示されると思います。
最初に開いたURLは http://localhost:8124/cgis/cgi_developers/protected/hello.cgi でした。 しかし「Please retry this」ボタンを押した後では、URLが変化しています。 http://localhost:8124 と /cgis の間に、/authentic/16桁の16進数列というパターンが挿入された形となっています。 この16桁の16進数列の部分をMoai認証キー(Moai_AuthenticKey)と呼びます。
protectedに置くことで悪意ある外部javascriptからこのCGIへ機械的なPOSTが行われるのを確実に防止する効果が期待できます。 ただ毎回上記のような表示が出てるのもやはり煩わしいですし使い勝手に影響がでます。 そこでこのセクションでは、Moai_AuthenticKeyをMoaiディレクトリから直接取得して認証する方法を紹介します。
Moai_AuthenticKeyの取得
ではここからはmoai_auth.cのコードの解説に入りましょう。 moai_auth.cはサーバ側からユーザに向けてMoai_AuthenticKeyを発行するためのCGIプログラムになります。
このプログラム自体は前回よりもシンプルです。 show_result関数については今までと同じくHTTPヘッダやHTMLを標準出力するものですが、 その最初あたりでCGIUtil_getMoaiAuthKey関数を呼び出しています。 この関数もまたcgi_utilで定義されたものですが、その定義部を以下に示します。
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir )
{
static const char* filename = "authentic_key.dat";
char path[ 256 ] = "";
FILE* fp;
copyStr_safely( path, sizeof(path), serv_dir, strlen(serv_dir) );
concatEx( path, sizeof(path), filename, strlen(filename) );
fp = fopen( path, "rb" );
if( fp ){
fgets( authentic_key, authentic_key_size, fp );
fclose( fp );
return 1;
} else {
copyStr_safely( authentic_key, authentic_key_size, "0123456789ABCDEF", 16 );
}
return 0;
}
上記の関数は、Moaiインストールディレクトリ直下に存在すると思われるauthentic_key.datファイルを開き、 その一行目に記述されているはずのMoai_AuthenticKeyの値をauthentic_keyへ取得するといったものになります。 第3引数のserv_dirにより、Moaiインストールディレクトリへの相対パスを指定します。
copyStr_safelyは文字列を安全にコピーするためのもので、serv_dirの値をpathへコピーしています。 またconcatExは文字列を安全に連結するためのもので、filenameの値をpathへコピーしています。
万一serv_dirで指定されたディレクトリにauthentic_key.datファイルが存在しない場合は、 代替処理としてauthentic_keyをこの関数で自作します。 16桁の16進数であれば何でも構いませんが、上記ではとりあえず"0123456789ABCDEF"としてあります。 この値はある程度予測されにくいものであるほうが望ましいですが、 しかしCGIUtil_loadAuthenticKey関数が呼ばれるたびに値が変化するようでは問題があります。 というのはユーザが最初にCGIプログラムへアクセスして、それに関するトランザクションを続ける間はその値は一定でなければならないためです。 この値はWebサーバが再起動するあたりのタイミングで変化するのが理想的と思われます。
CGIUtil_loadAuthenticKey関数が呼ばれる側も見ておきましょう。 以下のようになっています。
char auth_key[ 32 ] = "";
char* serv_dir = "../../../"; /* moai top dir */
CGIUtil_loadAuthenticKey( auth_key, sizeof(auth_key), serv_dir );
/* 途中略 */
printf( "Moai_AuthenticKey=[%s].\n", rejectHtmlTag(auth_key) );
moai_auth.cgiは、Moaiディレクトリ直下のcgis/cgi_developers/cgi_cディレクトリ内にありますので、 そこから逆算すると、moai_auth.cgi から Moaiディレクトリへの相対パスは、"../../../"となります。 そのため、上記のserv_dirでもそのように指定しています。 後はauth_keyを取得したらそれをブラウザ上で表示するといった単純な処理を行っています。
ブラウザ上で表示してはいますが、(既に述べた通り)他のURL上にあるjavascriptからはこの値を
(XMLHttpRequestによって機械的に)取得することはできません。
目次に戻る
Hello POST
文字列の投稿
今回は CGI へフォームの内容をPOSTしてみます(ただし文字列限定です)。 これをC言語で(ライブラリなしで)行うのはかなり大変ですが、 とりあえずこの記事ではやれるところまでやってみましょう。
Execute : cgi_c/post1.cgi Download View source code(post1.c) (157 lines)
#include "cgi_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const char*
rejectHtmlTag( const char* cstr )
{
/***
* 一応最低限のXSS対策はしておく.
* 本来はもっと真面目に無効化処理を行うべきですが、これは所詮サンプルなので極力簡単化するため、
* HTMLタグを含む可能性がある時点でNULLを返して弾くという猛烈な制限をかけて対処しています.
*/
if( cstr && ( strchr( cstr, '<' ) || strchr( cstr, '>' ) ) ){
return NULL;
}
return cstr;
}
static void
show_result( CGIEVar* evar, const char* query_string )
{
/***
* Output HTTP Header for CGI
*/
//printf( "Content-type: text/html;\r\n" );
/* ブラウザにキャッシュさせない。 */
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
printf( "\r\n" );
/***
* Output HTML for CGI
*/
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" );
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<META http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\">\n" );
printf( "<META http-equiv=\"Content-Script-Type\" content=\"text/javascript\">\n" );
printf( "<META http-equiv=\"Content-Style-Type\" content=\"text/css\">\n" );
printf( "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" );
printf( "<link href=\"/bulma.css\" rel=\"stylesheet\" type=\"text/css\" />\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "<div class=\"section\">\n" );
printf( "Moai CGI Post test.\n" );
printf( "<form action=\"post1.cgi\" method=\"POST\">\n" );
printf( " <input style=\"max-width:400px;\" class=input type=text name=email placeholder=\"E-mail\" size=28><br>\n" );
printf( " <textarea class=textarea name=com placeholder=\"Comment\" cols=48 rows=\"4\"></textarea><br>\n" );
printf( " <input style=\"padding:0.4em 1em;\" type=submit value=\"Submit\"><br>\n" );
printf( "</form>\n" );
printf( "<pre>\n" );
printf( "URL Query String Tokens: \n" );
/***
* これは所詮サンプルであるため、あまり柔軟性を考慮するとコードが煩雑になり過ぎて本質がぼやけてしまいます.
* ここでは、変数の最大数、値バッファや入力バッファの最大サイズなどを固定化してあります.
*/
{
size_t i = 0;
for( i=0; i<64; ++i ){
char key[ 256 ] = "";
char val[ 256 ] = "";
int result = CGIUtil_getQueryStringToken( query_string, i,
key, sizeof(key),
val, sizeof(val) );
if( result == 0 ){
break;
}
printf( "%s = [%s]\n", rejectHtmlTag(key), rejectHtmlTag(val) );
}
}
printf( "\n" );
/***
* CGIにおけるフォームの投稿データをpost_varsへと取得します.
* またこの関数においては第1引数evarの指定も必要となります.
*/
printf( "StdIn Posting Data: \n" );
{
char stdin_bfr[ 4096 ] = "";
char* end = NULL;
size_t content_length = strtoul( evar->content_length_, &end, 10 );
printf( "Content-Length = [%u]\n", (unsigned int)content_length );
if( evar->content_length_ != end ){
size_t i = 0;
CGIUtil_getStdInStr( stdin_bfr, sizeof(stdin_bfr), content_length );
printf( "Original data = [%s]\n", rejectHtmlTag(stdin_bfr) );
printf( "Tokens: \n" );
for( i=0; i<64; ++i ){
char key[ 256 ] = "";
char val[ 256 ] = "";
int result = CGIUtil_getQueryStringToken( stdin_bfr, i,
key, sizeof(key),
val, sizeof(val) );
if( result == 0 ){
break;
}
printf( "%s = [%s]\n", rejectHtmlTag(key), rejectHtmlTag(val) );
}
} else {
printf( "Error : content_length is not found.\n" );
}
}
printf( "</pre>" );
printf( "</div> <!-- section -->\n" );
printf( "</body></html>\n" );
fflush( stdout );
}
int main( void )
{
/***
* CGIにおける環境変数を取得します.
*/
CGIEVar* evar = CGIEVar_create();
/***
* URLにおける Query String が指定されている場合はそれを取得します.
* 指定されていない場合、この関数は空文字を返します.
*/
const char* query_string = evar->query_string_;
{
static const int is_binary_mode = 1;
/***
* Windowsにおける標準入力(0)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 0, is_binary_mode );
/***
* Windowsにおける標準出力(1)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 1, is_binary_mode );
}
/***
* query_string の内容は以下のように & で区切られる変数指定の連続となっているはずです.
*
* name1=val1&name2=val2&name3=val3&...
*
* これを & 文字に関して分割(split)して、結果を出力します.
* また標準入力よりCGIにおけるフォームの投稿データを取得して、結果を出力します.
* 長くなるため、サブルーチン化します.
*/
show_result( evar, query_string );
/***
* CGIEVar* は使用後解放する必要があります.
*/
CGIEVar_destroy( evar );
return 0;
}
Close
#include "cgi_util.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#if _MSC_VER >= 1400
# pragma warning(disable: 4996)
#endif
/***
* Target OSを判定するマクロ.
* TARGET_* という形式の名前となる.
*/
#if defined(_WIN32) || defined(_WIN64) || defined(_MSC_VER) || \
defined(__MINGW32__) || defined(__MINGW64__) || \
defined(__BORLANDC__) || defined(__DMC__)
/* Windows API */
# define TARGET_WINDOWS 1
#elif defined(__unix__) || defined(__linux__) || defined(__CYGWIN__) || \
defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__)
/* POSIX UNIX + X11 およびその互換システム */
# define TARGET_UNIX 1
#else
# error "Not supported OS"
#endif
#if defined(TARGET_WINDOWS)
/* windows API */
# include <windows.h> /* for Sleep */
# include <io.h> /* for setmode */
# include <fcntl.h> /* for setmode */
#else
/* UNIXというかPosixを使った実装 */
# include <unistd.h> /* for usleep */
#endif
#if defined(TARGET_WINDOWS)
#endif
#define M_MIN(x,y) ( (x)<(y) ? (x) : (y) )
static void
copyStr_safely( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
if( buf_size ){
const size_t cpy_size = M_MIN( cstr_leng, buf_size-1 );
memmove( buf, cstr, cpy_size );
buf[ cpy_size ] = '\0';
}
}
static size_t
S_strnlen( const char* buf, size_t buf_size )
{
char* p = (char*)memchr( buf, '\0', buf_size );
return ( p == NULL ) ? buf_size : (size_t)(p - buf);
}
static char*
concatEx( char* buf, size_t buf_size, const char* cstr, size_t cstr_leng )
{
size_t dst_len;
if( buf_size == 0 || buf == NULL ){ return NULL; }
dst_len = S_strnlen( buf, buf_size );
if( dst_len == buf_size ){ return NULL; }
if( cstr_leng < buf_size - dst_len ){
memmove( buf + dst_len, cstr, cstr_leng );
buf += ( dst_len + cstr_leng );
*buf = '\0';
return buf;
}
/* trancate over parts */
memmove( buf + dst_len, cstr, buf_size - 1 - dst_len );
buf += buf_size - 1;
*buf = '\0';
return buf;
}
#if defined(TARGET_WINDOWS)
static FILE* Internal_getStdFP( int no )
{
switch( no ){
case 0: return stdin;
case 1: return stdout;
case 2: return stderr;
default:
break;
}
return NULL;
}
#endif
void
CGIUtil_Internal_setMode( int no, int is_binary_mode )
{
#if defined(TARGET_WINDOWS)
/* モード変更に先立ってまず確実にfflushしておく必要がある */
fflush( (FILE*)Internal_getStdFP( no ) );
# if defined(__BORLANDC__)
setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# else
_setmode( no, is_binary_mode ? _O_BINARY : _O_TEXT );
# endif
#endif
}
void
CGIUtil_sleep( size_t msec )
{
#if defined(TARGET_WINDOWS)
Sleep( (DWORD)msec );
#else
usleep( msec * 1000 ); /* 指定はマイクロ秒 */
#endif
}
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length )
{
int chr = 0;
size_t count = 0;
if( stdin_bfr_size == 0 ){
return 0;
}
while( count < content_length && count < stdin_bfr_size-1 ){
/* 渡されたデータを標準入力より取得する */
chr = fgetc( stdin );
/* データ終了のチェック */
if( chr == EOF || chr == (int)content_length ){
/* content_lengthに満たないうちに EOFが出現 */
break;
} else if( count < stdin_bfr_size ){
stdin_bfr[ count ] = (unsigned char)chr;
++count;
}
}
/***
* count は最大でも stdin_bfr_size-1
* すなわち最大でもstdin_bfrの最後を指し示す.
* よって以下でstdin_bfrから確実にはみ出すことなくnull終端できる.
*/
stdin_bfr[ count ] = '\0';
return count;
}
static char*
EnvVar_get( const char* varname )
{
/***
* getenvが返すポインタはプログラマが予期しない形で非常に無効化しやすく危険である.
* このポインタが示す内容は、putenvやsetenvの呼び出しによってメモリが書き換えられ、
* ポインタが無効化される可能性がある. しかしそれだけではない. getenv関数はその
* 名前とは裏腹に環境変数が存在するメモリ領域を書き換える場合がある. よって、
* 次の単なるgetenvの呼び出しによってすらメモリが書き換えられ、このポインタが
* 無効化される可能性がある. このことは一見なんともないような次のようなコードが
* 完全に不適合なコードであることを示す.
*
* const char* var_TMP = getenv( "TMP" );
* const char* var_TEMP = getenv( "TEMP" ); // <= この時点でvar_TMPは無効化する恐れがある!
*
* 従ってユーザは、これが指す内容を直ちに別バッファへコピーすべきである.
* しかしこれでも万全ではない. getenvはスレッドセーフでもないため、上記の例で
* 別バッファへコピーするようにしたとしても、コピーが完了する前に他のスレッドにおいて、
* putenvやgetenvが呼び出されることで、var_TMPが突然無効化され、コピーに失敗するシナリオも
* 有り得る.
*
* よって、さらにこれを防ぐには、ここでGlobalMutexにより lock/unlockを掛ける必要がある.
* しかしこのcgi_utilは入門向けに用意されたユーティリティなのでそこまではやらない.
* 本格的なものについてはlibZnkのZnk_envarに実装されているので、そちらを使用していただきたい.
*/
const char* unsafe_ptr = NULL;
char* ans = NULL;
/* GlobalMutex_lock(); マルチスレッドの場合なら必要 */
unsafe_ptr = getenv( varname );
if( unsafe_ptr ){
ans = strdup( unsafe_ptr );
} else {
ans = strdup( "" );
}
/* GlobalMutex_unlock(); マルチスレッドの場合なら必要 */
return ans;
}
static void
EnvVar_free( char* val )
{
if( val ){
free( val );
}
}
CGIEVar*
CGIEVar_create( void )
{
CGIEVar* evar = malloc( sizeof( struct CGIEVar_tag ) );
evar->server_name_ = EnvVar_get( "SERVER_NAME" );
evar->server_port_ = EnvVar_get( "SERVER_PORT" );
evar->content_type_ = EnvVar_get( "CONTENT_TYPE" );
evar->content_length_ = EnvVar_get( "CONTENT_LENGTH" );
evar->remote_addr_ = EnvVar_get( "REMOTE_ADDR" );
evar->remote_host_ = EnvVar_get( "REMOTE_HOST" );
evar->remote_port_ = EnvVar_get( "REMOTE_PORT" );
evar->request_method_ = EnvVar_get( "REQUEST_METHOD" );
evar->query_string_ = EnvVar_get( "QUERY_STRING" );
evar->http_cookie_ = EnvVar_get( "HTTP_COOKIE" );
evar->http_user_agent_= EnvVar_get( "HTTP_USER_AGENT" );
evar->http_accept_ = EnvVar_get( "HTTP_ACCEPT" );
return evar;
}
void
CGIEVar_destroy( CGIEVar* evar )
{
if( evar ){
EnvVar_free( evar->server_name_ );
EnvVar_free( evar->server_port_ );
EnvVar_free( evar->content_type_ );
EnvVar_free( evar->content_length_ );
EnvVar_free( evar->remote_addr_ );
EnvVar_free( evar->remote_host_ );
EnvVar_free( evar->remote_port_ );
EnvVar_free( evar->request_method_ );
EnvVar_free( evar->query_string_ );
EnvVar_free( evar->http_cookie_ );
EnvVar_free( evar->http_user_agent_ );
EnvVar_free( evar->http_accept_ );
free( evar );
}
}
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size )
{
/***
* まず & で区切られたquery_idx番目のtokenを求める.
* そのようなtokenの開始位置をbegin,
* その終了位置(tokenの最後の文字の次の位置)をendとする.
*/
const char* begin = query_string;
const char* end = strchr( begin, '&' );
while( query_idx ){
if( end == NULL ){
/* Error : 要求されたquery_idx番目のtokenは存在しない */
return 0;
}
begin = end+1; /* &の次 */
end = strchr( begin, '&' );
--query_idx;
}
if( end == NULL ){
/***
* 要求されたquery_idx番目のtokenは最後にある.
* endをquery_stringの終端文字のある位置へ補正.
*/
end = query_string + strlen( query_string );
}
/***
* 次に query_idx番目のtoken内の = 位置を探索.
* これより前がkey, 後がvalの値であるから、
* これをそれぞれのバッファにコピーする.
*/
{
const char* eq_pos = strchr( begin, '=' );
if( eq_pos && eq_pos < end ){
copyStr_safely( key, key_size, begin, eq_pos-begin );
copyStr_safely( val, val_size, eq_pos+1, end-(eq_pos+1) );
} else {
/* Error : [ begin, end ) 内に '=' が存在しない */
return 0;
}
}
return 1;
}
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir )
{
static const char* filename = "authentic_key.dat";
char path[ 256 ] = "";
FILE* fp;
copyStr_safely( path, sizeof(path), serv_dir, strlen(serv_dir) );
concatEx( path, sizeof(path), filename, strlen(filename) );
fp = fopen( path, "rb" );
if( fp ){
fgets( authentic_key, authentic_key_size, fp );
fclose( fp );
return 1;
} else {
copyStr_safely( authentic_key, authentic_key_size, "0123456789ABCDEF", 16 );
}
return 0;
}
Close
#ifndef INCLUDE_GUARD__cgi_util_h__
#define INCLUDE_GUARD__cgi_util_h__
#include <stddef.h>
/***
* 標準入出力に関するバイナリモードの変更.
*/
void
CGIUtil_Internal_setMode( int no, int is_binary_mode );
/***
* msecミリ秒sleepします.
*/
void
CGIUtil_sleep( size_t msec );
/***
* @brief
* 標準入力からのデータをstdin_bfrへと格納する.
* stdin_bfrには最低でも stdin_bfr_size バイト以上のメモリが確保されていなければならない.
*
* @return
* 実際に取得されたバイト数を返す.
* すなわちこれがcontent_lengthと同じ場合はすべて取得できており、
* そうでなければ、content_length が stdin_bfr_size より大きいか、
* さもなければ予期せぬEOFが発生した可能性がある.
*/
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length );
/***
* CGIで扱う環境変数.
*/
typedef struct CGIEVar_tag {
char* server_name_;
char* server_port_;
char* content_type_;
char* content_length_;
char* remote_addr_;
char* remote_host_;
char* remote_port_;
char* request_method_;
char* query_string_;
char* http_cookie_;
char* http_user_agent_;
char* http_accept_;
} CGIEVar;
CGIEVar*
CGIEVar_create( void );
void
CGIEVar_destroy( CGIEVar* evar );
/***
* @brief
* CGIで扱うQuery String.
*
* Query String が指定されている場合はそれを取得する.
* 指定されていない場合、この関数は空文字を返す.
* Query String が指定されていない場合、evar->query_string_ は NULLとなっている.
*
* @return
* 成功ならば1、失敗ならば0を返す.
*/
//#define CGIUtil_getQueryString( evar ) ( (evar)->query_string_ ? (evar)->query_string_ : "" )
int
CGIUtil_getQueryStringToken( const char* query_string, size_t query_idx,
char* key, size_t key_size,
char* val, size_t val_size );
int
CGIUtil_loadAuthenticKey( char* authentic_key, size_t authentic_key_size, const char* serv_dir );
#endif /* INCLUDE_GUARD */
CloseこのCGIスクリプトは二つの仕事を同時に行っています。 一つはクライアントが送信するためのフォームを表示することであり、 もう一つはクライアントが送信したフォームの内容を獲得してその結果を表示することです。
送信したフォームの内容をCGIスクリプト側で獲得するにはどうすればよいでしょうか? 原理的にはCGIでは標準入力よりクライアントが送信したフォームの内容を獲得する仕様となっています。
ですが毎回そんなことをしていては大変ですので、 この処理は cgi_util の CGIUtil_getStdInStr関数にまとめてあります。 まずこの関数の定義部を以下に抜粋します。
size_t
CGIUtil_getStdInStr( char* stdin_bfr, size_t stdin_bfr_size, size_t content_length )
{
int chr = 0;
size_t count = 0;
if( stdin_bfr_size == 0 ){
return 0;
}
while( count < content_length && count < stdin_bfr_size-1 ){
/* 渡されたデータを標準入力より取得する */
chr = fgetc( stdin );
/* データ終了のチェック */
if( chr == EOF || chr == (int)content_length ){
/* content_lengthに満たないうちに EOFが出現 */
break;
} else if( count < stdin_bfr_size ){
stdin_bfr[ count ] = (unsigned char)chr;
++count;
}
}
/***
* count は最大でも stdin_bfr_size-1
* すなわち最大でもstdin_bfrの最後を指し示す.
* よって以下でstdin_bfrから確実にはみ出すことなくnull終端できる.
*/
stdin_bfr[ count ] = '\0';
return count;
}
C言語ではfgetc関数で標準入力から文字を一つずつ取得できます。 CGIUtil_getStdInStr関数では上記のようにwhile文でこれを(content_lengthで指定された回数だけ)繰り返し、 stdin_bfrに文字列として格納しています。
CGIUtil_getStdInStr関数を呼び出して使っている側も見ておきましょう。 以下のようになっています。
{
char stdin_bfr[ 4096 ] = "";
char* end = NULL;
size_t content_length = strtoul( evar->content_length_, &end, 10 );
printf( "Content-Length = [%u]\n", (unsigned int)content_length );
if( evar->content_length_ != end ){
size_t i = 0;
CGIUtil_getStdInStr( stdin_bfr, sizeof(stdin_bfr), content_length );
printf( "Original data = [%s]\n", rejectHtmlTag(stdin_bfr) );
printf( "Tokens: \n" );
for( i=0; i<64; ++i ){
char key[ 256 ] = "";
char val[ 256 ] = "";
int result = CGIUtil_getQueryStringToken( stdin_bfr, i,
key, sizeof(key),
val, sizeof(val) );
if( result == 0 ){
break;
}
printf( "%s = [%s]\n", rejectHtmlTag(key), rejectHtmlTag(val) );
}
} else {
printf( "Error : content_length is not found.\n" );
}
}
上記ではまずCGIEVar構造体であるevarのメンバcontent_length_を整数として解釈し、 それをcontent_lengthとして取得します。 content_lengthとは、これから受信されるデータのサイズを意味し、 大元はWebサーバによって環境変数 CONTENT_LENGTH に文字列として格納されているものです。 CGIEVar構造体の場合content_length_メンバの値がこれに該当します。 上記ではC言語の標準関数 strtoul で content_length_ の値を10進数整数として解釈し、文字列から非負整数(size_t型)へ変換しています。 このstrtoul関数を実行した結果、第2引数で与えたポインタ値 end が第1引数と異なっていれば、この変換は成功しています。
その後、CGIUtil_getStdInStr関数を呼び出し、結果をstdin_bfrバッファに格納します。 このとき同時にstdin_bfr_size(stdin_bfrの確保サイズ)を指定する必要があります。 CGIUtil_getStdInStr関数は、読み込みデータの大きさがstdin_bfrの確保サイズを超える場合、そこで適切に打ち切ってNull終端します。 この関数の戻り値は実際に読み込まれたバイトサイズですが、上記ではそこまでのチェックは行っていません。
stdin_bfrが標準入力から得られたデータ列のすべてとなりますが、このままでは使い辛いため、さらにこれを解析しなければいけません。 x-www-form-urlencoded形式でのPOSTの場合、これはQuery Stringと同様のフォーマットになっています。 そのため、Query Stringのセクションで見たのと同様の方法でkeyとvalを得て、その値を表示しています。 これが一応このプログラムのゴールです。
文字列ではなく画像を投稿したい場合、x-www-form-urlencoded形式ではなくmultipart/form-dataの形式でPOSTする必要があります。
しかしこの形式の解析は(文字列ライブラリなどを用いずに)ここで解説するにはちょっと大変であるため、
取り上げないことにします。
また全角文字列を投稿した場合、結果の報告表示はURLエンコーディングした状態になっているとおもいます。 本来はこれをデコードする必要がありますが、これも(文字列ライブラリなどを用いずに)ここで解説するにはちょっと大変であるため、 ここでは割愛します。 これらに対応したバージョンはlibZnkとlibRanoを紹介した後に改めて取り上げます。
また全角文字列を投稿した場合、結果の報告表示はURLエンコーディングした状態になっているとおもいます。 本来はこれをデコードする必要がありますが、これも(文字列ライブラリなどを用いずに)ここで解説するにはちょっと大変であるため、 ここでは割愛します。 これらに対応したバージョンはlibZnkとlibRanoを紹介した後に改めて取り上げます。
目次に戻る
Transfer-Encoding: chunked転送モードとsleep
fflushとチャンク
今回は Moai CGI でTransfer-Encoding: chunked 転送モードがどのように機能しているかを確認してみます。 これが一体何なのか、そしてそれがC言語のどのような記述に対応して機能するのかも説明します。
Execute : cgi_c/transfer_chunked.cgi View source code (67 lines)
#include "cgi_util.h"
#include <stdio.h>
static void
show_result( void )
{
/***
* Output HTML for CGI
*/
printf( "Content-type: text/html; charset=Shift_JIS\r\n" );
/* ブラウザにキャッシュさせない。 */
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
printf( "\r\n" );
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" );
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<META http-equiv=\"Content-type\" content=\"text/html; charset=Shift_JIS\">\n" );
printf( "<META http-equiv=\"Content-Script-Type\" content=\"text/javascript\">\n" );
printf( "<META http-equiv=\"Content-Style-Type\" content=\"text/css\">\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "Moai CGI Transfer chunked test.<br>" );
fflush( stdout );
CGIUtil_sleep( 1000 );
printf( "Chunk1 fflush<br>\n" );
fflush( stdout );
/* バッファに何もない場合は何も起こらない.
* (0-sizeなchunkが送信されることはない) */
fflush( stdout );
CGIUtil_sleep( 1000 );
printf( "Chunk2 fflush<br>\n" );
fflush( stdout );
CGIUtil_sleep( 1000 );
printf( "C" );
/* fflush を実行しなと、通常は中途半端な状態でchunkは送信されない */
CGIUtil_sleep( 1000 );
printf( "hunk3 fflush<br>\n" );
fflush( stdout );
CGIUtil_sleep( 1000 );
printf( "Chunk4<br>\n" ); /* 改行を入れただけではchunkは送信されない */
CGIUtil_sleep( 1000 );
printf( "End." );
printf( "</body></html>\n" );
/* 最後のfflushは別になくともプロセスの終了で自動的に送信される */
}
int main( void )
{
static const int is_binary_mode = 1;
/***
* Windowsにおける標準出力(1)における \n => \r\n 自動変換を無効にします.
*/
CGIUtil_Internal_setMode( 1, is_binary_mode );
show_result();
return 0;
}
CloseCGIプログラムで標準出力した文字列のデータは、ブラウザ上にHTMLとして表示されるのでした。 これはCGIプログラムからWebServerへ、そしてWebServerからブラウザへとデータがパイプライン方式で転送されていくことで実現します。 このデータが大量である場合そのデータ全体が完成するのを待つのではなく、 出来上がったものから逐次ブラウザへと運ばれていった方が効率がよいです。 そのためにデータ全体を適切なサイズとタイミングでいくつかの断片に区切りながら転送します。 この断片データをチャンク(chunk)と呼びます。
Transfer-Encoding とは転送するための符号化方式といったような意味ですから、 つまり Transfer-Encoding: chunked 転送モード とはそのようなチャンクに分けて転送する方式を意味します。 このチャンクのフォーマットを大まかに説明すると、まずサイズの情報があって改行、次にデータ本体が来て改行、 このような形のものが一つのチャンクデータとなります. あとはこの繰り返しです.
Cプログラムにおいて具体的にこのような転送を指示することはできませんが、 通常は標準関数であるfflush 関数の呼び出しで chunk が区切られて送信されます。 Moai WebServerは(というか一般的にWebServerは)、C言語のfflush関数の呼び出しを感知すると それを即座にchunkとしてブラウザへ転送します。
sleep
今回のプログラムではもう一つCGIUtil_sleep関数が呼ばれています。 この関数は指定したミリ秒スリープする(プログラムを停止する)といったもので、 例えば1000を指定した場合1000ミリ秒(つまり1秒)スリープします。
CGIプログラムでこのようなスリープと先ほど述べたfflushを組み合わせた場合、 ブラウザのロードの途中で部分的にHTMLを表示させることもできます。 fflushしたタイミングで途中まで文字列が表示され、sleepで指定した時間表示がそのまま止まり、 時間が過ぎるとまた続きの文字列が表示されるといった具合です。
目次に戻る
mkfgen
ライブラリの使用
ここまでライブラリを使用せずに(Cの標準ライブラリだけで)、CGIプログラムを作成してきましたが、 このあたりで限界であろうと思われます。 ここからさらに本格的なものを作ろうとする場合、C言語ではなんらかのライブラリの力を借りる必要があるでしょう。 そこでこのセクションでは、そのための環境の整備について説明します。
mkfgen
ライブラリを使用するにはコンパイラに適切なオプションを指定しなければなりません。 毎回そのような指定をするのも煩わしいですので、通常はMakefile等を作成しておいて、それを実行させます。
しかしMakefileを使用したとしてもその内容は一般にコンパイラによっても異なり、 マルチプラットフォームでの開発においてはその作成も煩わしいので、 何らかのツールを使ってMakefileを自動生成させるのがよいでしょう。 znk_projectが提供するmkfgenもそのようなツールの一つです。
ライブラリを指定するには、mkfgen.myfにその記述を追加する必要があります。 mkfgenについては一応こちらに完全なリファレンスがありますので、 詳しく知りたい方はそちらを参照してください。 ここではCGIプログラムを作成する上で、最低限押さえておくべきポイントに絞って説明します。
この記事では、znk_project 内にあるlibZnkとlibRanoという二つのライブラリを使いたいと思います。 実は cgi_developers/cgi_c に付属するmkfgen.myfおよびそれから生成されるMakefile群では 既にこのための設定が記載されているので、これらに関しては何もする必要がないのですが、 とりあえず自分でゼロから記述するための手順を以下に示しましょう。
- srcディレクトリ配下にあるcgi_simpleディレクトリを雛形として使う
- my_cgiディレクトリ内にあるmkfgen.myfをテキストエディタで開く
- configセクション内のinstall_dirの指定において、cgi_simpleの部分だけをあなたのツールの名前(my_cgi)に変更する
- product_listセクションにも同様にcgi_simpleという文字列があるので、これをあなたのツールの名前に変更する
- include_paths_commonセクションに以下のように二行を加える
- dependency_libs_commonセクションに以下のように二行を加える
- Windowsの場合、my_cgiディレクトリ内にあるauto_trigger.batをダブルクリックして実行し、 カーソルキーで以下を選択してEnterキーを押す
- この状態で試しに実際にコンパイルする
- 最後にコンパイルしたものをインストールする
これを同じくsrcディレクトリ内にコピーして、あなたの好きな名前に変えます。 ただし名前で使う文字は半角英数字、そして使う記号も精々「-」か「_」あたりの無難なものにしておきましょう。 そうしておかないとURLで指定するときに厄介なことになるかもしれません。 ここでは仮に my_cgi という名前にしたとします。 これをあなたが開発するCGI用ディレクトリとします。
修正前は以下のようになっているはずです。
install_dir = ['../../moai-v$(REL_VER)-$(PLATFORM)/cgis/cgi_simple']
これを以下のように修正します。
install_dir = ['../../moai-v$(REL_VER)-$(PLATFORM)/cgis/my_cgi']
修正前は以下のようになっているはずです。
@@L product_list
exec cgi_simple main.c
@@.
これを以下のように修正します。
@@L product_list
exec my_cgi main.c
@@.
main.c には main 関数が含まれるCファイル名を指定します。
それが main.c とは異なる名前なら、この部分をそのファイル名に変更してください。
尚、mkfgen.myf 内で指定する必要のあるCファイルは main 関数を含むものだけです
(main 関数を含まないその他のCファイルについては、mkfgen.myf 内で指定する必要はありません)。
これで出来上がる実行ファイルの名前はmy_cgi.cgiとなります。 またmake install時はMoai直下のcgis/my_cgiディレクトリへインストールされます。
修正前は以下のようになっているはずです。
@@L include_paths_common
@@.
これを以下のように修正します。
@@L include_paths_common
@{libZnk}
@{libRano}
@@.
修正前は以下のようになっているはずです。
@@L dependency_libs_common
@@.
これを以下のように修正します。
@@L dependency_libs_common
rlib: @{libZnk} Znk $(DL_VER)
rlib: @{libRano} Rano $(DL_VER)
@@.
include_paths_common と dependency_libs_common に関しては、
今回コピー元としたcgi_simpleのmkfgen.myfでは、これは既に記述されています。
ですから実際にはここでこれらに関しての修正を行う必要は今回はないのですが、
これらが不要な場合や他のライブラリが必要になった場合はこれを参考に同様の修正をしてみてください。
これでmkfgen.myfの編集は終わりですので上書き保存してください。 テキストエディタは終了して頂いて結構です。
6. mkfgen_update
Linuxの場合はsrc/my_cgiディレクトリをカレントディレクトリにしておいてmkfgenを直接実行します。 srcディレクトリの一つ上の階層にmkfsysというディレクトリがあり、mkfgenツールはその中にあります。 つまりmy_cgiの中から見れば二つ上の階層になりますから、以下のように実行します。
$ ../../mkfsys/mkfgen
これでmkfgen.myfの内容を元にmkfgenツールが実行され、あなたのMakefileが自動生成されます (既にMakefileが存在する場合は更新されます)。
Windowsの場合、my_cgiディレクトリ内にあるauto_trigger.batを実行し、 以下を選択してEnterキーを押します。
0. make
Linuxの場合はMakefile_linux.mak を指定してmakeします。 つまり以下のように実行します。
$ make -f Makefile_linux.mak
Windowsの場合、auto_trigger.batを実行して以下を選択してEnterキーです。
1. make install
Linuxの場合はMakefile_linux.mak を指定してmake installします。 つまり以下のように実行します。
$ make -f Makefile_linux.mak install
参考: CGIのランタイムライブラリはどのようにロードされるのか?
CGIのmkfgen.myf内でのconfigセクションにおいては、runtime_installがfalse指定されています. これはCGIのインストール先ディレクトリへ、このCGIが依存するランタイムライブラリ(DLL)を インストールしないことを意味します. (今回の場合、依存するランタイムライブラリとはZnk-2.*.dllとRano-2.*.dllです).
DLLの仕様に詳しい方は、このことを不思議に思われるかもしれません. Windowsでは特にPATH指定しない限り、このようなランタイムライブラリは、 実行バイナリと同じディレクトリ内か、さもなくばWindowsのシステムディレクトリ内に 存在しなければならないからです. しかし今回はそのどちらにも存在していません.
からくりはMoaiサーバにあります. MoaiはCGIを子プロセスとして起動しますが、このときにDLLのためのPATHの指定も裏で自動的に行われます. このとき検索パスとしてMoaiディレクトリがセットされますが、ここにはZnk-2.*.dllとRano-2.*.dllが存在します. このようにしてCGIのDLLロードは、問題なく行われる仕組みとなっています.
この仕様により各CGIディレクトリに冗長にも同じDLLをコピーせずに済むというわけです.
Close
目次に戻る
libZnkとlibRanoの使用
libZnkライブラリの使用
前回のセクションではmkfgenによってMakefileを生成し、 auto_trigger.batやmakeコマンドによってlibZnkやlibRanoとともにC言語がビルドできる状態にしました。 この先のセクションではこれを使用します。 そのためこのセクションではその準備としてlibZnkやlibRanoを使った基本的なプログラミングについて説明します。
文字列ZnkStrの使用
標準ライブラリだけを使用したC言語で一番問題になるのが、文字列の扱いでしょう。 特に他の言語とは異なり、C言語では文字列の連結が簡単にはできません。 一応strcatなどの標準関数が用意されてはいますが、動的にバッファサイズが拡張できるわけではないため 使い勝手はあまりよくなく、また安全面においても万全ではありません。
この講座はC++の講座ではないためあまり深入りはしませんが、C++の使用を許すならSTLで定義されたstd::stringを使うという手もあります。
ただしstd::stringはクラスであるため、動的ライブラリ(DLL)との親和性はあまりよくないという点には注意が必要です。
DLLの引数にはクラスを指定することはできません。これに対しては実装側をC++で閉じ込め、ヘッダ側をC言語のインターフェースとして
公開するといったテクニックもありますが、その場合std::stringを(C言語でも使用可能な)なんらかの代替型に変換しなければならないかもしれません。
libZnkではZnkStr型が提供されており、これを使うと(生成・破棄の面倒さは依然として残りますが) 文字列の扱いがかなり楽になります。 これは例えば以下のようにして使用します。
#include <Znk_str.h>
int main( void )
{
ZnkStr str = ZnkStr_new( "hello" ); /* create ZnkStr */
ZnkStr_add( str, " world" );
ZnkStr_add_c( str, '!' );
printf( "str=[%s] length=[%u]\n", ZnkStr_cstr(str), ZnkStr_leng(str) );
ZnkStr_delete( str ); /* destroy ZnkStr */
}
まずZnkStrを使うにはZnk_str.hをincludeする必要があります。 これを生成するにはZnkStr_new関数を、破棄するにはZnkStr_delete関数をそれぞれ用います。 生成したZnkStrは最後に必ずZnkStr_delete関数で破棄しなければなりません。
ZnkStrは内部に文字列データを保持しており、 上記ではZnkStr_new関数の引数に "hello" を指定することで文字列データを"hello"で初期化しています。 続けてZnkStr_add関数の引数に " world" を指定することでこの文字列に新しい文字列 " world" を連結しています。 この時点で内部の文字列は"hello world"となりました。 さらに上記ではZnkStr_add_c関数の引数に '!' を指定することで、単独の文字 '!' を連結しています。 結局最終的な内部文字列は "hello world!" になります。
この内部文字列をprintfで出力する場合、直接strをprintfに与えてはいけません。 一旦strから内部文字列を取り出し、printfにはそれを与える必要があります。 strから内部文字列を取り出すにはZnkStr_cstr関数を使います。 またstrの内部文字列の長さを得るにはZnkStr_leng関数を使います。 上記ではこれら二つの値をprintfで出力しています。
ZnkStrではprintfの書式を使った文字列連結をすることもできます。 例えば以下をご覧下さい。
#include <Znk_str.h>
int main( void )
{
ZnkStr msg = ZnkStr_new( "" ); /* create ZnkStr */
const char* name = "User-Agent";
const char* val = "FireMoai";
ZnkStr_addf( msg, "%s:%s\n", name, val );
ZnkStr_delete( msg ); /* destroy ZnkStr */
}
ZnkStr_addf関数はZnkStr_add関数と同じく文字列を連結するための関数ですが、 連結文字列の書式をあたかもprintf関数と同じように指定でき、 その書式を基に結果生成された文字列を連結します。 上記では、ZnkStr_addfに"%s:%s\n"といった書式を指定し、その後ろにname, valが指定されていますので、 結局連結される文字列は"User-Agent:FireMoai\n"といったものになります。
ZnkVarとZnkVarAryの使用
CGIプログラミングではQuery StringなどPOST用のデータを解析した結果を格納するために、 キーと値をペアにしたようなデータ構造が必要になります。 このような場合、ハッシュテーブルや連想配列といったものがあれば便利なのですが、 C言語ではデフォルトでは提供されていません。 ZnkVarはこのようなキーと値を保持することができるデータ構造です。
またZnkVarAryはZnkVarを配列のように扱うことができるデータ構造(コンテナ)です。 純粋なC言語の配列ではないため、通常の表記「[]」を使うことはできませんが、 関数を介した比較的安全なアクセスが可能です。
ZnkVarやZnkVarAryを使うにはZnk_var_ary.hを include する必要があります。 ZnkVarAryはZnkVarAry_create関数により生成し、 ZnkVarAry_destroy関数により破棄します。 通常、生成したZnkVarAryは最後に必ずZnkVarAry_destroy関数で破棄しなければなりません。
ZnkVarAryの要素数を取得するにはZnkVarAry_size関数を、 ZnkVarAryのそれぞれの要素にアクセスするにはZnkVarAry_at関数を用います。 例えば以下のようになります。
#include <Znk_var_ary.h>
int main( void )
{
ZnkVarAry post_vars = ZnkVarAry_create( true );
ZnkStr msg = ZnkStr_new( "" );
size_t i;
for( i=0; i<ZnkVarAry_size(post_vars); ++i ){
ZnkVar var = ZnkVarAry_at( post_vars, i );
const char* name = ZnkVar_name_cstr(var);
const char* val = ZnkVar_cstr(var);
ZnkStr_addf( msg, "%s = [%s]\n", name, val );
}
ZnkStr_delete( msg );
ZnkVarAry_destroy( post_vars );
}
上記ではZnkVarAry型のpost_varsをZnkVarAry_create関数で生成しています。 この状態ではZnkVarは一つも格納されていないため、ZnkVarAry_sizeの戻り値も 0 となります。 従って上記のfor文は、この例では一度も実行されることはないのですが、 仮に実行された場合、forブロック内のZnkVarAry_at関数により、i番目の要素が取り出されてZnkVar型の変数varに代入されます。 varが保持するキー文字列はZnkVar_name_cstr関数で、またvarが保持する値(文字列)はZnkVar_cstr関数でそれぞれ取得できます。
libRanoライブラリの使用
libRanoライブラリはlibZnkほど汎用的な機能を提供するものではありませんが、 例えばCGIに特化した機能などを提供します。 CGIプログラミングでは、RanoCGIEvar構造体が重要です。 RanoCGIEvar構造体はCGIで使用する全環境変数を保持します。 これまで使ってきたcgi_utilではその用途としてCGIEVar構造体を使用してきましたが、 まさにそれに相当するものです。
RanoCGIEvar構造体を使うには Rano_cgi_util.h を include する必要があります。 またRanoCGIEvar構造体はRanoCGIEvar_create関数で生成し、RanoCGIEvar_destroy関数で破棄します。 通常、生成したRanoCGIEvarは最後に必ずRanoCGIEvar_destroy関数で破棄しなければなりません。 例えば以下の通りです。
#include <Rano_cgi_util.h>
int main( void )
{
/***
* Create+Get Environment Variables for CGI
*/
RanoCGIEVar* evar = RanoCGIEVar_create();
/***
* Get query_string;
*/
const char* query_string = evar->query_string_ ? evar->query_string_ : "";
RanoCGIEVar_destroy( evar );
return 0;
}
RanoCGIEVar構造体もCGIEVar構造体と同様にそのメンバが環境変数の値となっており、 例えば環境変数QUERY_STRINGの値はメンバquery_string_に格納されています。 ただしRanoCGIEVar構造体の場合、メンバがNULL値になることもあります(対応する環境変数が定義されていなかった場合など)ので、 上記のように三項演算子でNULLチェックをしつつ取得する必要があります。
libRanoにはQuery Stringを解析する関数として、RanoCGIUtil_splitQueryStringも提供されています。 これを使う場合もRanoCGIEvar構造体と同じく Rano_cgi_util.h を include します。 RanoCGIUtil_splitQueryStringはQuery Stringの値を解析して、その結果をZnkVarAry型に格納します。 結果の格納先となるZnkVarAry型を第1引数に指定し、Query String型を第2引数に指定して使用します。 例えば以下の通りです。
#include <Rano_cgi_util.h>
int main( void )
{
/***
* Create+Get Environment Variables for CGI
*/
RanoCGIEVar* evar = RanoCGIEVar_create();
/***
* Get query_string;
*/
const char* query_string = evar->query_string_ ? evar->query_string_ : "";
/***
* Create post_vars.
*/
ZnkVarAry post_vars = ZnkVarAry_create( true );
/***
* Get post_vars from query_string.
*/
RanoCGIUtil_splitQueryString( post_vars, query_string, Znk_NPOS, false );
ZnkVarAry_destroy( post_vars );
RanoCGIEVar_destroy( evar );
return 0;
}
上記ではRanoCGIUtil_splitQueryString関数によってquery_stringを解析した結果を、 ZnkVarAry型変数のpost_varsに格納しています。 RanoCGIUtil_splitQueryString関数の第3引数はquery_stringの文字列長を指定しますが、 ここにZnk_NPOSと指定するとその文字列長を自動判別します(内部でstrlenを呼び出し判断します)。 また第4引数はQuery Stringの値に相当するURLエンコーディングした部分を元に戻すか否かです。 上記ではfalseを指定していますので特に何もしないそのままの値を取得します。
目次に戻る
画像のPOST
RanoCGIUtil_getPostedFormData関数
今回はフォームから画像をPOSTする処理を考えます。
Execute : cgi_c/post2.cgi View source code (216 lines)
#include <Rano_cgi_util.h>
#include "cgi_util.h"
#include <Znk_var_ary.h>
#include <Znk_htp_util.h>
#include <Znk_dir.h>
#include <stdio.h>
#include <string.h>
static bool
saveAsResultFile( const char* ext, const uint8_t* data, size_t data_size )
{
char result_filename[ 256 ] = "";
ZnkFile fp = NULL;
uint64_t write_size = 0;
ZnkDir_mkdirPath( "../publicbox/tmp", Znk_NPOS, '/', NULL );
Znk_snprintf( result_filename, sizeof(result_filename), "../publicbox/tmp/result_file.%s", ext );
fp = Znk_fopen( result_filename, "wb" );
if( fp ){
write_size = Znk_fwrite( data, data_size, fp );
Znk_fclose( fp );
}
return (bool)( write_size == data_size );
}
static bool
I_isAuth( ZnkVarAry post_vars, const char* auth_key, ZnkStr ermsg )
{
const size_t idx = ZnkVarAry_findIdx_byName( post_vars, "Moai_AuthenticKey", Znk_NPOS, false );
if( idx != Znk_NPOS ){
ZnkVar var = ZnkVarAry_at( post_vars, idx );
if( ZnkS_eq( ZnkVar_cstr(var), auth_key ) ){
/* authenticated */
return true;
}
}
ZnkStr_add( ermsg, "[NG]: post2 : Error : Not authenticated.\n" );
return false;
}
static void
show_result( const ZnkVarAry post_vars )
{
char auth_key[ 32 ] = "";
char* serv_dir = "../../../"; /* moai top dir */
bool is_auth = false;
CGIUtil_loadAuthenticKey( auth_key, sizeof(auth_key), serv_dir );
is_auth = I_isAuth( post_vars, auth_key, NULL );
/***
* Output HTTP Header for CGI
*/
printf( "Pragma: no-cache\r\n" );
printf( "Cache-Control: no-cache\r\n" );
printf( "\r\n" );
/***
* Output HTML for CGI
*/
printf( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n" );
printf( "<html>\n" );
printf( "<head>\n" );
printf( "<META http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\">\n" );
printf( "<META http-equiv=\"Content-Script-Type\" content=\"text/javascript\">\n" );
printf( "<META http-equiv=\"Content-Style-Type\" content=\"text/css\">\n" );
printf( "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n" );
printf( "<link href=\"/bulma.css\" rel=\"stylesheet\" type=\"text/css\" />\n" );
printf( "</head>\n" );
printf( "<body>\n" );
printf( "<div class=\"section\">\n" );
printf( "Moai CGI Post for multipart/form-data.<br> <br>\n" );
printf( "<form action=post2.cgi method=POST enctype=multipart/form-data>\n" );
printf( " <input type=hidden name=Moai_AuthenticKey value=\"%s\">\n", auth_key );
printf( " <input style=\"max-width:400px;\" class=input type=text name=email placeholder=\"E-mail\" size=28><br>\n" );
printf( " <textarea class=textarea name=com placeholder=\"Comment\" cols=48 rows=\"4\"></textarea><br>\n" );
printf( " <div class=\"file\">\n" );
printf( " <label class=\"file-label\">\n" );
printf( " <input class=\"file-input\" type=\"file\" name=upfile>\n" );
printf( " <span class=\"file-cta\"><span class=\"file-label\">添付ファイル</span></span>\n" );
printf( " </label>\n" );
printf( " </div>\n" );
printf( " <br>\n" );
printf( " <input style=\"padding:0.4em 1em;\" type=submit value=\"Submit\"><br>\n" );
printf( "</form>\n" );
printf( "<ul>\n" );
printf( "<li>添付File : GIF, JPG, PNG, WEBM. 2000KBまで</li>\n" );
printf( "</ul>\n" );
printf( "<pre>\n" );
printf( "is_auth=[%d]\n", is_auth );
printf( "PostVars:.\n" );
if( is_auth ){
const char* ext = NULL;
bool is_image = false;
/* ZnkStrはlibZnkが提供する文字列型です. */
ZnkStr msg = ZnkStr_new( "" );
size_t i;
for( i=0; i<ZnkVarAry_size(post_vars); ++i ){
/* Post変数を一つ取り出します.
* この変数の名前も得ておきます. */
ZnkVar var = ZnkVarAry_at( post_vars, i );
const char* name = ZnkVar_name_cstr(var);
if( ZnkVar_prim_type( var ) == ZnkPrim_e_Bfr ){
/* ZnkVarのタイプがZnkPrim_e_Bfr(任意のByte列).
* これは添付ファイルを意味します. */
const char* filename = ZnkVar_misc_cstr( var, "filename" );
const uint8_t* data = ZnkVar_data( var );
const size_t data_size = ZnkVar_data_size( var );
/* ZnkS_get_extension の戻り値はファイルの拡張子となります. */
/* この参照先は、post_varsの寿命が続く限りにおいては有効です. */
ext = ZnkS_get_extension( filename, '.' );
if( data_size > 2000000 ){ /* 2000KB以上 */
is_image = false;
ZnkStr_addf( msg, "Binary : %s orinal filename=[%s] : Error : This is too large.\n", name, filename );
} else {
/* 文字列が等しいかの比較を行います. */
/* ただし大文字小文字の区別はしません. */
if( ZnkS_eqCase( ext, "jpg" )
|| ZnkS_eqCase( ext, "png" )
|| ZnkS_eqCase( ext, "gif" )
|| ZnkS_eqCase( ext, "webm" )
){
/* 画像ファイルの拡張子であった場合はこのデータをresult_file.ext として保存します. */
ZnkStr_addf( msg, "Image : data_size=[%zu]\n", data_size );
if( saveAsResultFile( ext, data, data_size ) ){
is_image = true;
ZnkStr_addf( msg, "Image : %s orinal filename=[%s]\n", name, filename );
} else {
is_image = false;
ZnkStr_addf( msg, "Image : %s orinal filename=[%s] : Error : fwrite error.\n", name, filename );
}
} else {
is_image = false;
ZnkStr_addf( msg, "Binary : %s orinal filename=[%s]\n", name, filename );
}
}
} else {
/* ZnkVarのタイプがその他(Str).
* これはinputタグやtextareaタグ等に由来する文字列データ. */
const char* val = ZnkVar_cstr(var);
ZnkStr_addf( msg, "%s = [%s] (size=[%u])\n", name, val, ZnkVar_str_leng(var) );
}
}
/* XSS対策. msg内にある一切のHTMLタグの効果を打ち消します. */
ZnkHtpURL_negateHtmlTagEffection( msg ); /* for XSS */
if( is_image ){
/* 画像ファイルであった場合はimgタグによりそれを表示します. */
ZnkStr_addf( msg, "<br>\n" );
ZnkStr_addf( msg, "<img src=\"../publicbox/tmp/result_file.%s\">\n", ext );
ZnkStr_addf( msg, "<br>\n" );
}
fputs( ZnkStr_cstr(msg), stdout );
/* ZnkStrは使用後解放しなければなりません. */
ZnkStr_delete( msg );
}
printf( "</pre>" );
printf( "</div> <!-- section -->\n" );
printf( "</body></html>\n" );
fflush( stdout );
}
int main(int argc, char **argv)
{
/***
* Create+Get Environment Variables for CGI
*/
RanoCGIEVar* evar = RanoCGIEVar_create();
/***
* Get query_string;
*/
const char* query_string = evar->query_string_ ? evar->query_string_ : "";
/***
* Create post_vars.
*/
ZnkVarAry post_vars = ZnkVarAry_create( true );
/***
* for Windows : change stdin/stdout to binary-mode.
*/
Znk_Internal_setMode( 0, true );
Znk_Internal_setMode( 1, true );
/***
* Get post_vars from query_string.
*/
RanoCGIUtil_splitQueryString( post_vars, query_string, Znk_NPOS, false );
/***
* CGIにおけるフォームの投稿データをpost_varsへと取得します.
* またこの関数においては第1引数evarの指定も必要となります.
*/
RanoCGIUtil_getPostedFormData( evar, post_vars, NULL, NULL, NULL, NULL, true );
show_result( post_vars );
ZnkVarAry_destroy( post_vars );
RanoCGIEVar_destroy( evar );
return 0;
}
Closepost1.cの時とは異なり、今回はlibZnkとlibRanoも使用します。 文字列ではなく画像を投稿したい場合、x-www-form-urlencoded形式ではなくmultipart/form-data形式でPOSTする必要があります。 このmultipart/form-data形式にも対応した解析を行うのがRanoCGIUtil_getPostedFormData関数であり、 そのデータをZnkVarAry型へ変換します。
show_resultに入る前のmain関数内の処理を抜粋したものを以下に示します。
int main( void )
{
/***
* Create+Get Environment Variables for CGI
*/
RanoCGIEVar* evar = RanoCGIEVar_create();
/***
* Get query_string;
*/
const char* query_string = evar->query_string_ ? evar->query_string_ : "";
/***
* Create post_vars.
*/
ZnkVarAry post_vars = ZnkVarAry_create( true );
/***
* for Windows : change stdin/stdout to binary-mode.
*/
Znk_Internal_setMode( 0, true );
Znk_Internal_setMode( 1, true );
/***
* Get post_vars from query_string.
*/
RanoCGIUtil_splitQueryString( post_vars, query_string, Znk_NPOS, false );
/***
* CGIにおけるフォームの投稿データをpost_varsへと取得します.
* またこの関数においては第1引数evarの指定も必要となります.
*/
RanoCGIUtil_getPostedFormData( evar, post_vars, NULL, NULL, NULL, NULL, true );
show_result( post_vars );
ZnkVarAry_destroy( post_vars );
RanoCGIEVar_destroy( evar );
return 0;
}
RanoCGIUtil_getPostedFormData関数以外の部分は既に前回のセクションで見た通りで、 RanoCGIEVar構造体の取得や、RanoCGIUtil_splitQueryString関数によるQuery Stringの解析を行っています。 Znk_Internal_setMode関数は、cgi_util での CGIUtil_Internal_setMode関数と全く同じ機能を持つ関数で、 標準出力と標準入力の両方をバイナリモードに変換しています。
RanoCGIUtil_getPostedFormData関数には多くに引数がありますが、 第1引数にはRanoCGIEvar構造体を指定し、第2引数には結果を格納するためのZnkVarAry型変数を指定します。 それ以外は最後の引数を除いてNULLで結構です。
RanoCGIUtil_getPostedFormData関数は、multipart/form-data だけでなく、x-www-form-urlencoded形式にも対応しており、
最後の引数はその場合にURLエンコーディングをデコードするか否かを示します。
今回は multipart/form-data形式を扱いますので最後の引数はtrue/falseのどちらでもよいのですが、
上記ではとりあえずtrueと指定しています。
RanoCGIUtil_getPostedFormData関数の呼び出しが成功すると、その時点でpost_vars変数に 投稿画像のデータと投稿文字列のデータがすべて格納されている形になります。 あとはそれをshow_result関数内で適切にアクセスするだけの処理となります。
続いてshow_result関数内の説明に移ります。 まず最初に以下のようにMoai_AuthenticKeyを取得し、認証を行います。
char auth_key[ 32 ] = "";
char* serv_dir = "../../../"; /* moai top dir */
bool is_auth = false;
CGIUtil_loadAuthenticKey( auth_key, sizeof(auth_key), serv_dir );
is_auth = I_isAuth( post_vars, auth_key, NULL );
この認証が成功した場合は、投稿された画像がファイルに保存され、失敗した場合は保存されません。 ただし失敗した場合でもフォームの表示は行われます(初めてアクセスしたユーザにはこれを表示しなければなりません)。
今回のフォームには、E-mailを追加するための入力フィールドとコメントを記述するためのテキストエリア、 さらに添付ファイルを追加するためのダイアログを表示させるためのボタンが備わっています。
Moai CGI Post for multipart/form-data.<br> <br>
<form action=post2.rrks method=POST enctype=multipart/form-data>
<input type=hidden name=Moai_AuthenticKey value="0123456789ABCDEF">
<input style="max-width:400px;" class=input type=text name=email placeholder="E-mail" size=28><br>
<textarea class=textarea name=com placeholder="Comment" cols=48 rows="4"></textarea><br>
<div class="file">
<label class="file-label">
<input class="file-input" type="file" name=upfile>
<span class="file-cta"><span class="file-label">添付ファイル</span></span>
</label>
</div>
<br>
<input style="padding:0.4em 1em;" type=submit value="Submit"><br>
</form>
Moai_AuthenticKeyをhidden属性としてこれらのデータといっしょに送信するようになっている点に注意してください。 実際、上記のMoai_AuthenticKeyのinputタグを出力しているprintf関数は以下のようになっています。
printf( " <input type=hidden name=Moai_AuthenticKey value=\"%s\">\n", auth_key );
CGIプログラム内で取得したMoai_AuthenticKeyがhidden属性のinputタグに指定される形にになります。
post_varsの各要素ごとの切り分け
post2.cでは、is_authがtrueの場合はpost_vars全体の値を報告し、 その中に画像ファイルのデータが含まれる場合はそれを別途ファイルに保存します。 これを行っている部分を(長いですが)以下に抜粋します。
const char* ext = NULL;
bool is_image = false;
/* ZnkStrはlibZnkが提供する文字列型です. */
ZnkStr msg = ZnkStr_new( "" );
size_t i;
for( i=0; i<ZnkVarAry_size(post_vars); ++i ){
/* Post変数を一つ取り出します.
* この変数の名前も得ておきます. */
ZnkVar var = ZnkVarAry_at( post_vars, i );
const char* name = ZnkVar_name_cstr(var);
if( ZnkVar_prim_type( var ) == ZnkPrim_e_Bfr ){
/* ZnkVarのタイプがZnkPrim_e_Bfr(任意のByte列).
* これは添付ファイルを意味します. */
const char* filename = ZnkVar_misc_cstr( var, "filename" );
const uint8_t* data = ZnkVar_data( var );
const size_t data_size = ZnkVar_data_size( var );
/* ZnkS_get_extension の戻り値はファイルの拡張子となります. */
/* この参照先は、post_varsの寿命が続く限りにおいては有効です. */
ext = ZnkS_get_extension( filename, '.' );
if( data_size > 2000000 ){ /* 2000KB以上 */
is_image = false;
ZnkStr_addf( msg, "Binary : %s orinal filename=[%s] : Error : This is too large.\n", name, filename );
} else {
/* 文字列が等しいかの比較を行います. */
/* ただし大文字小文字の区別はしません. */
if( ZnkS_eqCase( ext, "jpg" )
|| ZnkS_eqCase( ext, "png" )
|| ZnkS_eqCase( ext, "gif" )
|| ZnkS_eqCase( ext, "webm" )
){
/* 画像ファイルの拡張子であった場合はこのデータをresult_file.ext として保存します. */
ZnkStr_addf( msg, "Image : data_size=[%zu]\n", data_size );
if( saveAsResultFile( ext, data, data_size ) ){
is_image = true;
ZnkStr_addf( msg, "Image : %s orinal filename=[%s]\n", name, filename );
} else {
is_image = false;
ZnkStr_addf( msg, "Image : %s orinal filename=[%s] : Error : fwrite error.\n", name, filename );
}
} else {
is_image = false;
ZnkStr_addf( msg, "Binary : %s orinal filename=[%s]\n", name, filename );
}
}
} else {
/* ZnkVarのタイプがその他(Str).
* これはinputタグやtextareaタグ等に由来する文字列データ. */
const char* val = ZnkVar_cstr(var);
ZnkStr_addf( msg, "%s = [%s] (size=[%u])\n", name, val, ZnkVar_str_leng(var) );
}
}
/* XSS対策. msg内にある一切のHTMLタグの効果を打ち消します. */
ZnkHtpURL_negateHtmlTagEffection( msg ); /* for XSS */
if( is_image ){
/* 画像ファイルであった場合はimgタグによりそれを表示します. */
ZnkStr_addf( msg, "<br>\n" );
ZnkStr_addf( msg, "<img src=\"./result_file.%s\">\n", ext );
ZnkStr_addf( msg, "<br>\n" );
}
fputs( ZnkStr_cstr(msg), stdout );
/* ZnkStrは使用後解放しなければなりません. */
ZnkStr_delete( msg );
post_varsの要素であるZnkVarでは、データの種類を表す情報も持っており、 ZnkVar_prim_type関数でこれを取得できます。 この値が列挙子 ZnkPrim_e_Bfr に等しければ、このZnkVarはバイナリデータであることを意味し、 即ち格納されているデータが投稿した画像データであることを意味します。 この値がそれ以外の列挙子(上記のコード中には登場しませんが)である場合、CGIの投稿データの範疇ではこれは 必ず列挙子 ZnkPrim_e_Str に等しいはずであり、このZnkVarは文字列データであることを意味します(たとえばE-mailやコメントなどのテキストデータです)。
上記のほとんどの処理は前者のバイナリデータを処理するためのもので占められています。 バイナリデータの本体は ZnkVar_data関数で、そのバイトサイズはZnkVar_data_size関数で取得でき、 上記ではそれをdata、data_size変数で受け取っています。 またZnkVarには投稿された時点での画像ファイルのファイル名も格納されており、ZnkVar_misc_cstr関数でそれを取得できます。 上記ではそれをfilename変数で受け取っています。
ZnkS_get_extension関数は指定したファイル名の拡張子を取得する関数であり、 上記ではそれをext変数で受け取っています。 ZnkS_eqCase関数は大文字小文字の違いを無視した文字列の比較を行います。 例えばZnkS_eqCase( ext, "jpg" )では、extの値が"jpg"、"JPG"、"Jpg"などいずれの場合でも等しいとみなされます。 上記では拡張子がjpg、png、gif、webmである場合画像ファイルとみなし(webmは正確には画像ではありませんが、とりあえず今回はここに含めます)、 saveAsResultFile関数を呼び出してこれをファイルとして保存します。 この画像ファイル保存をもって、今回のプログラムでは一応投稿された画像データの受理とみなします。
目次に戻る
テンプレートHTMLを使って表示する.
テンプレートによるHTMLの表示
今までのサンプルではCGI上で表示させるHTMLを出力させるためにprintfで一行ずつ書いていました。 しかしこれは、内部にダブルクォートなどが含まれる場合はそれをエスケープしなければなりませんし、 各行を書くたびに毎回最初にprintfを付けるのは煩わしいでしょう。 Rarakuなどの言語ではヒアドキュメントを使うという手もありますが、C言語ではそのようなものはありません。
特に動的な出力をさせるのでなければ別途HTMLファイルを用意し、単純にそれを読み込んで表示できれば便利です。 テンプレートHTMLはそのためのテキストデータファイルです。 ここではCGI上でテンプレートHTMLを使って大量のHTMLを便利に表示させる方法について解説します。
Execute : cgi_c/hello_template.cgi View source code (38 lines)
#include "cgi_util.h"
#include <Znk_bird.h>
#include <Znk_cgi_util.h>
#include <stdio.h>
static void
show_result()
{
/* Create ZnkBird. */
ZnkBird bird = ZnkBird_create( "#[", "]#" );
/* Register to BIRD-tag. */
ZnkBird_regist( bird, "your_name", "Zenkaku" );
/* Read template-html file, and expand BIRD-tag. */
{
ZnkStr ans = ZnkStr_new( "" );
ZnkCGIStr_addTemplateHTML( ans, bird, "../publicbox/hello_template.html", "utf-8" );
printf( "%s", ZnkStr_cstr(ans) );
ZnkStr_delete( ans );
}
/* Destroy ZnkBird. */
ZnkBird_destroy( bird );
}
int main(int argc, char **argv)
{
static const int is_binary_mode = 1;
/***
* for Windows : change stdin/stdout to binary-mode.
*/
CGIUtil_Internal_setMode( 1, is_binary_mode );
show_result();
return 0;
}
CloseZnkBird
まず ZnkBird 型について説明しましょう。 これは libZnk において定義されているため、まずは Znk_bird.h をincludeしておきます。
ZnkBirdとはやや奇妙な名前ですが、 これはBIRD(Basic Intrinsic Replacement Directive)タグと呼ばれるものを扱うためのものとなります (ちなみにこれはlibZnkが独自で提供するものであり、従って一般的な用語ではありません) 。
BIRDタグは開始記号(この記事では「#[」とします) と終了記号(この記事では「]#」とします) で識別子を囲ったタグであり、 その識別子の値(文字列)がZnkBird内部に登録されているならば、BIRDタグの部分はその値(文字列)に展開されるといったものになります。 つまりZnkBirdとはこのBIRDタグの識別子とその値を包括管理するデータ型です.
まだちょっとよく意味がわからないと思いますので具体的な例を挙げて説明しましょう。 以下のような内容を持つテンプレートHTMLを考えます。
<html><body>
Hello #[your_name]#!<br>
Your age is #[your_age]#<br>
</body></html>
今ZnkBird型の変数が、your_nameについては"Zenkaku"、your_ageについては"100"といった文字列データを保持しているものとしましょう。 これを上記のテンプレートに適用すると以下のように展開されます。
<html><body>
Hello Zenkaku!<br>
Your age is 100<br>
</body></html>
このような展開処理を行うためのものがZnkBirdです。
さて、ZnkBirdは以下のようにZnkBird_create関数を用いて生成します。
/* ZnkBirdの生成. */
ZnkBird bird = ZnkBird_create( "#[", "]#" );
ちなみに上記の(birdの直後にある)RrkBirdは省略可能ですが、今回はわかりやすさのために敢えて省略せずに書いています。 RrkBird_create関数には二つの引数があり、この部分にBIRDタグの開始記号と終了記号を指定します。
次に、以下のようにRrkBird_regist関数を用いて、キーに対する値を登録します。
/* BIRDタグの登録. */
ZnkBird_regist( bird, "your_name", "Zenkaku" );
上記では your_nameというキーに対し、"Mr.Moai" という値を登録しています。
最後に RrkCGIStr_addTemplateHTML関数により指定されたファイルをテンプレートとして、 RrkBirdを適用して展開します。 例えば以下の通りです。
/* テンプレートHTMLを読み込み、BIRDタグを展開して表示. */
RanoCGIUtil_printTemplateHTML( evar, bird, "../publicbox/hello_template.html", "utf-8" );
/* ZnkBirdの破棄. */
ZnkBird_destroy( bird );
上記ではテンプレートファイルとしてpublicbox/hello_template.htmlを指定しており、 その展開結果は text 変数に格納されます。 ZnkBirdを使用後はZnkBird_destroy関数により、これを忘れずに破棄する必要があります。
目次に戻る
時間のかかる処理の途中経過を表示する
進捗状況の表示
CGI で時間のかかる処理を行いたい場合、その進捗状況を表示するにはどうすればよいでしょうか? このセクションではその方法について考えます。
Execute : cgi_c/progress.cgi progress.cの全ソースコード (172 lines)
//#include <Rano_cgi_util.h>
#include "cgi_util.h"
#include <Znk_bird.h>
#include <Znk_cgi_util.h>
#include <Znk_thread.h>
#include <Znk_dir.h>
#include <Znk_str_fio.h>
#include <stdio.h>
#include <stdlib.h>
static size_t st_count = 0;
typedef struct ProgInfo_tag {
ZnkStr pascua_state_dir_;
ZnkStr filepath_state_ ;
ZnkStr filepath_msg_ ;
ZnkStr filepath_ermsg_ ;
ZnkStr id_ ;
} *ProgInfo;
static ProgInfo
ProgInfo_create( const char* pascua_state_dir, const char* id )
{
ProgInfo info = malloc( sizeof( struct ProgInfo_tag ) );
info->pascua_state_dir_ = ZnkStr_new( pascua_state_dir );
info->filepath_state_ = ZnkStr_newf( "%s/%s.state", pascua_state_dir, id );
info->filepath_msg_ = ZnkStr_newf( "%s/%s.msg", pascua_state_dir, id );
info->filepath_ermsg_ = ZnkStr_newf( "%s/%s.ermsg", pascua_state_dir, id );
info->id_ = ZnkStr_new( id );
return info;
}
static void
ProgInfo_destroy( ProgInfo info )
{
if( info ){
ZnkStr_delete( info->pascua_state_dir_ );
ZnkStr_delete( info->filepath_state_ );
ZnkStr_delete( info->filepath_msg_ );
ZnkStr_delete( info->filepath_ermsg_ );
ZnkStr_delete( info->id_ );
free( info );
}
}
static void
ProgInfo_mkdir( ProgInfo info )
{
ZnkDir_mkdirPath( ZnkStr_cstr(info->pascua_state_dir_), ZnkStr_leng(info->pascua_state_dir_), '/', NULL );
}
static void
ProgInfo_reset( ProgInfo info, bool is_clear_msg )
{
/***
* @note
* A completely empty string may result in an error with XMLHttpRequest.
*/
const char* path;
path = ZnkStr_cstr(info->filepath_state_);
ZnkStrFIO_writeCStrFile( "PascuaProgress_end", Znk_NPOS, path );
if( is_clear_msg ){
path = ZnkStr_cstr(info->filepath_msg_);
ZnkStrFIO_writeCStrFile( "None.", Znk_NPOS, path );
}
path = ZnkStr_cstr(info->filepath_ermsg_);
ZnkStrFIO_writeCStrFile( " ", Znk_NPOS, path );
}
static void
ProgInfo_begin( ProgInfo info )
{
ZnkStrFIO_writeCStrFile( "PascuaProgress_begin", Znk_NPOS, ZnkStr_cstr(info->filepath_state_) );
ZnkStrFIO_writeCStrFile( "[FYI]: Please wait.", Znk_NPOS, ZnkStr_cstr(info->filepath_msg_) );
}
static void
ProgInfo_end( ProgInfo info, ZnkStr ermsg )
{
if( ermsg ){
ZnkStrFIO_writeStrFile( ermsg, ZnkStr_cstr(info->filepath_ermsg_) );
}
ZnkStrFIO_writeCStrFile( "PascuaProgress_end", Znk_NPOS, ZnkStr_cstr(info->filepath_state_) );
}
static void
ProgInfo_update( ProgInfo info, const char* msg )
{
/**
* @note
* The longer a file is open for writing, the more time it is blocked from being read by Javascript.
* Therefore, the file should be closed as quickly as possible here.
*/
FILE* fp = fopen( ZnkStr_cstr(info->filepath_msg_), "wb" );
if( fp ){
fprintf( fp, "%s", msg );
fclose( fp );
}
}
static void
ProgInfo_cleanFiles( ProgInfo info )
{
ZnkDir_deleteFile_byForce( ZnkStr_cstr(info->filepath_state_) );
ZnkDir_deleteFile_byForce( ZnkStr_cstr(info->filepath_msg_) );
ZnkDir_deleteFile_byForce( ZnkStr_cstr(info->filepath_ermsg_) );
}
static void
very_long_work( ProgInfo prog_info )
{
ZnkStr ermsg = ZnkStr_new( "" );
ZnkStr tmp = ZnkStr_new( "" );
ProgInfo_begin( prog_info );
while( st_count <= 100 ){
ZnkStr_setf( tmp, "[FYI]: progress:%zu%", st_count );
ProgInfo_update( prog_info, ZnkStr_cstr(tmp) );
++st_count;
ZnkThread_sleep( 100 ); /* sleeping 100msec */
if( st_count == 50 ){
ZnkStr_add( ermsg, "[FYI]: very_long_work : count is 50.\n" );
}
}
ProgInfo_end( prog_info, ermsg );
ZnkThread_sleep( 3000 );
ProgInfo_cleanFiles( prog_info );
ZnkStr_delete( tmp );
ZnkStr_delete( ermsg );
}
int main(int argc, char **argv)
{
static const int is_binary_mode = 1;
static const char* prog_info_id = "upgrade";
const ProgInfo prog_info = ProgInfo_create( "../publicbox/tmp/pascua_state", prog_info_id );
ProgInfo_mkdir( prog_info );
ProgInfo_reset( prog_info, true );
/***
* for Windows : change stdin/stdout to binary-mode.
*/
CGIUtil_Internal_setMode( 1, is_binary_mode );
/* まずJavascript progress.jsを含んだHTMLを表示させる. */
{
ZnkBird bird = ZnkBird_create( "#[", "]#" );
ZnkStr ans = ZnkStr_new( "" );
/* Register to BIRD-tag. */
ZnkBird_regist( bird, "prog_info_id", prog_info_id );
ZnkCGIStr_addTemplateHTML( ans, bird, "../publicbox/progress.html", "utf-8" );
printf( "%s", ZnkStr_cstr(ans) );
ZnkStr_delete( ans );
ZnkBird_destroy( bird );
}
fflush( stdout );
/* Broken-Pipeを強制的に引き起こし、Web Server側でのReadループを強制終了させる. */
fclose( stdout );
/* 時間のかかる処理. */
very_long_work( prog_info );
ProgInfo_destroy( prog_info );
return 0;
}
Close
[cannot open file : [./../../../..//znk_project/src/cgi_developers/progress2.html]]
Close
/* 0-byte avoidance */
var PascuaProgress = (function(){ /* module-pattern */
'use strict';
/* public function */
/***
* new XmlHttpRequest
*/
function newXhr()
{
var xhr = false;
if( typeof ActiveXObject != "undefined" ){
try {
xhr = new ActiveXObject( "Microsoft.XMLHTTP" );
} catch (e) {
xhr = false;
}
}
if( !xhr && typeof XMLHttpRequest != "undefined" ){
xhr = new XMLHttpRequest();
}
return xhr;
}
function newInfo( state_path, msg_path, ermsg_path, dst_element_id, func_on_end ){
function PP_newPPXhr( path ){
this.xhr_ = PascuaProgress.newXhr();
/***
* 0 : New of xhr GET.
* 1 : In the middle of xhr GET response.
* 2 : Done of xhr GET.
*/
this.xhr_phase_ = 0;
this.text_ = "";
this.path_ = path;
this.count_404_ = 0;
}
function PP_gain_viaXhr( pp_xhr )
{
var xhr = pp_xhr.xhr_;
pp_xhr.xhr_phase_ = 1;
xhr.open( "GET", pp_xhr.path_ + '?PascuaProgress_date=' + new Date().getTime() );
xhr.onreadystatechange = function() {
if( xhr.readyState == 4 ){
var is_404 = false;
if( typeof xhr.status != "undefined" ){
if( xhr.status == 404 ){
++pp_xhr.count_404_;
is_404 = true;
}
}
if( !is_404 ){
pp_xhr.text_ = xhr.responseText;
}
pp_xhr.xhr_phase_ = 2;
} else if( xhr.readyState == 2 ){
/* @note: In IE8/IE9, xhr.status is not defined */
if( typeof xhr.status != "undefined" ){
if( xhr.status == 404 ){
++pp_xhr.count_404_;
}
}
}
};
xhr.send( null );
}
function PP_interpretState( info )
{
if( info.state_ == "PascuaProgress_end" ){
/* end */
info.state_ = "";
clearInterval( info.interval_id_ );
if( info.func_on_end_ != null ){
info.func_on_end_();
}
info.is_final_ = true;
} else if( info.state_ == "PascuaProgress_begin" ){
/* begin */
/* skip */
} else {
/* update display */
console.log( "PascuaProgressInfo : PP_interpretState : general string : " + info.state_ );
}
}
function PP_updatePhase( pp_xhr )
{
var phase = pp_xhr.xhr_phase_
if( phase == 0 ){
/* Try to get new-state */
PP_gain_viaXhr( pp_xhr );
} else if( phase == 1 ){
/* In the middle of xhr response : skip */
} else {
/* done */
}
}
function PP_processXhrState( info ){
var pp_xhr = info.pp_xhr_state_;
if( pp_xhr.xhr_ ){
PP_updatePhase( pp_xhr );
if( pp_xhr.xhr_phase_ == 2 ){
/* done */
pp_xhr.xhr_phase_ = 0;
info.state_ = pp_xhr.text_.replace( /\r?\n$/g, '' );
PP_interpretState( info );
}
} else {
console.log( "PascuaProgress : setInterval : xhr is null" );
clearInterval( info.interval_id_ );
}
if( pp_xhr.count_404_ >= 3 ){
console.log( "PascuaProgress : PP_processXhrState : detected 404" );
clearInterval( info.interval_id_ );
}
};
function PP_writeToDst( dst_element_id, text, is_add ){
var lines = text.split( '\n' )
var i=0;
for( i=0; i<lines.length; ++i ){
var line = lines[ i ];
//var str = text;
line = line.replace( /\r/g, '' );
line = line.replace( /&/g, '&' );
line = line.replace( /</g, '<' );
line = line.replace( />/g, '>' );
line = line.replace( /^\[FYI\]:/g, '[<span class=MstyMsgConsoleFYI>FYI</span>]:' );
line = line.replace( /^\[TRY\]:/g, '[<span class=MstyMsgConsoleTRY>TRY</span>]:' );
line = line.replace( /^\[NG\]:/g, '[<span class=MstyMsgConsoleNG>NG</span>]:' );
line = line.replace( /^\[OK\]:/g, '[<span class=MstyMsgConsoleOK>OK</span>]:' );
lines[ i] = line
}
var str = lines.join( '<br>\n' );
var dst = document.getElementById( dst_element_id );
if( dst == null ){
//alert( "dst is null. id=(" + dst_element_id + ")" );
}
if( is_add ){
dst.innerHTML += str;
} else {
dst.innerHTML = str;
}
}
function PP_processFinalMsg( dst_element_id, pp_xhr, is_add ){
PP_updatePhase( pp_xhr );
if( pp_xhr.xhr_phase_ == 2 ){
/* done */
pp_xhr.xhr_phase_ = 0;
PP_writeToDst( dst_element_id, pp_xhr.text_, is_add ) /* OK */
} else {
var count = 0;
var final_interval_id = setInterval( function(){
PP_updatePhase( pp_xhr );
if( pp_xhr.xhr_phase_ == 2 ){
/* done */
pp_xhr.xhr_phase_ = 0;
PP_writeToDst( dst_element_id, pp_xhr.text_, is_add )
clearInterval(final_interval_id); /* OK */
} else {
++count;
if( count >= 3 ){
PP_writeToDst( dst_element_id,
"[NG]: PascuaProgress : PP_processXhrMsg : Internal error : Cannot read report msg.\n", is_add )
clearInterval(final_interval_id);
} else {
/* retry */
}
}
}, 500 );
}
}
function PP_processXhrMsg( info ){
var pp_xhr_msg = info.pp_xhr_msg_;
var pp_xhr_ermsg = info.pp_xhr_ermsg_;
if( !pp_xhr_msg.xhr_ ){
console.log( "PascuaProgress : PP_processXhrMsg : pp_xhr_msg.xhr_ is null" );
info.is_final_ = false;
clearInterval( info.interval_id_ );
return;
}
if( pp_xhr_msg.count_404_ >= 3 ){
console.log( "PascuaProgress : PP_processXhrMsg : detected 404 (pp_xhr_msg)" );
info.is_final_ = false;
clearInterval( info.interval_id_ );
return;
}
if( !pp_xhr_ermsg.xhr_ ){
console.log( "PascuaProgress : PP_processXhrMsg : pp_xhr_ermsg.xhr_ is null" );
pp_xhr_ermsg = null;
}
if( pp_xhr_ermsg.count_404_ >= 3 ){
console.log( "PascuaProgress : PP_processXhrMsg : detected 404 (pp_xhr_ermsg)" );
pp_xhr_ermsg = null;
}
if( info.is_final_ ){
info.is_final_ = false;
PP_processFinalMsg( info.dst_element_id_, pp_xhr_msg, false );
if( pp_xhr_ermsg ){
PP_processFinalMsg( "PascuaProgress_ermsg", pp_xhr_ermsg, true );
}
} else {
PP_updatePhase( pp_xhr_msg );
//console.log( "PascuaProgress : PP_processXhrMsg : pp_xhr_msg.xhr_phase_=" + pp_xhr_msg.xhr_phase_ + " pp_xhr_msg=" + pp_xhr_msg.text_ );
if( pp_xhr_msg.xhr_phase_ == 2 ){
/* done */
pp_xhr_msg.xhr_phase_ = 0;
PP_writeToDst( info.dst_element_id_, pp_xhr_msg.text_, false )
}
}
};
function PP_createInterval( info )
{
var cb_func = function(){
PP_processXhrState( info );
PP_processXhrMsg( info );
};
return setInterval( cb_func, 500 );
}
this.state_ = "";
this.dst_element_id_ = dst_element_id;
this.pp_xhr_state_ = new PP_newPPXhr( state_path );
this.pp_xhr_msg_ = new PP_newPPXhr( msg_path );
this.pp_xhr_ermsg_ = new PP_newPPXhr( ermsg_path );
this.interval_id_ = PP_createInterval( this );
this.func_on_end_ = func_on_end;
this.is_final_ = false;
}
return {
newXhr: newXhr,
newInfo: newInfo,
__endof_public__: function(){}
};
})(); /* enodof PascuaProgress */
var PascuaProgress_info_list = [];
function PascuaProgressInfo_issue( state_path, msg_path, ermsg_path, dst_element_id, func_on_end )
{
PascuaProgress_info_list.push( new PascuaProgress.newInfo(
state_path, msg_path, ermsg_path, dst_element_id, func_on_end ) );
}
var PascuaProgress_result_url = "";
function PascuaProgress_setResultURL( result_url ){
PascuaProgress_result_url = result_url;
}
function PascuaProgress_replaceOnResult(){
location.replace( PascuaProgress_result_url );
}
var PascuaProgress_func_on_end = null;
function PascuaProgress_setFunc_OnEnd( func_on_end ){
PascuaProgress_func_on_end = func_on_end;
}
Close今回はHTMLとJavascriptについて複雑なコードが登場するため、別の意味で少し難しいかもしれません。
まず最初にprogress.c 内において、ProgInfo_create関数により、ProgInfo構造体を生成します。 以下にその部分を抜粋します。
static const char* prog_info_id = "upgrade";
const ProgInfo prog_info = ProgInfo_create( "../publicbox/tmp/pascua_state", prog_info_id );
ProgInfo_mkdir( prog_info );
ProgInfo_reset( prog_info, true );
この構造体は後で使用しますが、進捗状況を管理するためのデータです。 ProgInfo_create関数の第1引数には進捗状況を一時的に保持するファイルが置かれるパスが指定されており、 さらに第2引数にはそのファイル名を指定しています。今回の場合、prog_info_id の値は 「upgrade」となっておりますので、 upgrade.state、upgrade.msg、upgrade.ermsgの三つのファイルが publicbox/tmp/pascua_stateディレクトリ内に一時ファイルとして生成される形になります。 続いてProgInfo_mkdirではそのディレクトリの生成を行い、 ProgInfo_resetではそのディレクトリ内にこれら三つのファイルを初期状態を意味する内容で一旦生成します。
次に、テンプレートファイルを読み込んでそれを展開した結果を標準出力しています。 その部分を以下に抜粋します。
/* まずJavascript progress.jsを含んだHTMLを表示させる. */
{
ZnkBird bird = ZnkBird_create( "#[", "]#" );
ZnkStr ans = ZnkStr_new( "" );
/* Register to BIRD-tag. */
ZnkBird_regist( bird, "prog_info_id", prog_info_id );
ZnkCGIStr_addTemplateHTML( ans, bird, "../publicbox/progress.html", "utf-8" );
printf( ZnkStr_cstr(ans) );
ZnkStr_delete( ans );
ZnkBird_destroy( bird );
}
上記ではまずZnkBirdに"prog_info_id"をキーとして定数prog_info_idの値を登録します。 また「publicbox/progress.html」をテンプレートファイルとして読み込みこれを展開して表示させます。
ZnkCGIStr_addTemplateHTMLの中身についても簡単に説明しておきます。 この関数の定義は(libZnkのソース内において)以下のようになっています。
bool
ZnkCGIStr_addTemplateHTML( ZnkStr ans, ZnkBird bird, const char* template_html_file, const char* charset )
{
bool result = false;
ZnkCGIStr_addHttpHdr( ans, charset );
if( ZnkStrFIO_loadImgFile( ans, template_html_file, 0, Znk_NPOS, NULL ) ){
result = ZnkBird_expand_self( bird, ans, Znk_NPOS );
} else {
const char* css_file_urp = "";
const char* syntax_cls_prefix = "ZnkMsgConsole";
ZnkStr additional_head = ZnkStr_new( "" );
ZnkCGIStr_addCssLink( additional_head, css_file_urp );
ZnkConsoleMsg_addDefaultSyntaxStyleHtml( additional_head, syntax_cls_prefix );
ZnkCGIStr_beginHtml( ans, NULL, charset, ZnkStr_cstr(additional_head) );
{
ZnkStr str= ZnkStr_new( "" );
ZnkStr_addf( str, "[NG]: ZnkCGIStr_addTemplateHTML : Error : Cannot load template_file=[%s]\n", template_html_file );
ZnkConsoleMsg_specialErmsgSyntax( str, syntax_cls_prefix, NULL );
ZnkStr_add_str( ans, str );
ZnkStr_delete( str );
}
ZnkCGIStr_endHtml( ans );
ZnkStr_delete( additional_head );
}
return result;
}
ZnkStrFIO_loadImgFile関数(Znk_str_fio.hで宣言されています)は、指定されたファイルの全文を読みこんでそれを変数 (上記の例ではans)に格納するためのものです。ここではさらにZnkBird_expand_self関数においてansをbirdにより展開しています。 今回birdには prog_info_id が登録されており、progress.html内の以下の箇所において、 BIRDタグ「#[prog_info_id]#」が展開されます。
<script type="text/javascript">
var pascua_state = "publicbox/tmp/pascua_state";
PascuaProgressInfo_issue(
pascua_state + "/#[prog_info_id]#.state",
pascua_state + "/#[prog_info_id]#.msg",
pascua_state + "/#[prog_info_id]#.ermsg",
"PascuaProgress_#[prog_info_id]#_msg",
null
);
</script>
...途中略...
<body>
CGI Progress test.
<div class=MstyMsgConsole id=PascuaProgress_#[prog_info_id]#_msg> </div>
<br>
error log.
<div class=MstyMsgConsole id=PascuaProgress_ermsg></div>
</body>
ZnkCGIStr_addTemplateHTML関数の呼び出しの後、 最後にprint関数により、ansの内容(progress.htmlがbirdにより展開された内容)を一旦標準出力します。
ansの標準出力が終った後は、以下のようにfflush関数を呼び出し、 先ほど行った ans の内容の標準出力を確実に実行させます。
fflush( stdout );
さて、ここでこの次の処理に注意してください。 これがやや難解ですが以下のような処理を行います。
/* Broken-Pipeを強制的に引き起こし、Web Server側でのReadループを強制終了させる. */
fclose( stdout );
上記では標準出力のファイルディスクリプタであるstdoutをクローズしています。 一旦これをクローズすると、CGIプログラム上で標準出力を行うことはもうできなくなります。 つまり標準出力によるHTMLの生成はここで完全に終了です。 これにより、(progress.cgiを起動した)Web Server側に、progress.cgiの実行があたかも終了したかのように見せかけることができます (しかしながら実際にはprogress.cgiプログラムの実行自体はまだ続きます)。 Web Server側ではprogress.cgiの実行が終了したと認識しますので、ブラウザ側にこれまで標準出力した内容をすべて送信し、 ブラウザもまたこれを受信し、通常のCGIプログラムの送受信がこれにて一区切りつきます。
もう少し正確に言うと、標準出力をクローズすることでBroken-Pipeを強制的に引き起こし、
Web Server側でのRead処理を確実に完了させます。
そうしないとWeb Server側からブラウザに完了通知が送られず、結果的にブラウザの方が途中経過を表示中にタイムアウトを起こすかもしれません。
ところがブラウザが受信したprogress.htmlではpascua_progress.jsが起動するようになっており、 このpascua_progress.jsは、(タイマーにより)ある種のファイルの内容を決められた時間置きに監視する処理を行います。 そしてそのファイルの内容に応じて現在ブラウザに表示された内容を動的に書き換えます。
なんとなく進捗状況を表示するための仕組みが見えてきたかもしれませんが、まだよくわからないかもしれません。 progress.cgiの実行はまだ続いており、こちらではある種のファイルの内容を更新します。 一方、pascua_progress.jsの方の実行もブラウザ上で続いており、そちらでは同じくある種のファイルの内容を監視しつつ 動的に画面の描写を行います。
先にprogress.cgi(progress.c)の方を確認しましょう。 fclose関数の呼び出しの後は、「時間のかかる処理」としてvery_long_work関数を呼び出しています。 今この中で以下のように非常に時間のかかる処理を行うものとしましょう。
static void
very_long_work( ProgInfo prog_info )
{
ZnkStr ermsg = ZnkStr_new( "" );
ZnkStr tmp = ZnkStr_new( "" );
ProgInfo_begin( prog_info );
while( st_count <= 100 ){
ZnkStr_setf( tmp, "[FYI]: progress:%zu%", st_count );
ProgInfo_update( prog_info, ZnkStr_cstr(tmp) );
++st_count;
ZnkThread_sleep( 100 ); /* sleeping 100msec */
if( st_count == 50 ){
ZnkStr_add( ermsg, "[FYI]: very_long_work : count is 50.\n" );
}
}
ProgInfo_end( prog_info, ermsg );
ZnkThread_sleep( 3000 );
ProgInfo_cleanFiles( prog_info );
ZnkStr_delete( tmp );
ZnkStr_delete( ermsg );
}
上記で実際にやっていることは単にカウンタ(st_count)を更新してZnkThread_sleep関数で適当な時間休むといったものですが、 とりあえずこれはサンプルですのでここでは「時間のかかる処理」をこれで代用するものとしましょう。
さて、ここでようやく最初に生成した構造体prog_infoを使うわけですが、 まずこれを引数としてProgInfo_begin関数を呼び出します。 これによりpublicbox/tmp/pascua_stateディレクトリ内にあるupgrade.stateの内容がPascuaProgress_beginに、 またupgrade.msgの内容が「[FYI]: Please wait.」に変更されます。
このとき同時にpascua_progress.jsでは、このupgradeファイルの内容を確認しています。 upgrade.stateの内容がPascuaProgress_beginになったことでpascua_progress.js側に進捗状況の表示を開始すべきといった情報を伝達することができ、 またupgrade.msgの内容が「[FYI]: Please wait.」になったことでpascua_progress.jsはこれをユーザへの報告用メッセージとしてブラウザに表示します。 より正確にはprogress.html内の以下のように記述された部分(idがPascuaProgress_upgrade_msgとなっているdivタグ)に この内容を書き込みます。
CGI Progress test.
<div class=MstyMsgConsole id=PascuaProgress_upgrade_msg> </div>
<br>
このようにprogress.cとpascua_progress.js(progress.html)を行ったり来たりしながら説明していきますので混乱しないように注意してください。
続いてprogress.c側へ戻り、(very_long_work関数内の)whileループの実行に入ります。 whileブロック内では一定の時間間隔をもってProgInfo_updateが呼び出されます。 この関数ではupgrade.msgの内容を指定した文字列で書き換えます (ちなみに上記の例ではst_countを「進捗を表すパーセンテージ」と見立てています)。
このとき同時にpascua_progress.jsでは、このupgrade.msgファイルの内容を確認し、 この内容をユーザへの報告用メッセージとしてブラウザに表示します。 より正確には(先ほどと同じく)progress.html内のidがPascuaProgress_upgrade_msgとなっているdivタグにこれを書き込みます (このとき、このタグでそれまでに表示されていたメッセージは消去されます)。
続いてprogress.c側へ戻り、whileループを抜けた後の部分を見て行きます。 ProgInfo_end関数により、それまでに取得したermsgの内容をupgrade.ermsgに書き込みます(これは最終的なログメッセージの確認用です)。 またupgrade.stateファイルの内容を「PascuaProgress_end」に書き換えます。
このとき同時にpascua_progress.jsでは、upgrade.ermsgファイルの内容を確認し、 progress.html内の以下のように記述された部分(idがPascuaProgress_ermsgとなっているdivタグ)に この内容を書き込みます。
error log.
<div class=MstyMsgConsole id=PascuaProgress_ermsg></div>
また、upgrade.stateファイルの内容が「PascuaProgress_end」になっていることから、 pascua_progress.jsの実行をここで終了し、進捗状況の表示を終了します。 Webブラウザを使用しているユーザから見れば、この段階で進捗状況の表示も含め、すべての表示が完了となります。
最後にprogress.c側でProgInfo_cleanFiles関数を呼び出し、 一時ファイルであるupgrade.state、upgrade.msg、upgrade.ermsgをすべて消去します。 これでvery_long_work関数の実行も終了し、progress.c側の実行もこれですべて完了になります。
目次に戻る
おわりに
これで一通り「C言語によるCGI」が終わりました。 お疲れ様でした。この講座で扱うレベルのものを作るのでさえ、C言語ではその他の言語と比べると非常に大変であったと思います。 しかし敢えてC言語を選択し、またこの記事をご覧になれるほどの読者の方であれば、 ここまでの内容を基にC言語でも道を切り開いていけるのではないかとも思います。
目次に戻る
This article was written by:
Mr.Moai
@znk project