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