LCOV - code coverage report
Current view: top level - http/client - cookie.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 90.9 % 175 159
Test Date: 2026-11-10 10:38:07 Functions: 100.0 % 30 30

            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              : }
        

Generated by: LCOV version 2.0-1