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