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