WWWプログラミング

システム・プログラム

                                       電子・情報工学系
                                       新城 靖
                                       <yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/syspro-2002/2002-06-24
あるいは、次のページから手繰っていくこともできます。
http://www.hlla.is.tsukuba.ac.jp/~yas/coins/
http://www.is.tsukuba.ac.jp/~yas/index-j.html

■今日の重要な話

WWW サーバ側のプログラム クライアント側のプログラム アクセス制御

■CGIの考え方

WWW サーバは、普通は、ファイルに保存された HTML データやイメージ・データを 読み込み、WWWブラウザに返する。 CGI (Common Gateway Interface) では、ファイルが読み込まれる代わりにプログラムが実行され、その実行結果 がブラウザに返される。

Gateway とは、WWW で使われている HTTP というプロトコルへの入り口という 意味である。 プログラムの実行結果は、普通は、HTML にすることが多いが、普通のテキス トであることもイメージであることもある。

クライアント、WWWサーバ、CGIによるプロセス、ファイル

図? CGIの仕組み

CGI の利用例

◆CGIの設定(Apache)

情報学類には、www.coins.tsukuba.ac.jp と www2.coins.tsukuba.ac.jp の2 つのサーバがあり、CGI が動作可能であるように設定されているのは、www2 の方である。

CGI になるファイル名のパタンの例(設定により、これ以外も可能)。


/cgi-bin/name
/dir/name.cgi

◆cgi-hello.c

cgi-hello.c は、クライアントに HTML で hello. と返すCGI のプログラムで ある。
----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        cgi-hello.c --簡単な CGI のプログラム
   4:	        ~yas/syspro/www/cgi-hello.c
   5:	        Start: 2002/06/23 18:21:34
   6:	*/
   7:	
   8:	void print_header();
   9:	
  10:	main()
  11:	{
  12:	        print_header();
  13:	
  14:	        printf("<HTML>\n");
  15:	
  16:	        printf("hello.\n");
  17:	        printf("</HTML>\n");
  18:	        exit( 0 );
  19:	}
  20:	
  21:	void print_header()
  22:	{
  23:	        printf("Content-Type: text/html\r\n");
  24:	        printf("\r\n");
  25:	}
----------------------------------------------------------------------
CGI のプログラムは、HTTP のヘッダ(一番最初の 「HTTP/1.0 200 OK」 では なく、2行目以降)を標準出力に出力する所から始まる。Content-Type: 行は、 必ず付ける。空行がヘッダと本分の区切りである。本文には、この場合は、 HTML の文書を置いている。

実行例:


----------------------------------------------------------------------
% cd ~/public_html/syspro/cgi-examples/ [←]
% cp ~yas/syspro/www/cgi-hello.c . [←]
% cc cgi-hello.c -o cgi-hello.cgi [←]
% ./cgi-hello.cgi  [←]
Content-Type: text/html

<HTML>
hello.
</HTML>
% []
----------------------------------------------------------------------

CGI のプログラムを、直接シェルが実行することもできる。(引数の与え方に ついては、後述する。)

WWW ブラウザでは、次の URL を開く。

http://www2.coins.tsukuba.ac.jp/~yas/syspro/cgi-examples/cgi-hello.cgi

telnet では、次のようにして実行する。


----------------------------------------------------------------------
% telnet www2 80 [←]
Trying 130.158.86.5...
Connected to www2.
Escape character is '^]'.
GET /~yas/syspro/cgi-examples/cgi-hello.cgi HTTP/1.0[←]
[←]
HTTP/1.1 200 OK
Date: Sun, 23 Jun 2002 13:48:18 GMT
Server: Apache/1.3.22 (Unix)  (Red-Hat/Linux) DAV/1.0.2 mod_perl/1.24_01 mod_throttle/3.1.2
Connection: close
Content-Type: text/html


hello.

Connection closed by foreign host.
% []
----------------------------------------------------------------------

GET の代りに POST でもよい。

----------------------------------------------------------------------
% telnet www2 80 [←]
Trying 130.158.86.5...
Connected to www2.
Escape character is '^]'.
POST /~yas/syspro/cgi-examples/cgi-hello.cgi HTTP/1.0[←]
[←]
HTTP/1.1 200 OK
Date: Sun, 23 Jun 2002 13:50:17 GMT
Server: Apache/1.3.22 (Unix)  (Red-Hat/Linux) DAV/1.0.2 mod_perl/1.24_01 mod_throttle/3.1.2
Connection: close
Content-Type: text/html


hello.

Connection closed by foreign host.
% []
----------------------------------------------------------------------

◆フォーム

ブラウザからサーバに情報を送ってほしい時には、 フォーム(form)という機能を使う。

HTML記述例: http://www.coins.tsukuba.ac.jp/~yas/syspro/cgi-examples/cgi-printarg.html

<H1><A NAME="cgi-example-get">CGI の GET メソッドを使う例</A></H1>

<FORM ACTION="http://www2.coins.tsukuba.ac.jp/~yas/syspro/cgi-examples/cgi-printarg.cgi" method="get">
    <P>
    姓: <INPUT type="text" name="lastname">
    名: <INPUT type="text" name="firstname"><BR>
    <INPUT type="radio" name="sex" value="Male"> 男 <BR>
    <INPUT type="radio" name="sex" value="Female"> 女 <BR>
    電子メール: <INPUT type="text" name="email"><BR>
    <INPUT type="submit" value="send"> <INPUT type="reset">
    </P>
</FORM>

<H1><A NAME="cgi-example-post">CGI の POST メソッドを使う例</A></H1>

<FORM ACTION="http://www2.coins.tsukuba.ac.jp/~yas/syspro/cgi-examples/cgi-printarg.cgi" method="post">
    <P>
    姓: <INPUT type="text" name="lastname">
    名: <INPUT type="text" name="firstname"><BR>
    <INPUT type="radio" name="sex" value="Male"> 男 <BR>
    <INPUT type="radio" name="sex" value="Female"> 女 <BR>
    電子メール: <INPUT type="text" name="email"><BR>
    <INPUT type="submit" value="send"> <INPUT type="reset">
    </P>
</FORM>

表示例:

CGI の GET メソッドを使う例

姓: 名:


電子メール:

CGI の POST メソッドを使う例

姓: 名:


電子メール:

<FORM></FORM>で括られた部 分が1つのフォームになる。(1つのページに複数のフォームを置くことがで きる。)

<INPUT>の部分が、ブラウザからサーバに送られるデー タを表わす。

send」ボタンが押されると、ブラウザは、HTTPの POST メソッ ドを使って、サーバにデータを送る。

WWW サーバは、それを受け取ると、指定されたプログラムを実行する。実行さ れたプログラムは、環境変数と標準入力からデータを受け取ることができる。

環境変数

REQUEST_METHOD
データを送るのに使われたメソッド。POST か GET。 GET は、普通のページを得るものと同じ方法で、長いデータは送れない。
SCRIPT_NAME
プログラムの名前。
QUERY_STRING
URLの指定で「?」以下に指定された文字列
メソッドとして POST が使われた時、標準入力から <FORM></FORM>で指定されたパラメタを受け取ることが でる。

このデータは、「&」で区切られた 「フィールド名=」の並びになっている。

例:

lastname=姓&firstname=名&sex=Male&email=who@u-ust.ac.jp
環境変数 CONTENT_LENGTH からは、データの長さが得られる。

メソッドとして、GET が使われた時には、 環境変数 QUERY_STRING に 「&」で区切られた「フィール ド名=」の並びが含まれている。

送られてくるパラメタの中に漢字、 (&自身)、 URLで使えない文字が含まれていた場合、 「%hh(2桁の16進数)」という形になっている。

これを元にもどすにはcgiparse というプログラムや perl の命令 hex() が使われる。

◆cgi-printarg.c

次のプログラムは、CGI で実行された時に、その引数を表示するものである。 GET メソッド、および、POST メソッドの両方に対応している。
----------------------------------------------------------------------
   1:	
   2:	/*
   3:	        cgi-printarg.c -- CGI プログラムに対する引数を表示するプログラム
   4:	        ~yas/syspro/www/cgi-printarg.c
   5:	        Start: 2002/06/23 18:21:34
   6:	*/
   7:	
   8:	#include <stdlib.h>     /* getenv(), malloc() */
   9:	#include <stdio.h>      /* printf() */
  10:	#include <string.h>     /* strlen() */
  11:	
  12:	void print_header(void);
  13:	void safe_printenv( char *name );
  14:	char *get_query_string();
  15:	char *read_query_string();
  16:	void safe_print_string( char *str );
  17:	char *html_escape( char *str );
  18:	char *decode_hex( char *str );
  19:	int countchr( char *s, char c );
  20:	
  21:	main()
  22:	{
  23:	    char  *query_string ;
  24:	    char **qv ;
  25:	    int    qc ;
  26:	    int    i ;
  27:	
  28:	        print_header();
  29:	
  30:	        printf("<HTML><PRE>\n");
  31:	
  32:	        safe_printenv("REQUEST_METHOD");
  33:	        safe_printenv("SCRIPT_NAME");
  34:	        safe_printenv("QUERY_STRING");
  35:	        safe_printenv("CONTENT_LENGTH");
  36:	
  37:	        query_string = get_query_string();
  38:	        printf("query_string:\n");
  39:	        safe_print_string( query_string ); printf("\n");
  40:	
  41:	        if( make_qcqv( &qc, &qv, query_string ) < 0 )
  42:	        {
  43:	            printf("Error while parsing query string\n");
  44:	            exit( -1 );
  45:	        }
  46:	        for( i=0 ; i<qc ; i++ )
  47:	        {
  48:	            char *nohex ;
  49:	            printf("qv[%d]: ",i);
  50:	            safe_print_string(qv[i]);
  51:	            nohex = decode_hex( qv[i] );
  52:	            printf("(");
  53:	            safe_print_string( nohex );
  54:	            printf(")\n");
  55:	        }
  56:	
  57:	        printf("</PRE></HTML>\n");
  58:	        exit( 0 );
  59:	}
  60:	
  61:	void print_header()
  62:	{
  63:	        printf("Content-Type: text/html\r\n");
  64:	        printf("\r\n");
  65:	}
  66:	
  67:	void safe_printenv( char *name )
  68:	{
  69:	    char *val ;
  70:	    char *safe_val ;
  71:	
  72:	        printf("%s=",name );
  73:	        val = getenv( name );
  74:	        safe_print_string( val );
  75:	        printf("\n");
  76:	}
  77:	
----------------------------------------------------------------------
CGI のプログラムは、HTTP のヘッダ(一番最初の OK ではなく、2行目以降) を標準出力に出力する所から始まる。Content-Type: 行は、必ず付ける必要が ある。

safe_printenv() で、環境変数 REQUEST_METHOD, SCRIPT_NAME, QUERY_STRING, CONTENT_LENGTH の内容を標準出力に出力している。

get_query_string() は、クライアントから送られてきたパラメタ(query string)を読込み、文字列として返す関数である。これを、 safe_print_string() で標準出力に出力している。

make_qcqv() は、クライアントから送られてきたパラメタ(query string)を、 分解して1つひとつバラバラににするものである。分解した結果は、main() の引数である int argc, char *argv[] と同じ形になっている。qc が、argc, qv が argv に対応する。

for ループでは、qv[] をsafe_print_string() で標準出力に出力している。 さらに、decode_hex() で、%hh (%hhは16進数)の形式を、バイトに元に戻すものであ る。

safe_printenv() は、環境 変数をgetenv() で検索し、その値をsafe_print_string() で標準出力に出力 している。

----------------------------------------------------------------------
  78:	int
  79:	make_qcqv( int *qcp, char ***qvp, char *qs )
  80:	{
  81:	    char **qv ;
  82:	    int    qc_max, i, len ;
  83:	    char *s, *p ;
  84:	
  85:	        if( qs == 0 )
  86:	            return( -1 );
  87:	        qc_max = countchr(qs,'&')+1 ;
  88:	        qv = malloc( sizeof(char *)*(qc_max+1) );
  89:	        if( qv == 0 )
  90:	            return( -1 );
  91:	
  92:	        for( i=0 ; i<qc_max ; i++ )
  93:	        {
  94:	            while( *qs == '&' )
  95:	                qs ++ ;
  96:	            if( *qs == 0 )
  97:	                break;
  98:	            for( p = qs ; *p!='&' && *p!=0 ; p++ )
  99:	                continue;
 100:	            /* *p == '&' || *p=='0' */
 101:	            len =  p - qs ;
 102:	            s = malloc( len+1 );
 103:	            if( s == 0 )
 104:	            {
 105:	                int j ;
 106:	                for( j=0 ; j<i; j++ )
 107:	                {
 108:	                    free( qv[j] );
 109:	                    qv[j] = 0 ;
 110:	                }
 111:	                return( -1 );
 112:	            }
 113:	            memcpy( s, qs, len );
 114:	            s[len] = 0 ;
 115:	            qv[i] = s ;
 116:	            qs = p ;
 117:	        }
 118:	        qv[i] = 0 ;
 119:	        *qcp = i ;
 120:	        *qvp = qv ;
 121:	        return( i );
 122:	}
 123:	
----------------------------------------------------------------------
make_qcqv() は、「&」で区切られて送られてきたパラメタをバラバラに分解 し、main の引数である int argc, char *argv[] と同じ形にするものである。 まず、「&」の数を先に数えて、argv に相当するメモリを確保する。1つ多 く確保しているのは、最後に0を置くためである。ただし、「&&&」のよう に空のものは取り除いている。

for() 文で1つの「&」の左、または、終端の0の左のまでを切り出している。 qs が先頭、p が「&」、または、終端の0を差すようにする。malloc() で長 さ分(終端の0を含む)のメモリを確保してmemcpy() でコピーしている。 s[len] で終端の0を付け、qv[i] に保存している。qs を q にして、次のルー プに進む。

----------------------------------------------------------------------
 124:	char *get_query_string()
 125:	{
 126:	    char *request_method, *query_string;
 127:	        request_method = getenv("REQUEST_METHOD");
 128:	        if( request_method == 0 )
 129:	            return( 0 );
 130:	        else if( strcmp(request_method,"GET") == 0 )
 131:	        {
 132:	            query_string = getenv("QUERY_STRING");
 133:	            if( query_string == 0 )
 134:	                return( 0 );
 135:	            else
 136:	                return( strdup(query_string) );
 137:	        }
 138:	        else if( strcmp(request_method,"POST") == 0 )
 139:	        {
 140:	            return( read_query_string() );
 141:	        }
 142:	        else
 143:	        {
 144:	            printf("Unknown method: ");
 145:	            safe_print_string( request_method );
 146:	            printf("\n");
 147:	        }
 148:	}
 149:	
 150:	char *read_query_string()
 151:	{
 152:	    int   clen ;
 153:	    char *content_length ;
 154:	    char *buf ;
 155:	
 156:	        content_length = getenv("CONTENT_LENGTH");
 157:	        if( content_length == 0 )
 158:	        {
 159:	            return( 0 );
 160:	        }
 161:	        else
 162:	        {
 163:	            clen = atoi( content_length );
 164:	            buf = malloc( clen + 1 );
 165:	            if( buf == 0 )
 166:	            {
 167:	                printf("read_query_string(): no memory\n");
 168:	                exit( -1 );
 169:	            }
 170:	            if( read(0,buf,clen) != clen )
 171:	            {
 172:	                printf("read error.\n");
 173:	                exit( -1 );
 174:	            }
 175:	            buf[clen] = 0 ;
 176:	            return( buf );
 177:	        }
 178:	}
 179:	
----------------------------------------------------------------------
get_query_string() は、クライアントから送られてきたパラメタを、1つの 文字列として受け取るものである。GET メソッドの場合()は、単に環境変数 REQUEST_METHOD を読めばよい。ただし、文字列の長さの上限がきつい。

strdup() は、文字列をコピーするものである。使い終わったら、free() で解 放する。

POST メソッドの場合は、read_query_string() で続きの処理を行う。

----------------------------------------------------------------------
 180:	void safe_print_string( char *str )
 181:	{
 182:	    char *safe_str ;
 183:	
 184:	        if( str == 0 )
 185:	        {
 186:	            printf("(null)");
 187:	            return;
 188:	        }
 189:	        safe_str = html_escape( str );
 190:	        if( safe_str == 0 )
 191:	        {
 192:	            printf("(no memory)");
 193:	        }
 194:	        else
 195:	        {
 196:	            printf("%s",safe_str );
 197:	            free( safe_str );
 198:	        }
 199:	}
 200:	
 201:	char *html_escape( char *str )
 202:	{
 203:	    int len ;
 204:	    char c, *tmp, *p, *res ;
 205:	
 206:	        len = strlen( str );
 207:	        tmp = malloc( len * 6 + 1 );
 208:	        if( tmp == 0 )
 209:	            return( 0 );
 210:	        p = tmp ;
 211:	        while( c = *str++ )
 212:	        {
 213:	            switch( c )
 214:	            {
 215:	            case '&': memcpy(p,"&amp;", 5); p += 5 ; break;
 216:	            case '<': memcpy(p,"&lt;",  4); p += 4 ; break;
 217:	            case '>': memcpy(p,"&gt;",  4); p += 4 ; break;
 218:	            case '"': memcpy(p,"&quot;",6); p += 6 ; break;
 219:	            default:  *p = c ;              p++ ;    break;
 220:	            }
 221:	        }
 222:	        *p = 0 ;
 223:	        res = strdup( tmp );
 224:	        free( tmp );
 225:	        return( res );
 226:	}
 227:	
----------------------------------------------------------------------
safe_print_string() は、次の html_escape() を使って文字列を安全なもの にして表示する。

html_escape() は、次の文字について、置き換えている。たとえば、 「<」は、「&lt;」と変換している。これで、 <SCRIPT> のような危険なスクリプトが送り込まれたとしても 「&lt;SCRIPT&gt;」と表示と表示されるだけで、スクリプトは実 行されない。

len * 6 は、最大で元の文字列の6倍の長さになることに備えている。

----------------------------------------------------------------------
 228:	char *decode_hex( char *str )
 229:	{
 230:	    int len ;
 231:	    char c, *tmp, *p, *res ;
 232:	
 233:	        len = strlen( str );
 234:	        tmp = malloc( len + 1 );
 235:	        if( tmp == 0 )
 236:	            return( 0 );
 237:	        p = tmp ;
 238:	
 239:	        while( *str )
 240:	        {
 241:	            if( *str == '%' && isxdigit(*(str+1)) && isxdigit(*(str+2)) )
 242:	            {
 243:	                char hexstr[3] ;
 244:	                hexstr[0] = *(str+1);
 245:	                hexstr[1] = *(str+2);
 246:	                hexstr[2] = 0 ;
 247:	                c = strtol( hexstr, 0, 16 );
 248:	                *p ++ = c ;
 249:	                str += 3 ;
 250:	            }
 251:	            else
 252:	            {
 253:	                *p ++ = *str ;
 254:	                str ++ ;
 255:	            }
 256:	        }
 257:	        *p = 0 ;
 258:	        res = strdup( tmp );
 259:	        free( tmp );
 260:	        return( res );
 261:	}
 262:	
 263:	int countchr( char *s, char c )
 264:	{
 265:	    int count ;
 266:	        for( count=0 ; *s ; s++ )
 267:	            if( *s == c )
 268:	                count ++ ;
 269:	        return( count );
 270:	}
----------------------------------------------------------------------
decode_hex() は、「%hh(2桁の16進数)」を元のバイトに 戻すものである。isxdigit() は、16進数の文字として正しいかを調べてい る。strtol() で基底を 16 として変換している。

countchr() は、文字列の中でその文字がいくつ出てくるかを数えるものである。

実行例: http://www.coins.tsukuba.ac.jp/~yas/syspro/cgi-examples/cgi-printarg.html

REQUEST_METHOD=GET
SCRIPT_NAME=/~yas/syspro/cgi-examples/cgi-printarg.cgi
QUERY_STRING=lastname=arg1&firstname=arg2&sex=Male&email=arg3
CONTENT_LENGTH=(null)
query_string:
lastname=arg1&firstname=arg2&sex=Male&email=arg3
qv[0]: lastname=arg1(lastname=arg1)
qv[1]: firstname=arg2(firstname=arg2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=arg3(email=arg3)

REQUEST_METHOD=POST
SCRIPT_NAME=/~yas/syspro/cgi-examples/cgi-printarg.cgi
QUERY_STRING=
CONTENT_LENGTH=48
query_string:
lastname=arg1&firstname=arg2&sex=Male&email=arg3
qv[0]: lastname=arg1(lastname=arg1)
qv[1]: firstname=arg2(firstname=arg2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=arg3(email=arg3)

◆アクセスログとエラーログ

WWW サーバは、ページがアクセスされる度に記録を残す。この記録をアクセス ログという。アクセスログは、情報学類のサーバ www (orchid-a) や www2 (orchid-e) では、次の場所にある。

/var/log/httpd/access_log

以下は、アクセスログの例である。

----------------------------------------------------------------------
130.158.85.140 - - [23/Jun/2002:23:28:28 +0900] "GET /~yas/index-j.html HTTP/1.1" 304 - "-" "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:0.9.4.1) Gecko/20020508 Netscape6/6.2.3"
130.158.85.140 - - [23/Jun/2002:23:28:28 +0900] "GET /~yas/images/shinjo-picture-2002-05.jpg HTTP/1.1" 304 - "http://www2.coins.tsukuba.ac.jp/~yas/index-j.html" "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:0.9.4.1) Gecko/20020508 Netscape6/6.2.3"
130.158.85.140 - - [23/Jun/2002:23:28:28 +0900] "GET /~yas/images/mt-tsukuba-3-1996-05-10.gif HTTP/1.1" 304 - "http://www2.coins.tsukuba.ac.jp/~yas/index-j.html" "Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en-US; rv:0.9.4.1) Gecko/20020508 Netscape6/6.2.3"
----------------------------------------------------------------------
1行が1つのページの転送を意味する。左端は、クライアントの IP アドレス である。どのような要求が来たか、クライアントのソフトウェアなどがわかる。 自分が単にWWWブラウザを使っているだけでも、さまざまな情報をサーバに 送っているとに注意しなさい。

アクセスログの他に、エラーが起きた時にもログが作られる。これをエラーロ グという。エラーログは、情報学類のサーバ www (orchid-a) や www2 (orchid-e) では、次の場所にある。

/var/log/httpd/error_log

CGI のプログラムをデバッグする時には、このエラーログを見て原因を探る。

◆シェルからのCGIプログラムの実行

CGI のプログラムをシェルから実行してデバッグすることもできる。

GET の場合、環境変数 REQUEST_METHOD に GETと設定し、環境変数 QUERY_STRING にパラメタを設定する。


----------------------------------------------------------------------
% setenv REQUEST_METHOD GET [←]
% setenv QUERY_STRING 'lastname=name1&firstname=name2&sex=Male&email=who@dom' [←]
% ./cgi-printarg.cgi  [←]
Content-Type: text/html

<HTML><PRE>
REQUEST_METHOD=GET
SCRIPT_NAME=(null)
QUERY_STRING=lastname=name1&firstname=name2&sex=Male&email=who@dom
CONTENT_LENGTH=(null)
query_string:
lastname=name1&firstname=name2&sex=Male&email=who@dom
qv[0]: lastname=name1(lastname=name1)
qv[1]: firstname=name2(firstname=name2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=who@dom(email=who@dom)
</PRE></HTML>
% []
----------------------------------------------------------------------
POST の場合、まず、データをファイルに保存しておく。そのバイト数を wc コマンドなどで調べておく。環境変数 REQUEST_METHOD に POST と設定する。 環境変数 CONTENT_LENGTH にデータファイルの大きさを設定する(ここでは最 後の改行を取り除いたバイト数を指定している)。

----------------------------------------------------------------------
% echo 'lastname=name1&firstname=name2&sex=Male&email=who@dom' > data [←]
% wc data [←]
      1       1      54 data
% setenv REQUEST_METHOD POST [←]
% unsetenv QUERY_STRING [←]
% setenv CONTENT_LENGTH 53 [←]
% ./cgi-printarg.cgi < data [←]
Content-Type: text/html

<HTML><PRE>
REQUEST_METHOD=POST
SCRIPT_NAME=(null)
QUERY_STRING=(null)
CONTENT_LENGTH=53
query_string:
lastname=name1&firstname=name2&sex=Male&email=who@dom
qv[0]: lastname=name1(lastname=name1)
qv[1]: firstname=name2(firstname=name2)
qv[2]: sex=Male(sex=Male)
qv[3]: email=who@dom(email=who@dom)
</PRE></HTML>
% []
----------------------------------------------------------------------

◆CGI プログラムを実行するプロセスのユーザ属性

シェルからプロセスを生成しプログラムを実行する時には、その新しいプロセ スのユーザ属性は、シェルのものからコピーされる。すなわち、新しいプロセ スはシェルと同じユーザの権限で動作する。CGI の場合、遠隔の WWW ブラウ ザのユーザは、その WWW サーバにログインする権限はないが、CGI のプログ ラムを実行することができる。そのプログラムを実行するプロセスのユーザ属 性には、大きく次の2つの設定方法がある。 どちらの方式にも一長一短がある。 WWW サーバ・プロセスの権限で動作させた場合、

suEXEC を使うと、その逆になる。

◆スクリプト言語の利用

CGI のプログラムを作成する場合、トップレベル(クライアントにデータを送 る部分)は、C言語で記述するよりも、スクリプト言語で記述することが多い。 スクリプト言語は、インタプリタで動作するので、プログラミングが容易である。

普通のプログラミング言語は、アプリケーション・プログラム本体を記述する 時に使われる。 これに対して、 スクリプト言語 は、アプリケーション本体で はなく、アプリケーションの細かな動作を変更したり、アプリケーション本体 を変更することなく機能を追加したりするために使われる言語である。 普通のプログラミング言語は、アプリケーション・プログラマにより使われ、 コンパイラで機械語に変換されるので、実行時には機械語しか残っていない。 これに対して、スクリプト言語は、アプリケーションのユーザや、システム管 理者などによって使われ、プログラムは、アプリケーションに組み込まれた インタプリタで解釈実行される。 スクリプト言語を使うと、単に変数を設定することに比べて、 高度な機能拡張が機能になる。

スクリプト言語の例:

スクリプト言語 sh で記述した CGI のプログラムの例:

----------------------------------------------------------------------
% cat ./cgi-hello-sh.cgi [←]
#!/bin/sh

cat <<EOF
Content-Type: text/html

<HTML>
<H1>hello.</H1>
</HTML>
EOF
% ./cgi-hello-sh.cgi [←]
Content-Type: text/html

<HTML>
<H1>hello.</H1>
</HTML>
% []
----------------------------------------------------------------------

◆クロスサイトスクリプティング攻撃

CGI のプログラムをつくる時には、クロスサイトスクリプティング攻撃 (cross-site scripting atack, XSS atack)に気をつける。これは、クライア ントが送られてる文字列の中に<SCRIPT>のようなタグが含まれて いた場合、それをそのままクライアントに送り返すと、 クライアントで JavaScriptなどの ブラウザ上で動作するスクリプト言語が実行されることがある。 自分自身のサイトにあるフォームにはそのような問題がないとしても、 別のサイト(クロスサイト)に仕組まれたフォームからスクリプトが 含まれていることもある。

利用者が信頼していると設定しているホストにクロスサイトスクリプティング 攻撃への脆弱性があると、悪意をもった別のサイトから危険なスクリプトを実 行させることができる。

クライアントから送られてきた文字列は、必ず検査してから使う。 「」のようなタグがないかどうかを調べる。不用意に同じ文字列 を送り返してはいけない。送り返す時には、html_escape() のような方法で必 ずエスケープする。

◆他のプログラムの実行

他のプログラムを実行する時には、execve() のようなシステムコールを使い、 かつ、限られたプログラムしか実行しないようにすると安全性が高くなる。ク ライアントから送られてきた文字列をsystem() や popen() に渡してプログラ ムを実行する時には、必ず検査する。特にシェルが解釈する特殊な文字 「| & ; && || `」などが含 まれていた場合、意図しないプログラムが実行されることがある。

◆ファイルのロック

アクセスカウンタなどデータ・ファイルを書き換えるような CGI プログラム の場合、複数のプログラムが同時に実行された時のことを考慮する必要がある。 必要に応じて、ファイルのロックを行うなどして、一度に1つのプロセスしか プログラムを実行しないようにする。このことを、相互排除という。

相互排除について詳しくは、2学期のオペレーティング・システムで扱う。 特に、扱うべき資源が複数になった場合には、デッドロックに気をつける。

Unix では、ファイルに対するロックを扱うために、次のような機能を提供し ている。

flock() システムコール
ファイル単位のロック
fcntl() システムコールと F_GETLK, F_SETLK, F_SETLKW
レコード単位のロック
lockf() ライブラリ関数
レコード単位のロック
CGI のプログラムでなくても、ロックをしなければならないことがある。

◆セッション管理

CGI は、基本的には1回の操作で完結する。買い物かごを作るには、複数の CGI のプログラムの間で一連の操作を1つの連続したもの(セッション)とし て扱う必要がある。

セッション管理には、次のような方法がある。

セッション管理にクッキーを使う方法は、正しく使えば比較的安全である。し かし、クッキーはセッションを越えて有効なもの、異なるサイトに送られるも のも定義できる。また、クッキーにパスワードを埋め込むという幼稚で危険な 実装も時々見受けられる。

セッションは、認証とは別である。同じ人が複数のセッションを実行すること もある。

◆WWW クッキー

サーバは、その情報を利用して、適切なページ(たとえば前回最後に訪れたペー ジ)を表示させるようにすることができる。

◆WWW cookieとプライバシ

現在の Cookie の実現では、利用者のプライバシーを犯す危険性が高いという 問題が指摘されている。

普通のWWWサーバでは、要求を送ってきたコンピュータのIPアドレスを記 録しているので、コンピュータ単位でのアクセス状況を記録することはできる が、個人を特定することはできない。

クッキーを利用することにより、コンピュータではなくどの個人がアクセスし てきたかを記録することができる。

クッキーから電子メールのアドレスや氏名まで調べることはできない。 しかし、インターネットをサーフしている間にどこかでそれを打ち込んだが最 後、クッキーと電子メール・アドレスや氏名との対応が記録されてしまう危険 性がある。

参考 Netscape社によるWWWにおけるクッキー実現の案
http://www.netscape.com/newsref/std/cookie_spec.html

■SSI (Server Side Include)

SSI (Server Side Include) は、CGI よりも手軽にHTML の途中に、他 のファイルの内容やプログラムの実行結果を埋め込むための仕組。

SSI が使えるシステムでは、file.html の変わりに file.shtml とすると、SSI の機能が働くようになる。

例1: ファイルの内容の読込み

<!--#include file="filename"-->

例2: プログラム/bin/calの実行結果の埋め込み
<PRE>
<!--#exec cmd="/bin/cal"-->
</PRE>

例3:ファイル file.shtml の更新時刻の表示
最終に変更されたのは、
<!--#config timefmt="%Y/%m/%d %H:%M:%S"-->
<!--#flastmod file="file.shtml"-->
です。

timefmt の形式は、 ライブラリ関数 strftime() と同じ。 その他に、ファイルの大きさを得るための#fsize file="...", CGI を実行する #cgi vmf="...",環境変数を表示する #echo var="...",がある。

◆SSIとセキュリティ

SSI を使う時にも、CGI と同様に、 セキュリティに気をつける。 特に、 `#include'`#exec' で、大事なファイルが盗まれないように、細心の注意を払ながら利用する必要 がる。

■Javaアプレット

Java アプレット(applet) とは、 Javaというプログラム言語 で書かれたプログラムの一種。

画面に何かを表示する機能があり、WWWブラウザ上で動く。 Javaアプレットを使うと、アニメーションを作ったり、 CGI を使わずにユーザとの対話できる。

CGI では、プログラムは、WWWサーバが走っているコンピュータで動くが、 Javaアプレットは、プログラムはブラウザの上で動く。

◆Javaアプレットのタグ

Javaアプレットは、HTML からみると、 インライン・イメージと似ている。 Javaアプレットのためのタグは、 <APPLET></APPLET>

HTML記述:

<APPLET CODE="rctext.class" WIDTH="500" HEIGHT="50">
<PARAM NAME="message" VALUE="hello,world">
与えたメッセージが動き回るJavaアプレット。
</APPLET>

表示例:

与えたメッセージが動き回るJavaアプレット。

CODE属性で、Javaアプレットを保存している ファイルのURL(ただし、同一ホスト内)、 WIDTH属性で、幅、 HEIGHT属性で、高さを指定する。 <PARAM>タグを使えば、アプレットに オプションを渡すことがでる。

<APPLET></APPLET>の間には、 Java Applet に対応していないブラウザのためのテキストを書く。

ソース・プログラム

■JavaScript

JavaScript は、WWW ブラウザ上で動作するスクリプト言語である。

JavaScript は、文法が少し Java 言語に似ているが、Java とはまったく別の 言語である。WWW ページの中の Javaアプレットは、 「実行可能なインライン・イメージ」に似ている。これに対して JavaScript の記述は、「実行可能なインライン・テキスト」 に似ている。

JavaScrip のプログラムの例:

<SCRIPT LANGUAGE="JavaScript">
<!--
for( i=0 ; i<10; i++ )
  document.writeln("<P>hello,world</P>");
//-->
</SCRIPT>

これは、<P>hello,world</P> を 10 回書いた のと同じ効果がある。<!--//->は、JavaScriptを知らないブラウザにはコメント として扱われる。関数定義などは、ヘッダ部分 <HEAD></HEAD>に書くという方法もよく使われる。

◆JavaScript の言語としての特徴

◆JavaScript によるブラウザの制御

JavaScript の記述は、CGI と似ているところもあるが、JavaScript でないと できないものに、ブラウザの制御がある。たとえば、次の例では、ブラウザの (戻る)ボタンと同じ動きをさせることができる。

<A HREF="javascript:history.back();">戻る</A>

次の例は、<FORM></FORM>からパラメタを受け取るも のである。

<SCRIPT LANGUAGE="JavaScript">
function go(s,h,p)
{
	location.href = s.value + "://" + h.value + p.value ;
}
</SCRIPT>

<FORM NAME="form1">
<INPUT NAME="scheme" TYPE="text" VALUE="http"><BR>
<INPUT NAME="host"   TYPE="text"><BR>
<INPUT NAME="path"   TYPE="text" VALUE="/"><BR>
<INPUT TYPE="button" VALUE="go" onClick="go(form1.scheme,form1.host,form1.path)">
</FORM>

onClick 属性の値は、クリックした時に評 価される式であり、関数 go() が呼び出されている。引 数は、<FORM> の値である。関数 go() の中では、 .value フィールドから値が読み出されている。 location.href に代入することで そのページを表示させることができる。

この例では、<INPUT type="text"><INPUT type="button">が使われている。その他に、 <FORM>では、<INPUT TYPE="radio"><INPUT TYPE="checkbox"><SELECT>が使える。イベントとしては、 onClick が主に使われる。その他に、 onFocus が使われることもある。

JavaScript では、document.open() で新しく HTML の ドキュメントを生成して、それをブラウザに表示させることもできる。

■アクセス制御

WWWは、もともと大勢の人にメッセージを伝えるための仕組である。 あるページは、ある特定の人(同じ研究室・部署の人)だけに しか見えないように設定することもできる。

アクセス制御(access control) ユーザ(アクセスの主体) が、ファイルやWWWページなどの資源をアクセスする時、どんな アクセスの仕方なら正しいということを定義して、それがきちんと守られてい ることをということを保証することである。

WWW ページのアクセス制御は、次のような情報がよく使われる。

アクセス制御は、基本的には、WWWサーバの管理者の仕事である。 WWWサーバ (httpd) の設定ファイル ( /usr/local/etc/httpd/conf/httpd.conf, /var/www/conf/httpd.conf など ) に記述される。

個人のページについては、 .htaccess というファイルを作れば、個人で変更できるようになっている場合がある。

◆IPアドレスによるアクセス制御

筑波大学の学内にだけアクセス可能なページ
.htaccess:
----------------------------------------------------------------------
order deny,allow
deny from all
allow from 130.158.0.0/16 192.50.17.0/24
----------------------------------------------------------------------

◆パスワードが設定されたページのアクセス

例:パスワード・ファイル /home/lab/Denjo/yas/etc/passwd-doc1 に登録されているユーザだけが、 .htaccess があるディレクトリ以下にあるファイルをアクセスできる。

.htaccess:
----------------------------------------------------------------------
AuthType Basic
AuthName "Members Only"
AuthUserFile /home/lab/Denjo/yas/etc/passwd-doc1
require valid-user
----------------------------------------------------------------------
このパスワード・ファイルは、サーバ(www,www2)上で htpasswd というプログ ラムで作る。
----------------------------------------------------------------------
% ls /home/lab/Denjo/yas/etc/passwd-doc1 [←]
ls: /home/lab/Denjo/yas/etc/passwd-doc1: No such file or directory
% htpasswd -c /home/lab/Denjo/yas/etc/passwd-doc1 user1 [←]
Adding password for user1.
New password:user1のパスワードを打ち込む[←]
Re-type new password:user1のパスワードを打ち込む[←]
% htpasswd /home/lab/Denjo/yas/etc/passwd-doc1 user2 [←]
Adding user user2
New password:user2のパスワードを打ち込む[←]
Re-type new password:user2のパスワードを打ち込む[←]
% cat /home/lab/Denjo/yas/etc/passwd-doc1  [←]
user1:1fjr1tHIgoG7U
user2:qXaeA9Zge7Yqc
% []
----------------------------------------------------------------------
一番最初は、-c オプション付で実行する。passwd コマ ンドと同様に、打ち込んだパスワードは、画面には表示されず、また、確 認のために2回打つ必要がある。

htpasswd コマンドの結果、次のようなファイルが作られる。

----------------------------------------------------------------------
user1:1fjr1tHIgoG7U
user2:qXaeA9Zge7Yqc
----------------------------------------------------------------------
ユーザ名と暗号化されたパスワードから構成される。 このファイルは、~/public_html 以外の場所に置くと GET で盗まれることは ない。

このアクセス制御が有効なファイルをクライアントが GET したとする。


GET /dir1/file1.html HTTP/1.1[←]
[←]
すると、サーバから次のようなエラーが返される。

HTTP/1.1 401 Authorization Required
WWW-Authenticate: Basic realm="Members Only"
Content-Type: text/html


401 Authorization Required

...

.htaccessAuthTypeAuthName の内容が、HTTP の応答の WWW-Authenticate: に現れる。

このページをアクセスするには、クライアント(WWWブラウザ)は、ウインドウ を開いてユーザにユーザ名とパスワードを要求する。そして、次のような GET 要求をもう一度送る。


GET /dir1/file1.html HTTP/1.1[←]
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==[←]
[←]
Authorization: には、ユーザが打ち込んだユーザ名とパスワードが Base64 (64進数)で符合化されて含まれている。 これを受け取ったサーバは、 AuthUserFile で指定されたファイルを開いて、ユーザ名とパスワー ドパスワードを照合して、正しければアクセスを許する。

Base64 は、「暗号ではない」ので、簡単に元にもどせる。このAuthType Basic を使うアクセス制御の方法では、パスワードがそのままネットワークを 流れてしまうので、盗聴に弱い。

■練習問題と課題

★練習問題 76 httpd-log,WWW サーバ上のデータの観察

htttp://www.coins.tsukuba.ac.jp/ のページは、サーバ (www) 上の /var/www/htdocs/ 以下にある。

WWW ブラウザや telnet を使って得た結果と /var/www/htdocs/ 以下のファイ ルが同一であることをwww に ssh でログインして確認しなさい。

同様に www2 についても調べなさい。

★練習問題 77 httpd-log,アクセスログの観察

アクセスログは、サーバ(www,www2)上の /var/log/httpd/access_log というファイルにためられる。 これを、次のようにして観察しなさい。
  1. ssh で www または www2 にログインする。
  2. 次のように tail コマンドを実行する。tail コマンドは、 -f オプションを付けて実行すると終了しないので、 最後に ^C キーで終了させる。
    
    % tail -f /var/log/httpd/access_log [←]
    
  3. WWW ブラウザ(Netscape など)を実行して、 http://www.coins.tsukuba.ac.jp/や自分の WWW ページを開く。
  4. 「再読込み」を行う。
  5. データが流れていく時には、適当な所で ^C で tail コマンドを終了させる。
  6. 自分が使っているコンピュータのホスト名が表示されるかを見る。端末 のスクロール機能を使う。
同様に、/var/log/httpd/error_log についても調べなさい。

★練習問題 78 引数の表示

次の CGI プログラムに対して、フォームを定義し、何かデータを送ってみなさい。 http://www2.coins.tsukuba.ac.jp/~yas/syspro/cgi-examples/cgi-printarg.cgi

★練習問題 79 CGIによる足算

2つのデータを受け取り、その結果を加えた数を表示するようなCGI のプログ ラムを作成しなさい。

ヒント:2つの引数をフォームでクライアントからサーバへ送る。たとえば、 "arg1=10&arg2=20"のように送るようなフォームを作成する。 cgi-printarg.cで、余計な表示をなくす。 make_qcqv() で分解した後、"arg1=" や "arg2=" があるパラメタを検索する。 そのなかで、atoi() 等で数にもどし、足算を行う。そして、結果を printf() の %d などで表示する。

余裕があれば、メニューやラジオボタンなどで足算以外の計算もできるように しなさい。

★練習問題 80 カレンダの表示

fork() と execve() を使って、/usr/bin/cal を実行し、WWW ブラウザに今月 のカレンダを実行するプログラムを作りなさい。cal コマンドは、引数を付け ずに実行すると、今月のカレンダーを表示する。
% cal [←]
      June 2002
Su Mo Tu We Th Fr Sa 
                   1
 2  3  4  5  6  7  8
 9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
% []

ヒント: proc-create.c をCGI のプログラムに改造する。2002年5月でもなく、2002年6月でもなく、そ の CGI プログラムを実行した月のカレンダーを表示しなさい。固定した表示 するだけなら、CGI の意味はない。余計な表示は省略すること。

余裕があれば、今月のカレンダーではなく、任意の年・月のカレンダーを表示 しなさい。

この課題では、system() や popen() を用いてはならない。この課題に限らず、 もし、遠隔から任意のコマンドを実行可能なような CGI プログラムを作成し た時には、単位を出さない。

★練習問題 81 CGIプログラムの実行権限

CGI プログラムが、どのユーザの権限で動作しているかを調べるプログラムをつくりなさい。

ヒント:プロセスのユーザ属性を表示するプログラム を、CGI から実行させる。あるいは、id コマンドを実行させる。

★練習問題 82 IPアドレスによるアクセス制御

特定のホスト(たとえば、adonis10)からだけしかアクセスできないような .htaccess を定義しなさい。

★練習問題 83 パスワードによるアクセス制御

ある特定のユーザ名とパスワードを知っている人だけからしか アクセスできないような .htaccess を定義しなさい。 そして、htpasswd コマンドを使って、パスワードを設定しなさい。

注意:ユーザ名は、長くてもよい。Unix や Windows のユーザ名とは別のもの でよい。

注意:ここで設定したパスワードは、ネットワーク上を暗号化されずに流れる。 けっして Unix や Windows のログインのパスワードと同じものを使ってはな らない。