1 // Copyright 2023 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net.httpflags; 6 7 import android.content.Context; 8 import android.content.Intent; 9 import android.content.pm.ApplicationInfo; 10 import android.content.pm.PackageManager; 11 import android.content.pm.ResolveInfo; 12 import android.os.Build; 13 14 import androidx.annotation.Nullable; 15 import androidx.annotation.VisibleForTesting; 16 17 import org.chromium.base.Log; 18 import org.chromium.net.impl.CronetManifest; 19 20 import java.io.File; 21 import java.io.FileInputStream; 22 import java.io.FileNotFoundException; 23 import java.io.IOException; 24 25 /** 26 * Utilities for loading HTTP flags. 27 * 28 * <p>HTTP flags are a generic mechanism by which the host system (i.e. the Android system image) 29 * can provide values for a variety of configuration knobs to alter the behavior of the HTTP client 30 * stack. The idea is that the host system can use some kind of centralized configuration mechanism 31 * to remotely push changes to these settings while collecting data on the results. This in turn 32 * enables A/B experiments, progressive configuration rollouts, etc. 33 * 34 * <p>Currently, the interface with the host system is defined as follows: 35 * <ol> 36 * <li>The Android system image must provide an Android app that exposes a service matching the 37 * {@link #FLAGS_FILE_PROVIDER_INTENT_ACTION} action. 38 * <li>That Android app must expose a directory named after {@link #FLAGS_FILE_DIR_NAME} under the 39 * app's {@link ApplicationInfo#deviceProtectedDataDir}. 40 * <li>That directory must contain a file named after {@link #FLAGS_FILE_NAME} that must be readable 41 * by the process running {@link #load}. 42 * <li>The flag values are obtained from the contents of that file. The format is a binary proto 43 * that can be read through {@link Flags#parseDelimitedFrom} - see `flags.proto` for details. 44 * </ol> 45 * 46 * @see HttpFlagsInterceptor 47 */ 48 public final class HttpFlagsLoader { HttpFlagsLoader()49 private HttpFlagsLoader() {} 50 51 @VisibleForTesting 52 static final String FLAGS_FILE_PROVIDER_INTENT_ACTION = "android.net.http.FLAGS_FILE_PROVIDER"; 53 54 @VisibleForTesting static final String FLAGS_FILE_DIR_NAME = "app_httpflags"; 55 @VisibleForTesting static final String FLAGS_FILE_NAME = "flags.binarypb"; 56 57 private static final String TAG = "HttpFlagsLoader"; 58 59 /** 60 * Locates and loads the HTTP flags file from the host system. 61 * 62 * Note that this is an expensive call. 63 * 64 * @return The contents of the flags file, or null if the flags file could not be loaded for any 65 * reason. In the latter case, the callee will take care of logging the failure. 66 * 67 * @see ResolvedFlags 68 */ 69 @Nullable load(Context context)70 public static Flags load(Context context) { 71 if (!CronetManifest.shouldReadHttpFlags(context)) { 72 Log.d(TAG, "Not loading HTTP flags because they are disabled in the manifest"); 73 return null; 74 } 75 76 try { 77 ApplicationInfo providerApplicationInfo = getProviderApplicationInfo(context); 78 if (providerApplicationInfo == null) return null; 79 Log.d( 80 TAG, 81 "Found application exporting HTTP flags: %s", 82 providerApplicationInfo.packageName); 83 84 File flagsFile = getFlagsFileFromProvider(context, providerApplicationInfo); 85 Log.d(TAG, "HTTP flags file path: %s", flagsFile.getAbsolutePath()); 86 87 Flags flags = loadFlagsFile(flagsFile); 88 if (flags == null) return null; 89 Log.d(TAG, "Successfully loaded HTTP flags: %s", flags); 90 91 return flags; 92 } catch (RuntimeException exception) { 93 Log.e(TAG, "Unable to load HTTP flags file", exception); 94 return null; 95 } 96 } 97 98 @Nullable getProviderApplicationInfo(Context context)99 private static ApplicationInfo getProviderApplicationInfo(Context context) { 100 ResolveInfo resolveInfo = 101 context.getPackageManager() 102 .resolveService( 103 new Intent(FLAGS_FILE_PROVIDER_INTENT_ACTION), 104 // Make sure we only read flags files that are written by a package 105 // from the system image. This prevents random third-party apps 106 // from being able to inject flags into other apps, which would be 107 // a security risk. 108 PackageManager.MATCH_SYSTEM_ONLY); 109 if (resolveInfo == null) { 110 Log.w( 111 TAG, 112 "Unable to resolve the HTTP flags file provider package. This is expected if " 113 + "the host system is not set up to provide HTTP flags."); 114 return null; 115 } 116 117 return resolveInfo.serviceInfo.applicationInfo; 118 } 119 getFlagsFileFromProvider( Context context, ApplicationInfo providerApplicationInfo)120 private static File getFlagsFileFromProvider( 121 Context context, ApplicationInfo providerApplicationInfo) { 122 return new File( 123 new File( 124 new File( 125 Build.VERSION.SDK_INT >= 24 126 ? providerApplicationInfo.deviceProtectedDataDir 127 : providerApplicationInfo.dataDir), 128 FLAGS_FILE_DIR_NAME), 129 FLAGS_FILE_NAME); 130 } 131 132 @Nullable loadFlagsFile(File file)133 private static Flags loadFlagsFile(File file) { 134 try (FileInputStream fileInputStream = new FileInputStream(file)) { 135 return Flags.parseDelimitedFrom(fileInputStream); 136 } catch (FileNotFoundException exception) { 137 Log.w( 138 TAG, 139 "HTTP flags file `%s` is missing. This is expected if HTTP flags functionality " 140 + "is currently disabled in the host system.", 141 file.getPath()); 142 return null; 143 } catch (IOException exception) { 144 throw new RuntimeException("Unable to read HTTP flags file", exception); 145 } 146 } 147 } 148