diff --git a/.idea/jsonSchemas.xml b/.idea/jsonSchemas.xml
new file mode 100644
index 00000000..800175bc
--- /dev/null
+++ b/.idea/jsonSchemas.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 35eb1ddf..72cefee0 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,5 +2,8 @@
+
+
+
\ No newline at end of file
diff --git a/example/cgi/counter.py b/example/cgi/counter.py
new file mode 100755
index 00000000..3a78dd17
--- /dev/null
+++ b/example/cgi/counter.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import os
+from http.cookies import SimpleCookie
+
+
+def _to_int(v, default=0):
+ try:
+ return int(v)
+ except Exception:
+ return default
+
+
+# 受信クッキーを読み取り
+in_cookie = SimpleCookie()
+raw_cookie = os.environ.get("HTTP_COOKIE", "")
+if raw_cookie:
+ in_cookie.load(raw_cookie)
+
+count = _to_int(in_cookie["count"].value, 0) if "count" in in_cookie else 0
+count += 1
+
+# 送信クッキー(セッション cookie。永続化したい場合は Max-Age を設定)
+out_cookie = SimpleCookie()
+out_cookie["count"] = str(count)
+out_cookie["count"]["path"] = "/"
+# 例:1年保持したい場合は次行を有効化
+# out_cookie["count"]["max-age"] = "31536000"
+
+# ==== ヘッダー(LF 区切り) ====
+print("Status: 200 OK")
+print("Content-Type: text/html; charset=utf-8")
+for morsel in out_cookie.values():
+ # Morsel.OutputString() は "key=value; Path=/; ..." を返す(改行は含まない)
+ print("Set-Cookie: " + morsel.OutputString())
+print() # 空行でヘッダー終了(LF のみ)
+
+# ==== 本文 ====
+print(f"""
+
+
+
+ アクセスカウンタ
+
+
+ このブラウザでのアクセスは {count} 回目です。
+
+""")
diff --git a/example/cgi/timeout.cgi b/example/cgi/timeout.cgi
new file mode 100755
index 00000000..7017f8ae
--- /dev/null
+++ b/example/cgi/timeout.cgi
@@ -0,0 +1,15 @@
+#!/bin/bash
+
+# HTTP Header
+echo "Content-Type: text/html"
+sleep 10
+echo ""
+
+# HTML Content
+echo ""
+echo "CGI Test"
+echo ""
+echo "Hello, CGI!
"
+echo "This page was generated by a Shell script.
"
+echo ""
+echo ""
\ No newline at end of file
diff --git a/example/conf/webserv.toml b/example/conf/webserv.toml
index bf64cffc..ab28b854 100644
--- a/example/conf/webserv.toml
+++ b/example/conf/webserv.toml
@@ -13,34 +13,33 @@ path = '/other'
root = 'example/html'
index = 'home.html'
-# upload したファイルの取得
[[server.location]]
-path = '/uploads'
-root = 'example'
-autoindex = 'on'
+path = '/'
+root = 'example/uploads'
+allowed_methods = ['POST']
[[server.location]]
path = '/cgi'
root = 'example'
allowed_methods = ['GET', 'POST']
-cgi_extensions = ['.cgi']
+cgi_extensions = ['.cgi', 'py']
# --
[[server]]
-port = 8082
+port = 8081
server_name = ['upload.example.com']
-# upload 専用
+# upload したファイルの取得
[[server.location]]
path = '/'
root = 'example/uploads'
-allowed_methods = ['POST']
+autoindex = 'on'
# --
[[server]]
-port = 8081
+port = 8082
server_name = ['redirect.example.com']
# NOTE: /foo/bar はどこにリダイレクトするべきか?
diff --git a/example/html/form/index.html b/example/html/form/index.html
new file mode 100644
index 00000000..719f6221
--- /dev/null
+++ b/example/html/form/index.html
@@ -0,0 +1,53 @@
+
+
+
+
+
+ ファイルアップロードフォーム(/example/uploads)
+
+
+
+
+
+
+
diff --git a/example/html/form/index.js b/example/html/form/index.js
new file mode 100644
index 00000000..8096c5dd
--- /dev/null
+++ b/example/html/form/index.js
@@ -0,0 +1,93 @@
+(function () {
+ const form = document.getElementById('uploadForm');
+ const input = document.getElementById('fileInput');
+ const dropzone = document.getElementById('dropzone');
+ const fileList = document.getElementById('fileList');
+ const bar = document.getElementById('bar');
+ const resp = document.getElementById('response');
+ const submitBtn = document.getElementById('submitBtn');
+
+ function listFiles(files) {
+ if (!files || files.length === 0) {
+ fileList.textContent = '';
+ return;
+ }
+ const items = Array.from(files).map(f => `${f.name} (${(f.size / 1024).toFixed(1)} KB)`);
+ fileList.textContent = items.join('\n');
+ }
+
+ // ドロップゾーンの操作
+ function preventDefaults(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(ev => {
+ dropzone.addEventListener(ev, preventDefaults, false);
+ });
+ ['dragenter', 'dragover'].forEach(ev => {
+ dropzone.addEventListener(ev, () => dropzone.classList.add('dragover'), false);
+ });
+ ['dragleave', 'drop'].forEach(ev => {
+ dropzone.addEventListener(ev, () => dropzone.classList.remove('dragover'), false);
+ });
+ dropzone.addEventListener('click', () => input.click());
+ dropzone.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ input.click();
+ }
+ });
+ dropzone.addEventListener('drop', (e) => {
+ const dt = e.dataTransfer;
+ if (dt && dt.files && dt.files.length) {
+ input.files = dt.files; // 一括選択
+ listFiles(input.files);
+ }
+ });
+ input.addEventListener('change', () => listFiles(input.files));
+
+ // 非同期アップロード(XHR: 進捗取得用)
+ form.addEventListener('submit', function (e) {
+ // JavaScript 有効時は XHR で送信(進捗表示)
+ e.preventDefault();
+ resp.textContent = '送信中…';
+ bar.style.width = '0%';
+ submitBtn.disabled = true;
+
+ const fd = new FormData();
+ // CSRF など他のフィールドを含めたい場合は form.elements を走査
+ // ここではファイルのみ送信
+ if (input.files && input.files.length) {
+ // バックエンドの期待に合わせて name を調整
+ // ここでは同じ name("files") を複数回 append
+ Array.from(input.files).forEach(file => fd.append('files', file));
+ }
+
+ const xhr = new XMLHttpRequest();
+ xhr.open('POST', form.action, true);
+ xhr.upload.onprogress = function (evt) {
+ if (evt.lengthComputable) {
+ const percent = Math.round((evt.loaded / evt.total) * 100);
+ bar.style.width = percent + '%';
+ }
+ };
+ xhr.onload = function () {
+ submitBtn.disabled = false;
+ if (xhr.status >= 200 && xhr.status < 300) {
+ resp.classList.remove('error');
+ resp.textContent = xhr.responseText || 'アップロードが完了しました。';
+ bar.style.width = '100%';
+ } else {
+ resp.classList.add('error');
+ resp.textContent = `エラー: ${xhr.status} ${xhr.statusText}\n` + (xhr.responseText || '');
+ }
+ };
+ xhr.onerror = function () {
+ submitBtn.disabled = false;
+ resp.classList.add('error');
+ resp.textContent = 'ネットワークエラーにより送信に失敗しました。';
+ };
+ xhr.send(fd);
+ });
+})();
diff --git a/example/html/form/style.css b/example/html/form/style.css
new file mode 100644
index 00000000..db6c102b
--- /dev/null
+++ b/example/html/form/style.css
@@ -0,0 +1,164 @@
+:root {
+ --bg: #0f172a; /* slate-900 */
+ --panel: #111827; /* gray-900 */
+ --panel-2: #0b1220; /* darker */
+ --border: #1f2937; /* gray-800 */
+ --text: #e5e7eb; /* gray-200 */
+ --muted: #9ca3af; /* gray-400 */
+ --accent: #3b82f6; /* blue-500 */
+ --accent-2: #2563eb; /* blue-600 */
+ --ok: #10b981; /* emerald-500 */
+ --err: #ef4444; /* red-500 */
+}
+
+html, body {
+ height: 100%;
+}
+
+body {
+ margin: 0;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, "Hiragino Kaku Gothic ProN", "Hiragino Sans", Meiryo, "Noto Sans JP", sans-serif;
+ background: radial-gradient(1200px 800px at 20% 0%, #0b1220, var(--bg));
+ color: var(--text);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ display: grid;
+ place-items: center;
+ padding: 24px;
+}
+
+.card {
+ width: min(720px, 100%);
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.02), rgba(255, 255, 255, 0.01));
+ border: 1px solid var(--border);
+ border-radius: 16px;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.02);
+ overflow: hidden;
+}
+
+.header {
+ padding: 20px 24px;
+ background: linear-gradient(180deg, rgba(59, 130, 246, 0.12), rgba(59, 130, 246, 0));
+ border-bottom: 1px solid var(--border);
+}
+
+.title {
+ font-size: 18px;
+ font-weight: 700;
+}
+
+.desc {
+ color: var(--muted);
+ margin-top: 4px;
+ font-size: 14px;
+}
+
+form {
+ padding: 24px;
+ display: grid;
+ gap: 16px;
+}
+
+.field {
+ display: grid;
+ gap: 8px;
+}
+
+label {
+ font-weight: 600;
+ font-size: 14px;
+}
+
+.hint {
+ color: var(--muted);
+ font-size: 12px;
+}
+
+.dropzone {
+ border: 1.5px dashed #334155; /* slate-700 */
+ background: linear-gradient(180deg, rgba(2, 6, 23, 0.6), rgba(2, 6, 23, 0.3));
+ padding: 24px;
+ border-radius: 12px;
+ display: grid;
+ place-items: center;
+ text-align: center;
+ gap: 8px;
+ transition: border-color .2s ease, background .2s ease, transform .1s ease;
+}
+
+.dropzone.dragover {
+ border-color: var(--accent);
+ background: linear-gradient(180deg, rgba(37, 99, 235, 0.15), rgba(2, 6, 23, 0.3));
+ transform: translateY(-1px);
+}
+
+input[type="file"] {
+ width: 100%;
+}
+
+.controls {
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ align-items: center;
+}
+
+button[type="submit"] {
+ background: linear-gradient(180deg, var(--accent), var(--accent-2));
+ color: white;
+ border: 0;
+ padding: 10px 16px;
+ border-radius: 12px;
+ font-weight: 700;
+ cursor: pointer;
+ box-shadow: 0 8px 24px rgba(37, 99, 235, 0.3);
+}
+
+button[disabled] {
+ opacity: .65;
+ cursor: not-allowed;
+}
+
+.progress {
+ height: 10px;
+ width: 100%;
+ background: #0b1020;
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.bar {
+ height: 100%;
+ width: 0%;
+ background: linear-gradient(90deg, var(--accent), var(--ok));
+ transition: width .1s linear;
+}
+
+.filelist {
+ font-size: 13px;
+ color: var(--muted);
+}
+
+.result {
+ border-top: 1px solid var(--border);
+ background: rgba(255, 255, 255, 0.02);
+ padding: 16px 24px 24px;
+}
+
+.response {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ white-space: pre-wrap;
+ word-break: break-all;
+ font-size: 12px;
+ color: #d1fae5;
+}
+
+.error {
+ color: #fecaca;
+}
+
+.small {
+ font-size: 12px;
+ color: var(--muted);
+}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 91e05b7d..ea8788b7 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -18,6 +18,8 @@ add_library(webserv_lib STATIC
lib/utils/logger.hpp
lib/utils/string.cpp
lib/utils/string.hpp
+ lib/utils/time.cpp
+ lib/utils/time.hpp
lib/utils/types/option.hpp
lib/utils/types/result.hpp
lib/utils/types/unit.hpp
diff --git a/src/lib/cgi/factory.cpp b/src/lib/cgi/factory.cpp
index 30ab5c87..885dec5a 100644
--- a/src/lib/cgi/factory.cpp
+++ b/src/lib/cgi/factory.cpp
@@ -46,8 +46,16 @@ namespace cgi {
variables.push_back(MetaVariable("REMOTE_ADDR", param.foreignAddress.getIp()));
variables.push_back(MetaVariable("SERVER_NAME", param.serverName));
variables.push_back(MetaVariable("SERVER_PORT", param.serverPort));
+
+ // 独自実装?
variables.push_back(MetaVariable("DOCUMENT_ROOT", param.documentRoot));
+ // Cookie 対応
+ const Option cookieHeader = req.getHeader("Cookie");
+ if (cookieHeader.isSome()) {
+ variables.push_back(MetaVariable("HTTP_COOKIE", cookieHeader.unwrap()));
+ }
+
return cgi::Request::create(variables, body);
}
diff --git a/src/lib/core/action/run_cgi_action.cpp b/src/lib/core/action/run_cgi_action.cpp
index cb675768..8c8dbc2a 100644
--- a/src/lib/core/action/run_cgi_action.cpp
+++ b/src/lib/core/action/run_cgi_action.cpp
@@ -8,6 +8,7 @@
#include "core/handler/write_cgi_request_handler.hpp"
#include "../../cgi/meta_variable.hpp"
#include "utils/fd.hpp"
+#include "utils/time.hpp"
#include "utils/logger.hpp"
void RunCgiAction::execute(ActionContext &ctx) {
@@ -134,5 +135,6 @@ void RunCgiAction::parentRoutine(const ActionContext &ctx, const int socketFd, c
ctx.getState().getEventNotifier().unregisterEvent(Event(clientFd_, Event::kWrite));
ctx.getState().getEventHandlerRepository().remove(clientFd_, Event::kRead);
ctx.getState().getEventHandlerRepository().remove(clientFd_, Event::kWrite);
- ctx.getState().getCgiProcessRepository().set(childPid, {clientFd_, socketFd});
+ CgiProcessRepository::Data data = {clientFd_, socketFd, utils::Time::getCurrentTime()};
+ ctx.getState().getCgiProcessRepository().set(childPid, data);
}
diff --git a/src/lib/core/handler/read_request_handler.cpp b/src/lib/core/handler/read_request_handler.cpp
index e23dc99f..c635870d 100644
--- a/src/lib/core/handler/read_request_handler.cpp
+++ b/src/lib/core/handler/read_request_handler.cpp
@@ -1,6 +1,7 @@
#include "read_request_handler.hpp"
#include "write_response_body_handler.hpp"
#include "core/action/action.hpp"
+#include "http/response/response_builder.hpp"
#include "utils/logger.hpp"
#include "utils/types/try.hpp"
@@ -14,9 +15,28 @@ ReadRequestHandler::ReadRequestHandler(const VirtualServerResolver &vsResolver)
IEventHandler::InvokeResult ReadRequestHandler::invoke(const Context &ctx) {
LOG_DEBUG("start ReadRequestHandler");
+ const auto conn = ctx.getConnection().unwrap();
- ReadBuffer &readBuf = ctx.getConnection().unwrap().get().getReadBuffer();
- const Option req = TRY(reqReader_.readRequest(readBuf));
+ // アクティビティを更新
+ ctx.getConnection().unwrap().get().updateActivity();
+
+ ReadBuffer &readBuf = conn.get().getReadBuffer();
+ const Result