зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1268726 - isolate shared worker by first party domain. r=baku
Tor 15564: Isolate SharedWorker by first party domain uplift/refactor by Dave Huseby <dhuseby@mozilla.com> review tweaks
This commit is contained in:
Родитель
7784aa2e51
Коммит
dfebfaa34a
|
@ -3097,6 +3097,48 @@ nsContentUtils::CanLoadImage(nsIURI* aURI, nsISupports* aContext,
|
||||||
return NS_FAILED(rv) ? false : NS_CP_ACCEPTED(decision);
|
return NS_FAILED(rv) ? false : NS_CP_ACCEPTED(decision);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
mozilla::PrincipalOriginAttributes
|
||||||
|
nsContentUtils::GetOriginAttributes(nsIDocument* aDocument)
|
||||||
|
{
|
||||||
|
if (!aDocument) {
|
||||||
|
return mozilla::PrincipalOriginAttributes();
|
||||||
|
}
|
||||||
|
|
||||||
|
nsCOMPtr<nsILoadGroup> loadGroup = aDocument->GetDocumentLoadGroup();
|
||||||
|
if (loadGroup) {
|
||||||
|
return GetOriginAttributes(loadGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
mozilla::PrincipalOriginAttributes attrs;
|
||||||
|
mozilla::NeckoOriginAttributes nattrs;
|
||||||
|
nsCOMPtr<nsIChannel> channel = aDocument->GetChannel();
|
||||||
|
if (channel && NS_GetOriginAttributes(channel, nattrs)) {
|
||||||
|
attrs.InheritFromNecko(nattrs);
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// static
|
||||||
|
mozilla::PrincipalOriginAttributes
|
||||||
|
nsContentUtils::GetOriginAttributes(nsILoadGroup* aLoadGroup)
|
||||||
|
{
|
||||||
|
if (!aLoadGroup) {
|
||||||
|
return mozilla::PrincipalOriginAttributes();
|
||||||
|
}
|
||||||
|
mozilla::PrincipalOriginAttributes attrs;
|
||||||
|
mozilla::DocShellOriginAttributes dsattrs;
|
||||||
|
nsCOMPtr<nsIInterfaceRequestor> callbacks;
|
||||||
|
aLoadGroup->GetNotificationCallbacks(getter_AddRefs(callbacks));
|
||||||
|
if (callbacks) {
|
||||||
|
nsCOMPtr<nsILoadContext> loadContext = do_GetInterface(callbacks);
|
||||||
|
if (loadContext && loadContext->GetOriginAttributes(dsattrs)) {
|
||||||
|
attrs.InheritFromDocShellToDoc(dsattrs, nullptr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
}
|
||||||
|
|
||||||
// static
|
// static
|
||||||
bool
|
bool
|
||||||
nsContentUtils::IsInPrivateBrowsing(nsIDocument* aDoc)
|
nsContentUtils::IsInPrivateBrowsing(nsIDocument* aDoc)
|
||||||
|
|
|
@ -785,6 +785,18 @@ public:
|
||||||
bool aIsForWindow,
|
bool aIsForWindow,
|
||||||
uint32_t *aArgCount, const char*** aArgNames);
|
uint32_t *aArgCount, const char*** aArgNames);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns origin attributes of the document.
|
||||||
|
**/
|
||||||
|
static mozilla::PrincipalOriginAttributes
|
||||||
|
GetOriginAttributes(nsIDocument* aDoc);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns origin attributes of the load group.
|
||||||
|
**/
|
||||||
|
static mozilla::PrincipalOriginAttributes
|
||||||
|
GetOriginAttributes(nsILoadGroup* aLoadGroup);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if this document is in a Private Browsing window.
|
* Returns true if this document is in a Private Browsing window.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -196,7 +196,7 @@ CacheStorage::CreateOnWorker(Namespace aNamespace, nsIGlobalObject* aGlobal,
|
||||||
return ref.forget();
|
return ref.forget();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aWorkerPrivate->IsInPrivateBrowsing()) {
|
if (aWorkerPrivate->GetOriginAttributes().mPrivateBrowsingId > 0) {
|
||||||
NS_WARNING("CacheStorage not supported during private browsing.");
|
NS_WARNING("CacheStorage not supported during private browsing.");
|
||||||
RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR);
|
RefPtr<CacheStorage> ref = new CacheStorage(NS_ERROR_DOM_SECURITY_ERR);
|
||||||
return ref.forget();
|
return ref.forget();
|
||||||
|
|
|
@ -245,29 +245,24 @@ GetWorkerPref(const nsACString& aPref,
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function creates a key for a SharedWorker composed by "name|scriptSpec".
|
// This fn creates a key for a SharedWorker that contains the name, script
|
||||||
// If the name contains a '|', this will be replaced by '||'.
|
// spec, and the serialized origin attributes:
|
||||||
|
// "name|scriptSpec^key1=val1&key2=val2&key3=val3"
|
||||||
void
|
void
|
||||||
GenerateSharedWorkerKey(const nsACString& aScriptSpec, const nsACString& aName,
|
GenerateSharedWorkerKey(const nsACString& aScriptSpec,
|
||||||
bool aPrivateBrowsing, nsCString& aKey)
|
const nsACString& aName,
|
||||||
|
const PrincipalOriginAttributes& aAttrs,
|
||||||
|
nsCString& aKey)
|
||||||
{
|
{
|
||||||
|
nsAutoCString suffix;
|
||||||
|
aAttrs.CreateSuffix(suffix);
|
||||||
|
|
||||||
aKey.Truncate();
|
aKey.Truncate();
|
||||||
aKey.SetCapacity(aScriptSpec.Length() + aName.Length() + 3);
|
aKey.SetCapacity(aName.Length() + aScriptSpec.Length() + suffix.Length() + 2);
|
||||||
aKey.Append(aPrivateBrowsing ? "1|" : "0|");
|
aKey.Append(aName);
|
||||||
|
|
||||||
nsACString::const_iterator start, end;
|
|
||||||
aName.BeginReading(start);
|
|
||||||
aName.EndReading(end);
|
|
||||||
for (; start != end; ++start) {
|
|
||||||
if (*start == '|') {
|
|
||||||
aKey.AppendASCII("||");
|
|
||||||
} else {
|
|
||||||
aKey.Append(*start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aKey.Append('|');
|
aKey.Append('|');
|
||||||
aKey.Append(aScriptSpec);
|
aKey.Append(aScriptSpec);
|
||||||
|
aKey.Append(suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -1641,7 +1636,7 @@ RuntimeService::RegisterWorker(WorkerPrivate* aWorkerPrivate)
|
||||||
const nsCString& sharedWorkerName = aWorkerPrivate->WorkerName();
|
const nsCString& sharedWorkerName = aWorkerPrivate->WorkerName();
|
||||||
nsAutoCString key;
|
nsAutoCString key;
|
||||||
GenerateSharedWorkerKey(sharedWorkerScriptSpec, sharedWorkerName,
|
GenerateSharedWorkerKey(sharedWorkerScriptSpec, sharedWorkerName,
|
||||||
aWorkerPrivate->IsInPrivateBrowsing(), key);
|
aWorkerPrivate->GetOriginAttributes(), key);
|
||||||
MOZ_ASSERT(!domainInfo->mSharedWorkerInfos.Get(key));
|
MOZ_ASSERT(!domainInfo->mSharedWorkerInfos.Get(key));
|
||||||
|
|
||||||
SharedWorkerInfo* sharedWorkerInfo =
|
SharedWorkerInfo* sharedWorkerInfo =
|
||||||
|
@ -1718,7 +1713,7 @@ RuntimeService::RemoveSharedWorker(WorkerDomainInfo* aDomainInfo,
|
||||||
#ifdef DEBUG
|
#ifdef DEBUG
|
||||||
nsAutoCString key;
|
nsAutoCString key;
|
||||||
GenerateSharedWorkerKey(data->mScriptSpec, data->mName,
|
GenerateSharedWorkerKey(data->mScriptSpec, data->mName,
|
||||||
aWorkerPrivate->IsInPrivateBrowsing(), key);
|
aWorkerPrivate->GetOriginAttributes(), key);
|
||||||
MOZ_ASSERT(iter.Key() == key);
|
MOZ_ASSERT(iter.Key() == key);
|
||||||
#endif
|
#endif
|
||||||
iter.Remove();
|
iter.Remove();
|
||||||
|
@ -2434,9 +2429,10 @@ RuntimeService::CreateSharedWorkerFromLoadInfo(JSContext* aCx,
|
||||||
nsresult rv = aLoadInfo->mResolvedScriptURI->GetSpec(scriptSpec);
|
nsresult rv = aLoadInfo->mResolvedScriptURI->GetSpec(scriptSpec);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
|
MOZ_ASSERT(aLoadInfo->mPrincipal);
|
||||||
nsAutoCString key;
|
nsAutoCString key;
|
||||||
GenerateSharedWorkerKey(scriptSpec, aName,
|
GenerateSharedWorkerKey(scriptSpec, aName,
|
||||||
aLoadInfo->mPrivateBrowsing, key);
|
BasePrincipal::Cast(aLoadInfo->mPrincipal)->OriginAttributesRef(), key);
|
||||||
|
|
||||||
if (mDomainMap.Get(aLoadInfo->mDomain, &domainInfo) &&
|
if (mDomainMap.Get(aLoadInfo->mDomain, &domainInfo) &&
|
||||||
domainInfo->mSharedWorkerInfos.Get(key, &sharedWorkerInfo)) {
|
domainInfo->mSharedWorkerInfos.Get(key, &sharedWorkerInfo)) {
|
||||||
|
|
|
@ -350,7 +350,7 @@ public:
|
||||||
|
|
||||||
explicit CacheCreator(WorkerPrivate* aWorkerPrivate)
|
explicit CacheCreator(WorkerPrivate* aWorkerPrivate)
|
||||||
: mCacheName(aWorkerPrivate->ServiceWorkerCacheName())
|
: mCacheName(aWorkerPrivate->ServiceWorkerCacheName())
|
||||||
, mPrivateBrowsing(aWorkerPrivate->IsInPrivateBrowsing())
|
, mOriginAttributes(aWorkerPrivate->GetOriginAttributes())
|
||||||
{
|
{
|
||||||
MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
|
MOZ_ASSERT(aWorkerPrivate->IsServiceWorker());
|
||||||
MOZ_ASSERT(aWorkerPrivate->LoadScriptAsPartOfLoadingServiceWorkerScript());
|
MOZ_ASSERT(aWorkerPrivate->LoadScriptAsPartOfLoadingServiceWorkerScript());
|
||||||
|
@ -411,7 +411,7 @@ private:
|
||||||
nsTArray<RefPtr<CacheScriptLoader>> mLoaders;
|
nsTArray<RefPtr<CacheScriptLoader>> mLoaders;
|
||||||
|
|
||||||
nsString mCacheName;
|
nsString mCacheName;
|
||||||
bool mPrivateBrowsing;
|
PrincipalOriginAttributes mOriginAttributes;
|
||||||
};
|
};
|
||||||
|
|
||||||
NS_IMPL_ISUPPORTS0(CacheCreator)
|
NS_IMPL_ISUPPORTS0(CacheCreator)
|
||||||
|
@ -1467,7 +1467,7 @@ CacheCreator::CreateCacheStorage(nsIPrincipal* aPrincipal)
|
||||||
// If we're in private browsing mode, don't even try to create the
|
// If we're in private browsing mode, don't even try to create the
|
||||||
// CacheStorage. Instead, just fail immediately to terminate the
|
// CacheStorage. Instead, just fail immediately to terminate the
|
||||||
// ServiceWorker load.
|
// ServiceWorker load.
|
||||||
if (NS_WARN_IF(mPrivateBrowsing)) {
|
if (NS_WARN_IF(mOriginAttributes.mPrivateBrowsingId > 0)) {
|
||||||
return NS_ERROR_DOM_SECURITY_ERR;
|
return NS_ERROR_DOM_SECURITY_ERR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1478,7 +1478,8 @@ CacheCreator::CreateCacheStorage(nsIPrincipal* aPrincipal)
|
||||||
mCacheStorage =
|
mCacheStorage =
|
||||||
CacheStorage::CreateOnMainThread(mozilla::dom::cache::CHROME_ONLY_NAMESPACE,
|
CacheStorage::CreateOnMainThread(mozilla::dom::cache::CHROME_ONLY_NAMESPACE,
|
||||||
mSandboxGlobalObject,
|
mSandboxGlobalObject,
|
||||||
aPrincipal, mPrivateBrowsing,
|
aPrincipal,
|
||||||
|
false, /* privateBrowsing can't be true here */
|
||||||
true /* force trusted origin */,
|
true /* force trusted origin */,
|
||||||
error);
|
error);
|
||||||
if (NS_WARN_IF(error.Failed())) {
|
if (NS_WARN_IF(error.Failed())) {
|
||||||
|
|
|
@ -160,6 +160,8 @@ ServiceWorkerInfo::ServiceWorkerInfo(nsIPrincipal* aPrincipal,
|
||||||
, mSkipWaitingFlag(false)
|
, mSkipWaitingFlag(false)
|
||||||
{
|
{
|
||||||
MOZ_ASSERT(mPrincipal);
|
MOZ_ASSERT(mPrincipal);
|
||||||
|
// cache origin attributes so we can use them off main thread
|
||||||
|
mOriginAttributes = BasePrincipal::Cast(mPrincipal)->OriginAttributesRef();
|
||||||
MOZ_ASSERT(!mScope.IsEmpty());
|
MOZ_ASSERT(!mScope.IsEmpty());
|
||||||
MOZ_ASSERT(!mScriptSpec.IsEmpty());
|
MOZ_ASSERT(!mScriptSpec.IsEmpty());
|
||||||
MOZ_ASSERT(!mCacheName.IsEmpty());
|
MOZ_ASSERT(!mCacheName.IsEmpty());
|
||||||
|
|
|
@ -31,6 +31,7 @@ private:
|
||||||
const nsCString mScriptSpec;
|
const nsCString mScriptSpec;
|
||||||
const nsString mCacheName;
|
const nsString mCacheName;
|
||||||
ServiceWorkerState mState;
|
ServiceWorkerState mState;
|
||||||
|
PrincipalOriginAttributes mOriginAttributes;
|
||||||
|
|
||||||
// This id is shared with WorkerPrivate to match requests issued by service
|
// This id is shared with WorkerPrivate to match requests issued by service
|
||||||
// workers to their corresponding serviceWorkerInfo.
|
// workers to their corresponding serviceWorkerInfo.
|
||||||
|
@ -104,6 +105,12 @@ public:
|
||||||
return mState;
|
return mState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const PrincipalOriginAttributes&
|
||||||
|
GetOriginAttributes() const
|
||||||
|
{
|
||||||
|
return mOriginAttributes;
|
||||||
|
}
|
||||||
|
|
||||||
const nsString&
|
const nsString&
|
||||||
CacheName() const
|
CacheName() const
|
||||||
{
|
{
|
||||||
|
|
|
@ -1714,7 +1714,7 @@ ServiceWorkerPrivate::SpawnWorkerIfNeeded(WakeUpReason aWhy,
|
||||||
nsContentUtils::StorageAccess access =
|
nsContentUtils::StorageAccess access =
|
||||||
nsContentUtils::StorageAllowedForPrincipal(info.mPrincipal);
|
nsContentUtils::StorageAllowedForPrincipal(info.mPrincipal);
|
||||||
info.mStorageAllowed = access > nsContentUtils::StorageAccess::ePrivateBrowsing;
|
info.mStorageAllowed = access > nsContentUtils::StorageAccess::ePrivateBrowsing;
|
||||||
info.mPrivateBrowsing = false;
|
info.mOriginAttributes = mInfo->GetOriginAttributes();
|
||||||
|
|
||||||
nsCOMPtr<nsIContentSecurityPolicy> csp;
|
nsCOMPtr<nsIContentSecurityPolicy> csp;
|
||||||
rv = info.mPrincipal->GetCsp(getter_AddRefs(csp));
|
rv = info.mPrincipal->GetCsp(getter_AddRefs(csp));
|
||||||
|
|
|
@ -1739,7 +1739,6 @@ WorkerLoadInfo::WorkerLoadInfo()
|
||||||
, mXHRParamsAllowed(false)
|
, mXHRParamsAllowed(false)
|
||||||
, mPrincipalIsSystem(false)
|
, mPrincipalIsSystem(false)
|
||||||
, mStorageAllowed(false)
|
, mStorageAllowed(false)
|
||||||
, mPrivateBrowsing(true)
|
|
||||||
, mServiceWorkersTestingInWindow(false)
|
, mServiceWorkersTestingInWindow(false)
|
||||||
{
|
{
|
||||||
MOZ_COUNT_CTOR(WorkerLoadInfo);
|
MOZ_COUNT_CTOR(WorkerLoadInfo);
|
||||||
|
@ -1797,8 +1796,8 @@ WorkerLoadInfo::StealFrom(WorkerLoadInfo& aOther)
|
||||||
mXHRParamsAllowed = aOther.mXHRParamsAllowed;
|
mXHRParamsAllowed = aOther.mXHRParamsAllowed;
|
||||||
mPrincipalIsSystem = aOther.mPrincipalIsSystem;
|
mPrincipalIsSystem = aOther.mPrincipalIsSystem;
|
||||||
mStorageAllowed = aOther.mStorageAllowed;
|
mStorageAllowed = aOther.mStorageAllowed;
|
||||||
mPrivateBrowsing = aOther.mPrivateBrowsing;
|
|
||||||
mServiceWorkersTestingInWindow = aOther.mServiceWorkersTestingInWindow;
|
mServiceWorkersTestingInWindow = aOther.mServiceWorkersTestingInWindow;
|
||||||
|
mOriginAttributes = aOther.mOriginAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <class Derived>
|
template <class Derived>
|
||||||
|
@ -3426,7 +3425,7 @@ WorkerPrivateParent<Derived>::SetPrincipal(nsIPrincipal* aPrincipal,
|
||||||
mLoadInfo.mLoadGroup = aLoadGroup;
|
mLoadInfo.mLoadGroup = aLoadGroup;
|
||||||
|
|
||||||
mLoadInfo.mPrincipalInfo = new PrincipalInfo();
|
mLoadInfo.mPrincipalInfo = new PrincipalInfo();
|
||||||
mLoadInfo.mPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(aLoadGroup);
|
mLoadInfo.mOriginAttributes = nsContentUtils::GetOriginAttributes(aLoadGroup);
|
||||||
|
|
||||||
MOZ_ALWAYS_SUCCEEDS(
|
MOZ_ALWAYS_SUCCEEDS(
|
||||||
PrincipalToPrincipalInfo(aPrincipal, mLoadInfo.mPrincipalInfo));
|
PrincipalToPrincipalInfo(aPrincipal, mLoadInfo.mPrincipalInfo));
|
||||||
|
@ -4193,7 +4192,7 @@ WorkerPrivate::GetLoadInfo(JSContext* aCx, nsPIDOMWindowInner* aWindow,
|
||||||
loadInfo.mFromWindow = aParent->IsFromWindow();
|
loadInfo.mFromWindow = aParent->IsFromWindow();
|
||||||
loadInfo.mWindowID = aParent->WindowID();
|
loadInfo.mWindowID = aParent->WindowID();
|
||||||
loadInfo.mStorageAllowed = aParent->IsStorageAllowed();
|
loadInfo.mStorageAllowed = aParent->IsStorageAllowed();
|
||||||
loadInfo.mPrivateBrowsing = aParent->IsInPrivateBrowsing();
|
loadInfo.mOriginAttributes = aParent->GetOriginAttributes();
|
||||||
loadInfo.mServiceWorkersTestingInWindow =
|
loadInfo.mServiceWorkersTestingInWindow =
|
||||||
aParent->ServiceWorkersTestingInWindow();
|
aParent->ServiceWorkersTestingInWindow();
|
||||||
} else {
|
} else {
|
||||||
|
@ -4317,7 +4316,7 @@ WorkerPrivate::GetLoadInfo(JSContext* aCx, nsPIDOMWindowInner* aWindow,
|
||||||
nsContentUtils::StorageAccess access =
|
nsContentUtils::StorageAccess access =
|
||||||
nsContentUtils::StorageAllowedForWindow(globalWindow);
|
nsContentUtils::StorageAllowedForWindow(globalWindow);
|
||||||
loadInfo.mStorageAllowed = access > nsContentUtils::StorageAccess::eDeny;
|
loadInfo.mStorageAllowed = access > nsContentUtils::StorageAccess::eDeny;
|
||||||
loadInfo.mPrivateBrowsing = nsContentUtils::IsInPrivateBrowsing(document);
|
loadInfo.mOriginAttributes = nsContentUtils::GetOriginAttributes(document);
|
||||||
} else {
|
} else {
|
||||||
// Not a window
|
// Not a window
|
||||||
MOZ_ASSERT(isChrome);
|
MOZ_ASSERT(isChrome);
|
||||||
|
@ -4359,7 +4358,7 @@ WorkerPrivate::GetLoadInfo(JSContext* aCx, nsPIDOMWindowInner* aWindow,
|
||||||
loadInfo.mFromWindow = false;
|
loadInfo.mFromWindow = false;
|
||||||
loadInfo.mWindowID = UINT64_MAX;
|
loadInfo.mWindowID = UINT64_MAX;
|
||||||
loadInfo.mStorageAllowed = true;
|
loadInfo.mStorageAllowed = true;
|
||||||
loadInfo.mPrivateBrowsing = false;
|
loadInfo.mOriginAttributes = PrincipalOriginAttributes();
|
||||||
}
|
}
|
||||||
|
|
||||||
MOZ_ASSERT(loadInfo.mPrincipal);
|
MOZ_ASSERT(loadInfo.mPrincipal);
|
||||||
|
|
|
@ -783,10 +783,10 @@ public:
|
||||||
return mLoadInfo.mStorageAllowed;
|
return mLoadInfo.mStorageAllowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
const PrincipalOriginAttributes&
|
||||||
IsInPrivateBrowsing() const
|
GetOriginAttributes() const
|
||||||
{
|
{
|
||||||
return mLoadInfo.mPrivateBrowsing;
|
return mLoadInfo.mOriginAttributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if the SW testing per-window flag is set by devtools
|
// Determine if the SW testing per-window flag is set by devtools
|
||||||
|
|
|
@ -269,8 +269,8 @@ struct WorkerLoadInfo
|
||||||
bool mXHRParamsAllowed;
|
bool mXHRParamsAllowed;
|
||||||
bool mPrincipalIsSystem;
|
bool mPrincipalIsSystem;
|
||||||
bool mStorageAllowed;
|
bool mStorageAllowed;
|
||||||
bool mPrivateBrowsing;
|
|
||||||
bool mServiceWorkersTestingInWindow;
|
bool mServiceWorkersTestingInWindow;
|
||||||
|
PrincipalOriginAttributes mOriginAttributes;
|
||||||
|
|
||||||
WorkerLoadInfo();
|
WorkerLoadInfo();
|
||||||
~WorkerLoadInfo();
|
~WorkerLoadInfo();
|
||||||
|
|
Загрузка…
Ссылка в новой задаче