第6章:設定とテスト

はじめに

Webサーバーは、設定ファイルによって動作を柔軟に変更できる必要があります。本章では、Nginx風の設定ファイルのパースと、サーバーのテスト方法を学びます。

---

1. 設定ファイルの構造

1.1 Nginx風の設定

# webserv.conf

server {
    listen 8080;
    server_name localhost;
    root /var/www/html;
    index index.html index.htm;
    client_max_body_size 10m;

    error_page 404 /error/404.html;
    error_page 500 502 503 504 /error/50x.html;

    location / {
        autoindex on;
        allow_methods GET POST;
    }

    location /upload {
        allow_methods POST;
        upload_store /var/www/uploads;
    }

    location /api {
        allow_methods GET POST DELETE;
        return 301 /api/v2;
    }

    location /cgi-bin {
        cgi_pass /usr/bin/python3;
        cgi_extension .py;
    }
}

server {
    listen 8081;
    server_name example.com www.example.com;
    root /var/www/example;
}

1.2 設定項目一覧

サーバーレベル:
├── listen          # ポート番号(必須)
├── server_name     # サーバー名(ホスト名マッチング)
├── root            # ドキュメントルート
├── index           # デフォルトファイル
├── client_max_body_size  # 最大ボディサイズ
└── error_page      # エラーページ

ロケーションレベル:
├── root / alias    # パス設定
├── autoindex       # ディレクトリリスト
├── allow_methods   # 許可するメソッド
├── return          # リダイレクト
├── cgi_pass        # CGIインタープリタ
├── cgi_extension   # CGI拡張子
└── upload_store    # アップロードディレクトリ

---

2. 設定のデータ構造

2.1 設定クラス

struct LocationConfig {
    std::string path;
    std::string root;
    std::string alias;
    std::vector<std::string> index;
    bool autoindex;
    std::vector<std::string> allowMethods;
    std::string returnCode;
    std::string returnUrl;
    std::string cgiPass;
    std::string cgiExtension;
    std::string uploadStore;

    LocationConfig()
        : autoindex(false) {
        allowMethods = {"GET"};  // デフォルト
    }
};

struct ServerConfig {
    int port;
    std::vector<std::string> serverNames;
    std::string root;
    std::vector<std::string> index;
    size_t clientMaxBodySize;
    std::map<int, std::string> errorPages;
    std::vector<LocationConfig> locations;

    ServerConfig()
        : port(80),
          clientMaxBodySize(1024 * 1024) {  // 1MB
        index = {"index.html"};
    }
};

class Config {
private:
    std::vector<ServerConfig> _servers;

public:
    void parse(const std::string& filename);
    const std::vector<ServerConfig>& getServers() const { return _servers; }
    const ServerConfig* findServer(int port, const std::string& host) const;
    const LocationConfig* findLocation(const ServerConfig& server,
                                       const std::string& uri) const;
};

---

3. 設定ファイルのパース

3.1 トークナイザー

enum TokenType {
    TOKEN_STRING,
    TOKEN_OPEN_BRACE,    // {
    TOKEN_CLOSE_BRACE,   // }
    TOKEN_SEMICOLON,     // ;
    TOKEN_EOF
};

struct Token {
    TokenType type;
    std::string value;
    int line;
    int column;
};

class Tokenizer {
private:
    std::string _content;
    size_t _pos;
    int _line;
    int _column;

public:
    Tokenizer(const std::string& content)
        : _content(content), _pos(0), _line(1), _column(1) {}

    Token nextToken() {
        skipWhitespaceAndComments();

        if (_pos >= _content.size()) {
            return {TOKEN_EOF, "", _line, _column};
        }

        char c = _content[_pos];

        if (c == '{') {
            _pos++; _column++;
            return {TOKEN_OPEN_BRACE, "{", _line, _column - 1};
        }
        if (c == '}') {
            _pos++; _column++;
            return {TOKEN_CLOSE_BRACE, "}", _line, _column - 1};
        }
        if (c == ';') {
            _pos++; _column++;
            return {TOKEN_SEMICOLON, ";", _line, _column - 1};
        }

        return readString();
    }

private:
    void skipWhitespaceAndComments() {
        while (_pos < _content.size()) {
            char c = _content[_pos];

            if (std::isspace(c)) {
                if (c == '\n') {
                    _line++;
                    _column = 1;
                } else {
                    _column++;
                }
                _pos++;
            } else if (c == '#') {
                // コメント行末まで
                while (_pos < _content.size() && _content[_pos] != '\n') {
                    _pos++;
                }
            } else {
                break;
            }
        }
    }

    Token readString() {
        int startColumn = _column;
        std::string value;

        // クォートされた文字列
        if (_content[_pos] == '"' || _content[_pos] == '\'') {
            char quote = _content[_pos++];
            _column++;

            while (_pos < _content.size() && _content[_pos] != quote) {
                if (_content[_pos] == '\\' && _pos + 1 < _content.size()) {
                    _pos++;
                    _column++;
                }
                value += _content[_pos++];
                _column++;
            }

            if (_pos < _content.size()) {
                _pos++;  // 閉じクォート
                _column++;
            }
        } else {
            // 通常の文字列
            while (_pos < _content.size()) {
                char c = _content[_pos];
                if (std::isspace(c) || c == '{' || c == '}' || c == ';') {
                    break;
                }
                value += c;
                _pos++;
                _column++;
            }
        }

        return {TOKEN_STRING, value, _line, startColumn};
    }
};

3.2 パーサー

class ConfigParser {
private:
    Tokenizer _tokenizer;
    Token _currentToken;

public:
    ConfigParser(const std::string& content)
        : _tokenizer(content) {
        _currentToken = _tokenizer.nextToken();
    }

    std::vector<ServerConfig> parse() {
        std::vector<ServerConfig> servers;

        while (_currentToken.type != TOKEN_EOF) {
            if (_currentToken.value == "server") {
                servers.push_back(parseServer());
            } else {
                throw ConfigError("Expected 'server', got '" +
                                  _currentToken.value + "'",
                                  _currentToken.line);
            }
        }

        return servers;
    }

private:
    void advance() {
        _currentToken = _tokenizer.nextToken();
    }

    void expect(TokenType type) {
        if (_currentToken.type != type) {
            throw ConfigError("Unexpected token", _currentToken.line);
        }
        advance();
    }

    void expectString(const std::string& value) {
        if (_currentToken.type != TOKEN_STRING ||
            _currentToken.value != value) {
            throw ConfigError("Expected '" + value + "'", _currentToken.line);
        }
        advance();
    }

    ServerConfig parseServer() {
        ServerConfig server;

        expectString("server");
        expect(TOKEN_OPEN_BRACE);

        while (_currentToken.type != TOKEN_CLOSE_BRACE) {
            parseServerDirective(server);
        }

        expect(TOKEN_CLOSE_BRACE);

        return server;
    }

    void parseServerDirective(ServerConfig& server) {
        std::string directive = _currentToken.value;
        advance();

        if (directive == "listen") {
            server.port = std::stoi(_currentToken.value);
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "server_name") {
            while (_currentToken.type == TOKEN_STRING) {
                server.serverNames.push_back(_currentToken.value);
                advance();
            }
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "root") {
            server.root = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "index") {
            server.index.clear();
            while (_currentToken.type == TOKEN_STRING) {
                server.index.push_back(_currentToken.value);
                advance();
            }
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "client_max_body_size") {
            server.clientMaxBodySize = parseSize(_currentToken.value);
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "error_page") {
            std::vector<int> codes;
            while (_currentToken.type == TOKEN_STRING &&
                   std::isdigit(_currentToken.value[0])) {
                codes.push_back(std::stoi(_currentToken.value));
                advance();
            }
            std::string page = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);

            for (int code : codes) {
                server.errorPages[code] = page;
            }
        }
        else if (directive == "location") {
            server.locations.push_back(parseLocation());
        }
        else {
            throw ConfigError("Unknown directive: " + directive,
                              _currentToken.line);
        }
    }

    LocationConfig parseLocation() {
        LocationConfig location;

        location.path = _currentToken.value;
        advance();
        expect(TOKEN_OPEN_BRACE);

        while (_currentToken.type != TOKEN_CLOSE_BRACE) {
            parseLocationDirective(location);
        }

        expect(TOKEN_CLOSE_BRACE);

        return location;
    }

    void parseLocationDirective(LocationConfig& location) {
        std::string directive = _currentToken.value;
        advance();

        if (directive == "root") {
            location.root = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "alias") {
            location.alias = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "autoindex") {
            location.autoindex = (_currentToken.value == "on");
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "allow_methods") {
            location.allowMethods.clear();
            while (_currentToken.type == TOKEN_STRING) {
                location.allowMethods.push_back(_currentToken.value);
                advance();
            }
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "return") {
            location.returnCode = _currentToken.value;
            advance();
            if (_currentToken.type == TOKEN_STRING) {
                location.returnUrl = _currentToken.value;
                advance();
            }
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "cgi_pass") {
            location.cgiPass = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "cgi_extension") {
            location.cgiExtension = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else if (directive == "upload_store") {
            location.uploadStore = _currentToken.value;
            advance();
            expect(TOKEN_SEMICOLON);
        }
        else {
            throw ConfigError("Unknown location directive: " + directive,
                              _currentToken.line);
        }
    }

    size_t parseSize(const std::string& str) {
        size_t value = 0;
        size_t i = 0;

        while (i < str.size() && std::isdigit(str[i])) {
            value = value * 10 + (str[i] - '0');
            i++;
        }

        if (i < str.size()) {
            char unit = std::tolower(str[i]);
            if (unit == 'k') {
                value *= 1024;
            } else if (unit == 'm') {
                value *= 1024 * 1024;
            } else if (unit == 'g') {
                value *= 1024 * 1024 * 1024;
            }
        }

        return value;
    }
};

---

4. サーバー/ロケーションの選択

4.1 サーバー選択

const ServerConfig* Config::findServer(int port, const std::string& host) const {
    const ServerConfig* defaultServer = nullptr;

    for (const auto& server : _servers) {
        if (server.port != port) continue;

        // デフォルトサーバー(最初のマッチ)
        if (!defaultServer) {
            defaultServer = &server;
        }

        // server_nameでマッチング
        for (const auto& name : server.serverNames) {
            if (name == host) {
                return &server;
            }

            // ワイルドカードマッチング
            if (name[0] == '*') {
                std::string suffix = name.substr(1);
                if (host.size() >= suffix.size() &&
                    host.substr(host.size() - suffix.size()) == suffix) {
                    return &server;
                }
            }
        }
    }

    return defaultServer;
}

4.2 ロケーション選択

const LocationConfig* Config::findLocation(const ServerConfig& server,
                                           const std::string& uri) const {
    const LocationConfig* bestMatch = nullptr;
    size_t bestMatchLength = 0;

    for (const auto& location : server.locations) {
        const std::string& path = location.path;

        // プレフィックスマッチング
        if (uri.find(path) == 0) {
            if (path.size() > bestMatchLength) {
                bestMatchLength = path.size();
                bestMatch = &location;
            }
        }
    }

    return bestMatch;
}

---

5. 設定のバリデーション

5.1 必須項目のチェック

void Config::validate() const {
    for (size_t i = 0; i < _servers.size(); ++i) {
        const ServerConfig& server = _servers[i];

        // ポートのチェック
        if (server.port <= 0 || server.port > 65535) {
            throw ConfigError("Invalid port: " + std::to_string(server.port));
        }

        // rootのチェック
        if (server.root.empty()) {
            throw ConfigError("Server " + std::to_string(i) + ": root is required");
        }

        // rootディレクトリの存在確認
        struct stat st;
        if (stat(server.root.c_str(), &st) < 0 || !S_ISDIR(st.st_mode)) {
            throw ConfigError("Root directory does not exist: " + server.root);
        }

        // ポートの重複チェック
        for (size_t j = 0; j < i; ++j) {
            if (_servers[j].port == server.port) {
                // 同じポートの場合、server_nameが必要
                if (server.serverNames.empty()) {
                    throw ConfigError("Duplicate port " +
                                      std::to_string(server.port) +
                                      " requires server_name");
                }
            }
        }

        // ロケーションの検証
        for (const auto& location : server.locations) {
            validateLocation(location);
        }
    }
}

void Config::validateLocation(const LocationConfig& location) const {
    // メソッドの検証
    for (const auto& method : location.allowMethods) {
        if (method != "GET" && method != "POST" && method != "DELETE") {
            throw ConfigError("Unknown method: " + method);
        }
    }

    // CGI設定の検証
    if (!location.cgiPass.empty()) {
        if (location.cgiExtension.empty()) {
            throw ConfigError("cgi_pass requires cgi_extension");
        }

        struct stat st;
        if (stat(location.cgiPass.c_str(), &st) < 0) {
            throw ConfigError("CGI interpreter not found: " + location.cgiPass);
        }
    }

    // アップロードディレクトリの検証
    if (!location.uploadStore.empty()) {
        struct stat st;
        if (stat(location.uploadStore.c_str(), &st) < 0 ||
            !S_ISDIR(st.st_mode)) {
            throw ConfigError("Upload directory does not exist: " +
                              location.uploadStore);
        }
    }
}

---

6. テスト手法

6.1 curlによるテスト

# 基本的なGETリクエスト
curl -v http://localhost:8080/

# ヘッダーのみ取得
curl -I http://localhost:8080/

# POSTリクエスト
curl -X POST -d "name=John&email=john@example.com" http://localhost:8080/form

# ファイルアップロード
curl -X POST -F "file=@test.txt" http://localhost:8080/upload

# JSONデータ送信
curl -X POST -H "Content-Type: application/json" \
     -d '{"key":"value"}' http://localhost:8080/api

# DELETEリクエスト
curl -X DELETE http://localhost:8080/file.txt

# カスタムヘッダー
curl -H "Host: example.com" http://localhost:8080/

# タイムアウト設定
curl --connect-timeout 5 --max-time 10 http://localhost:8080/

# 詳細出力
curl -v --trace-time http://localhost:8080/

6.2 telnetによる生リクエスト

# 接続
telnet localhost 8080

# リクエストを手動入力
GET / HTTP/1.1
Host: localhost
Connection: close

# 空行を入力してリクエスト送信

6.3 netcatによるテスト

# リクエストを送信
echo -e "GET / HTTP/1.1\r\nHost: localhost\r\n\r\n" | nc localhost 8080

# ファイルからリクエストを送信
nc localhost 8080 < request.txt

# 接続維持
nc -k localhost 8080

---

7. 負荷テスト

7.1 siege

# インストール
brew install siege  # macOS
apt install siege   # Ubuntu

# 基本的な負荷テスト
siege -c 100 -t 30s http://localhost:8080/

# オプション
# -c: 同時接続数
# -t: テスト時間
# -r: リクエスト回数
# -b: ベンチマークモード(遅延なし)

# 複数URLのテスト
siege -c 50 -t 30s -f urls.txt

# 結果の例
# Transactions:         10000 hits
# Availability:         100.00 %
# Elapsed time:         29.95 secs
# Data transferred:     12.34 MB
# Response time:        0.15 secs
# Transaction rate:     334.00 trans/sec
# Throughput:           0.41 MB/sec
# Concurrency:          49.50
# Successful transactions: 10000
# Failed transactions:  0

7.2 ab(Apache Bench)

# 基本的なテスト
ab -n 10000 -c 100 http://localhost:8080/

# オプション
# -n: 総リクエスト数
# -c: 同時接続数
# -k: Keep-Alive有効
# -t: テスト時間

# POST テスト
ab -n 1000 -c 50 -p data.txt -T "application/x-www-form-urlencoded" \
   http://localhost:8080/form

7.3 wrk

# インストール
brew install wrk  # macOS

# 基本的なテスト
wrk -t 4 -c 100 -d 30s http://localhost:8080/

# オプション
# -t: スレッド数
# -c: 接続数
# -d: テスト時間

# Luaスクリプトでカスタムリクエスト
wrk -t 4 -c 100 -d 30s -s custom.lua http://localhost:8080/

---

8. デバッグ手法

8.1 ログ出力

enum LogLevel {
    LOG_DEBUG,
    LOG_INFO,
    LOG_WARN,
    LOG_ERROR
};

class Logger {
private:
    LogLevel _level;
    std::ostream& _out;

public:
    Logger(LogLevel level = LOG_INFO, std::ostream& out = std::cerr)
        : _level(level), _out(out) {}

    void debug(const std::string& msg) { log(LOG_DEBUG, msg); }
    void info(const std::string& msg) { log(LOG_INFO, msg); }
    void warn(const std::string& msg) { log(LOG_WARN, msg); }
    void error(const std::string& msg) { log(LOG_ERROR, msg); }

private:
    void log(LogLevel level, const std::string& msg) {
        if (level < _level) return;

        static const char* levelNames[] = {"DEBUG", "INFO", "WARN", "ERROR"};

        time_t now = time(NULL);
        struct tm* tm = localtime(&now);
        char timeStr[64];
        strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", tm);

        _out << "[" << timeStr << "] "
             << "[" << levelNames[level] << "] "
             << msg << std::endl;
    }
};

// グローバルロガー
Logger g_logger(LOG_DEBUG);

// 使用例
g_logger.info("Server started on port 8080");
g_logger.debug("Request: GET /index.html");
g_logger.error("Failed to open file: " + path);

8.2 リクエスト/レスポンスのダンプ

void dumpRequest(const Request& req) {
    std::cerr << "=== REQUEST ===" << std::endl;
    std::cerr << methodToString(req.getMethod()) << " "
              << req.getUri() << " "
              << req.getVersion() << std::endl;

    for (const auto& header : req.getHeaders()) {
        std::cerr << header.first << ": " << header.second << std::endl;
    }

    if (!req.getBody().empty()) {
        std::cerr << std::endl;
        std::cerr << "[Body: " << req.getBody().size() << " bytes]" << std::endl;
    }
    std::cerr << "===============" << std::endl;
}

void dumpResponse(const Response& resp) {
    std::cerr << "=== RESPONSE ===" << std::endl;
    std::cerr << "HTTP/1.1 " << resp.getStatusCode() << " "
              << resp.getStatusMessage() << std::endl;

    for (const auto& header : resp.getHeaders()) {
        std::cerr << header.first << ": " << header.second << std::endl;
    }

    std::cerr << std::endl;
    std::cerr << "[Body: " << resp.getBody().size() << " bytes]" << std::endl;
    std::cerr << "================" << std::endl;
}

8.3 Valgrindによるメモリチェック

# メモリリーク検出
valgrind --leak-check=full --show-leak-kinds=all ./webserv config.conf

# 詳細な追跡
valgrind --track-origins=yes ./webserv config.conf

# ファイルディスクリプタリーク
valgrind --track-fds=yes ./webserv config.conf

8.4 straceによるシステムコール追跡

# すべてのシステムコール
strace ./webserv config.conf

# ネットワーク関連のみ
strace -e trace=network ./webserv config.conf

# ファイル関連のみ
strace -e trace=file ./webserv config.conf

# 時間計測
strace -T ./webserv config.conf

---

9. 自動テストスクリプト

9.1 Pythonテストスクリプト

#!/usr/bin/env python3
# test_webserv.py

import requests
import subprocess
import time
import sys

BASE_URL = "http://localhost:8080"

def test_get_index():
    """GETリクエストのテスト"""
    r = requests.get(f"{BASE_URL}/")
    assert r.status_code == 200
    assert "Content-Type" in r.headers
    print("✓ GET / passed")

def test_404():
    """404エラーのテスト"""
    r = requests.get(f"{BASE_URL}/nonexistent")
    assert r.status_code == 404
    print("✓ 404 test passed")

def test_post():
    """POSTリクエストのテスト"""
    data = {"name": "test", "value": "123"}
    r = requests.post(f"{BASE_URL}/form", data=data)
    assert r.status_code in [200, 201]
    print("✓ POST test passed")

def test_upload():
    """ファイルアップロードのテスト"""
    files = {"file": ("test.txt", "Hello, World!", "text/plain")}
    r = requests.post(f"{BASE_URL}/upload", files=files)
    assert r.status_code in [200, 201]
    print("✓ Upload test passed")

def test_delete():
    """DELETEリクエストのテスト"""
    # まずファイルを作成
    files = {"file": ("delete_me.txt", "test", "text/plain")}
    r = requests.post(f"{BASE_URL}/upload", files=files)
    assert r.status_code in [200, 201]

    # 削除
    r = requests.delete(f"{BASE_URL}/uploads/delete_me.txt")
    assert r.status_code in [200, 204]
    print("✓ DELETE test passed")

def test_large_request():
    """大きなリクエストのテスト"""
    large_data = "x" * (1024 * 1024)  # 1MB
    r = requests.post(f"{BASE_URL}/upload", data=large_data)
    # client_max_body_sizeに応じてステータスが変わる
    print(f"✓ Large request test: status={r.status_code}")

def test_concurrent():
    """同時接続のテスト"""
    import concurrent.futures

    def make_request(i):
        r = requests.get(f"{BASE_URL}/")
        return r.status_code

    with concurrent.futures.ThreadPoolExecutor(max_workers=100) as executor:
        futures = [executor.submit(make_request, i) for i in range(100)]
        results = [f.result() for f in futures]

    success = sum(1 for r in results if r == 200)
    print(f"✓ Concurrent test: {success}/100 succeeded")

def run_all_tests():
    tests = [
        test_get_index,
        test_404,
        test_post,
        test_upload,
        test_delete,
        test_large_request,
        test_concurrent,
    ]

    passed = 0
    failed = 0

    for test in tests:
        try:
            test()
            passed += 1
        except Exception as e:
            print(f"✗ {test.__name__} failed: {e}")
            failed += 1

    print(f"\n{passed} passed, {failed} failed")
    return failed == 0

if __name__ == "__main__":
    sys.exit(0 if run_all_tests() else 1)

9.2 シェルスクリプトテスト

#!/bin/bash
# test_webserv.sh

PORT=8080
PASS=0
FAIL=0

# テスト関数
test_request() {
    local name="$1"
    local expected_status="$2"
    local curl_opts="$3"

    status=$(curl -s -o /dev/null -w "%{http_code}" $curl_opts http://localhost:$PORT/)

    if [ "$status" = "$expected_status" ]; then
        echo "✓ $name (status: $status)"
        ((PASS++))
    else
        echo "✗ $name (expected: $expected_status, got: $status)"
        ((FAIL++))
    fi
}

# GETテスト
test_request "GET /" "200" ""
test_request "GET /nonexistent" "404" "-X GET http://localhost:$PORT/nonexistent"

# POSTテスト
test_request "POST /form" "200" "-X POST -d 'test=data' http://localhost:$PORT/form"

# 結果
echo ""
echo "Passed: $PASS"
echo "Failed: $FAIL"

exit $FAIL

---

まとめ

本章で学んだこと:

  • 設定ファイル構造: Nginx風の階層構造
  • パーサー実装: トークナイザーとパーサー
  • サーバー/ロケーション選択: マッチングアルゴリズム
  • バリデーション: 設定の検証
  • curlテスト: 基本的なHTTPテスト
  • 負荷テスト: siege, ab, wrk
  • デバッグ: ログ、Valgrind、strace
  • 自動テスト: Python/シェルスクリプト

これでwebservの基本実装は完成です。実際のプロジェクトでは、エッジケースの処理やエラーハンドリングをさらに強化してください。