C 言語で echo サーバを作ってみよう (1)

前へ << C 言語で HTTP クライアントを作ってみよう (2) C 言語で echo サーバを作ってみよう (2) >> 次へ

echo サーバ

次に、C 言語で echo サーバを作ってみましょう。 perl に慣れてしまった軟弱な体には、結構こたえまっせ。

echo サーバを作ってみよう (2) で作成した、最も基本的な echo サーバを C 言語に書き直したものが、 以下のソースです。

echo-server-1.c

    1: /*
    2:  * $Id: echo-server-1.c,v 1.6 2005/02/19 16:01:53 68user Exp $
    3:  *
    4:  * echo サーバサンプル
    5:  *
    6:  * written by 68user  http://X68000.q-e-d.net/~68user/
    7:  */
    8: 
    9: #include <stdio.h>
   10: #include <stdlib.h>
   11: #include <string.h>
   12: #include <netdb.h>
   13: #include <sys/types.h>
   14: #include <sys/socket.h>
   15: #include <sys/uio.h>
   16: #include <unistd.h>
   17: #include <sys/param.h>
   18: #include <netinet/in.h>
   19: #include <arpa/inet.h>
   20: 
   21: #define BUF_LEN 256             /* バッファのサイズ */
   22: 
   23: /* ソケット socket から1行読み込み、読み込んだ文字列を p に格納する。
   24:    改行コードを読み込む前にソケットから read できなくなった場合は、
   25:    その時点で呼び出し元に戻る。
   26: 
   27:    戻り値で読み込んだ文字数を返す。p は \0 でターミネートする。
   28:  */
   29: int read_line(int socket, char *p){
   30:     int len = 0;
   31:     while (1){
   32:         int ret;
   33:         ret = read(socket, p, 1);
   34:         if ( ret == -1 ){
   35:             perror("read");
   36:             exit(1);
   37:         } else if ( ret == 0 ){
   38:             break;
   39:         }
   40:         if ( *p == '\n' ){
   41:             p++;
   42:             len++;
   43:             break;
   44:         }
   45:         p++;
   46:         len++;
   47:     }
   48:     *p = '\0';
   49:     return len;
   50: }
   51: 
   52: 
   53: int main(int argc, char *argv[]){
   54:     int connected_socket, listening_socket;
   55:     struct sockaddr_in sin;
   56:     int len, ret;
   57:     int sock_optval = 1;
   58:     int port = 5000;
   59:                                 /* リスニングソケットを作成 */
   60:     listening_socket = socket(AF_INET, SOCK_STREAM, 0);
   61:     if ( listening_socket == -1 ){
   62:         perror("socket");
   63:         exit(1);
   64:     }
   65:                                 /* ソケットオプション設定 */
   66:     if ( setsockopt(listening_socket, SOL_SOCKET, SO_REUSEADDR,
   67:                     &sock_optval, sizeof(sock_optval)) == -1 ){
   68:         perror("setsockopt");
   69:         exit(1);
   70:     }
   71:                                 /* アドレスファミリ・ポート番号・IPアドレス設定 */
   72:     sin.sin_family = AF_INET;
   73:     sin.sin_port = htons(port);
   74:     sin.sin_addr.s_addr = htonl(INADDR_ANY);
   75: 
   76:                                 /* ソケットにアドレス(=名前)を割り付ける */
   77:     if ( bind(listening_socket, (struct sockaddr *)&sin, sizeof(sin)) < 0 ){
   78:         perror("bind");
   79:         exit(1);
   80:     }
   81:                                 /* ポートを見張るよう、OS に命令する */
   82:     ret = listen(listening_socket, SOMAXCONN);
   83:     if ( ret == -1 ){
   84:         perror("listen");
   85:         exit(1);
   86:     }
   87:     printf("ポート %d を見張ります。\n", port);
   88: 
   89:     while (1){
   90:         struct hostent *peer_host;
   91:         struct sockaddr_in peer_sin;
   92: 
   93:         len = sizeof(peer_sin);
   94:                                 /* コネクション受け付け */
   95:         connected_socket = accept(listening_socket, (struct sockaddr *)&peer_sin, &len);
   96:         if ( connected_socket == -1 ){
   97:             perror("accept");
   98:             exit(1);
   99:         }
  100:                                 /* 相手側のホスト・ポート情報を表示 */
  101:         peer_host = gethostbyaddr((char *)&peer_sin.sin_addr.s_addr,
  102:                                   sizeof(peer_sin.sin_addr), AF_INET);
  103:         if ( peer_host == NULL ){
  104:             printf("gethostbyname failed\n");
  105:             exit(1);
  106:         }
  107: 
  108:         printf("接続: %s [%s] ポート %d\n",
  109:                peer_host->h_name,
  110:                inet_ntoa(peer_sin.sin_addr),
  111:                ntohs(peer_sin.sin_port)
  112:                );
  113: 
  114:         while (1){
  115:             int read_size;
  116:             char buf[BUF_LEN];
  117:                                 /* 1行読み込む */
  118:             read_size = read_line(connected_socket, buf);
  119:             if ( read_size == 0 ) break;
  120: 
  121:             printf("メッセージ: %s", buf);
  122:                                 /* クライアントに文字列をそのまま返す */
  123:             write(connected_socket, buf, strlen(buf));
  124:         }
  125: 
  126:         printf("接続が切れました。引き続きポート %d を見張ります。\n", port);
  127:         ret = close(connected_socket);
  128:         if ( ret == -1 ){
  129:             perror("close");
  130:             exit(1);
  131:         }
  132:     }
  133:     ret = close(listening_socket);
  134:     if ( ret == -1 ){
  135:         perror("close");
  136:         exit(1);
  137:     }
  138: 
  139:     return 0;
  140: }
perl 版は40行程度でしたが、C言語版は 100行以上になっています。 全体的な流れは perl 版と同じですが、注意してほしいのは 引数の型の違い・引数の数の違いですが、まぁ見ればわかるでしょう、 ということで説明はしません。

バグ

この echo サーバには重大な欠陥があります。 ソケットから 1行読み込む read_line 関数は、 バッファの大きさ (この例では 256 バイト) を越えてソケットから 読み込んでしまう可能性があります。 これは「バッファオーバーラン」と呼ばれる とても有名な問題です。

古来からいろんなプログラムがバッファオーバーランの餌食になってきました。 クラッカーがあるホストをクラックしようとするときの常套手段の一つに、 バッファオーバーランを起こし 任意のコード (パスワードを外部に流したりトロイの木馬を仕込んだり) を実行させる、 というものがあります。

ちなみに、(FreeBSD 2.2.7-RELEASE の) inetd 組み込みの echo サーバは

while ((i = read(s, buffer, sizeof(buffer))) > 0 &&
    write(s, buffer, i) > 0)
としています。 これだとバッファオーバーランは起こりませんが、 本当の意味での 一行単位での読み込みは実現できていません。 なぜなら、read は1行単位で読み込む関数ではないし、 そもそも1行分のデータが送られてきたという保証はないからです (現在1行の途中までのデータしか送られてきていないかもしれない)。

対策としては

  • inetd のように、一行単位での読み込みをあきらめる
  • 長すぎる行はエラーとする
  • リンクリストや realloc を使って無限長のバッファを実現する
がありますが、どうするかはプロトコルの性質を考えた上で 決めて下さい。
前へ << C 言語で HTTP クライアントを作ってみよう (2) C 言語で echo サーバを作ってみよう (2) >> 次へ

ご意見・ご指摘は Twitter: @68user までお願いします。