/* 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 "mozilla/net/CaptivePortalService.h" #include "mozilla/AppShutdown.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/Services.h" #include "mozilla/Preferences.h" #include "nsIObserverService.h" #include "nsServiceManagerUtils.h" #include "nsXULAppAPI.h" #include "xpcpublic.h" static constexpr auto kInterfaceName = u"captive-portal-inteface"_ns; static const char kOpenCaptivePortalLoginEvent[] = "captive-portal-login"; static const char kAbortCaptivePortalLoginEvent[] = "captive-portal-login-abort"; static const char kCaptivePortalLoginSuccessEvent[] = "captive-portal-login-success"; namespace mozilla { namespace net { static LazyLogModule gCaptivePortalLog("CaptivePortalService"); #undef LOG #define LOG(args) MOZ_LOG(gCaptivePortalLog, mozilla::LogLevel::Debug, args) NS_IMPL_ISUPPORTS(CaptivePortalService, nsICaptivePortalService, nsIObserver, nsISupportsWeakReference, nsITimerCallback, nsICaptivePortalCallback, nsINamed) static StaticRefPtr gCPService; // static already_AddRefed CaptivePortalService::GetSingleton() { if (gCPService) { return do_AddRef(gCPService); } gCPService = new CaptivePortalService(); ClearOnShutdown(&gCPService); return do_AddRef(gCPService); } CaptivePortalService::CaptivePortalService() { mLastChecked = TimeStamp::Now(); } CaptivePortalService::~CaptivePortalService() { LOG(("CaptivePortalService::~CaptivePortalService isParentProcess:%d\n", XRE_GetProcessType() == GeckoProcessType_Default)); } nsresult CaptivePortalService::PerformCheck() { LOG( ("CaptivePortalService::PerformCheck mRequestInProgress:%d " "mInitialized:%d mStarted:%d\n", mRequestInProgress, mInitialized, mStarted)); // Don't issue another request if last one didn't complete if (mRequestInProgress || !mInitialized || !mStarted) { return NS_OK; } if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); nsresult rv; if (!mCaptivePortalDetector) { mCaptivePortalDetector = do_CreateInstance("@mozilla.org/toolkit/captive-detector;1", &rv); if (NS_FAILED(rv)) { LOG(("Unable to get a captive portal detector\n")); return rv; } } LOG(("CaptivePortalService::PerformCheck - Calling CheckCaptivePortal\n")); mRequestInProgress = true; mCaptivePortalDetector->CheckCaptivePortal(kInterfaceName, this); return NS_OK; } nsresult CaptivePortalService::RearmTimer() { LOG(("CaptivePortalService::RearmTimer\n")); // Start a timer to recheck MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); if (mTimer) { mTimer->Cancel(); } if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { mTimer = nullptr; return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } // If we have successfully determined the state, and we have never detected // a captive portal, we don't need to keep polling, but will rely on events // to trigger detection. if (mState == NOT_CAPTIVE) { return NS_OK; } if (!mTimer) { mTimer = NS_NewTimer(); } if (mTimer && mDelay > 0) { LOG(("CaptivePortalService - Reloading timer with delay %u\n", mDelay)); return mTimer->InitWithCallback(this, mDelay, nsITimer::TYPE_ONE_SHOT); } return NS_OK; } nsresult CaptivePortalService::Initialize() { if (mInitialized) { return NS_OK; } mInitialized = true; // Only the main process service should actually do anything. The service in // the content process only mirrors the CP state in the main process. if (XRE_GetProcessType() != GeckoProcessType_Default) { return NS_OK; } nsCOMPtr observerService = mozilla::services::GetObserverService(); if (observerService) { observerService->AddObserver(this, kOpenCaptivePortalLoginEvent, true); observerService->AddObserver(this, kAbortCaptivePortalLoginEvent, true); observerService->AddObserver(this, kCaptivePortalLoginSuccessEvent, true); observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true); } LOG(("Initialized CaptivePortalService\n")); return NS_OK; } nsresult CaptivePortalService::Start() { if (!mInitialized) { return NS_ERROR_NOT_INITIALIZED; } if (xpc::AreNonLocalConnectionsDisabled() && !Preferences::GetBool("network.captive-portal-service.testMode", false)) { return NS_ERROR_NOT_AVAILABLE; } if (XRE_GetProcessType() != GeckoProcessType_Default) { // Doesn't do anything if called in the content process. return NS_OK; } if (mStarted) { return NS_OK; } if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; } MOZ_ASSERT(mState == UNKNOWN, "Initial state should be UNKNOWN"); mStarted = true; mEverBeenCaptive = false; // Get the delay prefs Preferences::GetUint("network.captive-portal-service.minInterval", &mMinInterval); Preferences::GetUint("network.captive-portal-service.maxInterval", &mMaxInterval); Preferences::GetFloat("network.captive-portal-service.backoffFactor", &mBackoffFactor); LOG(("CaptivePortalService::Start min:%u max:%u backoff:%.2f\n", mMinInterval, mMaxInterval, mBackoffFactor)); mSlackCount = 0; mDelay = mMinInterval; // When Start is called, perform a check immediately PerformCheck(); RearmTimer(); return NS_OK; } nsresult CaptivePortalService::Stop() { LOG(("CaptivePortalService::Stop\n")); if (XRE_GetProcessType() != GeckoProcessType_Default) { // Doesn't do anything when called in the content process. return NS_OK; } if (!mStarted) { return NS_OK; } if (mTimer) { mTimer->Cancel(); } mTimer = nullptr; mRequestInProgress = false; mStarted = false; mEverBeenCaptive = false; if (mCaptivePortalDetector) { mCaptivePortalDetector->Abort(kInterfaceName); } mCaptivePortalDetector = nullptr; // Clear the state in case anyone queries the state while detection is off. mState = UNKNOWN; return NS_OK; } void CaptivePortalService::SetStateInChild(int32_t aState) { // This should only be called in the content process, from ContentChild.cpp // in order to mirror the captive portal state set in the chrome process. MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Default); mState = aState; mLastChecked = TimeStamp::Now(); } //----------------------------------------------------------------------------- // CaptivePortalService::nsICaptivePortalService //----------------------------------------------------------------------------- NS_IMETHODIMP CaptivePortalService::GetState(int32_t* aState) { *aState = mState; return NS_OK; } NS_IMETHODIMP CaptivePortalService::RecheckCaptivePortal() { LOG(("CaptivePortalService::RecheckCaptivePortal\n")); if (XRE_GetProcessType() != GeckoProcessType_Default) { // Doesn't do anything if called in the content process. return NS_OK; } // This is called for user activity. We need to reset the slack count, // so the checks continue to be quite frequent. mSlackCount = 0; mDelay = mMinInterval; PerformCheck(); RearmTimer(); return NS_OK; } NS_IMETHODIMP CaptivePortalService::GetLastChecked(uint64_t* aLastChecked) { double duration = (TimeStamp::Now() - mLastChecked).ToMilliseconds(); *aLastChecked = static_cast(duration); return NS_OK; } //----------------------------------------------------------------------------- // CaptivePortalService::nsITimer // This callback gets called every mDelay miliseconds // It issues a checkCaptivePortal operation if one isn't already in progress //----------------------------------------------------------------------------- NS_IMETHODIMP CaptivePortalService::Notify(nsITimer* aTimer) { LOG(("CaptivePortalService::Notify\n")); MOZ_ASSERT(aTimer == mTimer); MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); PerformCheck(); // This is needed because we don't want to always make requests very often. // Every 10 checks, we the delay is increased mBackoffFactor times // to a maximum delay of mMaxInterval mSlackCount++; if (mSlackCount % 10 == 0) { mDelay = mDelay * mBackoffFactor; } if (mDelay > mMaxInterval) { mDelay = mMaxInterval; } // Note - if mDelay is 0, the timer will not be rearmed. RearmTimer(); return NS_OK; } //----------------------------------------------------------------------------- // CaptivePortalService::nsINamed //----------------------------------------------------------------------------- NS_IMETHODIMP CaptivePortalService::GetName(nsACString& aName) { aName.AssignLiteral("CaptivePortalService"); return NS_OK; } //----------------------------------------------------------------------------- // CaptivePortalService::nsIObserver //----------------------------------------------------------------------------- NS_IMETHODIMP CaptivePortalService::Observe(nsISupports* aSubject, const char* aTopic, const char16_t* aData) { if (XRE_GetProcessType() != GeckoProcessType_Default) { // Doesn't do anything if called in the content process. return NS_OK; } LOG(("CaptivePortalService::Observe() topic=%s\n", aTopic)); if (!strcmp(aTopic, kOpenCaptivePortalLoginEvent)) { // A redirect or altered content has been detected. // The user needs to log in. We are in a captive portal. StateTransition(LOCKED_PORTAL); mLastChecked = TimeStamp::Now(); mEverBeenCaptive = true; } else if (!strcmp(aTopic, kCaptivePortalLoginSuccessEvent)) { // The user has successfully logged in. We have connectivity. StateTransition(UNLOCKED_PORTAL); mLastChecked = TimeStamp::Now(); mSlackCount = 0; mDelay = mMinInterval; RearmTimer(); } else if (!strcmp(aTopic, kAbortCaptivePortalLoginEvent)) { // The login has been aborted StateTransition(UNKNOWN); mLastChecked = TimeStamp::Now(); mSlackCount = 0; } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) { Stop(); return NS_OK; } // Send notification so that the captive portal state is mirrored in the // content process. nsCOMPtr observerService = services::GetObserverService(); if (observerService) { nsCOMPtr cps(this); observerService->NotifyObservers(cps, NS_IPC_CAPTIVE_PORTAL_SET_STATE, nullptr); } return NS_OK; } void CaptivePortalService::NotifyConnectivityAvailable(bool aCaptive) { nsCOMPtr observerService = services::GetObserverService(); if (observerService) { nsCOMPtr cps(this); observerService->NotifyObservers(cps, NS_CAPTIVE_PORTAL_CONNECTIVITY, aCaptive ? u"captive" : u"clear"); } } //----------------------------------------------------------------------------- // CaptivePortalService::nsICaptivePortalCallback //----------------------------------------------------------------------------- NS_IMETHODIMP CaptivePortalService::Prepare() { LOG(("CaptivePortalService::Prepare\n")); MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) { return NS_OK; } // XXX: Finish preparation shouldn't be called until dns and routing is // available. if (mCaptivePortalDetector) { mCaptivePortalDetector->FinishPreparation(kInterfaceName); } return NS_OK; } NS_IMETHODIMP CaptivePortalService::Complete(bool success) { LOG(("CaptivePortalService::Complete(success=%d) mState=%d\n", success, mState)); MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default); mLastChecked = TimeStamp::Now(); // Note: this callback gets called when: // 1. the request is completed, and content is valid (success == true) // 2. when the request is aborted or times out (success == false) if (success) { if (mEverBeenCaptive) { StateTransition(UNLOCKED_PORTAL); NotifyConnectivityAvailable(true); } else { StateTransition(NOT_CAPTIVE); NotifyConnectivityAvailable(false); } } mRequestInProgress = false; return NS_OK; } void CaptivePortalService::StateTransition(int32_t aNewState) { int32_t oldState = mState; mState = aNewState; if ((oldState == UNKNOWN && mState == NOT_CAPTIVE) || (oldState == LOCKED_PORTAL && mState == UNLOCKED_PORTAL)) { nsCOMPtr observerService = services::GetObserverService(); if (observerService) { nsCOMPtr cps(this); observerService->NotifyObservers( cps, NS_CAPTIVE_PORTAL_CONNECTIVITY_CHANGED, nullptr); } } } } // namespace net } // namespace mozilla