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) + + + + +
+
+
ファイルアップロード(POST /example/uploads)
+

標準のフォーム送信と、JavaScript による非同期アップロード(進捗表示)に対応したサンプルです。JavaScript + が無効でも通常送信できます。

+
+ +
+
+ +
+
+ ドラッグ&ドロップ または クリック してファイルを選択 +
+
複数選択可。ファイルサイズや拡張子の制限は必要に応じてサーバ側で検証してください。 +
+
+ +
+
+ +
+ +
送信先: /example/uploads(POST, multipart/form-data
+
+ + + + +
+ +
+
サーバ応答
+
+
+
+ + 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, error::AppError> result = reqReader_.readRequest(readBuf); + if (result.isErr()) { + if (result.unwrapErr() == error::kHttpPayloadTooLarge) { + http::ResponseBuilder builder; + const http::Response res = builder.status(http::kStatusPayloadTooLarge).build(); + + std::vector actions; + actions.push_back(new RegisterEventAction(Event(ctx.getEvent().getFd(), Event::kWrite))); + actions.push_back(new UnregisterEventHandlerAction(ctx.getConnection().unwrap(), Event::kRead)); + actions.push_back(new RegisterEventHandlerAction(conn, Event::kWrite, new WriteResponseHandler(res))); + return Ok(actions); + } + return Err(result.unwrapErr()); + } + + const Option req = result.unwrap(); if (req.isNone()) { // read は高々 1 回なので、パースまで完了しなかった LOG_DEBUG("request is not fully read"); diff --git a/src/lib/core/server.cpp b/src/lib/core/server.cpp index c2bab85c..594d0eaa 100644 --- a/src/lib/core/server.cpp +++ b/src/lib/core/server.cpp @@ -6,7 +6,10 @@ #include "http/response/response_builder.hpp" #include "transport/listener.hpp" #include "utils/logger.hpp" +#include "utils/time.hpp" #include +#include +#include Server::Server(const config::Config &config) : config_(config) { const config::ServerContextList &servers = config_.getServers(); @@ -74,8 +77,12 @@ void Server::start() { if (!result.empty()) LOG_DEBUG("child process reaped"); for (std::vector::const_iterator it = result.begin(); it != result.end(); ++it) { - if (it->status == 0) continue; const Option data = state_.getCgiProcessRepository().get(it->pid); + state_.getCgiProcessRepository().remove(it->pid); + + if (it->status == 0) continue; + + // CGI スクリプトがエラーで終わった場合 if (data.isNone()) { LOG_WARNF( "reaped child process %d with non-zero status %d, but no client fd found", @@ -109,6 +116,9 @@ void Server::start() { invokeHandlers(ctx); } + + // タイムアウトチェック + removeTimeoutHandlers(); } } @@ -210,3 +220,74 @@ void Server::executeActions(ActionContext &actionCtx, std::vector act delete action; } } + +void Server::removeTimeoutHandlers() { + removeTimeoutRequestHandlers(); + removeTimeoutCgiProcesses(); +} + +void Server::removeTimeoutRequestHandlers() { + const std::time_t currentTime = utils::Time::getCurrentTime(); + + const std::vector timedOutFds = + state_.getConnectionRepository().getTimedOutConnectionFds(currentTime, REQUEST_TIMEOUT_SECONDS, listenerFds_); + + for (std::vector::const_iterator it = timedOutFds.begin(); it != timedOutFds.end(); ++it) { + const int fd = *it; + + // Read ハンドラーが登録されているかチェック(リクエスト待ち状態) + const Option > readHandler = state_.getEventHandlerRepository().get(fd, Event::kRead); + if (readHandler.isNone()) { + continue; + } + + LOG_INFOF("Request timeout for fd %d", fd); + + // タイムアウトレスポンスを設定 + http::ResponseBuilder builder; + http::Response response = builder.status(http::kStatusRequestTimeout).build(); + + // Read ハンドラーを削除し、Write ハンドラーを設定 + state_.getEventNotifier().unregisterEvent(Event(fd, Event::kRead)); + state_.getEventHandlerRepository().remove(fd, Event::kRead); + state_.getEventNotifier().registerEvent(Event(fd, Event::kWrite)); + state_.getEventHandlerRepository().set(fd, Event::kWrite, new WriteResponseHandler(response)); + } +} + +void Server::removeTimeoutCgiProcesses() { + const std::time_t currentTime = utils::Time::getCurrentTime(); + const std::vector > timedOutProcesses = + state_.getCgiProcessRepository().getTimedOutProcesses(currentTime, CGI_TIMEOUT_SECONDS); + + for (std::vector >::const_iterator it = timedOutProcesses.begin(); + it != timedOutProcesses.end(); + ++it) { + const pid_t pid = it->first; + const CgiProcessRepository::Data &data = it->second; + + LOG_INFOF("CGI timeout for pid %d", pid); + + // CGI プロセスを強制終了 + if (kill(pid, SIGTERM) == -1) { + LOG_WARNF("Failed to terminate CGI process %d: %s", pid, std::strerror(errno)); + } + + // プロセスソケットをクリーンアップ + const int processSocketFd = data.processSocketFd; + const int clientFd = data.clientFd; + + state_.getEventNotifier().unregisterEvent(Event(processSocketFd, Event::kRead)); + state_.getEventNotifier().unregisterEvent(Event(processSocketFd, Event::kWrite)); + state_.getEventHandlerRepository().remove(processSocketFd, Event::kRead); + state_.getEventHandlerRepository().remove(processSocketFd, Event::kWrite); + state_.getConnectionRepository().remove(processSocketFd); + state_.getCgiProcessRepository().remove(pid); + + // Gateway Timeout レスポンスを返す + http::ResponseBuilder builder; + http::Response response = builder.status(http::kStatusGatewayTimeout).build(); + state_.getEventNotifier().registerEvent(Event(clientFd, Event::kWrite)); + state_.getEventHandlerRepository().set(clientFd, Event::kWrite, new WriteResponseHandler(response)); + } +} diff --git a/src/lib/core/server.hpp b/src/lib/core/server.hpp index 9b85d5ab..c8bbb604 100644 --- a/src/lib/core/server.hpp +++ b/src/lib/core/server.hpp @@ -19,6 +19,9 @@ class Server { // VirtualServer は http::Router を持っていて、コピー不可なのでポインタで持つ typedef std::vector VirtualServerList; + static const int REQUEST_TIMEOUT_SECONDS = 5; + static const int CGI_TIMEOUT_SECONDS = 5; + config::Config config_; VirtualServerList virtualServers_; @@ -32,6 +35,9 @@ class Server { static void executeActions(ActionContext &actionCtx, std::vector actions); void invokeHandlers(const Context &ctx); void invokeSingleHandler(const Context &ctx, const Ref &handler, bool shouldCallHandler); + void removeTimeoutHandlers(); + void removeTimeoutRequestHandlers(); + void removeTimeoutCgiProcesses(); }; #endif diff --git a/src/lib/core/server_state.cpp b/src/lib/core/server_state.cpp index 9897b990..61234d56 100644 --- a/src/lib/core/server_state.cpp +++ b/src/lib/core/server_state.cpp @@ -1,6 +1,9 @@ #include "server_state.hpp" #include "utils/logger.hpp" #include "utils/ref.hpp" +#include "utils/time.hpp" +#include +#include ConnectionRepository::ConnectionRepository() {} @@ -45,6 +48,30 @@ void ConnectionRepository::remove(const int fd) { LOG_DEBUGF("connection removed from server"); } +std::vector ConnectionRepository::getTimedOutConnectionFds( + std::time_t currentTime, double timeoutSeconds, const std::set &excludeFds +) const { + std::vector timedOutFds; + + for (std::map::const_iterator it = connections_.begin(); it != connections_.end(); ++it) { + const int fd = it->first; + Connection *conn = it->second; + + // 除外リストに含まれるFDはスキップ + if (excludeFds.count(fd) > 0) { + continue; + } + + // タイムアウトチェック + const double elapsed = utils::Time::diffTimeSeconds(currentTime, conn->getLastActivityTime()); + if (elapsed > timeoutSeconds) { + timedOutFds.push_back(fd); + } + } + + return timedOutFds; +} + EventHandlerRepository::EventHandlerRepository() {} EventHandlerRepository::~EventHandlerRepository() { @@ -104,6 +131,23 @@ void CgiProcessRepository::remove(const pid_t pid) { pidToData_.erase(pid); } +std::vector > +CgiProcessRepository::getTimedOutProcesses(std::time_t currentTime, double timeoutSeconds) const { + std::vector > timedOutProcesses; + + for (std::map::const_iterator it = pidToData_.begin(); it != pidToData_.end(); ++it) { + const pid_t pid = it->first; + const Data &data = it->second; + + const double elapsed = utils::Time::diffTimeSeconds(currentTime, data.startTime); + if (elapsed > timeoutSeconds) { + timedOutProcesses.push_back(std::make_pair(pid, data)); + } + } + + return timedOutProcesses; +} + ServerState::ServerState() { // self-pipe の読み端を監視対象にする reaper_.attachToEventNotifier(&getEventNotifier()); diff --git a/src/lib/core/server_state.hpp b/src/lib/core/server_state.hpp index 61b35020..49573142 100644 --- a/src/lib/core/server_state.hpp +++ b/src/lib/core/server_state.hpp @@ -7,6 +7,9 @@ #include "transport/connection.hpp" #include "utils/types/option.hpp" #include +#include +#include +#include // TODO: 共通化するべき? @@ -23,6 +26,10 @@ class ConnectionRepository : public NonCopyable { void set(int fd, Connection *conn); void remove(int fd); + // タイムアウトしたコネクションのFDを取得 + std::vector + getTimedOutConnectionFds(std::time_t currentTime, double timeoutSeconds, const std::set &excludeFds) const; + private: std::map connections_; }; @@ -48,6 +55,7 @@ class CgiProcessRepository : public NonCopyable { struct Data { int clientFd; int processSocketFd; + std::time_t startTime; }; CgiProcessRepository() {} @@ -56,6 +64,9 @@ class CgiProcessRepository : public NonCopyable { void set(pid_t pid, Data data); void remove(pid_t pid); + // タイムアウトしたプロセスを取得 + std::vector > getTimedOutProcesses(std::time_t currentTime, double timeoutSeconds) const; + private: std::map pidToData_; }; diff --git a/src/lib/event/event_notifier.cpp b/src/lib/event/event_notifier.cpp index e4991c9a..5049f301 100644 --- a/src/lib/event/event_notifier.cpp +++ b/src/lib/event/event_notifier.cpp @@ -72,9 +72,9 @@ void EpollEventNotifier::unregisterEvent(const Event &event) { } } -EpollEventNotifier::WaitEventsResult EpollEventNotifier::waitEvents() { +EpollEventNotifier::WaitEventsResult EpollEventNotifier::waitEvents(int timeoutMs) { epoll_event evs[1024]; - const int numEvents = epoll_wait(epollFd_.get(), evs, 1024, -1); + const int numEvents = epoll_wait(epollFd_.get(), evs, 1024, timeoutMs); if (numEvents == -1) { LOG_ERRORF("epoll_wait failed: %s", std::strerror(errno)); return Err(error::kUnknown); @@ -160,7 +160,7 @@ void PollEventNotifier::unregisterEvent(const Event &event) { } } -IEventNotifier::WaitEventsResult PollEventNotifier::waitEvents() { +IEventNotifier::WaitEventsResult PollEventNotifier::waitEvents(int timeoutMs) { std::vector fds; for (EventMap::const_iterator it = registeredEvents_.begin(); it != registeredEvents_.end(); ++it) { pollfd pfd = {}; @@ -169,7 +169,7 @@ IEventNotifier::WaitEventsResult PollEventNotifier::waitEvents() { fds.push_back(pfd); } - const int result = poll(fds.data(), fds.size(), -1); + const int result = poll(fds.data(), fds.size(), timeoutMs); if (result == -1) { LOG_ERRORF("poll failed: %s", std::strerror(errno)); return Err(error::kUnknown); diff --git a/src/lib/event/event_notifier.hpp b/src/lib/event/event_notifier.hpp index 25802ff1..43927add 100644 --- a/src/lib/event/event_notifier.hpp +++ b/src/lib/event/event_notifier.hpp @@ -19,7 +19,7 @@ class IEventNotifier { virtual void unregisterEvent(const Event &event) = 0; typedef Result, error::AppError> WaitEventsResult; - virtual WaitEventsResult waitEvents() = 0; + virtual WaitEventsResult waitEvents(int timeoutMs = 1000) = 0; }; // epoll の抽象 @@ -29,7 +29,7 @@ class EpollEventNotifier : public IEventNotifier { void registerEvent(const Event &event); void unregisterEvent(const Event &event); - WaitEventsResult waitEvents(); + WaitEventsResult waitEvents(int timeoutMs = 1000); private: AutoFd epollFd_; @@ -46,7 +46,7 @@ class PollEventNotifier : public IEventNotifier { void registerEvent(const Event &event); void unregisterEvent(const Event &event); - WaitEventsResult waitEvents(); + WaitEventsResult waitEvents(int timeoutMs = 1000); private: typedef std::map EventMap; diff --git a/src/lib/http/request/reader/reading_headers_state.cpp b/src/lib/http/request/reader/reading_headers_state.cpp index 33a7c3a4..53c3c591 100644 --- a/src/lib/http/request/reader/reading_headers_state.cpp +++ b/src/lib/http/request/reader/reading_headers_state.cpp @@ -53,7 +53,7 @@ Result http::ReadingHeadersState const std::size_t len = contentLength.unwrap(); if (len > clientMaxBodySize) { LOG_WARN("content-length too large"); - return Err(error::kParseUnknown); + return Err(error::kHttpPayloadTooLarge); } return Ok(new ReadingBodyState(ctx_, len)); } diff --git a/src/lib/http/request/reader/request_reader.cpp b/src/lib/http/request/reader/request_reader.cpp index 1939b62e..527fc517 100644 --- a/src/lib/http/request/reader/request_reader.cpp +++ b/src/lib/http/request/reader/request_reader.cpp @@ -14,12 +14,7 @@ http::RequestReader::ReadRequestResult http::RequestReader::readRequest(ReadBuff const std::size_t bytesLoaded = TRY(readBuf.load()); // ReadingBodyState などが state を NULL に遷移させる while (readCtx_.getState() != NULL) { - const Result result = readCtx_.handle(readBuf); - if (result.isErr()) { - return Err(result.unwrapErr()); - } - - const IState::HandleStatus status = result.unwrap(); + const IState::HandleStatus status = TRY(readCtx_.handle(readBuf)); if (status == IState::kSuspend) { // バッファが足りない if (bytesLoaded == 0) { diff --git a/src/lib/transport/connection.cpp b/src/lib/transport/connection.cpp index ee166444..9e82cbc4 100644 --- a/src/lib/transport/connection.cpp +++ b/src/lib/transport/connection.cpp @@ -1,9 +1,10 @@ #include "connection.hpp" #include "utils/logger.hpp" +#include "utils/time.hpp" Connection::Connection(const int fd, const Address &localAddress, const Address &foreignAddress) : clientFd_(fd), localAddress_(localAddress), foreignAddress_(foreignAddress), fdReader_(clientFd_), - buffer_(fdReader_) {} + buffer_(fdReader_), lastActivityTime_(utils::Time::getCurrentTime()) {} Connection::~Connection() { LOG_DEBUG("Connection: destruct"); @@ -24,3 +25,11 @@ const Address &Connection::getForeignAddress() const { ReadBuffer &Connection::getReadBuffer() { return buffer_; } + +std::time_t Connection::getLastActivityTime() const { + return lastActivityTime_; +} + +void Connection::updateActivity() { + lastActivityTime_ = utils::Time::getCurrentTime(); +} diff --git a/src/lib/transport/connection.hpp b/src/lib/transport/connection.hpp index 363ced85..a0067928 100644 --- a/src/lib/transport/connection.hpp +++ b/src/lib/transport/connection.hpp @@ -5,6 +5,7 @@ #include "utils/auto_fd.hpp" #include "utils/io/read_buffer.hpp" #include "utils/io/reader.hpp" +#include // クライアントソケットの抽象 class Connection { @@ -16,6 +17,8 @@ class Connection { const Address &getLocalAddress() const; const Address &getForeignAddress() const; ReadBuffer &getReadBuffer(); + std::time_t getLastActivityTime() const; + void updateActivity(); private: AutoFd clientFd_; @@ -23,6 +26,7 @@ class Connection { Address foreignAddress_; io::FdReader fdReader_; // ReadBuffer に渡す IReader & の参照先として必要 ReadBuffer buffer_; + std::time_t lastActivityTime_; }; #endif diff --git a/src/lib/utils/time.cpp b/src/lib/utils/time.cpp new file mode 100644 index 00000000..72acd99e --- /dev/null +++ b/src/lib/utils/time.cpp @@ -0,0 +1,13 @@ +#include "time.hpp" + +namespace utils { + + std::time_t Time::getCurrentTime() { + return std::time(NULL); + } + + double Time::diffTimeSeconds(std::time_t end, std::time_t start) { + return std::difftime(end, start); + } + +} diff --git a/src/lib/utils/time.hpp b/src/lib/utils/time.hpp new file mode 100644 index 00000000..7fda74e0 --- /dev/null +++ b/src/lib/utils/time.hpp @@ -0,0 +1,16 @@ +#ifndef TIME_HPP +#define TIME_HPP + +#include + +namespace utils { + + class Time { + public: + static std::time_t getCurrentTime(); + static double diffTimeSeconds(std::time_t end, std::time_t start); + }; + +} + +#endif