diff --git a/include/webframe.hpp b/include/webframe.hpp index fbe1ce9..a2e30f4 100644 --- a/include/webframe.hpp +++ b/include/webframe.hpp @@ -33,6 +33,7 @@ #include #include #include +#include #if defined(_WIN32) && defined(WEBFRAME_DESKTOP_RUNTIME) #define WEBFRAME_WIN32_APP 1 @@ -83,6 +84,8 @@ namespace webframe * @return the path of the request */ virtual std::string get_path() const = 0; + + virtual std::string get_uri() const = 0; /** * @brief get the value of a specific header diff --git a/include/webframe/uri.hpp b/include/webframe/uri.hpp new file mode 100644 index 0000000..01ee237 --- /dev/null +++ b/include/webframe/uri.hpp @@ -0,0 +1,54 @@ +/* WebFrame + * + * Copyright (C) 2026 Maxtek Consulting + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +#ifndef WEBFRAME_URI_HPP +#define WEBFRAME_URI_HPP + +#include +#include +#include + +namespace webframe +{ + class uri + { + public: + uri(const std::string& str); + ~uri() = default; + + std::string get_scheme() const; + std::string get_host() const; + int get_port() const; + std::string get_path() const; + bool get_query(const std::string& key, std::string& value) const; + std::string get_fragment() const; + private: + static bool find_keyword(const std::string& input_url, size_t& st, size_t& before, const std::string& delim, std::string& result); + static bool split_query(const std::string& str, const std::string& delim, std::string& key, std::string& value); + void parse(const std::string& str); + std::string _scheme; + std::string _userinfo; + std::string _host; + int _port; + std::string _path; + std::unordered_map _query; + std::string _fragment; + }; +} + +#endif \ No newline at end of file diff --git a/src/runtimes/desktop/include/desktop/request.hpp b/src/runtimes/desktop/include/desktop/request.hpp index e27e677..a67b640 100644 --- a/src/runtimes/desktop/include/desktop/request.hpp +++ b/src/runtimes/desktop/include/desktop/request.hpp @@ -10,7 +10,11 @@ namespace webframe::desktop public: request(const wxWebViewHandlerRequest *request); ~request() = default; + webframe::method get_method() const override; + + std::string get_uri() const override; + std::string get_path() const override; bool get_header(const std::string &key, std::string &value) const override; std::pair get_body() const override; diff --git a/src/runtimes/desktop/request.cpp b/src/runtimes/desktop/request.cpp index 7521714..6bfbf8b 100644 --- a/src/runtimes/desktop/request.cpp +++ b/src/runtimes/desktop/request.cpp @@ -47,6 +47,11 @@ namespace webframe return std::string(uri_path.ToStdString()); } + std::string request::get_uri() const + { + return _request->GetURI().ToStdString(); + } + bool request::get_header(const std::string &key, std::string &value) const { wxString header_value = _request->GetHeader(key); diff --git a/src/runtimes/server/request.cpp b/src/runtimes/server/request.cpp index e8895b3..8a1afa8 100644 --- a/src/runtimes/server/request.cpp +++ b/src/runtimes/server/request.cpp @@ -36,6 +36,12 @@ namespace webframe::server } + std::string request::get_uri() const + { + const char *uri = evhttp_request_get_uri(_req); + return std::string(uri); + } + std::string request::get_path() const { const evhttp_uri *uri = evhttp_request_get_evhttp_uri(_req); diff --git a/src/runtimes/server/server.hpp b/src/runtimes/server/server.hpp index 1be501a..212d79f 100644 --- a/src/runtimes/server/server.hpp +++ b/src/runtimes/server/server.hpp @@ -16,6 +16,9 @@ namespace webframe::server ~request() = default; webframe::method get_method() const override; std::string get_path() const override; + + std::string get_uri() const override; + bool get_header(const std::string &key, std::string &value) const override; std::pair get_body() const override; void read_body(const std::function &callback) const override; diff --git a/src/uri.cpp b/src/uri.cpp new file mode 100644 index 0000000..53660b8 --- /dev/null +++ b/src/uri.cpp @@ -0,0 +1,210 @@ +#include + +namespace webframe +{ + uri::uri(const std::string &str) + { + parse(str); + } + + std::string uri::get_scheme() const + { + return _scheme; + } + + std::string uri::get_host() const + { + return _host; + } + + int uri::get_port() const + { + return _port; + } + + std::string uri::get_path() const + { + return _path; + } + + bool uri::get_query(const std::string &key, std::string &value) const + { + auto it = _query.find(key); + bool found(false); + if (it != _query.end()) + { + value = it->second; + found = true; + } + return found; + } + + std::string uri::get_fragment() const + { + return _fragment; + } + + bool uri::find_keyword(const std::string &input_url, size_t &st, size_t &before, const std::string &delim, std::string &result) + { + size_t temp_st = st; + + st = input_url.find(delim, before); + if (st == std::string::npos) + { + st = temp_st; + return false; + } + + result = input_url.substr(before, st - before); + before = st + delim.length(); + + if (result.empty()) + return false; + + return true; + } + + bool uri::split_query(const std::string &str, const std::string &delim, std::string &key, std::string &value) + { + size_t st = str.find(delim); + + if (st == std::string::npos) + { + key = str; + value = ""; + return false; + } + + key = str.substr(0, st); + value = str.substr(st + delim.length()); + + return true; + } + + void uri::parse(const std::string &str) + { + size_t st = 0; + size_t before = 0; + + // scheme 파싱 (예: "http", "https") + // has_authority: authority(host/userinfo/port) 파싱 여부를 결정하는 플래그 + // - "://" 있음 → scheme 있는 절대 URL + // - "//"로 시작 → scheme-relative URL + // - 그 외(상대 경로 등) → authority 없음, path 파싱만 수행 + bool has_authority = uri::find_keyword(str, st, before, "://", _scheme); + if (!has_authority && str.size() >= 2 && str[0] == '/' && str[1] == '/') + { + has_authority = true; + before = 2; + } + + if (has_authority) + { + // userinfo 파싱 — "@" 앞의 "user:pass" 부분 분리 + // authority 영역(다음 "/" 또는 "?" 또는 "#"까지)에서만 "@"를 탐색 + size_t authority_end = str.find_first_of("/?#", before); + size_t at_pos = str.find('@', before); + if (at_pos != std::string::npos && + (authority_end == std::string::npos || at_pos < authority_end)) + { + _userinfo = str.substr(before, at_pos - before); + before = at_pos + 1; + } + + // host 파싱 — "/" 또는 "?" 또는 "#"가 나오기 전까지가 host + // path가 없는 URL(예: http://example.com)도 처리 + size_t host_end = str.find_first_of("/?#", before); + if (host_end == std::string::npos) + { + _host = str.substr(before); + before = str.length(); + } + else + { + _host = str.substr(before, host_end - before); + before = host_end; + } + + _port = 8080; // default port + + // host에서 port 분리 (IPv6 bracketed notation 지원) + if (!_host.empty() && _host.front() == '[') + { + // IPv6: [2001:db8::1] 또는 [2001:db8::1]:8080 + size_t bracket_close = _host.find(']'); + if (bracket_close != std::string::npos) + { + // bracket 뒤에 ":port"가 있는지 확인 + if (bracket_close + 1 < _host.length() && _host[bracket_close + 1] == ':') + { + _port = std::atoi(_host.substr(bracket_close + 2).c_str()); + } + // bracket 안의 주소만 추출 ([ ] 제거) + _host = _host.substr(1, bracket_close - 1); + } + } + else + { + size_t colon_pos = _host.find(':'); + if (colon_pos != std::string::npos) + { + _port = std::atoi(_host.substr(colon_pos + 1).c_str()); + _host = _host.substr(0, colon_pos); + } + } + } + + // fragment(#) 분리 — 이후 path/query 파싱 범위를 제한 + size_t frag_pos = str.find('#', before); + size_t effective_end = (frag_pos != std::string::npos) ? frag_pos : str.length(); + + if (frag_pos != std::string::npos && frag_pos + 1 < str.length()) + { + _fragment = str.substr(frag_pos + 1); + } + + // path 파싱 — "?" 또는 "#" 이전까지의 "/" 구분 세그먼트 + size_t query_pos = str.find('?', before); + if (query_pos != std::string::npos && query_pos >= effective_end) + query_pos = std::string::npos; // "#" 뒤의 "?"는 무시 + + size_t path_end = effective_end; + if (query_pos != std::string::npos) + path_end = query_pos; + + _path = str.substr(before, path_end - before); + + // query string 파싱 — "#" 이전까지만 + if (query_pos != std::string::npos && query_pos + 1 < effective_end) + { + const std::string query_string = str.substr(query_pos + 1, effective_end - query_pos - 1); + + size_t q_before = 0; + while (q_before < query_string.length()) + { + size_t amp_pos = query_string.find('&', q_before); + std::string pair; + + if (amp_pos == std::string::npos) + { + pair = query_string.substr(q_before); + q_before = query_string.length(); + } + else + { + pair = query_string.substr(q_before, amp_pos - q_before); + q_before = amp_pos + 1; + } + + if (!pair.empty()) + { + std::string key, value; + uri::split_query(pair, "=", key, value); + if (!key.empty()) + _query.insert(std::unordered_map::value_type(key, value)); + } + } + } + } + +} \ No newline at end of file diff --git a/tests/include/test/message.hpp b/tests/include/test/message.hpp index 50d3cc1..e85b333 100644 --- a/tests/include/test/message.hpp +++ b/tests/include/test/message.hpp @@ -48,6 +48,7 @@ namespace webframe::test public: webframe::method get_method() const override; std::string get_path() const override; + std::string get_uri() const override { return ""; }; bool get_header(const std::string &key, std::string &value) const override; std::pair get_body() const override; void read_body(const std::function &callback) const override;