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