1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.connectivity;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.WorkerThread;
22 import android.app.AlarmManager;
23 import android.app.PendingIntent;
24 import android.content.BroadcastReceiver;
25 import android.content.ComponentName;
26 import android.content.ContentResolver;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.IntentFilter;
30 import android.content.ServiceConnection;
31 import android.net.IPacProxyInstalledListener;
32 import android.net.IPacProxyManager;
33 import android.net.ProxyInfo;
34 import android.net.TrafficStats;
35 import android.net.Uri;
36 import android.os.Handler;
37 import android.os.HandlerThread;
38 import android.os.IBinder;
39 import android.os.RemoteCallbackList;
40 import android.os.RemoteException;
41 import android.os.ServiceManager;
42 import android.os.SystemClock;
43 import android.os.SystemProperties;
44 import android.os.UserHandle;
45 import android.provider.Settings;
46 import android.util.Log;
47 import android.webkit.URLUtil;
48 
49 import com.android.internal.annotations.GuardedBy;
50 import com.android.internal.util.TrafficStatsConstants;
51 import com.android.net.IProxyCallback;
52 import com.android.net.IProxyPortListener;
53 import com.android.net.IProxyService;
54 import com.android.net.module.util.PermissionUtils;
55 
56 import java.io.ByteArrayOutputStream;
57 import java.io.IOException;
58 import java.net.URL;
59 import java.net.URLConnection;
60 
61 /**
62  * @hide
63  */
64 public class PacProxyService extends IPacProxyManager.Stub {
65     private static final String PAC_PACKAGE = "com.android.pacprocessor";
66     private static final String PAC_SERVICE = "com.android.pacprocessor.PacService";
67     private static final String PAC_SERVICE_NAME = "com.android.net.IProxyService";
68 
69     private static final String PROXY_PACKAGE = "com.android.proxyhandler";
70     private static final String PROXY_SERVICE = "com.android.proxyhandler.ProxyService";
71 
72     private static final String TAG = "PacProxyService";
73 
74     private static final String ACTION_PAC_REFRESH = "android.net.proxy.PAC_REFRESH";
75 
76     private static final String DEFAULT_DELAYS = "8 32 120 14400 43200";
77     private static final int DELAY_1 = 0;
78     private static final int DELAY_4 = 3;
79     private static final int DELAY_LONG = 4;
80     private static final long MAX_PAC_SIZE = 20 * 1000 * 1000;
81 
82     private String mCurrentPac;
83     @GuardedBy("mProxyLock")
84     private volatile Uri mPacUrl = Uri.EMPTY;
85 
86     private AlarmManager mAlarmManager;
87     @GuardedBy("mProxyLock")
88     private IProxyService mProxyService;
89     private PendingIntent mPacRefreshIntent;
90     private ServiceConnection mConnection;
91     private ServiceConnection mProxyConnection;
92     private Context mContext;
93 
94     private int mCurrentDelay;
95     private int mLastPort;
96 
97     private volatile boolean mHasSentBroadcast;
98     private volatile boolean mHasDownloaded;
99 
100     private final RemoteCallbackList<IPacProxyInstalledListener>
101             mCallbacks = new RemoteCallbackList<>();
102 
103     /**
104      * Used for locking when setting mProxyService and all references to mCurrentPac.
105      */
106     private final Object mProxyLock = new Object();
107 
108     /**
109      * Lock ensuring consistency between the values of mHasSentBroadcast, mHasDownloaded, the
110      * last URL and port, and the broadcast message being sent with the correct arguments.
111      * TODO : this should probably protect all instances of these variables
112      */
113     private final Object mBroadcastStateLock = new Object();
114 
115     /**
116      * Runnable to download PAC script.
117      * The behavior relies on the assumption it always runs on mNetThread to guarantee that the
118      * latest data fetched from mPacUrl is stored in mProxyService.
119      */
120     private Runnable mPacDownloader = new Runnable() {
121         @Override
122         @WorkerThread
123         public void run() {
124             String file;
125             final Uri pacUrl = mPacUrl;
126             if (Uri.EMPTY.equals(pacUrl)) return;
127             final int oldTag = TrafficStats.getAndSetThreadStatsTag(
128                     TrafficStatsConstants.TAG_SYSTEM_PAC);
129             try {
130                 file = get(pacUrl);
131             } catch (IOException ioe) {
132                 file = null;
133                 Log.w(TAG, "Failed to load PAC file: " + ioe);
134             } finally {
135                 TrafficStats.setThreadStatsTag(oldTag);
136             }
137             if (file != null) {
138                 synchronized (mProxyLock) {
139                     if (!file.equals(mCurrentPac)) {
140                         setCurrentProxyScript(file);
141                     }
142                 }
143                 mHasDownloaded = true;
144                 sendProxyIfNeeded();
145                 longSchedule();
146             } else {
147                 reschedule();
148             }
149         }
150     };
151 
152     private final Handler mNetThreadHandler;
153 
154     class PacRefreshIntentReceiver extends BroadcastReceiver {
onReceive(Context context, Intent intent)155         public void onReceive(Context context, Intent intent) {
156             mNetThreadHandler.post(mPacDownloader);
157         }
158     }
159 
PacProxyService(@onNull Context context)160     public PacProxyService(@NonNull Context context) {
161         mContext = context;
162         mLastPort = -1;
163         final HandlerThread netThread = new HandlerThread("android.pacproxyservice",
164                 android.os.Process.THREAD_PRIORITY_DEFAULT);
165         netThread.start();
166         mNetThreadHandler = new Handler(netThread.getLooper());
167 
168         mPacRefreshIntent = PendingIntent.getBroadcast(
169                 context, 0, new Intent(ACTION_PAC_REFRESH), PendingIntent.FLAG_IMMUTABLE);
170         context.registerReceiver(new PacRefreshIntentReceiver(),
171                 new IntentFilter(ACTION_PAC_REFRESH));
172     }
173 
getAlarmManager()174     private AlarmManager getAlarmManager() {
175         if (mAlarmManager == null) {
176             mAlarmManager = mContext.getSystemService(AlarmManager.class);
177         }
178         return mAlarmManager;
179     }
180 
181     @Override
addListener(IPacProxyInstalledListener listener)182     public void addListener(IPacProxyInstalledListener listener) {
183         PermissionUtils.enforceNetworkStackPermissionOr(mContext,
184                 android.Manifest.permission.NETWORK_SETTINGS);
185         mCallbacks.register(listener);
186     }
187 
188     @Override
removeListener(IPacProxyInstalledListener listener)189     public void removeListener(IPacProxyInstalledListener listener) {
190         PermissionUtils.enforceNetworkStackPermissionOr(mContext,
191                 android.Manifest.permission.NETWORK_SETTINGS);
192         mCallbacks.unregister(listener);
193     }
194 
195     /**
196      * Updates the PAC Proxy Service with current Proxy information. This is called by
197      * the ProxyTracker through PacProxyManager before a broadcast takes place to allow
198      * the PacProxyService to indicate that the broadcast should not be sent and the
199      * PacProxyService will trigger a new broadcast when it is ready.
200      *
201      * @param proxy Proxy information that is about to be broadcast.
202      */
203     @Override
setCurrentProxyScriptUrl(@ullable ProxyInfo proxy)204     public void setCurrentProxyScriptUrl(@Nullable ProxyInfo proxy) {
205         PermissionUtils.enforceNetworkStackPermissionOr(mContext,
206                 android.Manifest.permission.NETWORK_SETTINGS);
207 
208         synchronized (mBroadcastStateLock) {
209             if (proxy != null && !Uri.EMPTY.equals(proxy.getPacFileUrl())) {
210                 if (proxy.getPacFileUrl().equals(mPacUrl) && (proxy.getPort() > 0)) return;
211                 mPacUrl = proxy.getPacFileUrl();
212                 mCurrentDelay = DELAY_1;
213                 mHasSentBroadcast = false;
214                 mHasDownloaded = false;
215                 getAlarmManager().cancel(mPacRefreshIntent);
216                 bind();
217             } else {
218                 getAlarmManager().cancel(mPacRefreshIntent);
219                 synchronized (mProxyLock) {
220                     mPacUrl = Uri.EMPTY;
221                     mCurrentPac = null;
222                     if (mProxyService != null) {
223                         unbind();
224                     }
225                 }
226             }
227         }
228     }
229 
230     /**
231      * Does a post and reports back the status code.
232      *
233      * @throws IOException if the URL is malformed, or the PAC file is too big.
234      */
get(Uri pacUri)235     private static String get(Uri pacUri) throws IOException {
236         if (!URLUtil.isValidUrl(pacUri.toString()))  {
237             throw new IOException("Malformed URL:" + pacUri);
238         }
239 
240         final URL url = new URL(pacUri.toString());
241         URLConnection urlConnection;
242         try {
243             urlConnection = url.openConnection(java.net.Proxy.NO_PROXY);
244             // Catch the possible exceptions and rethrow as IOException to not to crash the system
245             // for illegal input.
246         } catch (IllegalArgumentException e) {
247             throw new IOException("Incorrect proxy type for " + pacUri);
248         } catch (UnsupportedOperationException e) {
249             throw new IOException("Unsupported URL connection type for " + pacUri);
250         }
251 
252         long contentLength = -1;
253         try {
254             contentLength = Long.parseLong(urlConnection.getHeaderField("Content-Length"));
255         } catch (NumberFormatException e) {
256             // Ignore
257         }
258         if (contentLength > MAX_PAC_SIZE) {
259             throw new IOException("PAC too big: " + contentLength + " bytes");
260         }
261         ByteArrayOutputStream bytes = new ByteArrayOutputStream();
262         byte[] buffer = new byte[1024];
263         int count;
264         while ((count = urlConnection.getInputStream().read(buffer)) != -1) {
265             bytes.write(buffer, 0, count);
266             if (bytes.size() > MAX_PAC_SIZE) {
267                 throw new IOException("PAC too big");
268             }
269         }
270         return bytes.toString();
271     }
272 
getNextDelay(int currentDelay)273     private int getNextDelay(int currentDelay) {
274         if (++currentDelay > DELAY_4) {
275             return DELAY_4;
276         }
277         return currentDelay;
278     }
279 
longSchedule()280     private void longSchedule() {
281         mCurrentDelay = DELAY_1;
282         setDownloadIn(DELAY_LONG);
283     }
284 
reschedule()285     private void reschedule() {
286         mCurrentDelay = getNextDelay(mCurrentDelay);
287         setDownloadIn(mCurrentDelay);
288     }
289 
getPacChangeDelay()290     private String getPacChangeDelay() {
291         final ContentResolver cr = mContext.getContentResolver();
292 
293         // Check system properties for the default value then use secure settings value, if any.
294         String defaultDelay = SystemProperties.get(
295                 "conn." + Settings.Global.PAC_CHANGE_DELAY,
296                 DEFAULT_DELAYS);
297         String val = Settings.Global.getString(cr, Settings.Global.PAC_CHANGE_DELAY);
298         return (val == null) ? defaultDelay : val;
299     }
300 
getDownloadDelay(int delayIndex)301     private long getDownloadDelay(int delayIndex) {
302         String[] list = getPacChangeDelay().split(" ");
303         if (delayIndex < list.length) {
304             return Long.parseLong(list[delayIndex]);
305         }
306         return 0;
307     }
308 
setDownloadIn(int delayIndex)309     private void setDownloadIn(int delayIndex) {
310         long delay = getDownloadDelay(delayIndex);
311         long timeTillTrigger = 1000 * delay + SystemClock.elapsedRealtime();
312         getAlarmManager().set(AlarmManager.ELAPSED_REALTIME, timeTillTrigger, mPacRefreshIntent);
313     }
314 
315     @GuardedBy("mProxyLock")
setCurrentProxyScript(String script)316     private void setCurrentProxyScript(String script) {
317         if (mProxyService == null) {
318             Log.e(TAG, "setCurrentProxyScript: no proxy service");
319             return;
320         }
321         try {
322             mProxyService.setPacFile(script);
323             mCurrentPac = script;
324         } catch (RemoteException e) {
325             Log.e(TAG, "Unable to set PAC file", e);
326         }
327     }
328 
bind()329     private void bind() {
330         if (mContext == null) {
331             Log.e(TAG, "No context for binding");
332             return;
333         }
334         Intent intent = new Intent();
335         intent.setClassName(PAC_PACKAGE, PAC_SERVICE);
336         if ((mProxyConnection != null) && (mConnection != null)) {
337             // Already bound: no need to bind again, just download the new file.
338             mNetThreadHandler.post(mPacDownloader);
339             return;
340         }
341         mConnection = new ServiceConnection() {
342             @Override
343             public void onServiceDisconnected(ComponentName component) {
344                 synchronized (mProxyLock) {
345                     mProxyService = null;
346                 }
347             }
348 
349             @Override
350             public void onServiceConnected(ComponentName component, IBinder binder) {
351                 synchronized (mProxyLock) {
352                     try {
353                         Log.d(TAG, "Adding service " + PAC_SERVICE_NAME + " "
354                                 + binder.getInterfaceDescriptor());
355                     } catch (RemoteException e1) {
356                         Log.e(TAG, "Remote Exception", e1);
357                     }
358                     ServiceManager.addService(PAC_SERVICE_NAME, binder);
359                     mProxyService = IProxyService.Stub.asInterface(binder);
360                     if (mProxyService == null) {
361                         Log.e(TAG, "No proxy service");
362                     } else {
363                         // If mCurrentPac is not null, then the PacService might have
364                         // crashed and restarted. The download task will not actually
365                         // call setCurrentProxyScript, so call setCurrentProxyScript here.
366                         if (mCurrentPac != null) {
367                             setCurrentProxyScript(mCurrentPac);
368                         } else {
369                             mNetThreadHandler.post(mPacDownloader);
370                         }
371                     }
372                 }
373             }
374         };
375         mContext.bindServiceAsUser(intent, mConnection,
376                 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE,
377                 UserHandle.SYSTEM);
378 
379         intent = new Intent();
380         intent.setClassName(PROXY_PACKAGE, PROXY_SERVICE);
381         mProxyConnection = new ServiceConnection() {
382             @Override
383             public void onServiceDisconnected(ComponentName component) {
384             }
385 
386             @Override
387             public void onServiceConnected(ComponentName component, IBinder binder) {
388                 IProxyCallback callbackService = IProxyCallback.Stub.asInterface(binder);
389                 if (callbackService != null) {
390                     try {
391                         callbackService.getProxyPort(new IProxyPortListener.Stub() {
392                             @Override
393                             public void setProxyPort(int port) {
394                                 if (mLastPort != -1) {
395                                     // Always need to send if port changed
396                                     // TODO: Here lacks synchronization because this write cannot
397                                     // guarantee that it's visible from sendProxyIfNeeded() when
398                                     // it's called by a Runnable which is post by mNetThread.
399                                     mHasSentBroadcast = false;
400                                 }
401                                 mLastPort = port;
402                                 if (port != -1) {
403                                     Log.d(TAG, "Local proxy is bound on " + port);
404                                     sendProxyIfNeeded();
405                                 } else {
406                                     Log.e(TAG, "Received invalid port from Local Proxy,"
407                                             + " PAC will not be operational");
408                                 }
409                             }
410                         });
411                     } catch (RemoteException e) {
412                         e.printStackTrace();
413                     }
414                 }
415             }
416         };
417         mContext.bindServiceAsUser(intent, mProxyConnection,
418                 Context.BIND_AUTO_CREATE | Context.BIND_NOT_FOREGROUND | Context.BIND_NOT_VISIBLE,
419                 mNetThreadHandler, UserHandle.SYSTEM);
420     }
421 
unbind()422     private void unbind() {
423         if (mConnection != null) {
424             mContext.unbindService(mConnection);
425             mConnection = null;
426         }
427         if (mProxyConnection != null) {
428             mContext.unbindService(mProxyConnection);
429             mProxyConnection = null;
430         }
431         mProxyService = null;
432         mLastPort = -1;
433     }
434 
sendPacBroadcast(ProxyInfo proxy)435     private void sendPacBroadcast(ProxyInfo proxy) {
436         final int length = mCallbacks.beginBroadcast();
437         for (int i = 0; i < length; i++) {
438             final IPacProxyInstalledListener listener = mCallbacks.getBroadcastItem(i);
439             if (listener != null) {
440                 try {
441                     listener.onPacProxyInstalled(null /* network */, proxy);
442                 } catch (RemoteException ignored) { }
443             }
444         }
445         mCallbacks.finishBroadcast();
446     }
447 
448     // This method must be called on mNetThreadHandler.
sendProxyIfNeeded()449     private void sendProxyIfNeeded() {
450         synchronized (mBroadcastStateLock) {
451             if (!mHasDownloaded || (mLastPort == -1)) {
452                 return;
453             }
454             if (!mHasSentBroadcast) {
455                 sendPacBroadcast(ProxyInfo.buildPacProxy(mPacUrl, mLastPort));
456                 mHasSentBroadcast = true;
457             }
458         }
459     }
460 }
461