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