1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this
3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4 
5 #include "mozilla/net/CaptivePortalService.h"
6 #include "mozilla/AppShutdown.h"
7 #include "mozilla/ClearOnShutdown.h"
8 #include "mozilla/Services.h"
9 #include "mozilla/Preferences.h"
10 #include "nsIObserverService.h"
11 #include "nsServiceManagerUtils.h"
12 #include "nsXULAppAPI.h"
13 #include "xpcpublic.h"
14 
15 static constexpr auto kInterfaceName = u"captive-portal-inteface"_ns;
16 
17 static const char kOpenCaptivePortalLoginEvent[] = "captive-portal-login";
18 static const char kAbortCaptivePortalLoginEvent[] =
19     "captive-portal-login-abort";
20 static const char kCaptivePortalLoginSuccessEvent[] =
21     "captive-portal-login-success";
22 
23 namespace mozilla {
24 namespace net {
25 
26 static LazyLogModule gCaptivePortalLog("CaptivePortalService");
27 #undef LOG
28 #define LOG(args) MOZ_LOG(gCaptivePortalLog, mozilla::LogLevel::Debug, args)
29 
30 NS_IMPL_ISUPPORTS(CaptivePortalService, nsICaptivePortalService, nsIObserver,
31                   nsISupportsWeakReference, nsITimerCallback,
32                   nsICaptivePortalCallback, nsINamed)
33 
34 static StaticRefPtr<CaptivePortalService> gCPService;
35 
36 // static
GetSingleton()37 already_AddRefed<nsICaptivePortalService> CaptivePortalService::GetSingleton() {
38   if (gCPService) {
39     return do_AddRef(gCPService);
40   }
41 
42   gCPService = new CaptivePortalService();
43   ClearOnShutdown(&gCPService);
44   return do_AddRef(gCPService);
45 }
46 
CaptivePortalService()47 CaptivePortalService::CaptivePortalService() {
48   mLastChecked = TimeStamp::Now();
49 }
50 
~CaptivePortalService()51 CaptivePortalService::~CaptivePortalService() {
52   LOG(("CaptivePortalService::~CaptivePortalService isParentProcess:%d\n",
53        XRE_GetProcessType() == GeckoProcessType_Default));
54 }
55 
PerformCheck()56 nsresult CaptivePortalService::PerformCheck() {
57   LOG(
58       ("CaptivePortalService::PerformCheck mRequestInProgress:%d "
59        "mInitialized:%d mStarted:%d\n",
60        mRequestInProgress, mInitialized, mStarted));
61   // Don't issue another request if last one didn't complete
62   if (mRequestInProgress || !mInitialized || !mStarted) {
63     return NS_OK;
64   }
65   if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
66     return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
67   }
68   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
69   nsresult rv;
70   if (!mCaptivePortalDetector) {
71     mCaptivePortalDetector =
72         do_CreateInstance("@mozilla.org/toolkit/captive-detector;1", &rv);
73     if (NS_FAILED(rv)) {
74       LOG(("Unable to get a captive portal detector\n"));
75       return rv;
76     }
77   }
78 
79   LOG(("CaptivePortalService::PerformCheck - Calling CheckCaptivePortal\n"));
80   mRequestInProgress = true;
81   mCaptivePortalDetector->CheckCaptivePortal(kInterfaceName, this);
82   return NS_OK;
83 }
84 
RearmTimer()85 nsresult CaptivePortalService::RearmTimer() {
86   LOG(("CaptivePortalService::RearmTimer\n"));
87   // Start a timer to recheck
88   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
89   if (mTimer) {
90     mTimer->Cancel();
91   }
92 
93   if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
94     mTimer = nullptr;
95     return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
96   }
97 
98   // If we have successfully determined the state, and we have never detected
99   // a captive portal, we don't need to keep polling, but will rely on events
100   // to trigger detection.
101   if (mState == NOT_CAPTIVE) {
102     return NS_OK;
103   }
104 
105   if (!mTimer) {
106     mTimer = NS_NewTimer();
107   }
108 
109   if (mTimer && mDelay > 0) {
110     LOG(("CaptivePortalService - Reloading timer with delay %u\n", mDelay));
111     return mTimer->InitWithCallback(this, mDelay, nsITimer::TYPE_ONE_SHOT);
112   }
113 
114   return NS_OK;
115 }
116 
Initialize()117 nsresult CaptivePortalService::Initialize() {
118   if (mInitialized) {
119     return NS_OK;
120   }
121   mInitialized = true;
122 
123   // Only the main process service should actually do anything. The service in
124   // the content process only mirrors the CP state in the main process.
125   if (XRE_GetProcessType() != GeckoProcessType_Default) {
126     return NS_OK;
127   }
128 
129   nsCOMPtr<nsIObserverService> observerService =
130       mozilla::services::GetObserverService();
131   if (observerService) {
132     observerService->AddObserver(this, kOpenCaptivePortalLoginEvent, true);
133     observerService->AddObserver(this, kAbortCaptivePortalLoginEvent, true);
134     observerService->AddObserver(this, kCaptivePortalLoginSuccessEvent, true);
135     observerService->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, true);
136   }
137 
138   LOG(("Initialized CaptivePortalService\n"));
139   return NS_OK;
140 }
141 
Start()142 nsresult CaptivePortalService::Start() {
143   if (!mInitialized) {
144     return NS_ERROR_NOT_INITIALIZED;
145   }
146 
147   if (xpc::AreNonLocalConnectionsDisabled() &&
148       !Preferences::GetBool("network.captive-portal-service.testMode", false)) {
149     return NS_ERROR_NOT_AVAILABLE;
150   }
151 
152   if (XRE_GetProcessType() != GeckoProcessType_Default) {
153     // Doesn't do anything if called in the content process.
154     return NS_OK;
155   }
156 
157   if (mStarted) {
158     return NS_OK;
159   }
160 
161   if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
162     return NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
163   }
164 
165   MOZ_ASSERT(mState == UNKNOWN, "Initial state should be UNKNOWN");
166   mStarted = true;
167   mEverBeenCaptive = false;
168 
169   // Get the delay prefs
170   Preferences::GetUint("network.captive-portal-service.minInterval",
171                        &mMinInterval);
172   Preferences::GetUint("network.captive-portal-service.maxInterval",
173                        &mMaxInterval);
174   Preferences::GetFloat("network.captive-portal-service.backoffFactor",
175                         &mBackoffFactor);
176 
177   LOG(("CaptivePortalService::Start min:%u max:%u backoff:%.2f\n", mMinInterval,
178        mMaxInterval, mBackoffFactor));
179 
180   mSlackCount = 0;
181   mDelay = mMinInterval;
182 
183   // When Start is called, perform a check immediately
184   PerformCheck();
185   RearmTimer();
186   return NS_OK;
187 }
188 
Stop()189 nsresult CaptivePortalService::Stop() {
190   LOG(("CaptivePortalService::Stop\n"));
191 
192   if (XRE_GetProcessType() != GeckoProcessType_Default) {
193     // Doesn't do anything when called in the content process.
194     return NS_OK;
195   }
196 
197   if (!mStarted) {
198     return NS_OK;
199   }
200 
201   if (mTimer) {
202     mTimer->Cancel();
203   }
204   mTimer = nullptr;
205   mRequestInProgress = false;
206   mStarted = false;
207   mEverBeenCaptive = false;
208   if (mCaptivePortalDetector) {
209     mCaptivePortalDetector->Abort(kInterfaceName);
210   }
211   mCaptivePortalDetector = nullptr;
212 
213   // Clear the state in case anyone queries the state while detection is off.
214   mState = UNKNOWN;
215   return NS_OK;
216 }
217 
SetStateInChild(int32_t aState)218 void CaptivePortalService::SetStateInChild(int32_t aState) {
219   // This should only be called in the content process, from ContentChild.cpp
220   // in order to mirror the captive portal state set in the chrome process.
221   MOZ_ASSERT(XRE_GetProcessType() != GeckoProcessType_Default);
222 
223   mState = aState;
224   mLastChecked = TimeStamp::Now();
225 }
226 
227 //-----------------------------------------------------------------------------
228 // CaptivePortalService::nsICaptivePortalService
229 //-----------------------------------------------------------------------------
230 
231 NS_IMETHODIMP
GetState(int32_t * aState)232 CaptivePortalService::GetState(int32_t* aState) {
233   *aState = mState;
234   return NS_OK;
235 }
236 
237 NS_IMETHODIMP
RecheckCaptivePortal()238 CaptivePortalService::RecheckCaptivePortal() {
239   LOG(("CaptivePortalService::RecheckCaptivePortal\n"));
240 
241   if (XRE_GetProcessType() != GeckoProcessType_Default) {
242     // Doesn't do anything if called in the content process.
243     return NS_OK;
244   }
245 
246   // This is called for user activity. We need to reset the slack count,
247   // so the checks continue to be quite frequent.
248   mSlackCount = 0;
249   mDelay = mMinInterval;
250 
251   PerformCheck();
252   RearmTimer();
253   return NS_OK;
254 }
255 
256 NS_IMETHODIMP
GetLastChecked(uint64_t * aLastChecked)257 CaptivePortalService::GetLastChecked(uint64_t* aLastChecked) {
258   double duration = (TimeStamp::Now() - mLastChecked).ToMilliseconds();
259   *aLastChecked = static_cast<uint64_t>(duration);
260   return NS_OK;
261 }
262 
263 //-----------------------------------------------------------------------------
264 // CaptivePortalService::nsITimer
265 // This callback gets called every mDelay miliseconds
266 // It issues a checkCaptivePortal operation if one isn't already in progress
267 //-----------------------------------------------------------------------------
268 NS_IMETHODIMP
Notify(nsITimer * aTimer)269 CaptivePortalService::Notify(nsITimer* aTimer) {
270   LOG(("CaptivePortalService::Notify\n"));
271   MOZ_ASSERT(aTimer == mTimer);
272   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
273 
274   PerformCheck();
275 
276   // This is needed because we don't want to always make requests very often.
277   // Every 10 checks, we the delay is increased mBackoffFactor times
278   // to a maximum delay of mMaxInterval
279   mSlackCount++;
280   if (mSlackCount % 10 == 0) {
281     mDelay = mDelay * mBackoffFactor;
282   }
283   if (mDelay > mMaxInterval) {
284     mDelay = mMaxInterval;
285   }
286 
287   // Note - if mDelay is 0, the timer will not be rearmed.
288   RearmTimer();
289 
290   return NS_OK;
291 }
292 
293 //-----------------------------------------------------------------------------
294 // CaptivePortalService::nsINamed
295 //-----------------------------------------------------------------------------
296 
297 NS_IMETHODIMP
GetName(nsACString & aName)298 CaptivePortalService::GetName(nsACString& aName) {
299   aName.AssignLiteral("CaptivePortalService");
300   return NS_OK;
301 }
302 
303 //-----------------------------------------------------------------------------
304 // CaptivePortalService::nsIObserver
305 //-----------------------------------------------------------------------------
306 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)307 CaptivePortalService::Observe(nsISupports* aSubject, const char* aTopic,
308                               const char16_t* aData) {
309   if (XRE_GetProcessType() != GeckoProcessType_Default) {
310     // Doesn't do anything if called in the content process.
311     return NS_OK;
312   }
313 
314   LOG(("CaptivePortalService::Observe() topic=%s\n", aTopic));
315   if (!strcmp(aTopic, kOpenCaptivePortalLoginEvent)) {
316     // A redirect or altered content has been detected.
317     // The user needs to log in. We are in a captive portal.
318     StateTransition(LOCKED_PORTAL);
319     mLastChecked = TimeStamp::Now();
320     mEverBeenCaptive = true;
321   } else if (!strcmp(aTopic, kCaptivePortalLoginSuccessEvent)) {
322     // The user has successfully logged in. We have connectivity.
323     StateTransition(UNLOCKED_PORTAL);
324     mLastChecked = TimeStamp::Now();
325     mSlackCount = 0;
326     mDelay = mMinInterval;
327 
328     RearmTimer();
329   } else if (!strcmp(aTopic, kAbortCaptivePortalLoginEvent)) {
330     // The login has been aborted
331     StateTransition(UNKNOWN);
332     mLastChecked = TimeStamp::Now();
333     mSlackCount = 0;
334   } else if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
335     Stop();
336     return NS_OK;
337   }
338 
339   // Send notification so that the captive portal state is mirrored in the
340   // content process.
341   nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
342   if (observerService) {
343     nsCOMPtr<nsICaptivePortalService> cps(this);
344     observerService->NotifyObservers(cps, NS_IPC_CAPTIVE_PORTAL_SET_STATE,
345                                      nullptr);
346   }
347 
348   return NS_OK;
349 }
350 
NotifyConnectivityAvailable(bool aCaptive)351 void CaptivePortalService::NotifyConnectivityAvailable(bool aCaptive) {
352   nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
353   if (observerService) {
354     nsCOMPtr<nsICaptivePortalService> cps(this);
355     observerService->NotifyObservers(cps, NS_CAPTIVE_PORTAL_CONNECTIVITY,
356                                      aCaptive ? u"captive" : u"clear");
357   }
358 }
359 
360 //-----------------------------------------------------------------------------
361 // CaptivePortalService::nsICaptivePortalCallback
362 //-----------------------------------------------------------------------------
363 NS_IMETHODIMP
Prepare()364 CaptivePortalService::Prepare() {
365   LOG(("CaptivePortalService::Prepare\n"));
366   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
367   if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
368     return NS_OK;
369   }
370   // XXX: Finish preparation shouldn't be called until dns and routing is
371   // available.
372   if (mCaptivePortalDetector) {
373     mCaptivePortalDetector->FinishPreparation(kInterfaceName);
374   }
375   return NS_OK;
376 }
377 
378 NS_IMETHODIMP
Complete(bool success)379 CaptivePortalService::Complete(bool success) {
380   LOG(("CaptivePortalService::Complete(success=%d) mState=%d\n", success,
381        mState));
382   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Default);
383   mLastChecked = TimeStamp::Now();
384 
385   // Note: this callback gets called when:
386   // 1. the request is completed, and content is valid (success == true)
387   // 2. when the request is aborted or times out (success == false)
388 
389   if (success) {
390     if (mEverBeenCaptive) {
391       StateTransition(UNLOCKED_PORTAL);
392       NotifyConnectivityAvailable(true);
393     } else {
394       StateTransition(NOT_CAPTIVE);
395       NotifyConnectivityAvailable(false);
396     }
397   }
398 
399   mRequestInProgress = false;
400   return NS_OK;
401 }
402 
StateTransition(int32_t aNewState)403 void CaptivePortalService::StateTransition(int32_t aNewState) {
404   int32_t oldState = mState;
405   mState = aNewState;
406 
407   if ((oldState == UNKNOWN && mState == NOT_CAPTIVE) ||
408       (oldState == LOCKED_PORTAL && mState == UNLOCKED_PORTAL)) {
409     nsCOMPtr<nsIObserverService> observerService =
410         services::GetObserverService();
411     if (observerService) {
412       nsCOMPtr<nsICaptivePortalService> cps(this);
413       observerService->NotifyObservers(
414           cps, NS_CAPTIVE_PORTAL_CONNECTIVITY_CHANGED, nullptr);
415     }
416   }
417 }
418 
419 }  // namespace net
420 }  // namespace mozilla
421