/* -*- 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/. */ #ifndef mozilla_contentanalysis_h #define mozilla_contentanalysis_h #include "mozilla/MoveOnlyFunction.h" #include "mozilla/MozPromise.h" #include "mozilla/dom/BrowsingContext.h" #include "mozilla/dom/MaybeDiscarded.h" #include "mozilla/dom/Promise.h" #include "mozilla/media/MediaUtils.h" #include "mozilla/WeakPtr.h" #include "nsIClipboard.h" #include "nsIContentAnalysis.h" #include "nsIThreadPool.h" #include "nsITransferable.h" #include "nsString.h" #include "nsTHashMap.h" #include "nsTHashSet.h" #include "nsTStringHasher.h" #include #include #include #ifdef XP_WIN # include #endif // XP_WIN class nsBaseClipboard; class nsIPrincipal; class nsIPrintSettings; class ContentAnalysisTest; namespace mozilla::dom { class CanonicalBrowsingContext; class DataTransfer; class WindowGlobalParent; } // namespace mozilla::dom namespace content_analysis::sdk { class Client; class ContentAnalysisRequest; class ContentAnalysisResponse; } // namespace content_analysis::sdk namespace mozilla::contentanalysis { class ContentAnalysisCallback; enum class DefaultResult : uint8_t { eBlock = 0, eWarn = 1, eAllow = 2, eLastValue = 2 }; class ContentAnalysisDiagnosticInfo final : public nsIContentAnalysisDiagnosticInfo { public: NS_DECL_ISUPPORTS NS_DECL_NSICONTENTANALYSISDIAGNOSTICINFO ContentAnalysisDiagnosticInfo(bool aConnectedToAgent, nsString aAgentPath, bool aFailedSignatureVerification, int64_t aRequestCount) : mConnectedToAgent(aConnectedToAgent), mAgentPath(std::move(aAgentPath)), mFailedSignatureVerification(aFailedSignatureVerification), mRequestCount(aRequestCount) {} private: virtual ~ContentAnalysisDiagnosticInfo() = default; bool mConnectedToAgent; nsString mAgentPath; bool mFailedSignatureVerification; int64_t mRequestCount; }; class ContentAnalysisRequest final : public nsIContentAnalysisRequest { public: NS_DECL_ISUPPORTS NS_DECL_NSICONTENTANALYSISREQUEST ContentAnalysisRequest(AnalysisType aAnalysisType, Reason aReason, nsString aString, bool aStringIsFilePath, nsCString aSha256Digest, nsCOMPtr aUrl, OperationType aOperationType, dom::WindowGlobalParent* aWindowGlobalParent, dom::WindowGlobalParent* aSourceWindowGlobal = nullptr, nsCString&& aUserActionId = nsCString()); ContentAnalysisRequest(AnalysisType aAnalysisType, Reason aReason, nsITransferable* aTransferable, dom::WindowGlobalParent* aWindowGlobal, dom::WindowGlobalParent* aSourceWindowGlobal); ContentAnalysisRequest(const nsTArray aPrintData, nsCOMPtr aUrl, nsString aPrinterName, Reason aReason, dom::WindowGlobalParent* aWindowGlobalParent); static nsresult GetFileDigest(const nsAString& aFilePath, nsCString& aDigestString); static RefPtr Clone( nsIContentAnalysisRequest* aRequest); private: virtual ~ContentAnalysisRequest(); ContentAnalysisRequest() = default; ContentAnalysisRequest(const ContentAnalysisRequest&) = delete; ContentAnalysisRequest& operator=(ContentAnalysisRequest&) = delete; // See nsIContentAnalysisRequest for values AnalysisType mAnalysisType; // See nsIContentAnalysisRequest for values Reason mReason; RefPtr mTransferable; RefPtr mDataTransfer; // Text content to analyze. Only one of textContent or filePath is defined. nsString mTextContent; // Name of file to analyze. Only one of textContent or filePath is defined. nsString mFilePath; // The URL containing the file download/upload or to which web content is // being uploaded. nsCOMPtr mUrl; // Sha256 digest of file. nsCString mSha256Digest; // URLs involved in the download. nsTArray> mResources; // Email address of user. nsString mEmail; // Unique identifier for this request nsCString mRequestToken; // Unique identifier for this user action. // For example, all requests that come from uploading multiple files // or one clipboard operation should have the same value. nsCString mUserActionId; // The number of requests associated with this mUserActionId. int64_t mUserActionRequestsCount = 1; // Type of text to display, see nsIContentAnalysisRequest for values OperationType mOperationTypeForDisplay; // String to display if mOperationTypeForDisplay is // OPERATION_CUSTOMDISPLAYSTRING nsString mOperationDisplayString; // The name of the printer being printed to nsString mPrinterName; RefPtr mWindowGlobalParent; #ifdef XP_WIN // The printed data to analyze, in PDF format HANDLE mPrintDataHandle = 0; // The size of the printed data in mPrintDataHandle uint64_t mPrintDataSize = 0; #endif // WindowGlobalParent that is the origin of the data in the request, if known. RefPtr mSourceWindowGlobal; // What to multiply the timeout for this request by. Only needed if there are // requests with multiple userActionIds that are logically grouped together. uint32_t mTimeoutMultiplier = 1; // Submit request to agent, even if it was already canceled. Always false // if not in tests. bool mTestOnlyAlwaysSubmitToAgent = false; friend class ::ContentAnalysisTest; template friend RefPtr mozilla::MakeRefPtr(Args&&...); }; #define CONTENTANALYSIS_IID \ {0xa37bed74, 0x4b50, 0x443a, {0xbf, 0x58, 0xf4, 0xeb, 0xbd, 0x30, 0x67, 0xb4}} class ContentAnalysisResponse; class ContentAnalysis final : public nsIContentAnalysis, public nsIObserver, public SupportsWeakPtr { public: NS_INLINE_DECL_STATIC_IID(CONTENTANALYSIS_IID) NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSICONTENTANALYSIS NS_DECL_NSIOBSERVER ContentAnalysis(); #if defined(XP_WIN) struct PrintAllowedResult final { bool mAllowed; dom::MaybeDiscarded mCachedStaticDocumentBrowsingContext; PrintAllowedResult(bool aAllowed, dom::MaybeDiscarded aCachedStaticDocumentBrowsingContext) : mAllowed(aAllowed), mCachedStaticDocumentBrowsingContext( aCachedStaticDocumentBrowsingContext) {} explicit PrintAllowedResult(bool aAllowed) : PrintAllowedResult(aAllowed, dom::MaybeDiscardedBrowsingContext()) {} }; struct PrintAllowedError final { nsresult mError; dom::MaybeDiscarded mCachedStaticDocumentBrowsingContext; PrintAllowedError(nsresult aError, dom::MaybeDiscarded aCachedStaticDocumentBrowsingContext) : mError(aError), mCachedStaticDocumentBrowsingContext( aCachedStaticDocumentBrowsingContext) {} explicit PrintAllowedError(nsresult aError) : PrintAllowedError(aError, dom::MaybeDiscardedBrowsingContext()) {} }; using PrintAllowedPromise = MozPromise; MOZ_CAN_RUN_SCRIPT static RefPtr PrintToPDFToDetermineIfPrintAllowed( dom::CanonicalBrowsingContext* aBrowsingContext, nsIPrintSettings* aPrintSettings); #endif // defined(XP_WIN) // Find the outermost browsing context that has same-origin access to // aBrowsingContext, and this is the URL we will pass to the Content Analysis // agent. static nsCOMPtr GetURIForBrowsingContext( dom::CanonicalBrowsingContext* aBrowsingContext); static bool CheckClipboardContentAnalysisSync( nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow, const nsCOMPtr& trans, nsIClipboard::ClipboardType aClipboardType); static void CheckClipboardContentAnalysis( nsBaseClipboard* aClipboard, mozilla::dom::WindowGlobalParent* aWindow, nsITransferable* aTransferable, nsIClipboard::ClipboardType aClipboardType, ContentAnalysisCallback* aResolver, bool aForFullClipboard = false); using FilesAllowedPromise = MozPromise, nsresult, true>; // Checks the passed in files in "batch mode", meaning that all requests will // be done even if some of them are BLOCKED. Unlike the other Check // methods, "batch mode" requests do not all share a user action ID. // This also consolidates the busy dialogs for the files into one that is // associated with the "primary" request's user action ID -- that is, the // user action ID of the first request generated. // Note that aURI is only necessary to pass in in gtests; otherwise we'll // get the URI from aWindow. static RefPtr CheckFilesInBatchMode( nsCOMArray&& aFiles, bool aAutoAcknowledge, mozilla::dom::WindowGlobalParent* aWindow, nsIContentAnalysisRequest::Reason aReason, nsIURI* aURI = nullptr); static RefPtr GetContentAnalysisFromService(); // Cancel all outstanding requests for the given user action ID. // aResult is used to determine what kind of cancellation this is // (user-initiated, timeout, blocked user action, internal error, etc). // The cancellation behavior is dependent on that value. In particular, // some causes lead to programmable default behaviors -- see e.g. // browser.contentanalysis.default_result and // browser.contentanalysis.timeout_result. Oothers, like user-initiated // and shutdown cancellations, have fixed behavior. void CancelWithError(nsCString&& aUserActionId, nsresult aResult); // These are the MIME types that Content Analysis can analyze. static constexpr const char* kKnownClipboardTypes[] = { kTextMime, kHTMLMime, kCustomTypesMime, kFileMime}; private: virtual ~ContentAnalysis(); // Remove unneeded copy constructor/assignment ContentAnalysis(const ContentAnalysis&) = delete; ContentAnalysis& operator=(ContentAnalysis&) = delete; // Only call this through CreateClientIfNecessary(), as it provides // synchronization to avoid doing this multiple times at once. nsresult CreateContentAnalysisClient(nsCString&& aPipePathName, nsString&& aClientSignatureSetting, bool aIsPerUser); // Thread pool that all agent communications happen on. Content Analysis // occasionally uses other (random) background threads for other purposes. nsCOMPtr mThreadPool; // Helper function to retry calling the client in case either the client // does not exist, or calling the client fails (indicating that the DLP agent // has terminated and possibly restarted) // // aClientCallFunc - gets called on a background thread after we have a // client. Returns a Result. An Err(nsresult) indicates // that the client call failed and we should try to reconnect. A successful // response indicates success (or at least that we should not try to // reconnect), and that value will be Resolve()d into the returned MozPromise. template RefPtr> CallClientWithRetry( StaticString aMethodName, U&& aClientCallFunc); nsresult RunAnalyzeRequestTask( const RefPtr& aRequest, bool aAutoAcknowledge, const RefPtr& aCallback); nsresult RunAcknowledgeTask( nsIContentAnalysisAcknowledgement* aAcknowledgement, const nsACString& aRequestToken); nsresult CreateClientIfNecessary(bool aForceCreate = false); // Actually send the request to the client and handle the response (or error). // Note that the response may be for a different request! static Result DoAnalyzeRequest( nsCString&& aUserActionId, content_analysis::sdk::ContentAnalysisRequest&& aRequest, bool aAutoAcknowledge, const std::shared_ptr& aClient, bool aTestOnlyIgnoreCanceled = false); static void HandleResponseFromAgent( content_analysis::sdk::ContentAnalysisResponse&& aResponse); struct UserActionIdAndAutoAcknowledge final { nsCString mUserActionId; bool mAutoAcknowledge; }; DataMutex> mRequestTokenToUserActionIdMap; void IssueResponse(ContentAnalysisResponse* response, nsCString&& aUserActionId, bool aAcknowledge, bool aIsTooLate); void NotifyResponseObservers(ContentAnalysisResponse* aResponse, nsCString&& aUserActionId, bool aAutoAcknowledge, bool aIsTimeout); void NotifyObserversAndMaybeIssueResponseFromAgent( ContentAnalysisResponse* aResponse, nsCString&& aUserActionId, bool aAutoAcknowledge); // Destroy the service. Happens during xpcom-shutdown-threads. void Close(); // Thread-safe check whether the service is being destroyed. bool IsShutDown(); // Did the URL filter completely handle the request or do we need to check // with the agent. enum UrlFilterResult { eCheck, eDeny, eAllow }; UrlFilterResult FilterByUrlLists(nsIContentAnalysisRequest* aRequest, nsIURI* aUri); void EnsureParsedUrlFilters(); using ContentAnalysisRequestArray = CopyableTArray>; using RequestsPromise = MozPromise; // Counts the number of times it receives an "allow content" and (1) calls // ContentResult on mCallback when all requests are approved, (2) calls // ContentResult and cancels outstanding scans when any one request is // rejected, or (3) calls Error and cancels outstanding scans when any one // fails. // Once constructed, this object is required to eventually issue a response to // the given callback. // This class doesn't care if it receives more calls than there are requests. // Canceling issues callback calls with no initiating request. This class // relays the verdicts on a first-come-first-served basis, so a cancel // that comes before an allow overrides that allow, and vice-versa. class MultipartRequestCallback : public nsIContentAnalysisCallback { public: NS_INLINE_DECL_REFCOUNTING(MultipartRequestCallback, override) NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; NS_DECL_NSICONTENTANALYSISCALLBACK static RefPtr Create( ContentAnalysis* aContentAnalysis, const nsTArray& aRequests, nsIContentAnalysisCallback* aCallback, bool aAutoAcknowledge); bool HasResponded() const { return mResponded; } private: MultipartRequestCallback() = default; virtual ~MultipartRequestCallback(); template friend RefPtr mozilla::MakeRefPtr(Args&&...); void Initialize( ContentAnalysis* aContentAnalysis, const nsTArray& aRequests, nsIContentAnalysisCallback* aCallback, bool aAutoAcknowledge); void CancelRequests(); void RemoveFromUserActionMap(); WeakPtr mWeakContentAnalysis; RefPtr mCallback; nsCString mUserActionId; // Number of CA requests remaining for this transaction. size_t mNumCARequestsRemaining; // True if we have issued a response for these requests. bool mResponded = false; }; Result, RefPtr> GetFinalRequestList(const ContentAnalysisRequestArray& aRequests); Result, nsresult> ExpandFolderRequest( nsIContentAnalysisRequest* aRequest, nsIFile* file); using ClientPromise = MozPromise, nsresult, false>; int64_t mRequestCount = 0; // Must only be resolved/rejected or Then()'d on the main thread. // // Note that if this promise is resolved, the resolve value will // be a non-null content_analysis::sdk::Client. However, if the // DLP agent process has terminated, it is possible that trying to // call into this client will return an error. Therefore, any // method that wants to call into the client should go through // CallClientWithRetry() to make it easy to try reconnecting // to the client. RefPtr mCaClientPromise MOZ_GUARDED_BY(sMainThreadCapability); bool mCreatingClient MOZ_GUARDED_BY(sMainThreadCapability) = false; bool mHaveResolvedClientPromise MOZ_GUARDED_BY(sMainThreadCapability) = false; bool mSetByEnterprise; struct UserActionData final { RefPtr mCallback; nsTHashSet mRequestTokens; RefPtr mTimeoutRunnable; bool mAutoAcknowledge; bool mIsHandlingTimeout = false; }; // This map is stored so that requests can be canceled while they are // still being checked. It is maintained by our inner class // MultipartRequestCallback. nsTHashMap mUserActionMap; void RemoveFromUserActionMap(nsCString&& aUserActionId); // The agent may respond to actions that we have canceled and we need to // remember how we handled them, whether it was to cancel (block) them, // or to issue a default response. struct CanceledResponse { nsIContentAnalysisAcknowledgement::FinalAction mAction; size_t mNumExpectedResponses; }; using UserActionIdToCanceledResponseMap = nsTHashMap; DataMutex mUserActionIdToCanceledResponseMap{ "ContentAnalysis::UserActionIdToCanceledResponseMap"}; class CachedClipboardResponse { public: CachedClipboardResponse() = default; Maybe GetCachedResponse( nsIURI* aURI, int32_t aClipboardSequenceNumber); void SetCachedResponse(const nsCOMPtr& aURI, int32_t aClipboardSequenceNumber, nsIContentAnalysisResponse::Action aAction); private: Maybe mClipboardSequenceNumber; nsTArray, nsIContentAnalysisResponse::Action>> mData; }; CachedClipboardResponse mCachedClipboardResponse; struct WarnResponseData { RefPtr mResponse; nsCString mUserActionId; bool mAutoAcknowledge; bool mWasTimeout; }; // Request token to warn response map. nsTHashMap mWarnResponseDataMap; std::vector mAllowUrlList; std::vector mDenyUrlList; bool mParsedUrlLists = false; bool mForbidFutureRequests = false; DataMutex mIsShutDown{false, "ContentAnalysis::IsShutDown"}; // Set of sets of user action IDs. Each set of IDs defines one compound // action. using UserActionSet = media::Refcountable>; using UserActionSets = mozilla::HashSet, PointerHasher>; UserActionSets mCompoundUserActions; friend class ContentAnalysisResponse; friend class ::ContentAnalysisTest; }; class ContentAnalysisResponse final : public nsIContentAnalysisResponse { public: NS_DECL_ISUPPORTS NS_DECL_NSICONTENTANALYSISRESULT NS_DECL_NSICONTENTANALYSISRESPONSE void SetOwner(ContentAnalysis* aOwner); void DoNotAcknowledge() { mDoNotAcknowledge = true; } void SetCancelError(CancelError aCancelError); void SetIsCachedResponse() { mIsCachedResponse = true; } void SetIsSyntheticResponse(bool aIsSyntheticResponse) { mIsSyntheticResponse = aIsSyntheticResponse; } private: virtual ~ContentAnalysisResponse() = default; // Remove unneeded copy constructor/assignment ContentAnalysisResponse(const ContentAnalysisResponse&) = delete; ContentAnalysisResponse& operator=(ContentAnalysisResponse&) = delete; explicit ContentAnalysisResponse( content_analysis::sdk::ContentAnalysisResponse&& aResponse, const nsCString& aUserActionId); ContentAnalysisResponse(Action aAction, const nsACString& aRequestToken, const nsACString& aUserActionId); // Use MakeRefPtr as factory. template friend RefPtr mozilla::MakeRefPtr(Args&&...); static already_AddRefed FromProtobuf( content_analysis::sdk::ContentAnalysisResponse&& aResponse, const nsCString& aUserActionId); void ResolveWarnAction(bool aAllowContent); // Action requested by the agent Action mAction; // Identifiers for the corresponding nsIContentAnalysisRequest nsCString mRequestToken; nsCString mUserActionId; // If mAction is eCanceled, this is the error explaining why the request was // canceled, or eUserInitiated if the user canceled it. CancelError mCancelError = CancelError::eUserInitiated; // ContentAnalysis (or, more precisely, its Client object) must outlive // the transaction. RefPtr mOwner; // Whether the response has been acknowledged bool mHasAcknowledged = false; // If true, the request was completely handled by URL filter lists, so it // was not sent to the agent and should not send an Acknowledge. bool mDoNotAcknowledge = false; // Whether this is a cached result that wasn't actually sent to the DLP agent. // This indicates that the request was a duplicate of a previously sent one, // so any dialogs (for block/warn) should not be shown. bool mIsCachedResponse = false; // Whether this is a synthesizic response from Firefox (as opposed to a // response from a DLP agent). // Synthetic responses ignore browser.contentanalysis.show_blocked_result and // always show a blocked result for blocked content, since there is no agent // that could have shown one for us. bool mIsSyntheticResponse = false; friend class ContentAnalysis; }; class ContentAnalysisAcknowledgement final : public nsIContentAnalysisAcknowledgement { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSICONTENTANALYSISACKNOWLEDGEMENT ContentAnalysisAcknowledgement(Result aResult, FinalAction aFinalAction); private: virtual ~ContentAnalysisAcknowledgement() = default; Result mResult; FinalAction mFinalAction; }; /** * This class: * 1. Asserts if the callback is not called before destruction. * 2. Takes a strong reference to the nsIContentAnalysisResult when * calling the callback, which guarantees that someone does. Otherwise, * if neither the caller nor the callback did, then the result would leak. */ class ContentAnalysisCallback final : public nsIContentAnalysisCallback { public: NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSICONTENTANALYSISCALLBACK ContentAnalysisCallback( std::function&& aContentResponseCallback, std::function&& aErrorCallback) : mContentResponseCallback(std::move(aContentResponseCallback)), mErrorCallback(std::move(aErrorCallback)) {} explicit ContentAnalysisCallback( std::function&& aContentResponseCallback); // Wrap a given callback, in case it doesn't provide the guarantees that // this one does (such as checking that it is eventually called). explicit ContentAnalysisCallback(nsIContentAnalysisCallback* aDecoratedCB) { mContentResponseCallback = [decoratedCB = RefPtr{aDecoratedCB}]( nsIContentAnalysisResult* aResult) { decoratedCB->ContentResult(aResult); }; mErrorCallback = [decoratedCB = RefPtr{aDecoratedCB}](nsresult aRv) { decoratedCB->Error(aRv); }; } private: virtual ~ContentAnalysisCallback() { MOZ_ASSERT(!mContentResponseCallback && !mErrorCallback && !mPromise, "ContentAnalysisCallback never called!"); } // Called after callbacks are called. void ClearCallbacks() { mContentResponseCallback = nullptr; mErrorCallback = nullptr; mPromise = nullptr; } explicit ContentAnalysisCallback(dom::Promise* aPromise); std::function mContentResponseCallback; std::function mErrorCallback; RefPtr mPromise; friend class ContentAnalysis; }; } // namespace mozilla::contentanalysis #endif // mozilla_contentanalysis_h