michael@0: //* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: #include "nsCRT.h" michael@0: #include "nsIHttpChannel.h" michael@0: #include "nsIObserverService.h" michael@0: #include "nsIStringStream.h" michael@0: #include "nsIUploadChannel.h" michael@0: #include "nsIURI.h" michael@0: #include "nsIUrlClassifierDBService.h" michael@0: #include "nsStreamUtils.h" michael@0: #include "nsStringStream.h" michael@0: #include "nsToolkitCompsCID.h" michael@0: #include "nsUrlClassifierStreamUpdater.h" michael@0: #include "prlog.h" michael@0: #include "nsIInterfaceRequestor.h" michael@0: #include "mozilla/LoadContext.h" michael@0: michael@0: static const char* gQuitApplicationMessage = "quit-application"; michael@0: michael@0: #undef LOG michael@0: michael@0: // NSPR_LOG_MODULES=UrlClassifierStreamUpdater:5 michael@0: #if defined(PR_LOGGING) michael@0: static const PRLogModuleInfo *gUrlClassifierStreamUpdaterLog = nullptr; michael@0: #define LOG(args) PR_LOG(gUrlClassifierStreamUpdaterLog, PR_LOG_DEBUG, args) michael@0: #else michael@0: #define LOG(args) michael@0: #endif michael@0: michael@0: michael@0: // This class does absolutely nothing, except pass requests onto the DBService. michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsIUrlClassiferStreamUpdater implementation michael@0: // Handles creating/running the stream listener michael@0: michael@0: nsUrlClassifierStreamUpdater::nsUrlClassifierStreamUpdater() michael@0: : mIsUpdating(false), mInitialized(false), mDownloadError(false), michael@0: mBeganStream(false), mUpdateUrl(nullptr), mChannel(nullptr) michael@0: { michael@0: #if defined(PR_LOGGING) michael@0: if (!gUrlClassifierStreamUpdaterLog) michael@0: gUrlClassifierStreamUpdaterLog = PR_NewLogModule("UrlClassifierStreamUpdater"); michael@0: #endif michael@0: michael@0: } michael@0: michael@0: NS_IMPL_ISUPPORTS(nsUrlClassifierStreamUpdater, michael@0: nsIUrlClassifierStreamUpdater, michael@0: nsIUrlClassifierUpdateObserver, michael@0: nsIRequestObserver, michael@0: nsIStreamListener, michael@0: nsIObserver, michael@0: nsIInterfaceRequestor, michael@0: nsITimerCallback) michael@0: michael@0: /** michael@0: * Clear out the update. michael@0: */ michael@0: void michael@0: nsUrlClassifierStreamUpdater::DownloadDone() michael@0: { michael@0: LOG(("nsUrlClassifierStreamUpdater::DownloadDone [this=%p]", this)); michael@0: mIsUpdating = false; michael@0: michael@0: mPendingUpdates.Clear(); michael@0: mDownloadError = false; michael@0: mSuccessCallback = nullptr; michael@0: mUpdateErrorCallback = nullptr; michael@0: mDownloadErrorCallback = nullptr; michael@0: } michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsIUrlClassifierStreamUpdater implementation michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::GetUpdateUrl(nsACString & aUpdateUrl) michael@0: { michael@0: if (mUpdateUrl) { michael@0: mUpdateUrl->GetSpec(aUpdateUrl); michael@0: } else { michael@0: aUpdateUrl.Truncate(); michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::SetUpdateUrl(const nsACString & aUpdateUrl) michael@0: { michael@0: LOG(("Update URL is %s\n", PromiseFlatCString(aUpdateUrl).get())); michael@0: michael@0: nsresult rv = NS_NewURI(getter_AddRefs(mUpdateUrl), aUpdateUrl); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsUrlClassifierStreamUpdater::FetchUpdate(nsIURI *aUpdateUrl, michael@0: const nsACString & aRequestBody, michael@0: const nsACString & aStreamTable) michael@0: { michael@0: nsresult rv; michael@0: uint32_t loadFlags = nsIChannel::INHIBIT_CACHING | michael@0: nsIChannel::LOAD_BYPASS_CACHE; michael@0: rv = NS_NewChannel(getter_AddRefs(mChannel), aUpdateUrl, nullptr, nullptr, this, michael@0: loadFlags); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mBeganStream = false; michael@0: michael@0: // If aRequestBody is empty, construct it for the test. michael@0: if (!aRequestBody.IsEmpty()) { michael@0: rv = AddRequestBody(aRequestBody); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: // Set the appropriate content type for file/data URIs, for unit testing michael@0: // purposes. michael@0: // This is only used for testing and should be deleted. michael@0: bool match; michael@0: if ((NS_SUCCEEDED(aUpdateUrl->SchemeIs("file", &match)) && match) || michael@0: (NS_SUCCEEDED(aUpdateUrl->SchemeIs("data", &match)) && match)) { michael@0: mChannel->SetContentType(NS_LITERAL_CSTRING("application/vnd.google.safebrowsing-update")); michael@0: } michael@0: michael@0: // Create a custom LoadContext for SafeBrowsing, so we can use callbacks on michael@0: // the channel to query the appId which allows separation of safebrowsing michael@0: // cookies in a separate jar. michael@0: nsCOMPtr sbContext = michael@0: new mozilla::LoadContext(NECKO_SAFEBROWSING_APP_ID); michael@0: rv = mChannel->SetNotificationCallbacks(sbContext); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: // Make the request michael@0: rv = mChannel->AsyncOpen(this, nullptr); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mStreamTable = aStreamTable; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsUrlClassifierStreamUpdater::FetchUpdate(const nsACString & aUpdateUrl, michael@0: const nsACString & aRequestBody, michael@0: const nsACString & aStreamTable) michael@0: { michael@0: LOG(("(pre) Fetching update from %s\n", PromiseFlatCString(aUpdateUrl).get())); michael@0: michael@0: nsCOMPtr uri; michael@0: nsresult rv = NS_NewURI(getter_AddRefs(uri), aUpdateUrl); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsAutoCString urlSpec; michael@0: uri->GetAsciiSpec(urlSpec); michael@0: michael@0: LOG(("(post) Fetching update from %s\n", urlSpec.get())); michael@0: michael@0: return FetchUpdate(uri, aRequestBody, aStreamTable); michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::DownloadUpdates( michael@0: const nsACString &aRequestTables, michael@0: const nsACString &aRequestBody, michael@0: nsIUrlClassifierCallback *aSuccessCallback, michael@0: nsIUrlClassifierCallback *aUpdateErrorCallback, michael@0: nsIUrlClassifierCallback *aDownloadErrorCallback, michael@0: bool *_retval) michael@0: { michael@0: NS_ENSURE_ARG(aSuccessCallback); michael@0: NS_ENSURE_ARG(aUpdateErrorCallback); michael@0: NS_ENSURE_ARG(aDownloadErrorCallback); michael@0: michael@0: if (mIsUpdating) { michael@0: LOG(("already updating, skipping update")); michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } michael@0: michael@0: if (!mUpdateUrl) { michael@0: NS_ERROR("updateUrl not set"); michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: } michael@0: michael@0: nsresult rv; michael@0: michael@0: if (!mInitialized) { michael@0: // Add an observer for shutdown so we can cancel any pending list michael@0: // downloads. quit-application is the same event that the download michael@0: // manager listens for and uses to cancel pending downloads. michael@0: nsCOMPtr observerService = michael@0: mozilla::services::GetObserverService(); michael@0: if (!observerService) michael@0: return NS_ERROR_FAILURE; michael@0: michael@0: observerService->AddObserver(this, gQuitApplicationMessage, false); michael@0: michael@0: mDBService = do_GetService(NS_URLCLASSIFIERDBSERVICE_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: mInitialized = true; michael@0: } michael@0: michael@0: rv = mDBService->BeginUpdate(this, aRequestTables); michael@0: if (rv == NS_ERROR_NOT_AVAILABLE) { michael@0: LOG(("already updating, skipping update")); michael@0: *_retval = false; michael@0: return NS_OK; michael@0: } else if (NS_FAILED(rv)) { michael@0: return rv; michael@0: } michael@0: michael@0: mSuccessCallback = aSuccessCallback; michael@0: mUpdateErrorCallback = aUpdateErrorCallback; michael@0: mDownloadErrorCallback = aDownloadErrorCallback; michael@0: michael@0: mIsUpdating = true; michael@0: *_retval = true; michael@0: michael@0: nsAutoCString urlSpec; michael@0: mUpdateUrl->GetAsciiSpec(urlSpec); michael@0: michael@0: LOG(("FetchUpdate: %s", urlSpec.get())); michael@0: //LOG(("requestBody: %s", aRequestBody.Data())); michael@0: michael@0: LOG(("Calling into FetchUpdate")); michael@0: return FetchUpdate(mUpdateUrl, aRequestBody, EmptyCString()); michael@0: } michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsIUrlClassifierUpdateObserver implementation michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::UpdateUrlRequested(const nsACString &aUrl, michael@0: const nsACString &aTable) michael@0: { michael@0: LOG(("Queuing requested update from %s\n", PromiseFlatCString(aUrl).get())); michael@0: michael@0: PendingUpdate *update = mPendingUpdates.AppendElement(); michael@0: if (!update) michael@0: return NS_ERROR_OUT_OF_MEMORY; michael@0: michael@0: // Allow data: and file: urls for unit testing purposes, otherwise assume http michael@0: if (StringBeginsWith(aUrl, NS_LITERAL_CSTRING("data:")) || michael@0: StringBeginsWith(aUrl, NS_LITERAL_CSTRING("file:"))) { michael@0: update->mUrl = aUrl; michael@0: } else { michael@0: // For unittesting update urls to localhost should use http, not https michael@0: // (otherwise the connection will fail silently, since there will be no michael@0: // cert available). michael@0: if (!StringBeginsWith(aUrl, NS_LITERAL_CSTRING("localhost"))) { michael@0: update->mUrl = NS_LITERAL_CSTRING("https://") + aUrl; michael@0: } else { michael@0: update->mUrl = NS_LITERAL_CSTRING("http://") + aUrl; michael@0: } michael@0: } michael@0: update->mTable = aTable; michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsUrlClassifierStreamUpdater::FetchNext() michael@0: { michael@0: if (mPendingUpdates.Length() == 0) { michael@0: return NS_OK; michael@0: } michael@0: michael@0: PendingUpdate &update = mPendingUpdates[0]; michael@0: LOG(("Fetching update url: %s\n", update.mUrl.get())); michael@0: nsresult rv = FetchUpdate(update.mUrl, EmptyCString(), michael@0: update.mTable); michael@0: if (NS_FAILED(rv)) { michael@0: LOG(("Error fetching update url: %s\n", update.mUrl.get())); michael@0: // We can commit the urls that we've applied so far. This is michael@0: // probably a transient server problem, so trigger backoff. michael@0: mDownloadErrorCallback->HandleEvent(EmptyCString()); michael@0: mDownloadError = true; michael@0: mDBService->FinishUpdate(); michael@0: return rv; michael@0: } michael@0: michael@0: mPendingUpdates.RemoveElementAt(0); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::StreamFinished(nsresult status, michael@0: uint32_t requestedDelay) michael@0: { michael@0: LOG(("nsUrlClassifierStreamUpdater::StreamFinished [%x, %d]", status, requestedDelay)); michael@0: if (NS_FAILED(status) || mPendingUpdates.Length() == 0) { michael@0: // We're done. michael@0: mDBService->FinishUpdate(); michael@0: return NS_OK; michael@0: } michael@0: michael@0: // Wait the requested amount of time before starting a new stream. michael@0: nsresult rv; michael@0: mTimer = do_CreateInstance("@mozilla.org/timer;1", &rv); michael@0: if (NS_SUCCEEDED(rv)) { michael@0: rv = mTimer->InitWithCallback(this, requestedDelay, michael@0: nsITimer::TYPE_ONE_SHOT); michael@0: } michael@0: michael@0: if (NS_FAILED(rv)) { michael@0: NS_WARNING("Unable to initialize timer, fetching next safebrowsing item immediately"); michael@0: return FetchNext(); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::UpdateSuccess(uint32_t requestedTimeout) michael@0: { michael@0: LOG(("nsUrlClassifierStreamUpdater::UpdateSuccess [this=%p]", this)); michael@0: if (mPendingUpdates.Length() != 0) { michael@0: NS_WARNING("Didn't fetch all safebrowsing update redirects"); michael@0: } michael@0: michael@0: // DownloadDone() clears mSuccessCallback, so we save it off here. michael@0: nsCOMPtr successCallback = mDownloadError ? nullptr : mSuccessCallback.get(); michael@0: DownloadDone(); michael@0: michael@0: nsAutoCString strTimeout; michael@0: strTimeout.AppendInt(requestedTimeout); michael@0: if (successCallback) { michael@0: successCallback->HandleEvent(strTimeout); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::UpdateError(nsresult result) michael@0: { michael@0: LOG(("nsUrlClassifierStreamUpdater::UpdateError [this=%p]", this)); michael@0: michael@0: // DownloadDone() clears mUpdateErrorCallback, so we save it off here. michael@0: nsCOMPtr errorCallback = mDownloadError ? nullptr : mUpdateErrorCallback.get(); michael@0: michael@0: DownloadDone(); michael@0: michael@0: nsAutoCString strResult; michael@0: strResult.AppendInt(static_cast(result)); michael@0: if (errorCallback) { michael@0: errorCallback->HandleEvent(strResult); michael@0: } michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: nsresult michael@0: nsUrlClassifierStreamUpdater::AddRequestBody(const nsACString &aRequestBody) michael@0: { michael@0: nsresult rv; michael@0: nsCOMPtr strStream = michael@0: do_CreateInstance(NS_STRINGINPUTSTREAM_CONTRACTID, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = strStream->SetData(aRequestBody.BeginReading(), michael@0: aRequestBody.Length()); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr uploadChannel = do_QueryInterface(mChannel, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = uploadChannel->SetUploadStream(strStream, michael@0: NS_LITERAL_CSTRING("text/plain"), michael@0: -1); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: nsCOMPtr httpChannel = do_QueryInterface(mChannel, &rv); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: rv = httpChannel->SetRequestMethod(NS_LITERAL_CSTRING("POST")); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsIStreamListenerObserver implementation michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::OnStartRequest(nsIRequest *request, michael@0: nsISupports* context) michael@0: { michael@0: nsresult rv; michael@0: bool downloadError = false; michael@0: nsAutoCString strStatus; michael@0: nsresult status = NS_OK; michael@0: michael@0: // Only update if we got http success header michael@0: nsCOMPtr httpChannel = do_QueryInterface(request); michael@0: if (httpChannel) { michael@0: rv = httpChannel->GetStatus(&status); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (NS_ERROR_CONNECTION_REFUSED == status || michael@0: NS_ERROR_NET_TIMEOUT == status) { michael@0: // Assume we're overloading the server and trigger backoff. michael@0: downloadError = true; michael@0: } michael@0: michael@0: if (NS_SUCCEEDED(status)) { michael@0: bool succeeded = false; michael@0: rv = httpChannel->GetRequestSucceeded(&succeeded); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: if (!succeeded) { michael@0: // 404 or other error, pass error status back michael@0: LOG(("HTTP request returned failure code.")); michael@0: michael@0: uint32_t requestStatus; michael@0: rv = httpChannel->GetResponseStatus(&requestStatus); michael@0: LOG(("HTTP request returned failure code: %d.", requestStatus)); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: strStatus.AppendInt(requestStatus); michael@0: downloadError = true; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (downloadError) { michael@0: mDownloadErrorCallback->HandleEvent(strStatus); michael@0: mDownloadError = true; michael@0: status = NS_ERROR_ABORT; michael@0: } else if (NS_SUCCEEDED(status)) { michael@0: mBeganStream = true; michael@0: rv = mDBService->BeginStream(mStreamTable); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: } michael@0: michael@0: mStreamTable.Truncate(); michael@0: michael@0: return status; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::OnDataAvailable(nsIRequest *request, michael@0: nsISupports* context, michael@0: nsIInputStream *aIStream, michael@0: uint64_t aSourceOffset, michael@0: uint32_t aLength) michael@0: { michael@0: if (!mDBService) michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: michael@0: LOG(("OnDataAvailable (%d bytes)", aLength)); michael@0: michael@0: nsresult rv; michael@0: michael@0: // Copy the data into a nsCString michael@0: nsCString chunk; michael@0: rv = NS_ConsumeStream(aIStream, aLength, chunk); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: //LOG(("Chunk (%d): %s\n\n", chunk.Length(), chunk.get())); michael@0: rv = mDBService->UpdateStream(chunk); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: michael@0: return NS_OK; michael@0: } michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::OnStopRequest(nsIRequest *request, nsISupports* context, michael@0: nsresult aStatus) michael@0: { michael@0: if (!mDBService) michael@0: return NS_ERROR_NOT_INITIALIZED; michael@0: michael@0: LOG(("OnStopRequest (status %x)", aStatus)); michael@0: michael@0: nsresult rv; michael@0: michael@0: if (NS_SUCCEEDED(aStatus)) { michael@0: // Success, finish this stream and move on to the next. michael@0: rv = mDBService->FinishStream(); michael@0: } else if (mBeganStream) { michael@0: // We began this stream and couldn't finish it. We have to cancel the michael@0: // update, it's not in a consistent state. michael@0: rv = mDBService->CancelUpdate(); michael@0: } else { michael@0: // The fetch failed, but we didn't start the stream (probably a michael@0: // server or connection error). We can commit what we've applied michael@0: // so far, and request again later. michael@0: rv = mDBService->FinishUpdate(); michael@0: } michael@0: michael@0: mChannel = nullptr; michael@0: michael@0: return rv; michael@0: } michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsIObserver implementation michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::Observe(nsISupports *aSubject, const char *aTopic, michael@0: const char16_t *aData) michael@0: { michael@0: if (nsCRT::strcmp(aTopic, gQuitApplicationMessage) == 0) { michael@0: if (mIsUpdating && mChannel) { michael@0: LOG(("Cancel download")); michael@0: nsresult rv; michael@0: rv = mChannel->Cancel(NS_ERROR_ABORT); michael@0: NS_ENSURE_SUCCESS(rv, rv); michael@0: mIsUpdating = false; michael@0: mChannel = nullptr; michael@0: } michael@0: if (mTimer) { michael@0: mTimer->Cancel(); michael@0: mTimer = nullptr; michael@0: } michael@0: } michael@0: return NS_OK; michael@0: } michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsIInterfaceRequestor implementation michael@0: michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::GetInterface(const nsIID & eventSinkIID, void* *_retval) michael@0: { michael@0: return QueryInterface(eventSinkIID, _retval); michael@0: } michael@0: michael@0: michael@0: /////////////////////////////////////////////////////////////////////////////// michael@0: // nsITimerCallback implementation michael@0: NS_IMETHODIMP michael@0: nsUrlClassifierStreamUpdater::Notify(nsITimer *timer) michael@0: { michael@0: LOG(("nsUrlClassifierStreamUpdater::Notify [%p]", this)); michael@0: michael@0: mTimer = nullptr; michael@0: michael@0: // Start the update process up again. michael@0: FetchNext(); michael@0: michael@0: return NS_OK; michael@0: } michael@0: