LCOV - code coverage report
Current view: top level - http/client - cookie.cpp (source / functions) Coverage Total Hit
Test: coverage_filtered.info Lines: 89.6 % 173 155
Test Date: 2026-02-20 15:38:22 Functions: 100.0 % 27 27

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

Generated by: LCOV version 2.0-1