// ### move this to qnetworkcookie_p.h and share with qnetworkaccesshttpbackend static QPair<QByteArray, QByteArray> nextField(const QByteArray &text, int &position, bool isNameValue) { // format is one of: // (1) token // (2) token = token // (3) token = quoted-string const int length = text.length(); position = nextNonWhitespace(text, position); int semiColonPosition = text.indexOf(';', position); if (semiColonPosition < 0) semiColonPosition = length; //no ';' means take everything to end of string int equalsPosition = text.indexOf('=', position); if (equalsPosition < 0 || equalsPosition > semiColonPosition) { if (isNameValue) return qMakePair(QByteArray(), QByteArray()); //'=' is required for name-value-pair (RFC6265 section 5.2, rule 2) equalsPosition = semiColonPosition; //no '=' means there is an attribute-name but no attribute-value } QByteArray first = text.mid(position, equalsPosition - position).trimmed(); QByteArray second; int secondLength = semiColonPosition - equalsPosition - 1; if (secondLength > 0) second = text.mid(equalsPosition + 1, secondLength).trimmed(); position = semiColonPosition; return qMakePair(first, second); }
QList<QNetworkCookie> QNetworkCookiePrivate::parseSetCookieHeaderLine(const QByteArray &cookieString) { // According to http://wp.netscape.com/newsref/std/cookie_spec.html,< // the Set-Cookie response header is of the format: // // Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure // // where only the NAME=VALUE part is mandatory // // We do not support RFC 2965 Set-Cookie2-style cookies QList<QNetworkCookie> result; QDateTime now = QDateTime::currentDateTime().toUTC(); int position = 0; const int length = cookieString.length(); while (position < length) { QNetworkCookie cookie; // The first part is always the "NAME=VALUE" part QPair<QByteArray,QByteArray> field = nextField(cookieString, position, true); if (field.first.isEmpty() || field.second.isNull()) // parsing error break; cookie.setName(field.first); cookie.setValue(field.second); position = nextNonWhitespace(cookieString, position); bool endOfCookie = false; while (!endOfCookie && position < length) { switch (cookieString.at(position++)) { case ',': // end of the cookie endOfCookie = true; break; case ';': // new field in the cookie field = nextField(cookieString, position, false); field.first = field.first.toLower(); // everything but the NAME=VALUE is case-insensitive if (field.first == "expires") { position -= field.second.length(); int end; for (end = position; end < length; ++end) if (isValueSeparator(cookieString.at(end))) break; QByteArray dateString = cookieString.mid(position, end - position).trimmed(); position = end; QDateTime dt = parseDateString(dateString.toLower()); if (!dt.isValid()) { return result; } cookie.setExpirationDate(dt); } else if (field.first == "domain") { QByteArray rawDomain = field.second; QString maybeLeadingDot; if (rawDomain.startsWith('.')) { maybeLeadingDot = QLatin1Char('.'); rawDomain = rawDomain.mid(1); } QString normalizedDomain = QUrl::fromAce(QUrl::toAce(QString::fromUtf8(rawDomain))); if (normalizedDomain.isEmpty() && !rawDomain.isEmpty()) return result; cookie.setDomain(maybeLeadingDot + normalizedDomain); } else if (field.first == "max-age") { bool ok = false; int secs = field.second.toInt(&ok); if (!ok) return result; cookie.setExpirationDate(now.addSecs(secs)); } else if (field.first == "path") { QString path = QUrl::fromPercentEncoding(field.second); cookie.setPath(path); } else if (field.first == "secure") { cookie.setSecure(true); } else if (field.first == "httponly") { cookie.setHttpOnly(true); } else if (field.first == "comment") { //cookie.setComment(QString::fromUtf8(field.second)); } else if (field.first == "version") { if (field.second != "1") { // oops, we don't know how to handle this cookie return result; } } else { // got an unknown field in the cookie // what do we do? } position = nextNonWhitespace(cookieString, position); } } if (!cookie.name().isEmpty()) result += cookie; } return result; }
// ### move this to qnetworkcookie_p.h and share with qnetworkaccesshttpbackend static QPair<QByteArray, QByteArray> nextField(const QByteArray &text, int &position, bool isNameValue) { // format is one of: // (1) token // (2) token = token // (3) token = quoted-string int i; const int length = text.length(); position = nextNonWhitespace(text, position); // parse the first part, before the equal sign for (i = position; i < length; ++i) { register char c = text.at(i); if (c == ';' || c == ',' || c == '=') break; } QByteArray first = text.mid(position, i - position).trimmed(); position = i; if (first.isEmpty()) return qMakePair(QByteArray(), QByteArray()); if (i == length || text.at(i) != '=') // no equal sign, we found format (1) return qMakePair(first, QByteArray()); QByteArray second; second.reserve(32); // arbitrary but works for most cases i = nextNonWhitespace(text, position + 1); if (i < length && text.at(i) == '"') { // a quote, we found format (3), where: // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) // qdtext = <any TEXT except <">> // quoted-pair = "\" CHAR // If its NAME=VALUE, retain the value as is // refer to ttp://bugreports.qt.nokia.com/browse/QTBUG-17746 if (isNameValue) second += '"'; ++i; while (i < length) { register char c = text.at(i); if (c == '"') { // end of quoted text if (isNameValue) second += '"'; break; } else if (c == '\\') { if (isNameValue) second += '\\'; ++i; if (i >= length) // broken line return qMakePair(QByteArray(), QByteArray()); c = text.at(i); } second += c; ++i; } for ( ; i < length; ++i) { register char c = text.at(i); if (c == ',' || c == ';') break; } position = i; } else { // no quote, we found format (2) position = i; for ( ; i < length; ++i) { register char c = text.at(i); if (c == ',' || c == ';' || isLWS(c)) break; } second = text.mid(position, i - position).trimmed(); position = i; } if (second.isNull()) second.resize(0); // turns into empty-but-not-null return qMakePair(first, second); }
static QHash<QByteArray, QByteArray> parseHttpOptionHeader(const QByteArray &header) { // The HTTP header is of the form: // header = #1(directives) // directives = token | value-directive // value-directive = token "=" (token | quoted-string) QHash<QByteArray, QByteArray> result; int pos = 0; while (true) { // skip spaces pos = nextNonWhitespace(header, pos); if (pos == header.length()) return result; // end of parsing // pos points to a non-whitespace int comma = header.indexOf(',', pos); int equal = header.indexOf('=', pos); if (comma == pos || equal == pos) // huh? Broken header. return result; // The key name is delimited by either a comma, an equal sign or the end // of the header, whichever comes first int end = comma; if (end == -1) end = header.length(); if (equal != -1 && end > equal) end = equal; // equal sign comes before comma/end QByteArray key = QByteArray(header.constData() + pos, end - pos).trimmed().toLower(); pos = end + 1; if (equal != -1) { // case: token "=" (token | quoted-string) // skip spaces pos = nextNonWhitespace(header, pos); if (pos == header.length()) // huh? Broken header return result; QByteArray value; value.reserve(header.length() - pos); if (header.at(pos) == '"') { // case: quoted-string // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) // qdtext = <any TEXT except <">> // quoted-pair = "\" CHAR ++pos; while (pos < header.length()) { register char c = header.at(pos); if (c == '"') { // end of quoted text break; } else if (c == '\\') { ++pos; if (pos >= header.length()) // broken header return result; c = header.at(pos); } value += c; ++pos; } } else { // case: token while (pos < header.length()) { register char c = header.at(pos); if (isSeparator(c)) break; value += c; ++pos; } } result.insert(key, value); // find the comma now: comma = header.indexOf(',', pos); if (comma == -1) return result; // end of parsing pos = comma + 1; } else { // case: token // key is already set result.insert(key, QByteArray()); } } }
bool TokenStream::nextNonWhitespaceIs(int match, int lookahead) const { return nextNonWhitespace(lookahead) == match; }
QList<QNetworkCookie> QNetworkCookiePrivate::parseSetCookieHeaderLine(const QByteArray &cookieString) { // According to http://wp.netscape.com/newsref/std/cookie_spec.html,< // the Set-Cookie response header is of the format: // // Set-Cookie: NAME=VALUE; expires=DATE; path=PATH; domain=DOMAIN_NAME; secure // // where only the NAME=VALUE part is mandatory // // We do not support RFC 2965 Set-Cookie2-style cookies QList<QNetworkCookie> result; const QDateTime now = QDateTime::currentDateTimeUtc(); int position = 0; const int length = cookieString.length(); while (position < length) { QNetworkCookie cookie; // The first part is always the "NAME=VALUE" part QPair<QByteArray,QByteArray> field = nextField(cookieString, position, true); if (field.first.isEmpty()) // parsing error break; cookie.setName(field.first); cookie.setValue(field.second); position = nextNonWhitespace(cookieString, position); while (position < length) { switch (cookieString.at(position++)) { case ';': // new field in the cookie field = nextField(cookieString, position, false); field.first = field.first.toLower(); // everything but the NAME=VALUE is case-insensitive if (field.first == "expires") { position -= field.second.length(); int end; for (end = position; end < length; ++end) if (isValueSeparator(cookieString.at(end))) break; QByteArray dateString = cookieString.mid(position, end - position).trimmed(); position = end; QDateTime dt = parseDateString(dateString.toLower()); if (dt.isValid()) cookie.setExpirationDate(dt); //if unparsed, ignore the attribute but not the whole cookie (RFC6265 section 5.2.1) } else if (field.first == "domain") { QByteArray rawDomain = field.second; //empty domain should be ignored (RFC6265 section 5.2.3) if (!rawDomain.isEmpty()) { QString maybeLeadingDot; if (rawDomain.startsWith('.')) { maybeLeadingDot = QLatin1Char('.'); rawDomain = rawDomain.mid(1); } //IDN domains are required by RFC6265, accepting utf8 as well doesn't break any test cases. QString normalizedDomain = QUrl::fromAce(QUrl::toAce(QString::fromUtf8(rawDomain))); if (!normalizedDomain.isEmpty()) { cookie.setDomain(maybeLeadingDot + normalizedDomain); } else { //Normalization fails for malformed domains, e.g. "..example.org", reject the cookie now //rather than accepting it but never sending it due to domain match failure, as the //strict reading of RFC6265 would indicate. return result; } } } else if (field.first == "max-age") { bool ok = false; int secs = field.second.toInt(&ok); if (ok) { if (secs <= 0) { //earliest representable time (RFC6265 section 5.2.2) cookie.setExpirationDate(QDateTime::fromSecsSinceEpoch(0)); } else { cookie.setExpirationDate(now.addSecs(secs)); } } //if unparsed, ignore the attribute but not the whole cookie (RFC6265 section 5.2.2) } else if (field.first == "path") { if (field.second.startsWith('/')) { // ### we should treat cookie paths as an octet sequence internally // However RFC6265 says we should assume UTF-8 for presentation as a string cookie.setPath(QString::fromUtf8(field.second)); } else { // if the path doesn't start with '/' then set the default path (RFC6265 section 5.2.4) // and also IETF test case path0030 which has valid and empty path in the same cookie cookie.setPath(QString()); } } else if (field.first == "secure") { cookie.setSecure(true); } else if (field.first == "httponly") { cookie.setHttpOnly(true); } else { // ignore unknown fields in the cookie (RFC6265 section 5.2, rule 6) } position = nextNonWhitespace(cookieString, position); } } if (!cookie.name().isEmpty()) result += cookie; } return result; }