はじめに
今回作るやつは色々セキュリティホールがあるらしいので絶対に公開しないでください!あくまでローカル用です。
この本が読み終わったのでその集大成としてラズパイで HTTP サーバを作ってみました(Linux も C 言語もほぼ知らないので細かい部分はよくわかってないです。。。)。
ふつうのLinuxプログラミング 第2版 Linuxの仕組みから学べるgccプログラミングの王道 新品価格 |
必要な知識ざっくりまとめ
必要そうな知識のざっくりまとめです。
gcc やオプションに関しては前回の記事をどうぞ。
シグナル
シグナルは、ユーザ(端末)やカーネルがプロセス(動作中のプログラム)に何かを通知する目的で使われるやつです(Ctrl + C で停止するのもシグナルのおかげ)。
プロセスでシグナルを捕捉してシグナルを受けたときの処理を記述する(停止するとか)。
ソケット
ネットワーク通信と言っても結局扱うのはストリーム(バイト列の通り道)。ソケットとはストリームを接続できる口のこと。
サーバプロセス
サーバ側では接続を待っているプロセス(サーバプロセス)があり、サーバ(ホスト)とサーバプロセスを特定するのが IP アドレスとポート番号(IP アドレスでネットワーク上のマシンを特定してポート番号でサーバプロセスを特定する感じ。。。たぶん)。
デーモン
デーモンは制御端末を持たないプロセスのこと。通常のプロセスの場合、起動ユーザがログアウトするとプロセスも終了してしまうのでサーバの場合は困る。
HTTPリクエスト
リクエストの構成は下記(今回のやつはリクエストラインだけを読む)。
- リクエストライン
メソッドなど - HTTP ヘッダ
付加情報 - \r\n
ヘッダの終わり - エンティティボディ
POST とかで使う
ex.
1 2 3 4 5 |
"GET http://example.com//index.html HTTP/1.1\r\n" "Connection: close\r\n" "Accept: */*\r\n" "Host: example.com\r\n" "\r\n" |
HTTPレスポンス
レスポンスの構成は下記。
- ステータスライン
ステータスコードなど - HTTP ヘッダ
付加情報 - \r\n
ヘッダの終わり - エンティティボディ
HTML など
ex.
1 2 3 4 5 6 7 8 9 10 11 |
"HTTP/1.1 200 OK\r\n" "Date: Fri, 11 Aug 2021 10:00:00 GMT\r\n" "Server: Apache/2.4.10 (Debian)\r\n" "Connection: close\r\n" "Content-Length: 163\r\n" "Contetn-Type: text/html\r\n" "\r\n" (HTML) ・ ・ ・ |
参考
- 第12章 プロセスにかかわる API
- 第13章 シグナルにかかわる API
- 第15章 ネットワークプログラミングの基礎
- 第16章 HTTP サーバを作る
HTTPサーバ概要
今回作るのは単純な GET リクエストで受けた指定の HTML ファイルを表示するだけのやつです。
下記のようにコマンドで起動してブラウザで http://pi@raspberrypi.local:8082/index.html
と入力すると index.html を表示できるようにします。
1 |
$./httpd <パス> --port=8082 |
- 面倒なエラーが起きたら即 exit(1)
- HTTP1.0 のみサポート
- 設定ファイルは使わない(全部引数で渡す)
- GET リクエストのみ対応
- レスポンスは HTML のみ
めんどくさいのですべて text/html 指定にする。 - IPv4 のみ対応(色々めんどくさいらしいので)
- ソケット接続
- 接続を並列で扱う
リクエスト処理中にも他のリクエストを受け付けないといけないので並列処理する。 - デーモン化
これでプロセス起動者がログアウトしてもプロセスが停止しない。 - syslog を使ったロギング
エラー時に syslog へ記録する(デーモン化してるので標準エラー出力が使えない)。 - デバッグモード
標準入出力使ってエラーを表示する。 - ポートの指定
--port=n
のオプションで指定する。
参考
- 第16章 HTTP サーバを作る
- 第17章 HTTP サーバを本格化する
ソース
全体の処理の流れは下記。
- オプション解析
- デバッグモードでなければデーモン化
- ソケット作成して接続待機
- リクエストの文字列を解析してリクエストラインを取得
- リクエストラインを解析してメソッド、パス、プロトコルを取得
- 引数のパスとリクエストを解析したパスからファイルへのパス作成
- ファイルを読み込んでレスポンス出力
make コマンド使いたかったのでファイルを5つに分けてみました(コードは基本的に第17章参考に書いてます)。
- common.h
- main.c
- server.c
- request.c
- response.c
common.h
1 2 3 4 5 6 7 8 9 10 11 12 |
#ifndef common_h #define common_h #define SERVER_NAME "HTTPServerDesu" struct HTTPRequest { int protocol_minor_version; char *method; char *path; }; #endif |
main.c
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 |
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <signal.h> #include <stdarg.h> #include <pwd.h> #include <grp.h> #include <syslog.h> #define _GNU_SOURCE #include <getopt.h> #include "common.h" #define USAGE "Usage: %s [--port=n] [--debug] <docroot>\n" typedef void (*sighandler_t)(int); static void install_signal_handlers(void); static void trap_signal(int sig, sighandler_t handler, int sa_flags); static void signal_exit(int sig); static void noop_handler(int sig); void* xmalloc(size_t sz); void log_exit(const char *fmt, ...); extern void become_daemon(void); extern int listen_socket(char *port); extern void server_main(int server, char *docroot); static int debug_mode = 0; static struct option longopts[] = { {"debug", no_argument, &debug_mode, 1}, {"port", required_argument, NULL, 'p'}, {"help", no_argument, NULL, 'h'}, {0, 0, 0, 0} }; int main(int argc, char *argv[]) { int server_fd; char *port = NULL; char *docroot; int opt; while ((opt = getopt_long(argc, argv, "", longopts, NULL)) != -1) { switch (opt) { case 0: break; case 'p': port = optarg; break; case 'h': fprintf(stdout, USAGE, argv[0]); exit(0); case '?': fprintf(stderr, USAGE, argv[0]); exit(1); } } if (optind != argc - 1) { fprintf(stderr, "%d: %d", optind, argc - 1); fprintf(stderr, USAGE, argv[0]); exit(1); } docroot = argv[optind]; install_signal_handlers(); server_fd = listen_socket(port); if (!debug_mode) { openlog(SERVER_NAME, LOG_PID|LOG_NDELAY, LOG_DAEMON); become_daemon(); } server_main(server_fd, docroot); exit(0); } // シグナルハンドラ設定 static void install_signal_handlers(void) { // プロセス終了 trap_signal(SIGTERM, signal_exit, SA_RESTART); // 子プロセス終了 trap_signal(SIGCHLD, noop_handler, SA_RESTART | SA_NOCLDWAIT); } // シグナル受信 static void trap_signal(int sig, sighandler_t handler, int sa_flags) { struct sigaction act; act.sa_handler = handler; sigemptyset(&act.sa_mask); act.sa_flags = sa_flags; if (sigaction(sig, &act, NULL) < 0) { log_exit("sigaction() failed: %s", strerror(errno)); } } static void signal_exit(int sig) { log_exit("exit by signal %d", sig); } static void noop_handler(int sig) { ; } // メモリ確保 void* xmalloc(size_t sz) { void *p; p = malloc(sz); if (!p) log_exit("failed to allocate memory"); return p; } // エラー時にログを出力して終了する void log_exit(const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (debug_mode) { // デバッグの場合は標準エラー出力 vfprintf(stderr, fmt, ap); fputc('\n', stderr); } else { // ログファイルに書く vsyslog(LOG_ERR, fmt, ap); } va_end(ap); exit(1); } |
server.c
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netdb.h> #include "common.h" #define MAX_BACKLOG 5 static void service(FILE *in, FILE *out, char *docroot); void become_daemon(void); int listen_socket(char *port); void server_main(int server, char *docroot); extern void log_exit(const char *fmt, ...); extern struct HTTPRequest* read_request(FILE *in); extern void free_request(struct HTTPRequest *req); extern void respond_to(char *method, char *path, FILE *out, char *docroot); // デーモン化する void become_daemon(void) { int n; // カレントディレクトリをルートディレクトリにする if (chdir("/") < 0) log_exit("chdir(2) failed: %s", strerror(errno)); // 標準入出力を/dev/nullにつなぐ freopen("/dev/null", "r", stdin); freopen("/dev/null", "w", stdout); freopen("/dev/null", "w", stderr); // デーモン化 n = fork(); if (n < 0) log_exit("fork(2) failed: %s", strerror(errno)); if (n != 0) _exit(0); if (setsid() < 0) log_exit("setsid(2) failed: %s", strerror(errno)); } // ソケット作成 int listen_socket(char *port) { struct addrinfo hints, *res, *ai; int err; // アドレス取得 memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_INET; // IPv4限定 hints.ai_socktype = SOCK_STREAM; // ストリーム形式の接続 hints.ai_flags = AI_PASSIVE; // サーバ用 if ((err = getaddrinfo(NULL, port, &hints, &res)) != 0) log_exit(gai_strerror(err)); for (ai = res; ai; ai = ai->ai_next) { int sock; // 指定のプロトコルのソケット作成 sock = socket(ai->ai_family, ai->ai_socktype, ai->ai_protocol); if (sock < 0) continue; // ソケットにアドレスを割り当てる if (bind(sock, ai->ai_addr, ai->ai_addrlen) < 0) { close(sock); continue; } // ソケットが接続待ち状態であることをカーネルに伝える if (listen(sock, MAX_BACKLOG) < 0) { close(sock); continue; } freeaddrinfo(res); return sock; } log_exit("failed to listen socket"); return -1; } // 接続されたら処理するようにする void server_main(int server_fd, char *docroot) { for (;;) { struct sockaddr_storage addr; socklen_t addrlen = sizeof addr; int sock; int pid; // 接続待ち sock = accept(server_fd, (struct sockaddr*)&addr, &addrlen); if (sock < 0) log_exit("accept(2) failed: %s", strerror(errno)); // 並列処理するためにフォーク pid = fork(); if (pid < 0) exit(3); if (pid == 0) { // 子プロセス FILE *inf = fdopen(sock, "r"); FILE *outf = fdopen(sock, "w"); service(inf, outf, docroot); exit(0); } close(sock); } } // リクエストを解析してレスポンスを出力 static void service(FILE *in, FILE *out, char *docroot) { struct HTTPRequest *req; req = read_request(in); respond_to(req->method, req->path, out, docroot); free_request(req); } |
request.c
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
#include <stdio.h> #include <stdlib.h> #include <errno.h> #include <string.h> #include <ctype.h> #include "common.h" #define LINE_BUF_SIZE 4096 static void read_request_line(struct HTTPRequest *req, FILE *in); static void upcase(char *str); struct HTTPRequest* read_request(FILE *in); void free_request(struct HTTPRequest *req); extern void log_exit(const char *fmt, ...); extern void* xmalloc(size_t sz); // リクエスト解析 struct HTTPRequest* read_request(FILE *in) { struct HTTPRequest *req; req = xmalloc(sizeof(struct HTTPRequest)); read_request_line(req, in); return req; } // リクエストの後処理(mallocしてるので) void free_request(struct HTTPRequest *req) { free(req->method); free(req->path); free(req); } // リクエストラインの解析 static void read_request_line(struct HTTPRequest *req, FILE *in) { char buf[LINE_BUF_SIZE]; char *path, *p; // 1行読み込み if (!fgets(buf, LINE_BUF_SIZE, in)) log_exit("no request line"); // メソッドを取り出す p = strchr(buf, ' '); // メソッドとパスの間のスペースを検索 if (!p) log_exit("parse error on request line (1): %s", buf); *p++ = '\0'; req->method = xmalloc(p - buf); strcpy(req->method, buf); upcase(req->method); // 大文字に変換 // パスを取り出す path = p; p = strchr(path, ' '); // パスとプロトコルの間のスペースを検索 if (!p) log_exit("parse error on request line (2): %s", buf); *p++ = '\0'; req->path = xmalloc(p - path); strcpy(req->path, path); // プロトコルのマイナーバージョンを取り出す // プロトコルの書式があってるか確認 if (strncasecmp(p, "HTTP/1.", strlen("HTTP/1.")) != 0) log_exit("parse error on request line (3): %s", buf); p += strlen("HTTP/1."); req->protocol_minor_version = atoi(p); } // 大文字に変換 static void upcase(char *str) { char *p; for (p = str; *p; p++) { *p = (char)toupper((int)*p); } } |
response.c
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 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 |
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <errno.h> #include <string.h> #include <time.h> #include <fcntl.h> #include "common.h" #define BLOCK_BUF_SIZE 1024 #define SERVER_VERSION "1.0" #define HTTP_MINOR_VERSION 0 struct FileInfo { char *path; long size; int ok; }; static void do_file_response(char *path, FILE *out, char *docroot); static void not_implemented(char *method, FILE *out); static void not_found(FILE *out); static void output_common_header_fields(FILE *out, char *status); static struct FileInfo* get_fileinfo(char *docroot, char *path); static char* build_fspath(char *docroot, char *path); static void free_fileinfo(struct FileInfo *info); void respond_to(char *method, char *path, FILE *out, char *docroot); extern void log_exit(const char *fmt, ...); extern void* xmalloc(size_t sz); // メソッド判定してレスポンス出力 void respond_to(char *method, char *path, FILE *out, char *docroot) { if (strcmp(method, "GET") == 0) do_file_response(path, out, docroot); else not_implemented(method, out); } // ファイル情報を取得してレスポンス出力 static void do_file_response(char *path, FILE *out, char *docroot) { struct FileInfo *info; // ファイル情報取得 info = get_fileinfo(docroot, path); if (!info->ok) { // ファイル情報取得できなかった場合(ディレクトリの場合とか) free_fileinfo(info); not_found(out); return; } output_common_header_fields(out, "200 OK"); fprintf(out, "Content-Length: %ld\r\n", info->size); fprintf(out, "Content-Type: %s\r\n", "text/html"); // HTML固定 fprintf(out, "\r\n"); int fd; char buf[BLOCK_BUF_SIZE]; ssize_t n; // 読み込み専用でファイルを開く fd = open(info->path, O_RDONLY); if (fd < 0) log_exit("failed to open %s: %s", info->path, strerror(errno)); // ファイルの中身を出力 for (;;) { n = read(fd, buf, BLOCK_BUF_SIZE); if (n < 0) log_exit("failed to read %s: %s", info->path, strerror(errno)); if (n == 0) break; if (fwrite(buf, 1, n, out) < n) log_exit("failed to write to socket"); } close(fd); fflush(out); free_fileinfo(info); } // 未実装のリクエストの場合の出力(GET以外) static void not_implemented(char *method, FILE *out) { output_common_header_fields(out, "501 Not Implemented"); fprintf(out, "Content-Type: text/html\r\n"); fprintf(out, "\r\n"); fprintf(out, "<html>\r\n"); fprintf(out, "<header>\r\n"); fprintf(out, "<title>501 Not Implemented</title>\r\n"); fprintf(out, "<header>\r\n"); fprintf(out, "<body>\r\n"); fprintf(out, "<p>The request method %s is not implemented</p>\r\n", method); fprintf(out, "</body>\r\n"); fprintf(out, "</html>\r\n"); fflush(out); } // 指定ファイルがない場合の出力 static void not_found(FILE *out) { output_common_header_fields(out, "404 Not Found"); fprintf(out, "Content-Type: text/html\r\n"); fprintf(out, "\r\n"); fprintf(out, "<html>\r\n"); fprintf(out, "<header><title>Not Found</title><header>\r\n"); fprintf(out, "<body><p>File not found</p></body>\r\n"); fprintf(out, "</html>\r\n"); fflush(out); } #define TIME_BUF_SIZE 64 // 全レスポンスの共通出力 static void output_common_header_fields(FILE *out, char *status) { time_t t; struct tm *tm; char buf[TIME_BUF_SIZE]; t = time(NULL); tm = gmtime(&t); if (!tm) log_exit("gmtime() failed: %s", strerror(errno)); strftime(buf, TIME_BUF_SIZE, "%a, %d %b %Y %H:%M:%S GMT", tm); fprintf(out, "HTTP/1.%d %s\r\n", HTTP_MINOR_VERSION, status); fprintf(out, "Date: %s\r\n", buf); fprintf(out, "Server: %s/%s\r\n", SERVER_NAME, SERVER_VERSION); fprintf(out, "Connection: close\r\n"); } // ファイル情報取得 static struct FileInfo* get_fileinfo(char *docroot, char *urlpath) { struct FileInfo *info; struct stat st; info = xmalloc(sizeof(struct FileInfo)); info->path = build_fspath(docroot, urlpath); info->ok = 0; // ファイルがあるか確認 if (lstat(info->path, &st) < 0) return info; // 普通のファイルか確認 if (!S_ISREG(st.st_mode)) return info; info->ok = 1; info->size = st.st_size; return info; } // ファイルパス取得(ほんとは正規化する必要がある) static char* build_fspath(char *docroot, char *urlpath) { char *path; if (urlpath != NULL && urlpath[0] == '/') { // デバッグモードじゃない場合にurlpathに/がつくのでとりあえず対策 path = xmalloc(strlen(docroot) + strlen(urlpath) + 1); sprintf(path, "%s%s", docroot, urlpath); } else { // +2は/と末尾の\0の分 path = xmalloc(strlen(docroot) + 1 + strlen(urlpath) + 1); sprintf(path, "%s/%s", docroot, urlpath); } return path; } // ファイル情報の後処理(mallocしてるので) static void free_fileinfo(struct FileInfo *info) { free(info->path); free(info); } |
make
フォルダ構成は下記。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
├── Makefile ├── dst │ ├── main.o │ ├── request.o │ ├── response.o │ └── server.o ├── httpd ├── page │ └── index.html └── src ├── common.h ├── main.c ├── request.c ├── response.c └── server.c |
Makefile はこんな感じ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
CC = gcc CFLAGS = -g -Wall -o OBJS = $(DST)main.o $(DST)server.o $(DST)request.o $(DST)response.o SRC = src/ DST = dst/ httpd: $(OBJS) $(CC) $(CFLAGS) $@ $(OBJS) $(DST)main.o: $(SRC)main.c $(CC) -c $(CFLAGS) $@ $< $(DST)server.o: $(SRC)server.c $(CC) -c $(CFLAGS) $@ $< $(DST)request.o: $(SRC)request.c $(CC) -c $(CFLAGS) $@ $< $(DST)response.o: $(SRC)response.c $(CC) -c $(CFLAGS) $@ $< |
make コマンド実行で httpd ファイルが生成されます。
実行
下記コマンドを実行!(--debug なしの場合デーモン化の影響でカレントディレクトリがルートになるので渡すパスはフルパスにする必要があります。pwd するとパスがとれるよ)
1 |
$ ./httpd ./page port=8082 --debug |
ブラウザでこちら http://pi@raspberrypi.local:8082/index.html
にアクセスすると下記のように表示されます。
おわりに
冒頭でもいいましたが色々セキュリティホールがあるのでこのまま公開するのはやめましょう!!
(リクエストで ../ とか指定すると指定ディレクトリ外のファイルをみれたり、CPU、メモリをこのプロセスが占有してしまう可能性があるなど色々問題があるらしいです。)
色々わからない部分もありますがとりあえず HTTP サーバができました。
デーモン化したプロセスってどうやって止めるんだろう??わからないので毎回下記のようにプロセス検索して kill してます。。。
1 2 |
$ps aux | grep httpd $kill プロセスID |
コメント