HNで公開: Cウェブサーバーを使ったウェブサイトのホスティング
(github.com/cozis)My Blog Technology
このウェブサーバーは、自分のブログをホストするために設計された最小限のウェブサーバーです。最初から公開インターネットに耐えられるよう堅牢に作られています。リバースプロキシは必要ありません。実際に動作している様子は http://playin.coz.is/index.html で確認できます。Redditでハッキングを募って、面白くて悪意のあるリクエストのログをギガバイト単位で収集しました。その一部は attempts.txt に保存してあり、後で暇つぶしにさらに見ていく予定です。
でも……なぜ?
私は自分専用のツールを作るのが好きで、何でも「battle-tested」でなければならないという話を聞くのにうんざりしていました。クラッシュしたらどうするのか? バグは修正できます。
仕様
- Linux専用
- HTTP/1.1、パイプライニング、keep-alive接続を実装
- HTTPS対応(BearSSLを使用し、TLS 1.2まで対応)
- 依存関係は最小限(HTTPS使用時は libc と BearSSL)
- 設定可能なタイムアウト
- アクセスログ、クラッシュログ、ログローテーション、ディスク使用量制限
Transfer-Encoding: Chunkedなし(411 Length Requiredで応答し、クライアントにContent-Length付きで再送させる)- 単一コア(より良いVPSを入手したら変更予定)
- 静的ファイルのキャッシュなし(まだ)
ベンチマーク
このプロジェクトの主眼は堅牢性ですが、決して遅くはありません。nginxとの簡単な比較です(静的エンドポイント、どちらも単一スレッド、1K接続制限)。
-
(blogtech)
$ wrk -c 500 -d 5s http://127.0.0.1:80/hello- 平均レイテンシ: 6.66ms
- リクエスト/秒: 76974.24
- 転送/秒: 6.09MB
-
(nginx)
$ wrk -c 500 -d 5s http://127.0.0.1:8080/hello- 平均レイテンシ: 149.11ms
- リクエスト/秒: 44227.78
- 転送/秒: 8.27MB
nginx設定:
worker_processes 1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location /hello {
add_header Content-Type text/plain;
return 200 "Hello, world!";
}
}
}
ビルドと実行
デフォルトでは、サーバーのビルドはHTTP専用です。
$ make
このコマンドにより、serve(リリースビルド)、serve_cov(カバレッジビルド)、serve_debug(デバッグビルド)の実行ファイルが生成されます。リリースビルドはポート80、デバッグビルドはポート8080で待ち受けます。
HTTPSを有効にするには、BearSSLをクローンしてビルドする必要があります。
$ mkdir 3p
$ cd 3p
$ git clone https://www.bearssl.org/git/BearSSL
$ cd BearSSL
$ make -j
$ cd ../../
$ make -B HTTPS=1
同じ実行ファイルが生成されますが、ポート443(リリース)または8081(デバッグ)でセキュア接続が利用可能になります。cert.pem と key.pem ファイルは実行ファイルと同じディレクトリに配置する必要があります。名前と場所を変更するには、次を修正します。
#define HTTPS_KEY_FILE "key.pem"
#define HTTPS_CERT_FILE "cert.pem"
ローカルでHTTPSをテストするには、自己署名証明書(および秘密鍵)を生成します。
openssl genpkey -algorithm RSA -out key.pem -pkeyopt rsa_keygen_bits:2048
openssl req -new -x509 -key key.pem -out cert.pem -days 365
使い方
サーバーは docroot/ フォルダから静的コンテンツを配信します。これを変更するには、respond 関数を修正します。
typedef struct {
Method method;
string path;
int major;
int minor;
int nheaders;
Header headers[MAX_HEADERS];
string content;
} Request;
void respond(Request request, ResponseBuilder *b) {
if (request.major != 1 || request.minor > 1) {
status_line(b, 505); // HTTP Version Not Supported
return;
}
if (request.method != M_GET) {
status_line(b, 405); // Method Not Allowed
return;
}
if (string_match_case_insensitive(request.path, LIT("/hello"))) {
status_line(b, 200);
append_content_s(b, LIT("Hello, world!"));
return;
}
if (serve_file_or_dir(b, LIT("/"), LIT("docroot/"), request.path, NULLSTR, false))
return;
status_line(b, 404);
append_content_s(b, LIT("Nothing here :|"));
}
ここで request.path フィールドを切り替えてエンドポイントを追加できます。パスはリクエストバッファのスライスにすぎません。URIはパースされません。
テスト
サーバーは定期的に valgrind と sanitizers(address、undefined)で実行し、wrk を使って負荷をかけています。また、HTTP/1.1仕様への準拠を確認するために tests/test.py に自動テストを追加しています。自分のウェブサイトをホストし、あちこちに投稿して負荷を維持しています。インターネット上で脆弱なウェブサイトをスキャンするあらゆるボットが、優秀なファザーになります。
既知の問題
- サーバーがHTTP/1.0クライアントにHTTP/1.1で応答する
コントリビュート
私は主に DEV ブランチで作業し、ときどき MAIN にマージしています。プルリクエストを開くときは DEV を対象にするとやりやすいでしょう。
GN⁺の要約
- このプロジェクトは、最小限の依存関係と堅牢性を目指したウェブサーバーです。
- HTTP/1.1 と HTTPS をサポートし、さまざまなログ機能と設定可能なタイムアウトを提供します。
- ベンチマーク結果では nginx より高速な応答時間を示しています。
- 開発者が自分のツールを作り、バグを修正する過程を楽しめるよう設計されています。
- 類似機能を持つプロジェクトとしては Nginx や Apache HTTP Server があります。
1件のコメント
Hacker Newsのコメント
リバースプロキシは不要: Jetty を使ってリバースプロキシなしでアプリをインターネットにデプロイしても問題はなかった
自作の C Web サーバー: 商用 Web サイトを運用していた C Web サーバーを作った
サービス構築の満足感: システム API を使って基本的なサービスを構築するのは非常に満足感がある
poll()関数が高い性能を示すことに驚いた小さなプロジェクトの紹介: 余暇の時間に始めた面白いプロジェクトを紹介している
Kore フレームワークの推薦: C アプリを書くときに公開部分を自分で書くのが面倒なら、Kore フレームワークを勧める
興味深いリンクの共有: sqlite.org の althttpd インスタンスは 1 日に 50 万件以上の HTTP リクエストを処理している
自作ツールを作る楽しさ: 何もかも「実戦テスト済み」でなければならないという意見にはうんざりしている
Chaos Communication Congress の講演: セキュリティ機能を備えた C 製のブログ / Web サーバーに関する講演を思い出させる
安定した Web サイト: トップページに表示されてもクラッシュしない Web サイト
基本に立ち返る: 必要なものだけを使って基本に立ち返るアプローチが好きだ