1 /*  $Id: ncbi_cookies.cpp 620642 2020-11-25 17:54:32Z lavr $
2  * ===========================================================================
3  *
4  *                            PUBLIC DOMAIN NOTICE
5  *               National Center for Biotechnology Information
6  *
7  *  This software/database is a "United States Government Work" under the
8  *  terms of the United States Copyright Act.  It was written as part of
9  *  the author's official duties as a United States Government employee and
10  *  thus cannot be copyrighted.  This software/database is freely available
11  *  to the public for use. The National Library of Medicine and the U.S.
12  *  Government have not placed any restriction on its use or reproduction.
13  *
14  *  Although all reasonable efforts have been taken to ensure the accuracy
15  *  and reliability of the software and data, the NLM and the U.S.
16  *  Government do not and cannot warrant the performance or results that
17  *  may be obtained by using this software or data. The NLM and the U.S.
18  *  Government disclaim all warranties, express or implied, including
19  *  warranties of performance, merchantability or fitness for any particular
20  *  purpose.
21  *
22  *  Please cite the author in any work or product based on this material.
23  *
24  * ===========================================================================
25  *
26  * Author:  Denis Vakatov
27  *
28  * File Description:
29  *   HTTP cookies:
30  *      CHttpCookie  - single cookie
31  *      CHttpCookies - set of cookies
32  */
33 
34 #include <ncbi_pch.hpp>
35 #include <corelib/ncbidiag.hpp>
36 #include <corelib/error_codes.hpp>
37 #include <corelib/ncbi_cookies.hpp>
38 
39 
40 #define NCBI_USE_ERRCODE_X Corelib_Cookies
41 
42 
43 BEGIN_NCBI_SCOPE
44 
45 ///////////////////////////////////////////////////////
46 //  CHttpCookie::
47 //
48 
49 
50 // Banned charachters for different parts of a cookie, used to validate
51 // incoming values and those set using SetXXXX().
52 // Control chars are also excluded for any part.
53 // Name banned chars.
54 static const char* kBannedChars_Name = "()<>@,;:\\\"/[]?={} \t";
55 // Value banned chars.
56 static const char* kBannedChars_Value = " \",;\\";
57 // Path banned chars.
58 static const char* kBannedChars_Path = ";";
59 // Extension banned chars.
60 static const char* kBannedChars_Extension = ";";
61 
62 // rfc1123-date = wkd, dd mmm yyyy hh:mm:ss GMT
63 static const CTimeFormat kCookieTimeFormat("w, D b Y h:m:s Z");
64 
65 
CHttpCookie(void)66 CHttpCookie::CHttpCookie(void)
67     : m_Expires(CTime::eEmpty, CTime::eGmt),
68       m_Secure(false),
69       m_HttpOnly(false),
70       m_Created(CTime::eCurrent, CTime::eGmt),
71       m_Accessed(CTime::eCurrent, CTime::eGmt),
72       m_HostOnly(false)
73 {
74 }
75 
76 
CHttpCookie(const CTempString & name,const CTempString & value,const CTempString & domain,const CTempString & path)77 CHttpCookie::CHttpCookie(const CTempString& name,
78                          const CTempString& value,
79                          const CTempString& domain,
80                          const CTempString& path)
81     : m_Name(name),
82       m_Value(value),
83       m_Path(path),
84       m_Expires(CTime::eEmpty, CTime::eGmt),
85       m_Secure(false),
86       m_HttpOnly(false),
87       m_Created(CTime::eCurrent, CTime::eGmt),
88       m_Accessed(CTime::eCurrent, CTime::eGmt),
89       m_HostOnly(false)
90 {
91     SetDomain(domain); // store canonical domain
92     if ( m_Name.empty() ) {
93         NCBI_THROW(CHttpCookieException, eValue, "Empty cookie name");
94     }
95 }
96 
97 
GetExpirationStr(void) const98 string CHttpCookie::GetExpirationStr(void) const
99 {
100     if ( m_Expires.IsEmpty() ) {
101         return kEmptyStr;
102     }
103 
104     return m_Expires.AsString(kCookieTimeFormat);
105 }
106 
107 
IsExpired(const CTime & now) const108 bool CHttpCookie::IsExpired(const CTime& now) const
109 {
110     return m_Expires.IsEmpty() ? false : m_Expires <= now;
111 }
112 
113 
IsValidValue(const string & value,EFieldType field,string * err_msg)114 bool CHttpCookie::IsValidValue(const string& value,
115                                EFieldType    field,
116                                string*       err_msg)
117 {
118     string attr;
119     bool allow_empty = true;
120     const char* banned;
121     switch ( field ) {
122     case eField_Name:
123         attr = "name";
124         allow_empty = false;
125         banned = kBannedChars_Name;
126         break;
127     case eField_Value:
128         attr = "value";
129         banned = kBannedChars_Value;
130         break;
131     case eField_Path:
132         attr = "path";
133         banned = kBannedChars_Path;
134         break;
135     case eField_Extension:
136         attr = "extension";
137         banned = kBannedChars_Extension;
138         break;
139     case eField_Domain:
140         {
141             // Domain = [alpha-num] + [alpha-num-hyphen]*
142             for (size_t pos = 0; pos < value.size(); ++pos) {
143                 char c = value[pos];
144                 if (pos > 0  &&  c == '-') {
145                     // non-leading hyphen is ok
146                     continue;
147                 }
148                 if (pos > 0  &&  c == '.'  &&  value[pos - 1] != '.') {
149                     // single non-leading dot is ok
150                     continue;
151                 }
152                 if ( !isalnum(value[pos]) ) {
153                     if ( err_msg ) {
154                         *err_msg = string("Banned char '") + value[pos] +
155                         "' in cookie domain: " + value +
156                         ", pos=" + NStr::SizetToString(pos);
157                     }
158                     return false;
159                 }
160             }
161             return true;
162         }
163     default:
164         // All other fields do not need validation.
165         return true;
166     }
167     _ASSERT(banned);
168     bool valid = allow_empty || !value.empty();
169     // Check banned chars.
170     string::size_type pos = value.find_first_of(banned);
171     if (pos != NPOS) {
172         valid = false;
173     }
174     else {
175         // Check control chars.
176         for (pos = 0; pos < value.size(); ++pos) {
177             if ( iscntrl(value[pos]) ) {
178                 valid = false;
179                 break;
180             }
181         }
182     }
183     if (!valid  &&  err_msg ) {
184         *err_msg = string("Banned char '") + value[pos] +
185             "' in cookie " + attr + ": " + value +
186             ", pos=" + NStr::SizetToString(pos);
187     }
188     return valid;
189 }
190 
191 
x_Validate(const string & value,EFieldType field) const192 void CHttpCookie::x_Validate(const string& value, EFieldType field) const
193 {
194     string err_msg;
195     switch ( field ) {
196     case eField_Name:
197         // Make sure name is valid, but do not encode.
198         if ( IsValidValue(value, eField_Name, &err_msg) ) return;
199     case eField_Value:
200         if ( IsValidValue(value, eField_Value, &err_msg) ) return;
201     case eField_Domain:
202         if ( IsValidValue(value, eField_Domain, &err_msg) ) return;
203     case eField_Path:
204         if ( IsValidValue(value, eField_Path, &err_msg) ) return;
205     case eField_Extension:
206         if ( IsValidValue(value, eField_Extension, &err_msg) ) return;
207     default:
208         return;
209     }
210     NCBI_THROW(CHttpCookieException, eValue, err_msg);
211 }
212 
213 
AsString(ECookieFormat format) const214 string CHttpCookie::AsString(ECookieFormat format) const
215 {
216     string ret;
217     x_Validate(m_Name, eField_Name);
218     x_Validate(m_Value, eField_Value);
219     x_Validate(m_Domain, eField_Domain);
220     x_Validate(m_Path, eField_Path);
221     x_Validate(m_Extension, eField_Extension);
222     switch ( format ) {
223     case eHTTPResponse:
224         {
225             ret = m_Name + "=";
226             if ( !m_Value.empty() ) {
227                 ret += m_Value;
228             }
229             if ( !m_Domain.empty() ) {
230                 ret += "; Domain=" + m_Domain;
231             }
232             if ( !m_Path.empty() ) {
233                 ret += "; Path=" + m_Path;
234             }
235             if ( !m_Expires.IsEmpty() ) {
236                 ret += "; Expires=" + GetExpirationStr();
237             }
238             if ( m_Secure ) {
239                 ret += "; Secure";
240             }
241             if ( m_HttpOnly ) {
242                 ret += "; HttpOnly";
243             }
244             if ( !m_Extension.empty() ) {
245                 ret += "; " + m_Extension;
246             }
247             break;
248         }
249     case eHTTPRequest:
250         {
251             ret = m_Name + "=";
252             if ( !m_Value.empty() ) {
253                 ret += m_Value;
254             }
255             // Clients should update last access time.
256             m_Accessed.SetCurrent();
257             break;
258         }
259     }
260     return ret;
261 }
262 
263 
264 // Helper function to sort cookies by name/domain/path/creation time.
sx_Compare(const CHttpCookie & c1,const CHttpCookie & c2)265 int CHttpCookie::sx_Compare(const CHttpCookie& c1, const CHttpCookie& c2)
266 {
267     PNocase nocase_cmp;
268     int x_cmp;
269 
270     // Longer domains go first.
271     x_cmp = int(c1.m_Domain.size() - c2.m_Domain.size());
272     if ( x_cmp ) {
273         return x_cmp;
274     }
275 
276     x_cmp = nocase_cmp(c1.m_Domain, c2.m_Domain);
277     if ( x_cmp ) {
278         return x_cmp;
279     }
280 
281     // Longer paths should go first.
282     x_cmp = int(c1.m_Path.size() - c2.m_Path.size());
283     if ( x_cmp ) {
284         return x_cmp;
285     }
286 
287     x_cmp = c1.m_Path.compare(c2.m_Path);
288     if ( x_cmp ) {
289         return x_cmp;
290     }
291 
292     x_cmp = nocase_cmp.Compare(c1.m_Name, c2.m_Name);
293     if ( x_cmp ) {
294         return x_cmp;
295     }
296 
297     // Since cookies are mapped by domain/path/name, we should never get here.
298     if (c1.m_Created != c2.m_Created) {
299         return c1.m_Created < c2.m_Created ? -1 : 1;
300     }
301 
302     return 0;
303 }
304 
305 
operator <(const CHttpCookie & cookie) const306 bool CHttpCookie::operator< (const CHttpCookie& cookie) const
307 {
308     return sx_Compare(*this, cookie) > 0;
309 }
310 
311 
operator ==(const CHttpCookie & cookie) const312 bool CHttpCookie::operator== (const CHttpCookie& cookie) const
313 {
314     return sx_Compare(*this, cookie) == 0;
315 }
316 
317 
Validate(void) const318 bool CHttpCookie::Validate(void) const
319 {
320     try {
321         if ( !IsValidValue(m_Name, eField_Name, NULL) ) return false;
322         if ( !IsValidValue(m_Value, eField_Value, NULL) ) return false;
323         if ( !IsValidValue(m_Domain, eField_Domain, NULL) ) return false;
324         if ( !IsValidValue(m_Path, eField_Path, NULL) ) return false;
325         if ( !IsValidValue(m_Extension, eField_Extension, NULL) ) return false;
326     }
327     catch (const CHttpCookieException&) {
328         return false;
329     }
330     return true;
331 }
332 
333 
334 // Returns time in seconds or -1
s_ParseTime(const string & value)335 int s_ParseTime(const string& value)
336 {
337     // Can not be shorter than 0:0:0
338     if (value.size() < 5) return -1;
339     int f[3] = {-1, -1, -1};
340     size_t p = 0;
341 
342     for (int i = 0; i < 3; ++i) {
343         if (p >= value.size()) break;
344         if ( !isdigit(value[p]) ) return -1;
345         f[i] = int(value[p] - '0');
346         ++p;
347         if (p >= value.size()) break;
348         if (value[p] != ':') {
349             if ( !isdigit(value[p]) ) return -1;
350             f[i] = f[i]*10 + int(value[p] - '0');
351             ++p;
352         }
353         if (p >= value.size()) break;
354         if (value[p] != ':') return -1;
355         ++p;
356     }
357 
358     // Not a time field
359     if (f[0] < 0  ||  f[1] < 0  ||  f[2] < 0) {
360         return -1;
361     }
362 
363     // Parsed, but the time is invalid.
364     if (f[0] > 23  ||  f[1] > 59  ||  f[2] > 59) return -2;
365 
366     return f[0]*3600 + f[1]*60 + f[2];
367 }
368 
369 
370 // Helper function to parse date and time.
s_ParseDateTime(const string & value)371 CTime s_ParseDateTime(const string& value)
372 {
373     static const char* kMonthNames = "jan feb mar apr may jun jul aug sep oct nov dec ";
374     static const char* kDayOfWeekNames = "sun mon tue wed thu fri sat ";
375 
376     // Parse expires - the format is rather flexible, so a predefined
377     // string format can not be used to initialize CTime.
378     size_t pos = 0;
379     size_t token_pos = 0;
380     int day = -1;
381     int mon = -1;
382     int year = -1;
383     int time = -1;
384     for (; pos <= value.size(); ++pos) {
385         char c = pos < value.size() ? value[pos] : ';';
386         // Wait for a delimiter or end of string.
387         if (isalnum(c)  ||  c == ':') continue;
388         // Anything non alphanumeric and not colon is field delimiter.
389         if (pos - token_pos < 1) {
390             // Merge delimiters.
391             token_pos = pos + 1;
392             continue;
393         }
394         string field = value.substr(token_pos, pos - token_pos);
395         token_pos = pos + 1;
396 
397         // Time
398         if (time < 0  &&  field.size() > 4  &&  (field[1] == ':'  ||  field[2] == ':')) {
399             time = s_ParseTime(field);
400             if (time >= 0) continue; // found time
401             if (time < -1) {
402                 time = -1;
403                 break;
404             }
405         }
406 
407         // Day
408         if (day < 0  &&  field.size() <= 2) {
409             day = NStr::StringToNumeric<int>(field, NStr::fConvErr_NoThrow);
410             if (day < 1  ||  day > 31) {
411                 day = -1;
412                 break;
413             }
414             continue;
415         }
416 
417         // Month
418         if (mon <= 0  &&  field.size() == 3) {
419             size_t mpos = NStr::FindNoCase(kMonthNames, field);
420             if (mpos != NPOS) {
421                 mon = int(mpos/4 + 1);
422                 continue;
423             }
424 
425             // Skip day of week and GMT.
426             mpos = NStr::FindNoCase(kDayOfWeekNames, field);
427             if (mpos != NPOS  ||  NStr::EqualNocase(field, "GMT") ) {
428                 continue;
429             }
430 
431             mon = -1;
432             break;
433         }
434 
435         // Year
436         if (year < 0  &&  (field.size() == 2  ||  field.size() == 4)) {
437             year = NStr::StringToNumeric<int>(field, NStr::fConvErr_NoThrow);
438             if (year == 0  &&  errno != 0) {
439                 year = -1;
440                 continue;
441             }
442             if (year < 100) {
443                 year += (year < 70) ? 2000 : 1900;
444             }
445             if (year < 1601) {
446                 year = -1;
447                 break;
448             }
449         }
450     }
451 
452     if (time < 0  ||  day < 0  ||  mon < 0  ||  year < 0) {
453         return CTime(CTime::eEmpty);
454     }
455     CTime ret(year, mon, day, 0, 0, 0, 0, CTime::eGmt);
456     ret.AddSecond(time);
457     return ret;
458 }
459 
460 
Parse(const CTempString & str)461 bool CHttpCookie::Parse(const CTempString& str)
462 {
463     // Reset all fields.
464     m_Name.clear();
465     m_Value.clear();
466     m_Domain.clear();
467     m_Path.clear();
468     m_Expires.Clear();
469     m_Secure = false;
470     m_HttpOnly = false;
471     m_Extension.clear();
472     m_HostOnly = false;
473     // Update the creation and access time to current.
474     m_Created.SetCurrent();
475     m_Accessed.SetCurrent();
476 
477     string err_msg;
478     size_t pos = str.find(';');
479     string nv = str.substr(0, pos);
480     string attr_str = str.substr(pos + 1);
481     pos = nv.find('=');
482     if (pos == NPOS) {
483         m_Name = nv;
484         ERR_POST_X(1, Info << "Missing value for cookie: " << m_Name);
485         return false;
486     }
487     m_Name = NStr::TruncateSpaces(nv.substr(0, pos));
488     if ( m_Name.empty() ) {
489         ERR_POST_X(2, Info << "Empty cookie name.");
490         return false;
491     }
492     if ( !IsValidValue(m_Name, eField_Name, &err_msg) ) {
493         ERR_POST_X(3, Info << err_msg);
494         return false;
495     }
496     m_Value = NStr::TruncateSpaces(nv.substr(pos + 1));
497     if ( !IsValidValue(m_Value, eField_Value, &err_msg) ) {
498         ERR_POST_X(3, Info << err_msg);
499         return false;
500     }
501     // Remove dquotes if any.
502     if (m_Value.size() > 2  &&  m_Value[0] == '"'  &&  m_Value[m_Value.size() - 1] == '"') {
503         m_Value = m_Value.substr(1, m_Value.size() - 2);
504     }
505 
506     if ( attr_str.empty() ) {
507         return true;
508     }
509 
510     // Parse additional attributes.
511     list<string> attrs;
512     NStr::Split(attr_str, ";", attrs,
513         NStr::fSplit_MergeDelimiters | NStr::fSplit_Truncate);
514     string expires, maxage;
515     ITERATE(list<string>, it, attrs) {
516         pos = it->find('=');
517         string name = NStr::TruncateSpaces(it->substr(0, pos));
518         string value;
519         if (pos != NPOS) {
520             value = NStr::TruncateSpaces(it->substr(pos + 1));
521         }
522         // Assume all values are valid. If they are not, exception
523         // will be thrown on an attempt to write the cookie.
524         if ( NStr::EqualNocase(name, "domain") ) {
525             m_Domain = value;
526             NStr::ToLower(m_Domain);
527             if ( NStr::EndsWith(m_Domain, '.') ) {
528                 // Ignore domain if it ends with '.'
529                 m_Domain.clear();
530             }
531             // If the domain is missing, set it to the request host.
532             if ( m_Domain.empty() ) {
533                 m_HostOnly = true;
534             }
535             else {
536                 // Ignore leading '.'
537                 if (m_Domain[0] == '.') {
538                     m_Domain = m_Domain.substr(1);
539                 }
540             }
541             if ( !IsValidValue(m_Domain, eField_Domain, &err_msg) ) {
542                 ERR_POST_X(4, Info << err_msg);
543                 return false;
544             }
545         }
546         else if ( NStr::EqualNocase(name, "path") ) {
547             m_Path = value;
548             if ( !IsValidValue(m_Path, eField_Path, &err_msg) ) {
549                 ERR_POST_X(5, Info << err_msg);
550                 return false;
551             }
552         }
553         else if ( NStr::EqualNocase(name, "expires") ) {
554             expires = value;
555         }
556         else if ( NStr::EqualNocase(name, "max-age") ) {
557             maxage = value;
558         }
559         else if ( NStr::EqualNocase(name, "secure") ) {
560             m_Secure = true;
561         }
562         else if ( NStr::EqualNocase(name, "httponly") ) {
563             m_HttpOnly = true;
564         }
565         else {
566             // All unsupported attributes go to extension field.
567             if ( !m_Extension.empty() ) {
568                 m_Extension += "; ";
569             }
570             if ( !name.empty() ) {
571                 m_Extension += name;
572                 if ( !value.empty() ) {
573                     m_Extension += "=";
574                 }
575             }
576             if ( !value.empty() ) {
577                 m_Extension += value;
578             }
579         }
580     }
581     // Prefer Max-Age over Expires.
582     if ( !maxage.empty() ) {
583         // Parse max-age
584         Uint8 sec = NStr::StringToNumeric<Uint8>(maxage);
585         if (sec == 0  &&  errno) {
586             ERR_POST_X(6, Info << "Invalid MaxAge value in cookie: " << maxage);
587             return false;
588         }
589         else {
590             m_Expires.SetCurrent();
591             m_Expires.AddSecond(sec);
592             m_Expires.SetTimeZone(CTime::eGmt);
593         }
594     }
595     else if ( !expires.empty() ) {
596         m_Expires = s_ParseDateTime(expires);
597         if ( m_Expires.IsEmpty() ) {
598             ERR_POST_X(7, Info << "Invalid Expires value in cookie: " << expires);
599             return false;
600         }
601     }
602     return true;
603 }
604 
605 
Match(const CUrl & url) const606 bool CHttpCookie::Match(const CUrl& url) const
607 {
608     if ( url.IsEmpty() ) {
609         return true;
610     }
611     // Check scheme.
612     bool secure = NStr::EqualNocase("https", url.GetScheme());
613     bool http = secure || NStr::EqualNocase("http", url.GetScheme());
614     if ((m_Secure  &&  !secure)  ||  (m_HttpOnly  && !http)) {
615         return false;
616     }
617 
618     if ( !MatchDomain(url.GetHost()) ) {
619         return false;
620     }
621 
622     if ( !MatchPath(url.GetPath()) ) {
623         return false;
624     }
625 
626     return true;
627 }
628 
629 
MatchDomain(const string & host) const630 bool CHttpCookie::MatchDomain(const string& host) const
631 {
632     string h = host;
633     NStr::ToLower(h);
634     if ( m_HostOnly ) {
635         return host == m_Domain;
636     }
637     size_t pos = h.find(m_Domain);
638     // Domain matching: cookie domain must be identical to host,
639     // or be a suffix of host and the last char before the suffix
640     // must be '.'.
641     if (pos == NPOS  ||
642         pos + m_Domain.size() != h.size()  ||
643         (pos > 0  &&  h[pos - 1] != '.')) {
644         return false;
645     }
646     return true;
647 }
648 
649 
MatchPath(const string & path) const650 bool CHttpCookie::MatchPath(const string& path) const
651 {
652     if ( m_Path.empty() ) {
653         // Treat empty path as root ('/').
654         return true;
655     }
656     string p = path;
657     // Truncate path to the last (or the only one) '/' char.
658     size_t last_sep = p.find('/');
659     if (last_sep != NPOS) {
660         size_t next;
661         while ((next = p.find('/', last_sep + 1)) != NPOS) {
662             last_sep = next;
663         }
664     }
665     if (p.empty()  ||  p[0] != '/'  ||  last_sep == NPOS) {
666         p = '/';
667     }
668     else if (last_sep > 0) {
669         p = p.substr(0, last_sep);
670     }
671 
672     if ( !NStr::StartsWith(p, m_Path) ) {
673         return false;
674     }
675     if (m_Path != p  &&  m_Path[m_Path.size() - 1] != '/'  &&  p[m_Path.size()] != '/') {
676         return false;
677     }
678     return true;
679 }
680 
681 
Reset(void)682 void CHttpCookie::Reset(void)
683 {
684     m_Value.clear();
685     m_Domain.clear();
686     m_Path.clear();
687     m_Expires.Clear();
688     m_Secure = false;
689     m_HttpOnly = false;
690     m_Extension.clear();
691     m_Created.Clear();
692     m_Accessed.Clear();
693     m_HostOnly = false;
694 }
695 
696 
697 ///////////////////////////////////////////////////////
698 //  CHttpCookies::
699 //
700 
701 
~CHttpCookies(void)702 CHttpCookies::~CHttpCookies(void)
703 {
704 }
705 
706 
Add(const CHttpCookie & cookie)707 void CHttpCookies::Add(const CHttpCookie& cookie)
708 {
709     CHttpCookie* found = x_Find(
710         cookie.GetDomain(), cookie.GetPath(), cookie.GetName());
711     if ( found ) {
712         *found = cookie;
713     }
714     else {
715         m_CookieMap[sx_RevertDomain(cookie.GetDomain())].push_back(cookie);
716     }
717 }
718 
719 
Add(ECookieHeader header,const CTempString & str,const CUrl * url)720 size_t CHttpCookies::Add(ECookieHeader header,
721                          const CTempString& str,
722                          const CUrl*   url)
723 {
724     // Check header type, if Cookie - split at ';' and
725     // process each name/value pair. Otherwise process
726     // the whole line as a single Set-Cookie.
727     CHttpCookie cookie;
728     size_t count = 0;
729     if (header == eHeader_Cookie) {
730         list<string> cookies;
731         NStr::Split(str, ";", cookies,
732             NStr::fSplit_MergeDelimiters | NStr::fSplit_Truncate);
733         ITERATE(list<string>, it, cookies) {
734             if ( cookie.Parse(*it) ) {
735                 Add(cookie);
736                 ++count;
737             }
738         }
739     }
740     else {
741         // Set-Cookie
742         if ( !cookie.Parse(str) ) {
743             return 0;
744         }
745 
746         // Validate the new cookie against the url.
747         // NOTE: If there were any redirects, the effective URL may be
748         // different from the original one. Caller must take care of this
749         // case and provide the actual URL.
750         if ( url ) {
751             if ( cookie.GetDomain().empty() ) {
752                 cookie.SetDomain(url->GetHost());
753                 cookie.SetHostOnly(true);
754             }
755             if ( cookie.GetPath().empty() ) {
756                 cookie.SetPath(url->GetPath());
757             }
758             // Check if there's an existing cookie with different http/secure
759             // flags.
760             CHttpCookie* found = x_Find(
761                 cookie.GetDomain(), cookie.GetPath(), cookie.GetName());
762             if (found  &&  !found->Match(*url)) {
763                 return 0;
764             }
765             // The new cookie must match the originating host/path.
766             if ( !cookie.Match(*url) ) {
767                 return 0;
768             }
769         }
770         Add(cookie);
771         // A server may send expired cookie to remove it.
772         if ( cookie.IsExpired() ) {
773             Cleanup();
774             count = 0;
775         }
776     }
777     return count;
778 }
779 
780 
781 typedef pair<string, size_t> TDomainCount;
782 typedef list<TDomainCount> TDomainList;
783 
784 // Helper function to sort domains by nuber of cookies, descending.
s_DomainCountLess(const TDomainCount & dc1,const TDomainCount & dc2)785 static bool s_DomainCountLess(const TDomainCount& dc1, const TDomainCount& dc2)
786 {
787     return dc1.second > dc2.second;
788 }
789 
790 
Cleanup(size_t max_count)791 void CHttpCookies::Cleanup(size_t max_count)
792 {
793     size_t count = 0;
794     // First remove expired cookies.
795     // While doing this also collect number of cookies for each domain.
796     TDomainList domains;
797     ERASE_ITERATE(TCookieMap, map_it, m_CookieMap) {
798         ERASE_ITERATE(TCookieList, list_it, map_it->second) {
799             if ( list_it->IsExpired() ) {
800                 map_it->second.erase(list_it);
801             }
802         }
803         if ( map_it->second.empty() ) {
804             m_CookieMap.erase(map_it);
805         }
806         else {
807             TDomainCount dc(map_it->first, map_it->second.size());
808             count += dc.second;
809             domains.push_back(dc);
810         }
811     }
812     // Below the goal?
813     if (max_count == 0  ||  count <= max_count) {
814         return;
815     }
816 
817     // Next step is to remove domains with max number of cookies.
818     domains.sort(s_DomainCountLess);
819     ITERATE(TDomainList, it, domains) {
820         TCookieMap::iterator dit = m_CookieMap.find(it->first);
821         _ASSERT(dit != m_CookieMap.end());
822         count -= it->second;
823         m_CookieMap.erase(dit);
824         if (count <= max_count) {
825             return;
826         }
827     }
828     // Still above the limit - remove all cookies.
829     m_CookieMap.clear();
830 }
831 
832 
x_Find(const string & domain,const string & path,const string & name)833 CHttpCookie* CHttpCookies::x_Find(const string& domain,
834                                   const string& path,
835                                   const string& name)
836 {
837     string rdomain = sx_RevertDomain(domain);
838     TCookieMap::iterator domain_it = m_CookieMap.lower_bound(rdomain);
839     if (domain_it != m_CookieMap.end()  &&  domain_it->first == rdomain) {
840         NON_CONST_ITERATE(TCookieList, it, domain_it->second) {
841             if (path == it->GetPath()  &&
842                 NStr::EqualNocase(name, it->GetName())) {
843                 return &(*it);
844             }
845         }
846     }
847     return 0;
848 }
849 
850 
sx_RevertDomain(const string & domain)851 string CHttpCookies::sx_RevertDomain(const string& domain)
852 {
853     list<string> names;
854     NStr::Split(domain, ".", names,
855         NStr::fSplit_MergeDelimiters | NStr::fSplit_Truncate);
856     string ret;
857     REVERSE_ITERATE(list<string>, it, names) {
858         if ( !ret.empty() ) {
859             ret += '.';
860         }
861         ret += *it;
862     }
863     return ret;
864 }
865 
866 
867 ///////////////////////////////////////////////////////
868 //  CHttpCookie_CI::
869 //
870 
871 
CHttpCookie_CI(void)872 CHttpCookie_CI::CHttpCookie_CI(void)
873     : m_Cookies(0)
874 {
875 }
876 
877 
CHttpCookie_CI(const CHttpCookies & cookies,const CUrl * url)878 CHttpCookie_CI::CHttpCookie_CI(const CHttpCookies& cookies, const CUrl* url)
879     : m_Cookies(&cookies)
880 {
881     if ( url ) {
882         m_Url = *url;
883     }
884     m_MapIt = url ?
885         m_Cookies->m_CookieMap.lower_bound(
886         CHttpCookies::sx_RevertDomain(m_Url.GetHost())) :
887         m_Cookies->m_CookieMap.begin();
888     if (m_MapIt != m_Cookies->m_CookieMap.end()) {
889         m_ListIt = m_MapIt->second.begin();
890     }
891     else {
892         m_Cookies = NULL;
893     }
894     x_Settle();
895 }
896 
897 
CHttpCookie_CI(const CHttpCookie_CI & other)898 CHttpCookie_CI::CHttpCookie_CI(const CHttpCookie_CI& other)
899 {
900     *this = other;
901 }
902 
903 
operator =(const CHttpCookie_CI & other)904 CHttpCookie_CI& CHttpCookie_CI::operator=(const CHttpCookie_CI& other)
905 {
906     if (this != &other) {
907         m_Cookies = other.m_Cookies;
908         if ( m_Cookies ) {
909             m_MapIt = other.m_MapIt;
910             m_ListIt = other.m_ListIt;
911         }
912     }
913     return *this;
914 }
915 
916 
operator ++(void)917 CHttpCookie_CI& CHttpCookie_CI::operator++(void)
918 {
919     x_CheckState();
920     x_Next();
921     x_Settle();
922     return *this;
923 }
924 
925 
operator *(void) const926 const CHttpCookie& CHttpCookie_CI::operator*(void) const
927 {
928     x_CheckState();
929     return *m_ListIt;
930 }
931 
932 
operator ->(void) const933 const CHttpCookie* CHttpCookie_CI::operator->(void) const
934 {
935     x_CheckState();
936     return &(*m_ListIt);
937 }
938 
939 
x_IsValid(void) const940 bool CHttpCookie_CI::x_IsValid(void) const
941 {
942     // All internal iterators must be valid.
943     if (!m_Cookies  ||
944         m_MapIt == m_Cookies->m_CookieMap.end()  ||
945         m_ListIt == m_MapIt->second.end()) return false;
946     // Check if cookie matches the filter.
947     return m_ListIt->Match(m_Url);
948 }
949 
950 
x_Compare(const CHttpCookie_CI & other) const951 int CHttpCookie_CI::x_Compare(const CHttpCookie_CI& other) const
952 {
953     if ( !m_Cookies ) {
954         // null <= anything
955         return other.m_Cookies ? -1 : 0;
956     }
957     if ( !other.m_Cookies ) {
958         // not-null > null
959         return 1;
960     }
961     if (m_Cookies != other.m_Cookies) {
962         return m_Cookies < other.m_Cookies;
963     }
964 
965     // If m_Cookies != null, both iterators must be valid.
966     _ASSERT(m_MapIt != m_Cookies->m_CookieMap.end());
967     _ASSERT(m_ListIt != m_MapIt->second.end());
968     _ASSERT(other.m_MapIt != m_Cookies->m_CookieMap.end());
969     _ASSERT(other.m_ListIt != other.m_MapIt->second.end());
970 
971     if (m_MapIt != other.m_MapIt) {
972         return m_MapIt->first < other.m_MapIt->first ? -1 : 1;
973     }
974     if (m_ListIt != other.m_ListIt) {
975             return *m_ListIt < *other.m_ListIt;
976     }
977     return 0;
978 }
979 
980 
x_CheckState(void) const981 void CHttpCookie_CI::x_CheckState(void) const
982 {
983     if ( x_IsValid() ) return;
984     NCBI_THROW(CHttpCookieException, eIterator, "Bad cookie iterator state");
985 }
986 
987 
x_Next(void)988 void CHttpCookie_CI::x_Next(void)
989 {
990     if (m_ListIt != m_MapIt->second.end()) {
991         ++m_ListIt;
992     }
993     else {
994         ++m_MapIt;
995         if (m_MapIt != m_Cookies->m_CookieMap.end()) {
996             m_ListIt = m_MapIt->second.begin();
997         }
998         else {
999             m_Cookies = NULL;
1000         }
1001     }
1002 }
1003 
1004 
x_Settle(void)1005 void CHttpCookie_CI::x_Settle(void)
1006 {
1007     while ( m_Cookies  &&  !x_IsValid() ) {
1008         x_Next();
1009     }
1010 }
1011 
1012 
GetErrCodeString(void) const1013 const char* CHttpCookieException::GetErrCodeString(void) const
1014 {
1015     switch (GetErrCode()) {
1016     case eValue:    return "Bad cookie";
1017     case eIterator: return "Ivalid cookie iterator";
1018     default:        return CException::GetErrCodeString();
1019     }
1020 }
1021 
1022 
1023 END_NCBI_SCOPE
1024