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