筑波大学 システム情報工学研究科
コンピュータサイエンス専攻, 電子・情報工学系
新城 靖
<yas@is.tsukuba.ac.jp>
このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2010/No7.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2010/
http://www.coins.tsukuba.ac.jp/~yas/
TAや教員に聞くには、練習がいる。
$ cc wcat.c -o wcat
$ ./wcat
a.out という名前で実行形式、mv コマンドで名前を変えてもよい。
$ cc wcat.c
$ mv a.out wcat
$ ./wcat
make コマンドには、デフォルトのルールでソースコードfile.c から実行形式
file を作成するルールが含まれている。Makefile を書かなくても使える。
$ ls wcat.c
wcat.c
$ make wcat
cc wcat.c -o wcat
$ ./wcat
1:
2: /*
3: echo-server-nofork-fdopen.c -- 受け取った文字列をそのまま返すサーバ(fork無し版)
4: ~yas/syspro/ipc/echo-server-nofork-fdopen.c
5: Created on 2004/05/09 19:08:47
6: */
7: #include <stdio.h>
8: #include <stdlib.h> /* exit() */
9: #include <sys/types.h> /* socket(), wait4() */
10: #include <sys/socket.h> /* socket() */
11: #include <netinet/in.h> /* struct sockaddr_in */
12: #include <sys/resource.h> /* wait4() */
13: #include <sys/wait.h> /* wait4() */
14: #include <netdb.h> /* getnameinfo() */
15: #include <string.h> /* strlen() */
16:
17: extern void echo_server( int portno, int ip_version );
18: extern void echo_receive_request_and_send_reply( int com );
19: extern void print_my_host_port( int portno );
20: extern void tcp_peeraddr_print( int com );
21: extern void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
22: extern tcp_acc_port( int portno, int pf );
23: extern int fdopen_sock( int sock, FILE **inp, FILE **outp );
24:
25: main( int argc, char *argv[] )
26: {
27: int portno, ip_version;
28: if( !(argc == 2 || argc==3) ) {
29: fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] );
30: exit( 1 );
31: }
32: portno = strtol( argv[1],0,10 );
33: if( argc == 3 )
34: ip_version = strtol( argv[2],0,10 );
35: else
36: ip_version = 4; /* IPv4 by default */
37: echo_server( portno, ip_version );
38: }
39:
main() は、引数として、ポート番号、IPのバージョン(4か6)を取る。
文字列で受け取ったポート番号を strtol() で int へ変換している。
IPのバージョンが省略されたら IPv4 とする。
最後に、echo_server() を呼ぶ。
40: void
41: echo_server( int portno, int ip_version )
42: {
43: int acc,com ;
44: acc = tcp_acc_port( portno, ip_version );
45: if( acc<0 )
46: exit( -1 );
47: print_my_host_port( portno );
48: while( 1 )
49: {
50: if( (com = accept( acc,0,0 )) < 0 )
51: {
52: perror("accept");
53: exit( -1 );
54: }
55: tcp_peeraddr_print( com );
56: echo_receive_request_and_send_reply( com );
57: }
58: }
tcp_acc_port() は、引数で与えられたポート番号を使って接続要求受付用ポー トを作成し、そのファイル記述子(ソケット)を返す。 このファイル記述子は、クライアント側とは異なり、そまままでは通信に 用いることはできない。 print_my_host_port() は、telnet で接続する時のヒントを表示する。
サーバのプログラムの特徴は、内部に無限ループを持っていることである。 サーバは、普通の状態では、終了しない。
accept() は、接続要求を待つシステムコールである。クライアントから接続 が来るまで、システムコールを実行したまま止まっているように見える。接続 要求が届くと、TCP/IP通信路の開設され、通信用ポートのファイル記述子が返 される。このファイル記述子は、クライアント側と同様に 標準入出力(0,1,2)や open() システム・コールの結果と同じもので、 ファイルに対する write() システムコールや read() システムコールの第一引数とし て使うことができる。つまり、write() システムコールを使うと、ネットワー クに対してデータを送り出すことができ、read() システムコールを使うとネッ トワークからデータを受け取ることができる。最後に不要になったら close() で解放する。
tcp_peeraddr_print() を呼び出し、通信相手(peer)のIPアドレスとポート番号 を表示する。
最後に、echo_receive_request_and_send_reply() を呼び出し、その通信相手 についての処理を行う。
59:
60: #define BUFFERSIZE 1024
61:
62: void
63: echo_receive_request_and_send_reply( int com )
64: {
65: char line[BUFFERSIZE] ;
66: int rcount ;
67: int wcount ;
68: FILE *in, *out ;
69:
70: if( fdopen_sock(com,&in,&out) < 0 )
71: {
72: fprintf(stderr,"fdooen()\n");
73: exit( 1 );
74: }
75: while( fgets(line,BUFFERSIZE,in) )
76: {
77: rcount = strlen( line );
78: printf("[%d] received (fd==%d) %d bytes, [%s]\n",getpid(),com,rcount,line );
79: fflush( stdout );
80: fprintf(out,"%s",line );
81: }
82: printf("[%d] connection (fd==%d) closed.\n",getpid(),com );
83: fclose( in );
84: fclose( out );
85: }
86:
この echo_receive_request_and_send_reply() は、特定のクライアント専用の
echo サービスを提供する。クライアントからの要求が続く限り、動作する。
このプログラムでは、 クライアント側 と同様に fdopen_sock() を使って、通信可能なファイル記 述子 com から2つの FILE * を作成している。1つは、入力用、1つは出力 用である。その結果、 高水準入出力ライブラリ を使って通信が行えるようになっている。fprintf() で出力用の FILE * に書 き込むと、ネットワークに対してデータが送り出される。入力用の FILE * に fgets() を行うと、ネットワークからデータを受け取ることができる。
クライアントからの要求は、fgets() で読込んでいる。それを、サーバ側の端 末に printf() で表示している。fflush() は、printf() の内部のバッファ (stdoutのバッファ)に溜っているデータを書き出すものである。
クライアントには、fprintf() で結果を送り返している。こちらは、 fdopen_sock() でバッファリングを抑止しているのでfflush() は不要になって いる。
サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。
$ ls -l echo-server-nofork-fdopen.c
ls: echo-server-nofork-fdopen.c: No such file or directory
# ファイルが存在しないことの確認
$ cp ~yas/syspro/ipc/echo-server-nofork-fdopen.c .
$ ls -l echo-server-nofork-fdopen.c
-rw-r--r-- 1 yas prof 5165 5 31 20:41 echo-server-nofork-fdopen.c
$ make echo-server-nofork-fdopen
cc echo-server-nofork-fdopen.c -o echo-server-nofork-fdopen
$ ./echo-server-nofork-fdopen
Usage: ./echo-server-nofork-fdopen portno {ipversion}
$ ./echo-server-nofork-fdopen 1231
run telnet cosmos10(v6) 1231
[3058] connection (fd==4) from 130.158.86.150:53256
[3058] received (fd==4) 5 bytes, [123
]
[3058] received (fd==4) 5 bytes, [456
]
[3058] received (fd==4) 5 bytes, [789
]
[3058] connection (fd==4) closed.
^C
$
クライアント側
$ telnet cosmos10 1231
Trying 130.158.86.150...
Connected to cosmos10.coins.tsukuba.ac.jp.
Escape character is '^]'.
123
123
456
456
789
789
^]
telnet> quit
Connection closed.
$
複数のクライアントに対してサービスの同時に提供するには次のような方法が ある。
GET /index.html HTTP/1.0←↓
←↓
HTTPの要求は、要求行(request line)から始まり、最後は、空行で終わる。実
際の HTTP の要求は、次のように、要求行と空行の間にオプションが付く。
GET /index.html HTTP/1.0←↓
Host: www.coins.tsukuba.ac.jp←↓
User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7←↓
Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5←↓
Accept-Language: ja-jp←↓
Accept-Encoding: gzip, deflate←↓
Connection: keep-alive←↓
←↓
オプションは全部無視しても、最低限の HTTP サーバを作成することはできる。
1:
2: /*
3: http-server-.c -- 常に同じ内容を返す HTTP サーバ(forkなし版)
4: ~yas/syspro/ipc/http-server.c
5: Created on: 2002/06/03 00:48:43
6: */
7: #include <stdio.h>
8: #include <stdlib.h> /* exit() */
9: #include <sys/types.h> /* socket(), wait4() */
10: #include <sys/socket.h> /* socket() */
11: #include <netinet/in.h> /* struct sockaddr_in */
12: #include <sys/resource.h> /* wait4() */
13: #include <sys/wait.h> /* wait4() */
14: #include <netdb.h> /* getnameinfo() */
15: #include <string.h> /* strlen() */
16:
17: extern void http_server( int portno, int ip_version );
18: extern void http_receive_request_and_send_reply( int com );
19: extern int http_receive_request( FILE *in );
20: extern int http_send_reply( FILE *out );
21: extern int http_send_reply_bad_request( FILE *out );
22: extern void print_my_host_port_http( int portno );
23: extern void tcp_peeraddr_print( int com );
24: extern void sockaddr_print( struct sockaddr *addrp, socklen_t addr_len );
25: extern int tcp_acc_port( int portno, int ip_version );
26: extern int fdopen_sock( int sock, FILE **inp, FILE **outp );
27: extern char *fgets_chop(char *str, int size, FILE *stream);
28:
29: main( int argc, char *argv[] )
30: {
31: int portno, ip_version;
32: if( !(argc == 2 || argc==3) ) {
33: fprintf(stderr,"Usage: %s portno {ipversion}\n",argv[0] );
34: exit( 1 );
35: }
36: portno = strtol( argv[1],0,10 );
37: if( argc == 3 )
38: ip_version = strtol( argv[2],0,10 );
39: else
40: ip_version = 4; /* IPv4 by default */
41: http_server( portno,ip_version );
42: }
43:
main() は、引数として、ポート番号を取る。
文字列で受け取ったポート番号を strtol() で int へ変換している。
最後に、http_server() を呼ぶ。
echo-server-nofork-fdopen.cのmain()とほとんど同じ。
44: void
45: http_server( int portno, int ip_version )
46: {
47: int acc,com ;
48: acc = tcp_acc_port( portno, ip_version );
49: if( acc<0 )
50: exit( -1 );
51: print_my_host_port_http( portno );
52: while( 1 )
53: {
54: if( (com = accept( acc,0,0 )) < 0 )
55: {
56: perror("accept");
57: exit( -1 );
58: }
59: tcp_peeraddr_print( com );
60: http_receive_request_and_send_reply( com );
61: }
62: }
63:
echo-server-nofork-fdopen.cのecho_server()とほとんど同じ。違いは、次の所だけ。
64: #define BUFFERSIZE 1024
65:
66: void
67: http_receive_request_and_send_reply( int com )
68: {
69: FILE *in, *out ;
70: int err ;
71: if( fdopen_sock(com,&in,&out) < 0 )
72: {
73: fprintf(stderr,"fdooen()\n");
74: exit( 1 );
75: }
76: if( http_receive_request( in ) )
77: {
78: http_send_reply( out );
79: }
80: else
81: {
82: http_send_reply_bad_request( out );
83: }
84: fclose( in );
85: fclose( out );
86: }
87:
この関数では、
echo-server-nofork-fdopen.cのecho_receive_request_and_send_reply() と同様に、
fdopen_sock() を使って、通信可能なファイル記
述子 com から2つの FILE * を作成している。
http_receive_request() を呼び出し、HTTP で要求を受け取っている。 もしエラーがなければ、http_send_reply() を呼び出し、HTTP で OK の応答を送信する。 エラーが有れば、http_send_reply_bad_request() を呼び出し、エラーの 応答を送信する。
88: int
89: http_receive_request( FILE *in )
90: {
91: char requestline[BUFFERSIZE] ;
92: char rheader[BUFFERSIZE] ;
93:
94: if( fgets_chop(requestline,BUFFERSIZE,in) <= 0 )
95: {
96: printf("No request line.\n");
97: return( 0 );
98: }
99: printf("requestline is [%s]\n",requestline );
100: if( strchr(requestline,'<') ||
101: strstr(requestline,"..") )
102: {
103: printf("Dangerous request line found. exit.\n");
104: return( 0 );
105: }
106:
107: while( fgets_chop(rheader,BUFFERSIZE,in) )
108: {
109: if( strcmp(rheader,"") == 0 )
110: break;
111: printf("Ignored: %s\n",rheader );
112: }
113: return( 1 );
114: }
115:
http_receive_request() は、まず、HTTPの要求の最初の行(request line)を
fgets_chop() で読み込む。fgets_chop() は、fgets() と同じだが、
行末の改行 \n や \r を取り除く。
要求行の内容に対して簡単なエラーチェックをしている。次のような パタンが含まれるとエラーにしている。
< が現れる
".." が現れる
最初の要求行に続き、オプションの行を読み出している。 オプションの最後は、空行で終わる。 空行が来るまで、オプションの行を読み出す。
このプログラムでは、オプションを一切使用していない。しかし、HTTP の要求 で送られてきた要求を全て読み出している。プロトコル上、必ず読み出す必要 がある。
116: int
117: http_send_reply( FILE *out )
118: {
119: fprintf(out,"HTTP/1.0 200 OK\r\nContent-Type: text/html\r\n\r\n");
120: fprintf(out,"<html><head></head><body>hello.</body></html>\n");
121: }
122:
http_send_reply() は、HTTPのヘッダと本体を出力している。 ヘッダは、status line に Content-Typeが続く。 空行以降が本体である。
本体では、HTMLの文書を出力している。
123: int
124: http_send_reply_bad_request( FILE *out )
125: {
126: fprintf(out,"HTTP/1.0 400 Bad Request\r\nContent-Type: text/html\r\n\r\n");
127: fprintf(out,"<html><head></head><body>400 Bad Request</body></html>\n");
128: }
129:
http_send_reply_bad_request() は、HTTPのヘッダと本体を出力している。 ヘッダは、status line (エラー)に Content-Typeが続く。 空行以降が本体である。
本体では、HTMLの文書を出力している。
\n や
\r を削除する。
サーバ側。 サーバは、終了しないので、最後に、^C を押して、割り込みを掛け て終了させる。
注意:全員がポート番号 1231 を使うとプログラムが動かないことがある。
$ cp ~yas/syspro/ipc/http-server.c .
$ make http-server
cc http-server.c -o http-server
$ ./http-server
Usage: ./http-server portno {ipversion}
$ ./http-server 1231
open http://cosmos10(v6):1231/index.html
[3110] connection (fd==4) from 130.158.81.193:57893
requestline is [GET /index.html HTTP/1.1]
Ignored: Host: cosmos10.coins.tsukuba.ac.jp:1231
Ignored: User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7
Ignored: Accept: application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Ignored: Accept-Language: ja-jp
Ignored: Accept-Encoding: gzip, deflate
Ignored: Connection: keep-alive
[3110] connection (fd==4) from 130.158.81.193:57894
requestline is [GET /favicon.ico HTTP/1.1]
Ignored: Host: cosmos10.coins.tsukuba.ac.jp:1231
Ignored: User-Agent: Mozilla/5.0 (Macintosh; U; PPC Mac OS X 10_5_8; ja-jp) AppleWebKit/531.22.7 (KHTML, like Gecko) Version/4.0.5 Safari/531.22.7
Ignored: Referer: http://cosmos10.coins.tsukuba.ac.jp:1231/index.html
Ignored: Accept: */*
Ignored: Accept-Language: ja-jp
Ignored: Accept-Encoding: gzip, deflate
Ignored: Connection: keep-alive
^C
$
クライアント側
$ open http://cosmos10:1231/index.html
Safari や Firefox で "http://cosmos10:1231/index.html" を開く。
requestline is [...])
Ignored: ...)
$ cp ~yas/syspro/ipc/http-response.c .
$ emacs http-response.c
$ cc http-response.c -o http-response
$ ./http-response /index.html
HTTP/1.0 200 OK
Content-Type: text/html
<HTML>
中略
</HTML>
$
この課題では、セキュリティに気をつけなさい。必ず次の条件を満たすように
しなさい。
~/public_html)以下のファイルし
かアクセスできないようにすること。それには、ファイルを開く(open(),
fopen())時に、頭に
環境変数
HOME の値と "/public_html" を
snprintf() 等
で付加する方法がある。環境変数は、
ライブラリ関数
getenv()
を使うと入手できる。
char *home;
home = getenv("HOME");
...
他の方法としては、プログラムの中に定数として埋め込む方法が考えられる。
なお、「~」 は、open() システム・
コールや fopen() ライブラリ関数では使えない。「~」 は、シェルや
emacs が独自にホーム・ディレクトリを調べて置き換えている。
"HTTP/1.0 200 OK<以下省略>" を、ファイルが存在しなけれ
ば、"HTTP/1.0 404 Not Found<以下省略>" を返す。
| Content-Type: | 拡張子 |
|---|---|
| text/html | .html |
| text/plain | .txt |
Content-Type: とし
て、application/octet-stream を返してよい。
"HTTP/1.0 200 OK\r\n" の行を fprintf() で stdout に出力する。
Content-Type: の行を fprintf() で stdout に出力する。
拡張子の取り出しには、strrchr()、比較には、strcmp() を使う方法がある。
"HTTP/1.0 404 Not Found<以下省略>" のエラー
メッセージを出力する。
なお、この課題は、HTTP の応答を画面(stdout)に出力するが、ネットワーク通 信を一切行わない。
練習問題(703) を拡張して、 バイナリ・ファイルを扱えるようにしなさい。 次の型(Content-Type:)を扱えるようにしなさい。
| Content-Type: | 拡張子 |
|---|---|
| text/html | .html |
| text/plain | .txt |
| image/gif | .gif |
| image/jpeg | .jpeg |
| image/png | .png |
$ cp ~yas/syspro/ipc/http-request-analyze.c .
$ emacs http-request-analyze.c
$ cc http-request-analyze.c -o http-request-analyze
$ cat > reqest.data
GET /index.html HTTP/1.0
Accept: image/png,*/*;q=0.5
Accept-Language: ja,en;q=0.7,en-us;q=0.3
^D
$ ./http-request-analyze < reqest.data
/index.html
$
このプログラムは、次のような動作を行う。
なお、この課題は、HTTP の要求を標準入力(stdin)から読むが、ネットワーク 通信を一切行わない。
この課題を行う場合、次のファイルに含まれる関数を利用してもよい。
~yas/syspro/string/string-split.c
int string_split( char *str, char del, int *countp, char ***vecp )
void free_string_vector( int count, char **vec )
string_split() で得た vec のメモリを解放する。
int count;
char **vec;
int i ;
str = "GET /index.html HTTP/1.0";
if( string_split( str, ' ', &count, &vec ) < 0 )
{
perror("string_split-malloc");
exit( 1 );
}
for( i=0 ; i< count; i++ )
printf("%d: %s\n", i, vec[i] );
free_string_vector( count, vec );
まず、 練習問題(704) を実施しなさい。 次に、 練習問題(705) を実施しなさい。 そして、その時作成した関数を用いて、HTTP サーバを作成しなさい。 この課題を行う場合、http-server.c に含まれる関数を利用してもよい。
実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。
GET /?month=6&year=2010 HTTP/1.0
カレンダの出力については、cal コマンドを実行した結果を用いても良い。こ
れに対して、HTTP で結果を返す。Content-Type: としては、text/plain に
するか、text/html で <PRE></PRE>を使う方法が
考えられる。その他の手法でもよい。
このサーバを実行するための HTML の form を用意しなさい。
実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。
.cgi のプログラムを CGI と見なし、プログラムを実行し、そ
の結果をクライアントに返しなさい。
実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。
クライアントから送られてくるデータを、コマンドの標準入力に接続して実行 するプログラムを作りなさい。たとえば、次のようなコマンドを実行すること を考える。
% ./spipe-last 1231 head -5
<クライアントから送られてきた文字列のうち、先頭の5行だけがサーバの画面に表示される>
%
spipe-last は、指定されたポート番号で tcp_acc_port() を行う。
クライアントからの接続要求を受け付けた後、不要なポートを閉じ、標準出力を切
り替えて、コマンドラインから指定されたコマンドを実行する。
すなわち、dup(), dup2(), close() などで、標準出力を切り替えて、
execve() などで、プログラムを実行する。
コマンドを実行したらそのまま終了する。
クライアントとしては、telnet を用いてもよい。 または、以下の spipe-first を用いることもできる。
この課題では、fork() を行わないことを推奨する。 TCP/IP なら本来双方向で使えるが、この練習問題では単方向だけを 使うことで、パイプの機能を代替する。
また、fork() とパイプを使う実現方法もある。コマンドを実行するする時に fork() して実行し、その標準入力をパイプにして、TCP/IP で受け取ったデー タをパイプに書き込んでもよい。
% ./spipe-first hostname port ls -l
spipe-first は、指定されたホスト名とポート番号でtcp_connect() を行う。
不要なポートを閉じ、標準出力を切り替えた後、指定されたコマンドを実行す
る。すなわち、dup(), dup2(), close() などで、標準入出力を切り替えて、
execve() などで、プログラムを実行する。
コマンドを実行したらそのまま終了する。
この課題では、spipe-last と同様に、fork() を行わないことを推奨する。
% ./spipe-middle myport serverhost serverport sort +4
spipe-middle は、第2引数と第3引数で指定されたサーバにtcp_connect() で接
続要求を行う。
tcp_acc_port() により、第1引数で指定された
ポート番号で接続受付用ポートを作成する。
spipe-first から接続要求を受け取ると、
不要なポートを閉じ、標準入出力を切り替える。そして、第4引数以降で指定さ
れたコマンドを実行する。
finger サーバを作成しなさい。これは、受け取った文字列を引数にして、 finger コマンドを実行するとよい。そして、実行結果を、接続先に返すよう にするとよい。
finger コマンドのプロセスと finger サーバの間は、パイプで接続しなさい。 今までの例題で利用した方法を組み合わせることで実現することができるはず である(pipe(),fork(),dup(),close(),execve()など)。popen() ライブラリ 関数を利用してもよい。
finger コマンドのプロセスと finger サーバ間をパイプで結ぶ代わりに、 finger コマンドを exec する前に、配管工事をして、標準出力(1)をTCP/IPの ストリームに接続する方法もある。
受け取った文字列をそのまま popen() や system() などに渡すのは危険である。 というのも、これらの引数は、シェル(/bin/sh)が解釈するからである。 popen() や system() を使う場合には、 finger コ マンドが受付けるのに相応しいもの( isalpha() や isdigit() の並び)かどう かを検査しなさい。もし、シェルが解釈する特殊な文字列(|;<> など)が含まれていると、意図しないプログラムが実行させられることがある。
fork() や execve() を用いて直接 /usr/bin/finger を実行する方法はシェル を経由しないので、比較的安全である。
getpwnam() ライブラリ関数や utmp ファイルを用いて、finger コマンドと似たような 動きを実現してもよい。
HTTP proxy 作りなさい。次の機能を、1つ以上付けなさい。
上の tcp_peeraddr_print() は、IP アドレスを数字で表示していた。 これを、ホスト名で表示するようにしなさい。
実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。
実際の WWW ブラウザ (Firefoxなど)で、動作を確認しなさい。telnet だけで は、きちんと HTTP のプロトコルに従っているか確認できないので、不十分で ある。