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