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