String CookieManager::generateHtmlFragmentForCookies() { CookieLog("CookieManager - generateHtmlFragmentForCookies\n"); Vector<ParsedCookie*> cookieCandidates; for (HashMap<String, CookieMap*>::iterator it = m_managerMap.begin(); it != m_managerMap.end(); ++it) it->second->getAllChildCookies(&cookieCandidates); String result; ParsedCookie* cookie = 0; result.append(String("<table style=\"word-wrap:break-word\" cellSpacing=\"0\" cellPadding=\"0\" border=\"1\"><tr><th>Domain</th><th>Path</th><th>Protocol</th><th>Name</th><th>Value</th><th>Secure</th><th>HttpOnly</th><th>Session</th></tr>")); for (size_t i = 0; i < cookieCandidates.size(); ++i) { cookie = cookieCandidates[i]; result.append(String("<tr><td align=\"center\">")); result.append(cookie->domain()); result.append(String("<td align=\"center\">")); result.append(cookie->path()); result.append(String("<td align=\"center\">")); result.append(cookie->protocol()); result.append(String("<td align=\"center\">")); result.append(cookie->name()); result.append(String("<td align=\"center\" style= \"word-break:break-all\">")); result.append(cookie->value()); result.append(String("<td align=\"center\">")); result.append(String(cookie->isSecure() ? "Yes" : "No")); result.append(String("<td align=\"center\">")); result.append(String(cookie->isHttpOnly() ? "Yes" : "No")); result.append(String("<td align=\"center\">")); result.append(String(cookie->isSession() ? "Yes" : "No")); result.append(String("</td></tr>")); } result.append(String("</table>")); return result; }
void CookieManager::getBackingStoreCookies() { Vector<ParsedCookie*> cookies = cookieBackingStore().getAllCookies(); for (size_t i = 0; i < cookies.size(); ++i) { ParsedCookie* newCookie = cookies[i]; if (newCookie->hasExpired()) { cookieBackingStore().remove(newCookie); delete newCookie; } else { CookieMap* curMap = m_managerMap.get(newCookie->domain()); if (!curMap) { curMap = new CookieMap(); m_managerMap.add(newCookie->domain(), curMap); } // Use the straightforward add curMap->add(newCookie); m_count++; } } }
// The cookie String passed into this method will only contian the name value pairs as well as other related cookie // attributes such as max-age and domain. Set-Cookie should never be part of this string. ParsedCookie* CookieParser::parseOneCookie(const String& cookie, unsigned start, unsigned end, double curTime) { ParsedCookie* res = new ParsedCookie(curTime); if (!res) LOG_AND_DELETE("Out of memory"); res->setProtocol(m_defaultCookieURL.protocol()); // Parse [NAME "="] VALUE unsigned tokenEnd = start; // Token end contains the position of the '=' or the end of a token unsigned pairEnd = start; // Pair end contains always the position of the ';' // find the *first* ';' and the '=' (if they exist) bool quoteFound = false; bool foundEqual = false; while (pairEnd < end && (cookie[pairEnd] != ';' || quoteFound)) { if (tokenEnd == start && cookie[pairEnd] == '=') { tokenEnd = pairEnd; foundEqual = true; } if (cookie[pairEnd] == '"') quoteFound = !quoteFound; pairEnd++; } unsigned tokenStart = start; bool hasName = false; // This is a hack to avoid changing too much in this // brutally brittle code. if (tokenEnd != start) { // There is a '=' so parse the NAME unsigned nameEnd = tokenEnd; // The tokenEnd is the position of the '=' so the nameEnd is one less nameEnd--; // Remove lightweight spaces. while (nameEnd && isLightweightSpace(cookie[nameEnd])) nameEnd--; while (tokenStart < nameEnd && isLightweightSpace(cookie[tokenStart])) tokenStart++; if (nameEnd + 1 <= tokenStart) LOG_AND_DELETE("Empty name. Rejecting the cookie"); String name = cookie.substring(tokenStart, nameEnd + 1 - start); res->setName(name); hasName = true; } // Now parse the VALUE tokenStart = tokenEnd + 1; if (!hasName) --tokenStart; // Skip lightweight spaces in our token while (tokenStart < pairEnd && isLightweightSpace(cookie[tokenStart])) tokenStart++; tokenEnd = pairEnd; while (tokenEnd > tokenStart && isLightweightSpace(cookie[tokenEnd - 1])) tokenEnd--; String value; if (tokenEnd == tokenStart) { // Firefox accepts empty value so we will do the same value = String(); } else value = cookie.substring(tokenStart, tokenEnd - tokenStart); if (hasName) res->setValue(value); else if (foundEqual) { delete res; return 0; } else res->setName(value); // No NAME=VALUE, only NAME while (pairEnd < end) { // Switch to the next pair as pairEnd is on the ';' and fast-forward any lightweight spaces. pairEnd++; while (pairEnd < end && isLightweightSpace(cookie[pairEnd])) pairEnd++; tokenStart = pairEnd; tokenEnd = tokenStart; // initialize token end to catch first '=' while (pairEnd < end && cookie[pairEnd] != ';') { if (tokenEnd == tokenStart && cookie[pairEnd] == '=') tokenEnd = pairEnd; pairEnd++; } // FIXME : should we skip lightweight spaces here ? unsigned length = tokenEnd - tokenStart; unsigned tokenStartSvg = tokenStart; String parsedValue; if (tokenStart != tokenEnd) { // There is an equal sign so remove lightweight spaces in VALUE tokenStart = tokenEnd + 1; while (tokenStart < pairEnd && isLightweightSpace(cookie[tokenStart])) tokenStart++; tokenEnd = pairEnd; while (tokenEnd > tokenStart && isLightweightSpace(cookie[tokenEnd - 1])) tokenEnd--; parsedValue = cookie.substring(tokenStart, tokenEnd - tokenStart); } else { // If the parsedValue is empty, initialise it in case we need it parsedValue = String(); // Handle a token without value. length = pairEnd - tokenStart; } // Detect which "cookie-av" is parsed // Look at the first char then parse the whole for performance issue switch (cookie[tokenStartSvg]) { case 'P': case 'p' : { if (length >= 4 && cookie.find("ath", tokenStartSvg + 1, false)) { // We need the path to be decoded to match those returned from KURL::path(). // The path attribute may or may not include percent-encoded characters. Fortunately // if there are no percent-encoded characters, decoding the url is a no-op. res->setPath(decodeURLEscapeSequences(parsedValue)); } else LOG_AND_DELETE("Invalid cookie %s (path)", cookie.ascii().data()); break; } case 'D': case 'd' : { if (length >= 6 && cookie.find("omain", tokenStartSvg + 1, false)) { if (parsedValue.length() > 1 && parsedValue[0] == '"' && parsedValue[parsedValue.length() - 1] == '"') parsedValue = parsedValue.substring(1, parsedValue.length() - 2); // If the domain does not start with a dot, add one for security checks, // For example: ab.c.com dose not domain match b.c.com; String realDomain = parsedValue[0] == '.' ? parsedValue : "." + parsedValue; res->setDomain(realDomain); } else LOG_AND_DELETE("Invalid cookie %s (domain)", cookie.ascii().data()); break; } case 'E' : case 'e' : { if (length >= 7 && cookie.find("xpires", tokenStartSvg + 1, false)) res->setExpiry(parsedValue); else LOG_AND_DELETE("Invalid cookie %s (expires)", cookie.ascii().data()); break; } case 'M' : case 'm' : { if (length >= 7 && cookie.find("ax-age", tokenStartSvg + 1, false)) res->setMaxAge(parsedValue); else LOG_AND_DELETE("Invalid cookie %s (max-age)", cookie.ascii().data()); break; } case 'C' : case 'c' : { if (length >= 7 && cookie.find("omment", tokenStartSvg + 1, false)) // We do not have room for the comment part (and so do Mozilla) so just log the comment. LOG(Network, "Comment %s for ParsedCookie : %s\n", parsedValue.ascii().data(), cookie.ascii().data()); else LOG_AND_DELETE("Invalid cookie %s (comment)", cookie.ascii().data()); break; } case 'V' : case 'v' : { if (length >= 7 && cookie.find("ersion", tokenStartSvg + 1, false)) { // Although the out-of-dated Cookie Spec(RFC2965, http://tools.ietf.org/html/rfc2965) defined // the value of version can only contain DIGIT, some random sites, e.g. https://devforums.apple.com // would use double quotation marks to quote the digit. So we need to get rid of them for compliance. if (parsedValue.length() > 1 && parsedValue[0] == '"' && parsedValue[parsedValue.length() - 1] == '"') parsedValue = parsedValue.substring(1, parsedValue.length() - 2); if (parsedValue.toInt() != 1) LOG_AND_DELETE("ParsedCookie version %d not supported (only support version=1)", parsedValue.toInt()); } else LOG_AND_DELETE("Invalid cookie %s (version)", cookie.ascii().data()); break; } case 'S' : case 's' : { // Secure is a standalone token ("Secure;") if (length >= 6 && cookie.find("ecure", tokenStartSvg + 1, false)) res->setSecureFlag(true); else LOG_AND_DELETE("Invalid cookie %s (secure)", cookie.ascii().data()); break; } case 'H': case 'h': { // HttpOnly is a standalone token ("HttpOnly;") if (length >= 8 && cookie.find("ttpOnly", tokenStartSvg + 1, false)) res->setIsHttpOnly(true); else LOG_AND_DELETE("Invalid cookie %s (HttpOnly)", cookie.ascii().data()); break; } default : { // If length == 0, we should be at the end of the cookie (case : ";\r") so ignore it if (length) LOG_ERROR("Invalid token for cookie %s", cookie.ascii().data()); } } } // Check if the cookie is valid with respect to the size limit. if (!res->isUnderSizeLimit()) LOG_AND_DELETE("ParsedCookie %s is above the 4kb in length : REJECTED", cookie.ascii().data()); // If some pair was not provided, during parsing then apply some default value // the rest has been done in the constructor. // If no domain was provided, set it to the host if (!res->domain()) res->setDefaultDomain(m_defaultCookieURL); // According to the Cookie Specificaiton (RFC6265, section 4.1.2.4 and 5.2.4, http://tools.ietf.org/html/rfc6265), // If no path was provided or the first character of the path value is not '/', set it to the host's path // // REFERENCE // 4.1.2.4. The Path Attribute // // The scope of each cookie is limited to a set of paths, controlled by // the Path attribute. If the server omits the Path attribute, the user // agent will use the "directory" of the request-uri's path component as // the default value. (See Section 5.1.4 for more details.) // ........... // 5.2.4. The Path Attribute // // If the attribute-name case-insensitively matches the string "Path", // the user agent MUST process the cookie-av as follows. // // If the attribute-value is empty or if the first character of the // attribute-value is not %x2F ("/"): // // Let cookie-path be the default-path. // // Otherwise: // // Let cookie-path be the attribute-value. // // Append an attribute to the cookie-attribute-list with an attribute- // name of Path and an attribute-value of cookie-path. if (!res->path() || !res->path().length() || !res->path().startsWith("/", false)) { String path = m_defaultCookieURL.string().substring(m_defaultCookieURL.pathStart(), m_defaultCookieURL.pathAfterLastSlash() - m_defaultCookieURL.pathStart() - 1); if (path.isEmpty()) path = "/"; // Since this is reading the raw url string, it could contain percent-encoded sequences. We // want it to be comparable to the return value of url.path(), which is not percent-encoded, // so we must remove the escape sequences. res->setPath(decodeURLEscapeSequences(path)); } return res; }
void CookieDatabaseBackingStore::invokeSendChangesToDatabase() { ASSERT(isCurrentThread()); if (!m_db.isOpen()) { LOG_ERROR("Timer Fired, but database is closed."); return; } Vector<CookieAction> changedCookies; { MutexLocker lock(m_mutex); changedCookies.swap(m_changedCookies); ASSERT(m_changedCookies.isEmpty()); } if (changedCookies.isEmpty()) { CookieLog("CookieBackingStore - Timer fired, but no cookies in changelist"); return; } CookieLog("CookieBackingStore - Timer fired, sending changes to database. We have %d changes", changedCookies.size()); SQLiteTransaction transaction(m_db, false); transaction.begin(); // Iterate through every element in the change list to make calls // If error occurs, ignore it and continue to the next statement size_t sizeOfChange = changedCookies.size(); for (size_t i = 0; i < sizeOfChange; i++) { SQLiteStatement* m_statement; const ParsedCookie cookie = changedCookies[i].first; UpdateParameter action = changedCookies[i].second; if (action == Delete) { m_statement = m_deleteStatement; CookieLog("CookieBackingStore - deleting cookie %s.", cookie.toString().utf8().data()); // Binds all the values if (m_statement->bindText(1, cookie.name()) || m_statement->bindText(2, cookie.domain()) || m_statement->bindText(3, cookie.path()) || m_statement->bindText(4, cookie.protocol())) { LOG_ERROR("Cannot bind cookie data to delete"); LOG_ERROR("SQLite Error Message: %s", m_db.lastErrorMsg()); ASSERT_NOT_REACHED(); continue; } } else { if (action == Update) { CookieLog("CookieBackingStore - updating cookie %s.", cookie.toString().utf8().data()); m_statement = m_updateStatement; } else { CookieLog("CookieBackingStore - inserting cookie %s.", cookie.toString().utf8().data()); m_statement = m_insertStatement; } // Binds all the values if (m_statement->bindText(1, cookie.name()) || m_statement->bindText(2, cookie.value()) || m_statement->bindText(3, cookie.domain()) || m_statement->bindText(4, cookie.path()) || m_statement->bindDouble(5, cookie.expiry()) || m_statement->bindDouble(6, cookie.lastAccessed()) || m_statement->bindInt64(7, cookie.isSecure()) || m_statement->bindInt64(8, cookie.isHttpOnly()) || m_statement->bindDouble(9, cookie.creationTime()) || m_statement->bindText(10, cookie.protocol())) { LOG_ERROR("Cannot bind cookie data to save"); LOG_ERROR("SQLite Error Message: %s", m_db.lastErrorMsg()); ASSERT_NOT_REACHED(); continue; } } int rc = m_statement->step(); m_statement->reset(); if (rc != SQLResultOk && rc != SQLResultDone) { LOG_ERROR("Cannot make call to the database"); LOG_ERROR("SQLite Error Message: %s", m_db.lastErrorMsg()); ASSERT_NOT_REACHED(); continue; } } transaction.commit(); CookieLog("CookieBackingStore - transaction complete"); }
// Parse the string without "Set-Cookie" according to Firefox grammar (loosely RFC 2109 compliant) // see netwerk/cookie/src/nsCookieService.cpp comment for it ParsedCookie* CookieParser::parseOneCookie(const String& cookie, unsigned start, unsigned end, double curTime) { ParsedCookie* res = new ParsedCookie(curTime); if (!res) { LOG_ERROR("Out of memory"); return 0; } // Parse [NAME "="] VALUE unsigned tokenEnd = start; // Token end contains the position of the '=' or the end of a token unsigned pairEnd = start; // Pair end contains always the position of the ';' // find the *first* ';' and the '=' (if they exist) // FIXME : should handle quoted string while (pairEnd < end && cookie[pairEnd] != ';') { if (tokenEnd == start && cookie[pairEnd] == '=') tokenEnd = pairEnd; pairEnd++; } unsigned tokenStart = start; if (tokenEnd != start) { // There is a '=' so parse the NAME unsigned nameEnd = tokenEnd; // Remove lightweight spaces. while (nameEnd && isLightweightSpace(cookie[nameEnd])) nameEnd--; while (tokenStart < nameEnd && isLightweightSpace(cookie[tokenStart])) tokenStart++; if (nameEnd == tokenStart) { LOG_ERROR("Empty name. Rejecting the cookie"); delete res; return 0; } String name = cookie.substring(tokenStart, nameEnd - start); res->setName(name); } // Now parse the VALUE tokenStart = tokenEnd + 1; // Skip lightweight spaces in our token while (tokenStart < pairEnd && isLightweightSpace(cookie[tokenStart])) tokenStart++; tokenEnd = pairEnd; while (tokenEnd > tokenStart && isLightweightSpace(cookie[tokenEnd])) tokenEnd--; String value; if (tokenEnd == tokenStart) { // Firefox accepts empty value so we will do the same value = String(); } else value = cookie.substring(tokenStart, tokenEnd - tokenStart); res->setValue(value); while (pairEnd < end) { // Switch to the next pair as pairEnd is on the ';' and fast-forward any lightweight spaces. pairEnd++; while (pairEnd < end && isLightweightSpace(cookie[pairEnd])) pairEnd++; tokenStart = pairEnd; tokenEnd = tokenStart; // initiliasize token end to catch first '=' while (pairEnd < end && cookie[pairEnd] != ';') { if (tokenEnd == tokenStart && cookie[pairEnd] == '=') tokenEnd = pairEnd; pairEnd++; } // FIXME : should we skip lightweight spaces here ? unsigned length = tokenEnd - tokenStart; unsigned tokenStartSvg = tokenStart; String parsedValue; if (tokenStart != tokenEnd) { // There is an equal sign so remove lightweight spaces in VALUE tokenStart = tokenEnd + 1; while (tokenStart < pairEnd && isLightweightSpace(cookie[tokenStart])) tokenStart++; tokenEnd = pairEnd; while (tokenEnd > tokenStart && isLightweightSpace(cookie[tokenEnd])) tokenEnd--; parsedValue = cookie.substring(tokenStart, tokenEnd - tokenStart); } else { // If the parsedValue is empty, initialise it in case we need it parsedValue = String(); // Handle a token without value. length = pairEnd - tokenStart; } // Detect which "cookie-av" is parsed // Look at the first char then parse the whole for performance issue switch (cookie[tokenStartSvg]) { case 'P': case 'p' : { if (length >= 4 && cookie.find("ath", tokenStartSvg + 1, false)) res->setPath(parsedValue); else { LOG_ERROR("Invalid cookie %s (path)", cookie.ascii().data()); delete res; return 0; } break; } case 'D': case 'd' : { if (length >= 6 && cookie.find("omain", tokenStartSvg + 1, false)) { // If the domain does not start with a dot, add one for security checks String realDomain = parsedValue[0] == '.' ? parsedValue : "." + parsedValue; res->setDomain(realDomain); } else { LOG_ERROR("Invalid cookie %s (domain)", cookie.ascii().data()); delete res; return 0; } break; } case 'E' : case 'e' : { if (length >= 7 && cookie.find("xpires", tokenStartSvg + 1, false)) res->setExpiry(parsedValue); else { LOG_ERROR("Invalid cookie %s (expires)", cookie.ascii().data()); delete res; return 0; } break; } case 'M' : case 'm' : { if (length >= 7 && cookie.find("ax-age", tokenStartSvg + 1, false)) res->setMaxAge(parsedValue); else { LOG_ERROR("Invalid cookie %s (max-age)", cookie.ascii().data()); delete res; return 0; } break; } case 'C' : case 'c' : { if (length >= 7 && cookie.find("omment", tokenStartSvg + 1, false)) // We do not have room for the comment part (and so do Mozilla) so just log the comment. LOG(Network, "Comment %s for ParsedCookie : %s\n", parsedValue.ascii().data(), cookie.ascii().data()); else { LOG_ERROR("Invalid cookie %s (comment)", cookie.ascii().data()); delete res; return 0; } break; } case 'V' : case 'v' : { if (length >= 7 && cookie.find("ersion", tokenStartSvg + 1, false)) { if (parsedValue.toInt() != 1) { LOG_ERROR("ParsedCookie version %d not supported (only support version=1)", parsedValue.toInt()); delete res; return 0; } } else { LOG_ERROR("Invalid cookie %s (version)", cookie.ascii().data()); delete res; return 0; } break; } case 'S' : case 's' : { // Secure is a standalone token ("Secure;") if (length >= 6 && cookie.find("ecure", tokenStartSvg + 1, false)) res->setSecureFlag(true); else { LOG_ERROR("Invalid cookie %s (secure)", cookie.ascii().data()); delete res; return 0; } break; } case 'H': case 'h': { // HttpOnly is a standalone token ("HttpOnly;") if (length >= 8 && cookie.find("ttpOnly", tokenStartSvg + 1, false)) res->setIsHttpOnly(true); else { LOG_ERROR("Invalid cookie %s (HttpOnly)", cookie.ascii().data()); delete res; return 0; } break; } default : { // If length == 0, we should be at the end of the cookie (case : ";\r") so ignore it if (length) { LOG_ERROR("Invalid token for cookie %s", cookie.ascii().data()); delete res; return 0; } } } } // Check if the cookie is valid with respect to the size limit/ if (!res->isUnderSizeLimit()) { LOG_ERROR("ParsedCookie %s is above the 4kb in length : REJECTED", cookie.ascii().data()); delete res; return 0; } // If some pair was not provided, during parsing then apply some default value // the rest has been done in the constructor. // If no domain was provided, set it to the host if (!res->domain() || !res->domain().length()) res->setDomain("." + m_defaultCookieURL.host()); // If no path was provided, set it to the host's path if (!res->path() || !res->path().length()) res->setPath(m_defaultCookieURL.path()); return res; }