第5章:レスポンス生成とCGI

はじめに

Webサーバーの主要な役割は、リクエストに応じて適切なレスポンスを生成することです。本章では、静的ファイル配信、ディレクトリリスト、エラーページ、そしてCGI(Common Gateway Interface)の実装を学びます。

---

1. レスポンス生成の基礎

1.1 レスポンスの構造

HTTP/1.1 200 OK\r\n              ← ステータスライン
Date: Mon, 01 Jan 2024 00:00:00 GMT\r\n
Server: webserv/1.0\r\n
Content-Type: text/html\r\n
Content-Length: 1234\r\n
Connection: keep-alive\r\n
\r\n                              ← 空行(ヘッダー終了)
<!DOCTYPE html>                  ← ボディ
<html>
...
</html>

1.2 Responseクラスの設計

class Response {
private:
    int         _statusCode;
    std::string _statusMessage;
    std::map<std::string, std::string> _headers;
    std::vector<char> _body;
    bool _chunked;

public:
    Response();
    Response(int statusCode);

    void setStatus(int code);
    void setHeader(const std::string& name, const std::string& value);
    void setBody(const std::string& body);
    void setBody(const std::vector<char>& body);
    void setBody(const char* data, size_t len);

    std::string buildResponse() const;
    std::vector<char> buildBinaryResponse() const;

private:
    std::string getReasonPhrase(int code) const;
    std::string getCurrentDate() const;
};

Response::Response() : _statusCode(200), _chunked(false) {
    _statusMessage = "OK";
    setHeader("Server", "webserv/1.0");
    setHeader("Date", getCurrentDate());
}

Response::Response(int statusCode) : _statusCode(statusCode), _chunked(false) {
    _statusMessage = getReasonPhrase(statusCode);
    setHeader("Server", "webserv/1.0");
    setHeader("Date", getCurrentDate());
}

void Response::setStatus(int code) {
    _statusCode = code;
    _statusMessage = getReasonPhrase(code);
}

std::string Response::buildResponse() const {
    std::ostringstream oss;

    // ステータスライン
    oss << "HTTP/1.1 " << _statusCode << " " << _statusMessage << "\r\n";

    // ヘッダー
    for (const auto& header : _headers) {
        oss << header.first << ": " << header.second << "\r\n";
    }

    // Content-Length(設定されていない場合)
    if (_headers.find("Content-Length") == _headers.end() && !_chunked) {
        oss << "Content-Length: " << _body.size() << "\r\n";
    }

    // 空行
    oss << "\r\n";

    // ボディ
    oss.write(_body.data(), _body.size());

    return oss.str();
}

std::string Response::getReasonPhrase(int code) const {
    static std::map<int, std::string> phrases = {
        {100, "Continue"},
        {200, "OK"},
        {201, "Created"},
        {204, "No Content"},
        {301, "Moved Permanently"},
        {302, "Found"},
        {304, "Not Modified"},
        {400, "Bad Request"},
        {403, "Forbidden"},
        {404, "Not Found"},
        {405, "Method Not Allowed"},
        {413, "Request Entity Too Large"},
        {500, "Internal Server Error"},
        {501, "Not Implemented"},
        {502, "Bad Gateway"},
        {503, "Service Unavailable"},
        {504, "Gateway Timeout"}
    };

    auto it = phrases.find(code);
    return (it != phrases.end()) ? it->second : "Unknown";
}

std::string Response::getCurrentDate() const {
    time_t now = time(NULL);
    struct tm* gmt = gmtime(&now);

    char buffer[100];
    strftime(buffer, sizeof(buffer), "%a, %d %b %Y %H:%M:%S GMT", gmt);

    return std::string(buffer);
}

---

2. 静的ファイル配信

2.1 ファイル読み込み

#include <fstream>
#include <sys/stat.h>

class StaticFileHandler {
private:
    std::string _rootDir;

public:
    StaticFileHandler(const std::string& root) : _rootDir(root) {}

    Response handleRequest(const Request& req) {
        std::string path = resolvePath(req.getUri());

        // パストラバーサル対策
        if (!isPathSafe(path)) {
            return createErrorResponse(403, "Forbidden");
        }

        // ファイルの存在確認
        struct stat st;
        if (stat(path.c_str(), &st) < 0) {
            return createErrorResponse(404, "Not Found");
        }

        // ディレクトリの場合
        if (S_ISDIR(st.st_mode)) {
            return handleDirectory(path, req);
        }

        // ファイルの場合
        return serveFile(path);
    }

private:
    std::string resolvePath(const std::string& uri) {
        std::string path = _rootDir;

        // URIの先頭の/を除去して結合
        if (!uri.empty() && uri[0] == '/') {
            path += uri;
        } else {
            path += "/" + uri;
        }

        return path;
    }

    bool isPathSafe(const std::string& path) {
        // 正規化されたパスを取得
        char resolved[PATH_MAX];
        if (realpath(path.c_str(), resolved) == NULL) {
            return false;
        }

        // rootDir外へのアクセスを禁止
        return std::string(resolved).find(_rootDir) == 0;
    }

    Response serveFile(const std::string& path) {
        std::ifstream file(path, std::ios::binary);
        if (!file) {
            return createErrorResponse(500, "Cannot read file");
        }

        // ファイルサイズ取得
        file.seekg(0, std::ios::end);
        size_t size = file.tellg();
        file.seekg(0, std::ios::beg);

        // ファイル読み込み
        std::vector<char> content(size);
        file.read(content.data(), size);

        // レスポンス生成
        Response resp(200);
        resp.setHeader("Content-Type", getMimeType(path));
        resp.setBody(content);

        return resp;
    }

    std::string getMimeType(const std::string& path) {
        static std::map<std::string, std::string> mimeTypes = {
            {".html", "text/html; charset=utf-8"},
            {".htm",  "text/html; charset=utf-8"},
            {".css",  "text/css"},
            {".js",   "application/javascript"},
            {".json", "application/json"},
            {".xml",  "application/xml"},
            {".txt",  "text/plain"},
            {".png",  "image/png"},
            {".jpg",  "image/jpeg"},
            {".jpeg", "image/jpeg"},
            {".gif",  "image/gif"},
            {".svg",  "image/svg+xml"},
            {".ico",  "image/x-icon"},
            {".pdf",  "application/pdf"},
            {".zip",  "application/zip"},
            {".mp3",  "audio/mpeg"},
            {".mp4",  "video/mp4"}
        };

        size_t pos = path.rfind('.');
        if (pos != std::string::npos) {
            std::string ext = path.substr(pos);
            auto it = mimeTypes.find(ext);
            if (it != mimeTypes.end()) {
                return it->second;
            }
        }

        return "application/octet-stream";
    }
};

2.2 ディレクトリリスト(autoindex)

#include <dirent.h>

Response StaticFileHandler::handleDirectory(const std::string& path,
                                            const Request& req) {
    // indexファイルを探す
    std::vector<std::string> indexFiles = {"index.html", "index.htm"};

    for (const auto& indexFile : indexFiles) {
        std::string indexPath = path + "/" + indexFile;
        struct stat st;
        if (stat(indexPath.c_str(), &st) == 0 && S_ISREG(st.st_mode)) {
            return serveFile(indexPath);
        }
    }

    // autoindexが有効な場合
    if (_autoindex) {
        return generateDirectoryListing(path, req.getUri());
    }

    return createErrorResponse(403, "Forbidden");
}

Response StaticFileHandler::generateDirectoryListing(const std::string& path,
                                                     const std::string& uri) {
    DIR* dir = opendir(path.c_str());
    if (!dir) {
        return createErrorResponse(500, "Cannot open directory");
    }

    std::ostringstream html;
    html << "<!DOCTYPE html>\n"
         << "<html>\n"
         << "<head>\n"
         << "<meta charset=\"utf-8\">\n"
         << "<title>Index of " << uri << "</title>\n"
         << "<style>\n"
         << "body { font-family: monospace; margin: 20px; }\n"
         << "h1 { border-bottom: 1px solid #ccc; }\n"
         << "table { border-collapse: collapse; }\n"
         << "th, td { text-align: left; padding: 5px 20px; }\n"
         << "a { text-decoration: none; }\n"
         << "a:hover { text-decoration: underline; }\n"
         << "</style>\n"
         << "</head>\n"
         << "<body>\n"
         << "<h1>Index of " << uri << "</h1>\n"
         << "<table>\n"
         << "<tr><th>Name</th><th>Size</th><th>Modified</th></tr>\n";

    // 親ディレクトリへのリンク
    if (uri != "/") {
        html << "<tr><td><a href=\"../\">../</a></td><td>-</td><td>-</td></tr>\n";
    }

    struct dirent* entry;
    std::vector<std::pair<std::string, struct stat>> entries;

    while ((entry = readdir(dir)) != NULL) {
        std::string name = entry->d_name;
        if (name == "." || name == "..") continue;

        std::string fullPath = path + "/" + name;
        struct stat st;
        if (stat(fullPath.c_str(), &st) == 0) {
            entries.push_back({name, st});
        }
    }
    closedir(dir);

    // ソート(ディレクトリ優先、名前順)
    std::sort(entries.begin(), entries.end(),
        [](const auto& a, const auto& b) {
            bool aIsDir = S_ISDIR(a.second.st_mode);
            bool bIsDir = S_ISDIR(b.second.st_mode);
            if (aIsDir != bIsDir) return aIsDir > bIsDir;
            return a.first < b.first;
        });

    for (const auto& entry : entries) {
        const std::string& name = entry.first;
        const struct stat& st = entry.second;
        bool isDir = S_ISDIR(st.st_mode);

        // サイズ
        std::string size = isDir ? "-" : formatSize(st.st_size);

        // 更新日時
        char dateStr[50];
        struct tm* tm = localtime(&st.st_mtime);
        strftime(dateStr, sizeof(dateStr), "%Y-%m-%d %H:%M", tm);

        // リンク
        std::string displayName = name + (isDir ? "/" : "");
        std::string href = name + (isDir ? "/" : "");

        html << "<tr>"
             << "<td><a href=\"" << href << "\">" << displayName << "</a></td>"
             << "<td>" << size << "</td>"
             << "<td>" << dateStr << "</td>"
             << "</tr>\n";
    }

    html << "</table>\n"
         << "</body>\n"
         << "</html>\n";

    Response resp(200);
    resp.setHeader("Content-Type", "text/html; charset=utf-8");
    resp.setBody(html.str());

    return resp;
}

std::string StaticFileHandler::formatSize(off_t size) {
    const char* units[] = {"B", "KB", "MB", "GB"};
    int unitIndex = 0;
    double dsize = size;

    while (dsize >= 1024 && unitIndex < 3) {
        dsize /= 1024;
        unitIndex++;
    }

    std::ostringstream oss;
    if (unitIndex == 0) {
        oss << size << " " << units[unitIndex];
    } else {
        oss << std::fixed << std::setprecision(1) << dsize << " " << units[unitIndex];
    }

    return oss.str();
}

---

3. エラーページ

3.1 デフォルトエラーページ

class ErrorPageHandler {
private:
    std::map<int, std::string> _customPages;  // ステータス → ファイルパス

public:
    void setCustomPage(int status, const std::string& path) {
        _customPages[status] = path;
    }

    Response createErrorResponse(int status, const std::string& message = "") {
        // カスタムページがあれば使用
        auto it = _customPages.find(status);
        if (it != _customPages.end()) {
            std::ifstream file(it->second);
            if (file) {
                std::string content((std::istreambuf_iterator<char>(file)),
                                     std::istreambuf_iterator<char>());
                Response resp(status);
                resp.setHeader("Content-Type", "text/html; charset=utf-8");
                resp.setBody(content);
                return resp;
            }
        }

        // デフォルトページ
        return createDefaultErrorPage(status, message);
    }

private:
    Response createDefaultErrorPage(int status, const std::string& message) {
        std::string reasonPhrase = getReasonPhrase(status);
        std::string msg = message.empty() ? reasonPhrase : message;

        std::ostringstream html;
        html << "<!DOCTYPE html>\n"
             << "<html>\n"
             << "<head>\n"
             << "<meta charset=\"utf-8\">\n"
             << "<title>" << status << " " << reasonPhrase << "</title>\n"
             << "<style>\n"
             << "body { font-family: sans-serif; text-align: center; padding: 50px; }\n"
             << "h1 { font-size: 72px; margin: 0; color: #333; }\n"
             << "p { font-size: 24px; color: #666; }\n"
             << "</style>\n"
             << "</head>\n"
             << "<body>\n"
             << "<h1>" << status << "</h1>\n"
             << "<p>" << msg << "</p>\n"
             << "<hr>\n"
             << "<p><small>webserv/1.0</small></p>\n"
             << "</body>\n"
             << "</html>\n";

        Response resp(status);
        resp.setHeader("Content-Type", "text/html; charset=utf-8");
        resp.setBody(html.str());

        return resp;
    }
};

---

4. ファイルアップロード

4.1 multipart/form-dataのパース

POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxk
Content-Length: 1234

------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain

ファイルの内容...
------WebKitFormBoundary7MA4YWxk
Content-Disposition: form-data; name="description"

説明文
------WebKitFormBoundary7MA4YWxk--

struct MultipartPart {
    std::map<std::string, std::string> headers;
    std::string name;
    std::string filename;
    std::string contentType;
    std::vector<char> data;
};

class MultipartParser {
private:
    std::string _boundary;

public:
    MultipartParser(const std::string& contentType) {
        // boundary を抽出
        size_t pos = contentType.find("boundary=");
        if (pos != std::string::npos) {
            _boundary = "--" + contentType.substr(pos + 9);
            // 引用符を除去
            if (_boundary.front() == '"') {
                _boundary = _boundary.substr(1, _boundary.size() - 2);
            }
        }
    }

    std::vector<MultipartPart> parse(const std::string& body) {
        std::vector<MultipartPart> parts;

        size_t pos = 0;
        while (true) {
            // 境界を探す
            size_t start = body.find(_boundary, pos);
            if (start == std::string::npos) break;
            start += _boundary.length();

            // 終了境界かチェック
            if (body.substr(start, 2) == "--") break;

            // CRLFをスキップ
            start += 2;

            // 次の境界を探す
            size_t end = body.find(_boundary, start);
            if (end == std::string::npos) break;

            // パート解析
            std::string partData = body.substr(start, end - start - 2);
            MultipartPart part = parsePart(partData);
            parts.push_back(part);

            pos = end;
        }

        return parts;
    }

private:
    MultipartPart parsePart(const std::string& data) {
        MultipartPart part;

        // ヘッダーとボディの境界
        size_t headerEnd = data.find("\r\n\r\n");
        if (headerEnd == std::string::npos) return part;

        // ヘッダー解析
        std::string headers = data.substr(0, headerEnd);
        std::istringstream iss(headers);
        std::string line;

        while (std::getline(iss, line)) {
            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }

            size_t colonPos = line.find(':');
            if (colonPos != std::string::npos) {
                std::string name = line.substr(0, colonPos);
                std::string value = trim(line.substr(colonPos + 1));
                part.headers[name] = value;

                // Content-Dispositionの解析
                if (name == "Content-Disposition") {
                    parseContentDisposition(value, part);
                }
            }
        }

        // ボディ
        std::string bodyStr = data.substr(headerEnd + 4);
        part.data.assign(bodyStr.begin(), bodyStr.end());

        return part;
    }

    void parseContentDisposition(const std::string& value, MultipartPart& part) {
        // name="..." を抽出
        size_t namePos = value.find("name=\"");
        if (namePos != std::string::npos) {
            size_t start = namePos + 6;
            size_t end = value.find('"', start);
            part.name = value.substr(start, end - start);
        }

        // filename="..." を抽出
        size_t filenamePos = value.find("filename=\"");
        if (filenamePos != std::string::npos) {
            size_t start = filenamePos + 10;
            size_t end = value.find('"', start);
            part.filename = value.substr(start, end - start);
        }
    }
};

4.2 ファイル保存

class UploadHandler {
private:
    std::string _uploadDir;
    size_t _maxFileSize;

public:
    UploadHandler(const std::string& dir, size_t maxSize)
        : _uploadDir(dir), _maxFileSize(maxSize) {}

    Response handleUpload(const Request& req) {
        // Content-Typeを確認
        std::string contentType = req.getHeader("content-type");
        if (contentType.find("multipart/form-data") == std::string::npos) {
            return ErrorPageHandler().createErrorResponse(400, "Bad Request");
        }

        // マルチパート解析
        MultipartParser parser(contentType);
        std::vector<MultipartPart> parts = parser.parse(req.getBody());

        // ファイルを保存
        std::vector<std::string> savedFiles;

        for (const auto& part : parts) {
            if (part.filename.empty()) continue;

            // サイズチェック
            if (part.data.size() > _maxFileSize) {
                return ErrorPageHandler().createErrorResponse(
                    413, "File too large");
            }

            // ファイル名のサニタイズ
            std::string safeName = sanitizeFilename(part.filename);
            std::string path = _uploadDir + "/" + safeName;

            // 重複回避
            path = getUniquePath(path);

            // 保存
            std::ofstream file(path, std::ios::binary);
            if (!file) {
                return ErrorPageHandler().createErrorResponse(
                    500, "Cannot save file");
            }
            file.write(part.data.data(), part.data.size());

            savedFiles.push_back(path);
        }

        // 成功レスポンス
        Response resp(201);
        resp.setHeader("Content-Type", "application/json");

        std::ostringstream json;
        json << "{\"uploaded\":[";
        for (size_t i = 0; i < savedFiles.size(); ++i) {
            if (i > 0) json << ",";
            json << "\"" << savedFiles[i] << "\"";
        }
        json << "]}";
        resp.setBody(json.str());

        return resp;
    }

private:
    std::string sanitizeFilename(const std::string& filename) {
        std::string result;
        for (char c : filename) {
            // 安全な文字のみ許可
            if (std::isalnum(c) || c == '.' || c == '-' || c == '_') {
                result += c;
            }
        }

        // パストラバーサル対策
        while (result.find("..") != std::string::npos) {
            size_t pos = result.find("..");
            result.erase(pos, 2);
        }

        return result.empty() ? "unnamed" : result;
    }

    std::string getUniquePath(const std::string& path) {
        if (!fileExists(path)) return path;

        // ファイル名と拡張子を分離
        size_t dotPos = path.rfind('.');
        std::string base = (dotPos != std::string::npos) ?
                           path.substr(0, dotPos) : path;
        std::string ext = (dotPos != std::string::npos) ?
                          path.substr(dotPos) : "";

        // 番号を付けて重複回避
        int counter = 1;
        std::string newPath;
        do {
            std::ostringstream oss;
            oss << base << "_" << counter << ext;
            newPath = oss.str();
            counter++;
        } while (fileExists(newPath));

        return newPath;
    }
};

---

5. CGI(Common Gateway Interface)

5.1 CGIの概要

CGIの仕組み:
1. サーバーがリクエストを受信
2. CGIスクリプトを子プロセスで実行
3. 環境変数でリクエスト情報を渡す
4. 標準入力でボディを渡す
5. 標準出力からレスポンスを受け取る

+--------+          +--------+          +--------+
| Client | -------> | Server | -------> |  CGI   |
|        | <------- |        | <------- | Script |
+--------+          +--------+          +--------+
                    fork/exec           stdin/stdout
                    環境変数             レスポンス

5.2 CGI環境変数

std::map<std::string, std::string> CGIHandler::buildEnvVars(
    const Request& req, const std::string& scriptPath) {

    std::map<std::string, std::string> env;

    // 必須変数
    env["REQUEST_METHOD"] = methodToString(req.getMethod());
    env["QUERY_STRING"] = req.getQueryString();
    env["CONTENT_TYPE"] = req.getHeader("content-type");
    env["CONTENT_LENGTH"] = std::to_string(req.getBody().size());

    // サーバー情報
    env["SERVER_NAME"] = "localhost";
    env["SERVER_PORT"] = "8080";
    env["SERVER_PROTOCOL"] = "HTTP/1.1";
    env["SERVER_SOFTWARE"] = "webserv/1.0";
    env["GATEWAY_INTERFACE"] = "CGI/1.1";

    // スクリプト情報
    env["SCRIPT_NAME"] = req.getUri();
    env["SCRIPT_FILENAME"] = scriptPath;
    env["PATH_INFO"] = "";
    env["PATH_TRANSLATED"] = "";

    // クライアント情報
    env["REMOTE_ADDR"] = "127.0.0.1";
    env["REMOTE_HOST"] = "";

    // HTTPヘッダー(HTTP_*形式)
    for (const auto& header : req.getHeaders()) {
        std::string name = "HTTP_" + toUpperSnakeCase(header.first);
        env[name] = header.second;
    }

    return env;
}

std::string CGIHandler::toUpperSnakeCase(const std::string& str) {
    std::string result;
    for (char c : str) {
        if (c == '-') {
            result += '_';
        } else {
            result += std::toupper(c);
        }
    }
    return result;
}

5.3 CGI実行

#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

class CGIHandler {
private:
    std::string _cgiPath;      // CGIインタープリタのパス
    std::string _cgiExtension; // 拡張子(.py, .php等)
    int _timeout;              // タイムアウト秒数

public:
    CGIHandler(const std::string& path, const std::string& ext, int timeout = 30)
        : _cgiPath(path), _cgiExtension(ext), _timeout(timeout) {}

    Response execute(const Request& req, const std::string& scriptPath) {
        // パイプ作成
        int pipeIn[2];   // 親→子(リクエストボディ)
        int pipeOut[2];  // 子→親(レスポンス)

        if (pipe(pipeIn) < 0 || pipe(pipeOut) < 0) {
            return ErrorPageHandler().createErrorResponse(500, "Pipe failed");
        }

        // フォーク
        pid_t pid = fork();

        if (pid < 0) {
            close(pipeIn[0]); close(pipeIn[1]);
            close(pipeOut[0]); close(pipeOut[1]);
            return ErrorPageHandler().createErrorResponse(500, "Fork failed");
        }

        if (pid == 0) {
            // 子プロセス
            executeChild(req, scriptPath, pipeIn, pipeOut);
            exit(1);  // execveが失敗した場合
        }

        // 親プロセス
        close(pipeIn[0]);   // 読み込み側を閉じる
        close(pipeOut[1]);  // 書き込み側を閉じる

        // リクエストボディを送信
        const std::string& body = req.getBody();
        if (!body.empty()) {
            write(pipeIn[1], body.data(), body.size());
        }
        close(pipeIn[1]);

        // 子プロセスの出力を読み込み
        std::string output = readWithTimeout(pipeOut[0], pid);
        close(pipeOut[0]);

        // 子プロセスの終了を待つ
        int status;
        waitpid(pid, &status, 0);

        if (output.empty()) {
            return ErrorPageHandler().createErrorResponse(502, "Bad Gateway");
        }

        return parseCGIOutput(output);
    }

private:
    void executeChild(const Request& req, const std::string& scriptPath,
                      int pipeIn[2], int pipeOut[2]) {
        // 標準入出力をパイプにリダイレクト
        dup2(pipeIn[0], STDIN_FILENO);
        dup2(pipeOut[1], STDOUT_FILENO);

        close(pipeIn[0]); close(pipeIn[1]);
        close(pipeOut[0]); close(pipeOut[1]);

        // 環境変数の設定
        std::map<std::string, std::string> envMap = buildEnvVars(req, scriptPath);
        std::vector<char*> envp;

        for (const auto& pair : envMap) {
            std::string envStr = pair.first + "=" + pair.second;
            char* env = strdup(envStr.c_str());
            envp.push_back(env);
        }
        envp.push_back(NULL);

        // 作業ディレクトリを変更
        std::string dir = scriptPath.substr(0, scriptPath.rfind('/'));
        chdir(dir.c_str());

        // 実行
        char* argv[] = {
            const_cast<char*>(_cgiPath.c_str()),
            const_cast<char*>(scriptPath.c_str()),
            NULL
        };

        execve(_cgiPath.c_str(), argv, envp.data());

        // execveが失敗した場合
        perror("execve");
        exit(1);
    }

    std::string readWithTimeout(int fd, pid_t pid) {
        std::string output;
        char buffer[4096];
        time_t startTime = time(NULL);

        // 非ブロッキングに設定
        int flags = fcntl(fd, F_GETFL, 0);
        fcntl(fd, F_SETFL, flags | O_NONBLOCK);

        while (true) {
            // タイムアウトチェック
            if (time(NULL) - startTime > _timeout) {
                kill(pid, SIGKILL);
                return "";
            }

            ssize_t n = read(fd, buffer, sizeof(buffer));

            if (n > 0) {
                output.append(buffer, n);
            } else if (n == 0) {
                // EOF
                break;
            } else {
                if (errno == EAGAIN || errno == EWOULDBLOCK) {
                    // データがまだない
                    usleep(10000);  // 10ms待機
                    continue;
                }
                break;
            }
        }

        return output;
    }

    Response parseCGIOutput(const std::string& output) {
        // CGI出力形式:
        // Content-Type: text/html\r\n
        // Status: 200 OK\r\n(オプション)
        // \r\n
        // ボディ

        size_t headerEnd = output.find("\r\n\r\n");
        if (headerEnd == std::string::npos) {
            // ヘッダーがない場合
            Response resp(200);
            resp.setHeader("Content-Type", "text/html");
            resp.setBody(output);
            return resp;
        }

        // ヘッダー解析
        std::string headers = output.substr(0, headerEnd);
        std::string body = output.substr(headerEnd + 4);

        Response resp(200);

        std::istringstream iss(headers);
        std::string line;

        while (std::getline(iss, line)) {
            if (!line.empty() && line.back() == '\r') {
                line.pop_back();
            }

            size_t colonPos = line.find(':');
            if (colonPos != std::string::npos) {
                std::string name = line.substr(0, colonPos);
                std::string value = trim(line.substr(colonPos + 1));

                if (name == "Status") {
                    // Status: 302 Found
                    int code = std::stoi(value);
                    resp.setStatus(code);
                } else {
                    resp.setHeader(name, value);
                }
            }
        }

        resp.setBody(body);

        return resp;
    }
};

5.4 サンプルCGIスクリプト

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

import os
import sys

# CGIヘッダー
print("Content-Type: text/html")
print("")  # 空行(ヘッダー終了)

# HTML出力
print("<!DOCTYPE html>")
print("<html>")
print("<head><title>CGI Test</title></head>")
print("<body>")
print("<h1>Hello from CGI!</h1>")

# 環境変数を表示
print("<h2>Environment Variables:</h2>")
print("<ul>")
for key, value in os.environ.items():
    if key.startswith("HTTP_") or key in ["REQUEST_METHOD", "QUERY_STRING",
                                           "CONTENT_TYPE", "CONTENT_LENGTH"]:
        print(f"<li><b>{key}</b>: {value}</li>")
print("</ul>")

# POSTデータを表示
if os.environ.get("REQUEST_METHOD") == "POST":
    print("<h2>POST Data:</h2>")
    print("<pre>")
    print(sys.stdin.read())
    print("</pre>")

print("</body>")
print("</html>")

---

6. リダイレクト

6.1 HTTPリダイレクト

Response createRedirectResponse(int status, const std::string& location) {
    Response resp(status);
    resp.setHeader("Location", location);

    std::string body = "Redirecting to " + location;
    resp.setHeader("Content-Type", "text/plain");
    resp.setBody(body);

    return resp;
}

// 使用例
// 301: 恒久的リダイレクト
Response permanentRedirect = createRedirectResponse(301, "/new-location");

// 302: 一時的リダイレクト
Response tempRedirect = createRedirectResponse(302, "/temp-location");

// 307: 一時的リダイレクト(メソッド保持)
Response tempRedirectPreserveMethod = createRedirectResponse(307, "/api/v2");

---

7. DELETE処理

7.1 ファイル削除

Response DeleteHandler::handleDelete(const Request& req) {
    std::string path = resolvePath(req.getUri());

    // パストラバーサル対策
    if (!isPathSafe(path)) {
        return ErrorPageHandler().createErrorResponse(403, "Forbidden");
    }

    // ファイルの存在確認
    struct stat st;
    if (stat(path.c_str(), &st) < 0) {
        return ErrorPageHandler().createErrorResponse(404, "Not Found");
    }

    // ディレクトリは削除不可
    if (S_ISDIR(st.st_mode)) {
        return ErrorPageHandler().createErrorResponse(403, "Cannot delete directory");
    }

    // 削除実行
    if (unlink(path.c_str()) < 0) {
        return ErrorPageHandler().createErrorResponse(500, "Delete failed");
    }

    // 成功レスポンス
    Response resp(204);  // No Content
    return resp;
}

---

まとめ

本章で学んだこと:

  • レスポンス生成: ステータスライン、ヘッダー、ボディ
  • 静的ファイル配信: MIMEタイプ、パストラバーサル対策
  • ディレクトリリスト: autoindex機能
  • エラーページ: デフォルトとカスタム
  • ファイルアップロード: multipart/form-data
  • CGI: fork/exec、環境変数、パイプ通信
  • リダイレクト: 301, 302, 307
  • DELETE処理: ファイル削除

次章では、設定ファイルとテストを学びます。