content: /**
   * Copyright (c) 2018, The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *     http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package com.android.server.connectivity;
  
  import static android.provider.Settings.Global.GLOBAL_HTTP_PROXY_EXCLUSION_LIST;
  import static android.provider.Settings.Global.GLOBAL_HTTP_PROXY_HOST;
  import static android.provider.Settings.Global.GLOBAL_HTTP_PROXY_PAC;
  import static android.provider.Settings.Global.GLOBAL_HTTP_PROXY_PORT;
  import static android.provider.Settings.Global.HTTP_PROXY;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.content.ContentResolver;
  import android.content.Context;
  import android.content.Intent;
  import android.net.Proxy;
  import android.net.ProxyInfo;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.Handler;
  import android.os.UserHandle;
  import android.provider.Settings;
  import android.text.TextUtils;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  
  import java.util.Objects;
  
  /**
   * A class to handle proxy for ConnectivityService.
   *
   * @hide
   */
  public class ProxyTracker {
      private static final String TAG = ProxyTracker.class.getSimpleName();
      private static final boolean DBG = true;
  
      @NonNull
      private final Context mContext;
  
      @NonNull
      private final Object mProxyLock = new Object();
      // The global proxy is the proxy that is set device-wide, overriding any network-specific
      // proxy. Note however that proxies are hints ; the system does not enforce their use. Hence
      // this value is only for querying.
      @Nullable
      @GuardedBy("mProxyLock")
      private ProxyInfo mGlobalProxy = null;
      // The default proxy is the proxy that applies to no particular network if the global proxy
      // is not set. Individual networks have their own settings that override this. This member
      // is set through setDefaultProxy, which is called when the default network changes proxies
      // in its LinkProperties, or when ConnectivityService switches to a new default network, or
      // when PacManager resolves the proxy.
      @Nullable
      @GuardedBy("mProxyLock")
      private volatile ProxyInfo mDefaultProxy = null;
      // Whether the default proxy is enabled.
      @GuardedBy("mProxyLock")
      private boolean mDefaultProxyEnabled = true;
  
      // The object responsible for Proxy Auto Configuration (PAC).
      @NonNull
      private final PacManager mPacManager;
  
      public ProxyTracker(@NonNull final Context context,
              @NonNull final Handler connectivityServiceInternalHandler, final int pacChangedEvent) {
          mContext = context;
          mPacManager = new PacManager(context, connectivityServiceInternalHandler, pacChangedEvent);
      }
  
      // Convert empty ProxyInfo's to null as null-checks are used to determine if proxies are present
      // (e.g. if mGlobalProxy==null fall back to network-specific proxy, if network-specific
      // proxy is null then there is no proxy in place).
      @Nullable
      private static ProxyInfo canonicalizeProxyInfo(@Nullable final ProxyInfo proxy) {
          if (proxy != null && TextUtils.isEmpty(proxy.getHost())
                  && Uri.EMPTY.equals(proxy.getPacFileUrl())) {
              return null;
          }
          return proxy;
      }
  
      // ProxyInfo equality functions with a couple modifications over ProxyInfo.equals() to make it
      // better for determining if a new proxy broadcast is necessary:
      // 1. Canonicalize empty ProxyInfos to null so an empty proxy compares equal to null so as to
      //    avoid unnecessary broadcasts.
      // 2. Make sure all parts of the ProxyInfo's compare true, including the host when a PAC URL
      //    is in place.  This is important so legacy PAC resolver (see com.android.proxyhandler)
      //    changes aren't missed.  The legacy PAC resolver pretends to be a simple HTTP proxy but
      //    actually uses the PAC to resolve; this results in ProxyInfo's with PAC URL, host and port
      //    all set.
      public static boolean proxyInfoEqual(@Nullable final ProxyInfo a, @Nullable final ProxyInfo b) {
          final ProxyInfo pa = canonicalizeProxyInfo(a);
          final ProxyInfo pb = canonicalizeProxyInfo(b);
          // ProxyInfo.equals() doesn't check hosts when PAC URLs are present, but we need to check
          // hosts even when PAC URLs are present to account for the legacy PAC resolver.
          return Objects.equals(pa, pb) && (pa == null || Objects.equals(pa.getHost(), pb.getHost()));
      }
  
      /**
       * Gets the default system-wide proxy.
       *
       * This will return the global proxy if set, otherwise the default proxy if in use. Note
       * that this is not necessarily the proxy that any given process should use, as the right
       * proxy for a process is the proxy for the network this process will use, which may be
       * different from this value. This value is simply the default in case there is no proxy set
       * in the network that will be used by a specific process.
       * @return The default system-wide proxy or null if none.
       */
      @Nullable
      public ProxyInfo getDefaultProxy() {
          // This information is already available as a world read/writable jvm property.
          synchronized (mProxyLock) {
              if (mGlobalProxy != null) return mGlobalProxy;
              if (mDefaultProxyEnabled) return mDefaultProxy;
              return null;
          }
      }
  
      /**
       * Gets the global proxy.
       *
       * @return The global proxy or null if none.
       */
      @Nullable
      public ProxyInfo getGlobalProxy() {
          // This information is already available as a world read/writable jvm property.
          synchronized (mProxyLock) {
              return mGlobalProxy;
          }
      }
  
      /**
       * Read the global proxy settings and cache them in memory.
       */
      public void loadGlobalProxy() {
          ContentResolver res = mContext.getContentResolver();
          String host = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_HOST);
          int port = Settings.Global.getInt(res, GLOBAL_HTTP_PROXY_PORT, 0);
          String exclList = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST);
          String pacFileUrl = Settings.Global.getString(res, GLOBAL_HTTP_PROXY_PAC);
          if (!TextUtils.isEmpty(host) || !TextUtils.isEmpty(pacFileUrl)) {
              ProxyInfo proxyProperties;
              if (!TextUtils.isEmpty(pacFileUrl)) {
                  proxyProperties = new ProxyInfo(pacFileUrl);
              } else {
                  proxyProperties = new ProxyInfo(host, port, exclList);
              }
              if (!proxyProperties.isValid()) {
                  if (DBG) Slog.d(TAG, "Invalid proxy properties, ignoring: " + proxyProperties);
                  return;
              }
  
              synchronized (mProxyLock) {
                  mGlobalProxy = proxyProperties;
              }
          }
          loadDeprecatedGlobalHttpProxy();
          // TODO : shouldn't this function call mPacManager.setCurrentProxyScriptUrl ?
      }
  
      /**
       * Read the global proxy from the deprecated Settings.Global.HTTP_PROXY setting and apply it.
       */
      public void loadDeprecatedGlobalHttpProxy() {
          final String proxy = Settings.Global.getString(mContext.getContentResolver(), HTTP_PROXY);
          if (!TextUtils.isEmpty(proxy)) {
              String data[] = proxy.split(":");
              if (data.length == 0) {
                  return;
              }
  
              final String proxyHost = data[0];
              int proxyPort = 8080;
              if (data.length > 1) {
                  try {
                      proxyPort = Integer.parseInt(data[1]);
                  } catch (NumberFormatException e) {
                      return;
                  }
              }
              final ProxyInfo p = new ProxyInfo(proxyHost, proxyPort, "");
              setGlobalProxy(p);
          }
      }
  
      /**
       * Sends the system broadcast informing apps about a new proxy configuration.
       *
       * Confusingly this method also sets the PAC file URL. TODO : separate this, it has nothing
       * to do in a "sendProxyBroadcast" method.
       */
      public void sendProxyBroadcast() {
          final ProxyInfo defaultProxy = getDefaultProxy();
          final ProxyInfo proxyInfo = null != defaultProxy ? defaultProxy : new ProxyInfo("", 0, "");
          if (mPacManager.setCurrentProxyScriptUrl(proxyInfo) == PacManager.DONT_SEND_BROADCAST) {
              return;
          }
          if (DBG) Slog.d(TAG, "sending Proxy Broadcast for " + proxyInfo);
          Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);
          intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING |
                  Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
          intent.putExtra(Proxy.EXTRA_PROXY_INFO, proxyInfo);
          final long ident = Binder.clearCallingIdentity();
          try {
              mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
          } finally {
              Binder.restoreCallingIdentity(ident);
          }
      }
  
      /**
       * Sets the global proxy in memory. Also writes the values to the global settings of the device.
       *
       * @param proxyInfo the proxy spec, or null for no proxy.
       */
      public void setGlobalProxy(@Nullable ProxyInfo proxyInfo) {
          synchronized (mProxyLock) {
              // ProxyInfo#equals is not commutative :( and is public API, so it can't be fixed.
              if (proxyInfo == mGlobalProxy) return;
              if (proxyInfo != null && proxyInfo.equals(mGlobalProxy)) return;
              if (mGlobalProxy != null && mGlobalProxy.equals(proxyInfo)) return;
  
              final String host;
              final int port;
              final String exclList;
              final String pacFileUrl;
              if (proxyInfo != null && (!TextUtils.isEmpty(proxyInfo.getHost()) ||
                      !Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))) {
                  if (!proxyInfo.isValid()) {
                      if (DBG) Slog.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
                      return;
                  }
                  mGlobalProxy = new ProxyInfo(proxyInfo);
                  host = mGlobalProxy.getHost();
                  port = mGlobalProxy.getPort();
                  exclList = mGlobalProxy.getExclusionListAsString();
                  pacFileUrl = Uri.EMPTY.equals(proxyInfo.getPacFileUrl())
                          ? "" : proxyInfo.getPacFileUrl().toString();
              } else {
                  host = "";
                  port = 0;
                  exclList = "";
                  pacFileUrl = "";
                  mGlobalProxy = null;
              }
              final ContentResolver res = mContext.getContentResolver();
              final long token = Binder.clearCallingIdentity();
              try {
                  Settings.Global.putString(res, GLOBAL_HTTP_PROXY_HOST, host);
                  Settings.Global.putInt(res, GLOBAL_HTTP_PROXY_PORT, port);
                  Settings.Global.putString(res, GLOBAL_HTTP_PROXY_EXCLUSION_LIST, exclList);
                  Settings.Global.putString(res, GLOBAL_HTTP_PROXY_PAC, pacFileUrl);
              } finally {
                  Binder.restoreCallingIdentity(token);
              }
  
              sendProxyBroadcast();
          }
      }
  
      /**
       * Sets the default proxy for the device.
       *
       * The default proxy is the proxy used for networks that do not have a specific proxy.
       * @param proxyInfo the proxy spec, or null for no proxy.
       */
      public void setDefaultProxy(@Nullable ProxyInfo proxyInfo) {
          synchronized (mProxyLock) {
              if (Objects.equals(mDefaultProxy, proxyInfo)) return;
              if (proxyInfo != null &&  !proxyInfo.isValid()) {
                  if (DBG) Slog.d(TAG, "Invalid proxy properties, ignoring: " + proxyInfo);
                  return;
              }
  
              // This call could be coming from the PacManager, containing the port of the local
              // proxy. If this new proxy matches the global proxy then copy this proxy to the
              // global (to get the correct local port), and send a broadcast.
              // TODO: Switch PacManager to have its own message to send back rather than
              // reusing EVENT_HAS_CHANGED_PROXY and this call to handleApplyDefaultProxy.
              if ((mGlobalProxy != null) && (proxyInfo != null)
                      && (!Uri.EMPTY.equals(proxyInfo.getPacFileUrl()))
                      && proxyInfo.getPacFileUrl().equals(mGlobalProxy.getPacFileUrl())) {
                  mGlobalProxy = proxyInfo;
                  sendProxyBroadcast();
                  return;
              }
              mDefaultProxy = proxyInfo;
  
              if (mGlobalProxy != null) return;
              if (mDefaultProxyEnabled) {
                  sendProxyBroadcast();
              }
          }
      }
  }
  