Line data Source code
1 : #include "cookie.hpp"
2 : #include <algorithm>
3 : #include <cctype>
4 : #include <chrono>
5 : #include <format>
6 : #include <sstream>
7 : #include <iomanip>
8 : #include <ctime>
9 :
10 : namespace thinger::http {
11 :
12 372 : std::string cookie::trim(const std::string& str) {
13 372 : const auto start = str.find_first_not_of(" \t");
14 376 : if (start == std::string::npos) return "";
15 370 : const auto end = str.find_last_not_of(" \t");
16 370 : return str.substr(start, end - start + 1);
17 : }
18 :
19 222 : bool cookie::iequals(const std::string& a, const std::string& b) {
20 222 : if (a.size() != b.size()) return false;
21 102 : return std::equal(a.begin(), a.end(), b.begin(), [](char c1, char c2) {
22 562 : return std::tolower(static_cast<unsigned char>(c1)) ==
23 562 : std::tolower(static_cast<unsigned char>(c2));
24 102 : });
25 : }
26 :
27 : // Parse HTTP date format (RFC 7231)
28 6 : static int64_t parse_http_date(const std::string& date_str) {
29 6 : std::tm tm = {};
30 6 : std::istringstream ss(date_str);
31 :
32 : // Format 1: "Wdy, DD Mon YYYY HH:MM:SS GMT" (RFC 1123)
33 6 : ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S");
34 6 : if (!ss.fail()) {
35 6 : return static_cast<int64_t>(std::mktime(&tm));
36 : }
37 :
38 : // Format 2: "Wdy, DD-Mon-YY HH:MM:SS GMT" (RFC 850)
39 0 : ss.clear();
40 0 : ss.str(date_str);
41 0 : ss >> std::get_time(&tm, "%a, %d-%b-%y %H:%M:%S");
42 0 : if (!ss.fail()) {
43 0 : return static_cast<int64_t>(std::mktime(&tm));
44 : }
45 :
46 : // Format 3: "Wdy Mon DD HH:MM:SS YYYY" (asctime)
47 0 : ss.clear();
48 0 : ss.str(date_str);
49 0 : ss >> std::get_time(&tm, "%a %b %d %H:%M:%S %Y");
50 0 : if (!ss.fail()) {
51 0 : return static_cast<int64_t>(std::mktime(&tm));
52 : }
53 :
54 0 : return 0;
55 6 : }
56 :
57 60 : cookie::cookie(std::string name, std::string value)
58 60 : : name_(std::move(name)), value_(std::move(value)) {}
59 :
60 58 : std::vector<std::string> cookie::split_parts(const std::string& cookie_string) {
61 58 : std::vector<std::string> parts;
62 58 : std::string current;
63 1742 : for (char c : cookie_string) {
64 1684 : if (c == ';') {
65 76 : if (!current.empty()) {
66 76 : parts.push_back(trim(current));
67 76 : current.clear();
68 : }
69 : } else {
70 1608 : current += c;
71 : }
72 : }
73 58 : if (!current.empty()) {
74 56 : parts.push_back(trim(current));
75 : }
76 116 : return parts;
77 58 : }
78 :
79 58 : cookie cookie::parse(const std::string& cookie_string) {
80 58 : cookie result;
81 58 : auto parts = split_parts(cookie_string);
82 58 : if (parts.empty()) return result;
83 :
84 : // The first part is name=value: without '=', the cookie is invalid
85 56 : const auto& name_value = parts[0];
86 56 : auto eq_pos = name_value.find('=');
87 56 : if (eq_pos == std::string::npos) return result;
88 54 : result.name_ = trim(name_value.substr(0, eq_pos));
89 54 : result.value_ = trim(name_value.substr(eq_pos + 1));
90 :
91 130 : for (size_t i = 1; i < parts.size(); ++i) result.set_attribute(parts[i]);
92 54 : return result;
93 58 : }
94 :
95 76 : void cookie::set_attribute(const std::string& part) {
96 76 : auto eq_pos = part.find('=');
97 76 : if (eq_pos == std::string::npos) {
98 20 : std::string flag = trim(part);
99 40 : if (iequals(flag, "Secure")) {
100 12 : secure_ = true;
101 16 : } else if (iequals(flag, "HttpOnly")) {
102 8 : http_only_ = true;
103 : }
104 20 : return;
105 20 : }
106 :
107 56 : std::string attr_name = trim(part.substr(0, eq_pos));
108 56 : std::string attr_value = trim(part.substr(eq_pos + 1));
109 112 : if (iequals(attr_name, "Path")) {
110 16 : path_ = attr_value;
111 80 : } else if (iequals(attr_name, "Domain")) {
112 8 : domain_ = attr_value;
113 64 : } else if (iequals(attr_name, "Expires")) {
114 6 : expires_ = parse_http_date(attr_value);
115 52 : } else if (iequals(attr_name, "Max-Age")) {
116 12 : set_max_age_attribute(attr_value);
117 28 : } else if (iequals(attr_name, "SameSite")) {
118 28 : if (iequals(attr_value, "Strict")) {
119 6 : same_site_ = same_site_policy::strict;
120 16 : } else if (iequals(attr_value, "Lax")) {
121 4 : same_site_ = same_site_policy::lax;
122 8 : } else if (iequals(attr_value, "None")) {
123 4 : same_site_ = same_site_policy::none;
124 : }
125 : }
126 56 : }
127 :
128 12 : void cookie::set_max_age_attribute(const std::string& value) {
129 : try {
130 12 : int64_t max_age = std::stoll(value);
131 12 : max_age_ = max_age;
132 : // Expires too, from Max-Age
133 12 : auto expires_time = std::chrono::system_clock::now() + std::chrono::seconds(max_age);
134 12 : expires_ = std::chrono::system_clock::to_time_t(expires_time);
135 0 : } catch (...) {
136 : // An invalid Max-Age is ignored
137 0 : }
138 12 : }
139 :
140 : // Setters
141 4 : cookie& cookie::set_name(std::string name) {
142 4 : name_ = std::move(name);
143 4 : return *this;
144 : }
145 :
146 6 : cookie& cookie::set_value(std::string value) {
147 6 : value_ = std::move(value);
148 6 : return *this;
149 : }
150 :
151 6 : cookie& cookie::set_path(std::string path) {
152 6 : path_ = std::move(path);
153 6 : return *this;
154 : }
155 :
156 6 : cookie& cookie::set_domain(std::string domain) {
157 6 : domain_ = std::move(domain);
158 6 : return *this;
159 : }
160 :
161 6 : cookie& cookie::set_expires(int64_t expires) {
162 6 : expires_ = expires;
163 6 : return *this;
164 : }
165 :
166 10 : cookie& cookie::set_max_age(std::optional<int64_t> max_age) {
167 10 : max_age_ = max_age;
168 10 : return *this;
169 : }
170 :
171 8 : cookie& cookie::set_secure(bool secure) {
172 8 : secure_ = secure;
173 8 : return *this;
174 : }
175 :
176 6 : cookie& cookie::set_http_only(bool http_only) {
177 6 : http_only_ = http_only;
178 6 : return *this;
179 : }
180 :
181 8 : cookie& cookie::set_same_site(same_site_policy policy) {
182 8 : same_site_ = policy;
183 8 : return *this;
184 : }
185 :
186 : // Getters
187 66 : const std::string& cookie::get_name() const {
188 66 : return name_;
189 : }
190 :
191 36 : const std::string& cookie::get_value() const {
192 36 : return value_;
193 : }
194 :
195 14 : const std::string& cookie::get_path() const {
196 14 : return path_;
197 : }
198 :
199 10 : const std::string& cookie::get_domain() const {
200 10 : return domain_;
201 : }
202 :
203 10 : int64_t cookie::get_expires() const {
204 10 : return expires_;
205 : }
206 :
207 20 : std::optional<int64_t> cookie::get_max_age() const {
208 20 : return max_age_;
209 : }
210 :
211 16 : bool cookie::is_secure() const {
212 16 : return secure_;
213 : }
214 :
215 12 : bool cookie::is_http_only() const {
216 12 : return http_only_;
217 : }
218 :
219 18 : same_site_policy cookie::get_same_site() const {
220 18 : return same_site_;
221 : }
222 :
223 90 : bool cookie::is_valid() const {
224 90 : return !name_.empty();
225 : }
226 :
227 10 : bool cookie::is_expired() const {
228 : // Max-Age=0 or negative means cookie is expired (delete immediately)
229 10 : if (max_age_.has_value() && max_age_.value() <= 0) {
230 4 : return true;
231 : }
232 : // Session cookie (no expiry set) never expires
233 6 : if (expires_ == 0) {
234 2 : return false;
235 : }
236 4 : auto now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
237 4 : return expires_ < now;
238 : }
239 :
240 18 : std::string cookie::to_string() const {
241 18 : std::ostringstream oss;
242 18 : oss << name_ << "=" << value_;
243 :
244 18 : if (!path_.empty()) {
245 4 : oss << "; Path=" << path_;
246 : }
247 18 : if (!domain_.empty()) {
248 4 : oss << "; Domain=" << domain_;
249 : }
250 18 : if (max_age_.has_value()) {
251 4 : oss << "; Max-Age=" << max_age_.value();
252 14 : } else if (expires_ > 0) {
253 0 : const auto tp = std::chrono::system_clock::from_time_t(
254 0 : static_cast<std::time_t>(expires_));
255 0 : oss << "; Expires=" << std::format("{:%a, %d %b %Y %H:%M:%S} GMT", tp);
256 : }
257 18 : if (secure_) {
258 4 : oss << "; Secure";
259 : }
260 18 : if (http_only_) {
261 4 : oss << "; HttpOnly";
262 : }
263 18 : switch (same_site_) {
264 4 : case same_site_policy::strict:
265 4 : oss << "; SameSite=Strict";
266 4 : break;
267 12 : case same_site_policy::lax:
268 12 : oss << "; SameSite=Lax";
269 12 : break;
270 2 : case same_site_policy::none:
271 2 : oss << "; SameSite=None";
272 2 : break;
273 : }
274 :
275 36 : return oss.str();
276 18 : }
277 :
278 : }
|