/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "OriginParser.h"
#include <regex>
#include "mozilla/OriginAttributes.h"
#include "mozilla/dom/quota/Constants.h"
#include "mozilla/dom/quota/QuotaCommon.h"
namespace mozilla::dom::quota {
// static
auto OriginParser::ParseOrigin(
const nsACString& aOrigin, nsCString& aSpec,
OriginAttributes* aAttrs,
nsCString& aOriginalSuffix) -> ResultType {
MOZ_ASSERT(!aOrigin.IsEmpty());
MOZ_ASSERT(aAttrs);
nsCString origin(aOrigin);
int32_t pos = origin.RFindChar(
'^');
if (pos == kNotFound) {
aOriginalSuffix.Truncate();
}
else {
aOriginalSuffix = Substring(origin, pos);
}
OriginAttributes originAttributes;
nsCString originNoSuffix;
bool ok = originAttributes.PopulateFromOrigin(aOrigin, originNoSuffix);
if (!ok) {
return InvalidOrigin;
}
OriginParser parser(originNoSuffix);
*aAttrs = originAttributes;
return parser.Parse(aSpec);
}
auto OriginParser::Parse(nsACString& aSpec) -> ResultType {
while (mTokenizer.hasMoreTokens()) {
const nsDependentCSubstring& token = mTokenizer.nextToken();
HandleToken(token);
if (mError) {
break;
}
if (!mHandledTokens.IsEmpty()) {
mHandledTokens.AppendLiteral(
", ");
}
mHandledTokens.Append(
'\'');
mHandledTokens.Append(token);
mHandledTokens.Append(
'\'');
}
if (!mError && mTokenizer.separatorAfterCurrentToken()) {
HandleTrailingSeparator();
}
if (mError) {
QM_WARNING(
"Origin '%s' failed to parse, handled tokens: %s", mOrigin.get(),
mHandledTokens.get());
return (mSchemeType == eChrome || mSchemeType == eAbout) ? ObsoleteOrigin
: InvalidOrigin;
}
MOZ_ASSERT(mState == eComplete || mState == eHandledTrailingSeparator);
// For IPv6 URL, it should at least have three groups.
MOZ_ASSERT_IF(mIPGroup > 0, mIPGroup >= 3);
nsAutoCString spec(mScheme);
if (mSchemeType == eFile) {
spec.AppendLiteral(
"://");
if (mUniversalFileOrigin) {
MOZ_ASSERT(mPathnameComponents.Length() == 1);
spec.Append(mPathnameComponents[0]);
}
else {
for (uint32_t count = mPathnameComponents.Length(), index = 0;
index < count; index++) {
spec.Append(
'/');
spec.Append(mPathnameComponents[index]);
}
}
aSpec = spec;
return ValidOrigin;
}
if (mSchemeType == eAbout) {
if (mMaybeObsolete) {
// The "moz-safe-about+++home" was acciedntally created by a buggy nightly
// and can be safely removed.
return mHost.EqualsLiteral(
"home") ? ObsoleteOrigin : InvalidOrigin;
}
spec.Append(
':');
}
else if (mSchemeType != eChrome) {
spec.AppendLiteral(
"://");
}
spec.Append(mHost);
if (!mPort.IsNull()) {
spec.Append(
':');
spec.AppendInt(mPort.Value());
}
aSpec = spec;
return mScheme.EqualsLiteral(
"app") ? ObsoleteOrigin : ValidOrigin;
}
void OriginParser::HandleScheme(
const nsDependentCSubstring& aToken) {
MOZ_ASSERT(!aToken.IsEmpty());
MOZ_ASSERT(mState == eExpectingAppIdOrScheme || mState == eExpectingScheme);
bool isAbout =
false;
bool isMozSafeAbout =
false;
bool isFile =
false;
bool isChrome =
false;
if (aToken.EqualsLiteral(
"http") || aToken.EqualsLiteral(
"https") ||
(isAbout = aToken.EqualsLiteral(
"about") ||
(isMozSafeAbout = aToken.EqualsLiteral(
"moz-safe-about"))) ||
aToken.EqualsLiteral(
"indexeddb") ||
(isFile = aToken.EqualsLiteral(
"file")) || aToken.EqualsLiteral(
"app") ||
aToken.EqualsLiteral(
"resource") ||
aToken.EqualsLiteral(
"moz-extension") ||
(isChrome = aToken.EqualsLiteral(kChromeOrigin)) ||
aToken.EqualsLiteral(
"uuid")) {
mScheme = aToken;
if (isAbout) {
mSchemeType = eAbout;
mState = isMozSafeAbout ? eExpectingEmptyToken1OrHost : eExpectingHost;
}
else if (isChrome) {
mSchemeType = eChrome;
if (mTokenizer.hasMoreTokens()) {
mError =
true;
}
mState = eComplete;
}
else {
if (isFile) {
mSchemeType = eFile;
}
mState = eExpectingEmptyToken1;
}
return;
}
QM_WARNING(
"'%s' is not a valid scheme!", nsCString(aToken).get());
mError =
true;
}
void OriginParser::HandlePathnameComponent(
const nsDependentCSubstring& aToken) {
MOZ_ASSERT(!aToken.IsEmpty());
MOZ_ASSERT(mState == eExpectingEmptyTokenOrDriveLetterOrPathnameComponent ||
mState == eExpectingEmptyTokenOrPathnameComponent);
MOZ_ASSERT(mSchemeType == eFile);
mPathnameComponents.AppendElement(aToken);
mState = mTokenizer.hasMoreTokens() ? eExpectingEmptyTokenOrPathnameComponent
: eComplete;
}
void OriginParser::HandleToken(
const nsDependentCSubstring& aToken) {
switch (mState) {
case eExpectingAppIdOrScheme: {
if (aToken.IsEmpty()) {
QM_WARNING(
"Expected an app id or scheme (not an empty string)!");
mError =
true;
return;
}
if (IsAsciiDigit(aToken.First())) {
// nsDependentCSubstring doesn't provice ToInteger()
nsCString token(aToken);
nsresult rv;
Unused << token.ToInteger(&rv);
if (NS_SUCCEEDED(rv)) {
mState = eExpectingInMozBrowser;
return;
}
}
HandleScheme(aToken);
return;
}
case eExpectingInMozBrowser: {
if (aToken.Length() != 1) {
QM_WARNING(
"'%zu' is not a valid length for the inMozBrowser flag!",
aToken.Length());
mError =
true;
return;
}
if ((aToken.First() !=
't') && (aToken.First() !=
'f')) {
QM_WARNING(
"'%s' is not a valid value for the inMozBrowser flag!",
nsCString(aToken).get());
mError =
true;
return;
}
mState = eExpectingScheme;
return;
}
case eExpectingScheme: {
if (aToken.IsEmpty()) {
QM_WARNING(
"Expected a scheme (not an empty string)!");
mError =
true;
return;
}
HandleScheme(aToken);
return;
}
case eExpectingEmptyToken1: {
if (!aToken.IsEmpty()) {
QM_WARNING(
"Expected the first empty token!");
mError =
true;
return;
}
mState = eExpectingEmptyToken2;
return;
}
case eExpectingEmptyToken2: {
if (!aToken.IsEmpty()) {
QM_WARNING(
"Expected the second empty token!");
mError =
true;
return;
}
if (mSchemeType == eFile) {
mState = eExpectingEmptyTokenOrUniversalFileOrigin;
}
else {
if (mSchemeType == eAbout) {
mMaybeObsolete =
true;
}
mState = eExpectingHost;
}
return;
}
case eExpectingEmptyTokenOrUniversalFileOrigin: {
MOZ_ASSERT(mSchemeType == eFile);
if (aToken.IsEmpty()) {
mState = mTokenizer.hasMoreTokens()
? eExpectingEmptyTokenOrDriveLetterOrPathnameComponent
: eComplete;
return;
}
if (aToken.EqualsLiteral(
"UNIVERSAL_FILE_URI_ORIGIN")) {
mUniversalFileOrigin =
true;
mPathnameComponents.AppendElement(aToken);
mState = eComplete;
return;
}
QM_WARNING(
"Expected the third empty token or "
"UNIVERSAL_FILE_URI_ORIGIN!");
mError =
true;
return;
}
case eExpectingHost: {
if (aToken.IsEmpty()) {
QM_WARNING(
"Expected a host (not an empty string)!");
mError =
true;
return;
}
mHost = aToken;
if (aToken.First() ==
'[') {
MOZ_ASSERT(mIPGroup == 0);
++mIPGroup;
mState = eExpectingIPV6Token;
MOZ_ASSERT(mTokenizer.hasMoreTokens());
return;
}
if (mTokenizer.hasMoreTokens()) {
if (mSchemeType == eAbout) {
QM_WARNING(
"Expected an empty string after host!");
mError =
true;
return;
}
mState = eExpectingPort;
return;
}
mState = eComplete;
return;
}
case eExpectingPort: {
MOZ_ASSERT(mSchemeType == eNone);
if (aToken.IsEmpty()) {
QM_WARNING(
"Expected a port (not an empty string)!");
mError =
true;
return;
}
// nsDependentCSubstring doesn't provice ToInteger()
nsCString token(aToken);
nsresult rv;
uint32_t port = token.ToInteger(&rv);
if (NS_SUCCEEDED(rv)) {
mPort.SetValue() = port;
}
else {
QM_WARNING(
"'%s' is not a valid port number!", token.get());
mError =
true;
return;
}
mState = eComplete;
return;
}
case eExpectingEmptyTokenOrDriveLetterOrPathnameComponent: {
MOZ_ASSERT(mSchemeType == eFile);
if (aToken.IsEmpty()) {
mPathnameComponents.AppendElement(
""_ns);
mState = mTokenizer.hasMoreTokens()
? eExpectingEmptyTokenOrPathnameComponent
: eComplete;
return;
}
if (aToken.Length() == 1 && IsAsciiAlpha(aToken.First())) {
mMaybeDriveLetter =
true;
mPathnameComponents.AppendElement(aToken);
mState = mTokenizer.hasMoreTokens()
? eExpectingEmptyTokenOrPathnameComponent
: eComplete;
return;
}
HandlePathnameComponent(aToken);
return;
}
case eExpectingEmptyTokenOrPathnameComponent: {
MOZ_ASSERT(mSchemeType == eFile);
if (aToken.IsEmpty()) {
if (mMaybeDriveLetter) {
MOZ_ASSERT(mPathnameComponents.Length() == 1);
nsCString& pathnameComponent = mPathnameComponents[0];
pathnameComponent.Append(
':');
mMaybeDriveLetter =
false;
}
else {
mPathnameComponents.AppendElement(
""_ns);
}
mState = mTokenizer.hasMoreTokens()
? eExpectingEmptyTokenOrPathnameComponent
: eComplete;
return;
}
HandlePathnameComponent(aToken);
return;
}
case eExpectingEmptyToken1OrHost: {
MOZ_ASSERT(mSchemeType == eAbout &&
mScheme.EqualsLiteral(
"moz-safe-about"));
if (aToken.IsEmpty()) {
mState = eExpectingEmptyToken2;
}
else {
mHost = aToken;
mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete;
}
return;
}
case eExpectingIPV6Token: {
// A safe check for preventing infinity recursion.
if (++mIPGroup > 8) {
mError =
true;
return;
}
mHost.AppendLiteral(
":");
mHost.Append(aToken);
if (!aToken.IsEmpty() && aToken.Last() ==
']') {
mState = mTokenizer.hasMoreTokens() ? eExpectingPort : eComplete;
}
return;
}
default:
MOZ_CRASH(
"Should never get here!");
}
}
void OriginParser::HandleTrailingSeparator() {
MOZ_ASSERT(mState == eComplete);
MOZ_ASSERT(mSchemeType == eFile);
mPathnameComponents.AppendElement(
""_ns);
mState = eHandledTrailingSeparator;
}
bool IsUUIDOrigin(
const nsCString& aOrigin) {
if (!StringBeginsWith(aOrigin, kUUIDOriginScheme)) {
return false;
}
static const std::regex pattern(
"^uuid://[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab"
"][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$");
return regex_match(aOrigin.get(), pattern);
}
bool IsUserContextSuffix(
const nsACString& aSuffix, uint32_t aUserContextId) {
OriginAttributes originAttributes;
MOZ_ALWAYS_TRUE(originAttributes.PopulateFromSuffix(aSuffix));
return originAttributes.mUserContextId == aUserContextId;
}
bool IsUserContextPattern(
const OriginAttributesPattern& aPattern,
uint32_t aUserContextId) {
const auto& userContextId = aPattern.mUserContextId;
if (!userContextId.WasPassed()) {
return false;
}
return userContextId.Value() == aUserContextId;
}
}
// namespace mozilla::dom::quota