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.telemetry; 6 7 import android.util.Log; 8 9 import org.json.JSONException; 10 import org.json.JSONObject; 11 import org.json.JSONTokener; 12 13 import java.util.ArrayList; 14 import java.util.Locale; 15 import java.util.Set; 16 17 /** Parses the experimentalOptions string */ 18 public final class ExperimentalOptions { 19 private static final String TAG = ExperimentalOptions.class.getSimpleName(); 20 21 public static final int UNSET_INT_VALUE = -1; 22 23 // Declare static experimental options field trial names 24 private static final String QUIC = "QUIC"; 25 private static final String ASYNC_DNS = "AsyncDNS"; 26 private static final String STALE_DNS = "StaleDNS"; 27 28 // The JSONObject created from the experimentalOptions String 29 private JSONObject mJson = new JSONObject(); 30 ExperimentalOptions(String experimentalOptions)31 public ExperimentalOptions(String experimentalOptions) { 32 if (!isNullOrEmpty(experimentalOptions)) { 33 try { 34 mJson = (JSONObject) new JSONTokener(experimentalOptions).nextValue(); 35 } catch (JSONException | ClassCastException e) { 36 Log.d( 37 TAG, 38 String.format( 39 "Experimental options could not be parsed, using default values." 40 + " Error: %s", 41 e.getMessage())); 42 } 43 } 44 } 45 getConnectionOptionsOption()46 public String getConnectionOptionsOption() { 47 return parseExperimentalOptionsString( 48 getOrDefault(QUIC, "connection_options", null, String.class)); 49 } 50 getStoreServerConfigsInPropertiesOption()51 public OptionalBoolean getStoreServerConfigsInPropertiesOption() { 52 return OptionalBoolean.fromBoolean( 53 getOrDefault(QUIC, "store_server_configs_in_properties", null, Boolean.class)); 54 } 55 getMaxServerConfigsStoredInPropertiesOption()56 public int getMaxServerConfigsStoredInPropertiesOption() { 57 return getOrDefault( 58 QUIC, "max_server_configs_stored_in_properties", UNSET_INT_VALUE, Integer.class); 59 } 60 61 @SuppressWarnings("GoodTime") // CronetStatsLog expects int getIdleConnectionTimeoutSecondsOption()62 public int getIdleConnectionTimeoutSecondsOption() { 63 return getOrDefault( 64 QUIC, "idle_connection_timeout_seconds", UNSET_INT_VALUE, Integer.class); 65 } 66 getGoawaySessionsOnIpChangeOption()67 public OptionalBoolean getGoawaySessionsOnIpChangeOption() { 68 return OptionalBoolean.fromBoolean( 69 getOrDefault(QUIC, "goaway_sessions_on_ip_change", null, Boolean.class)); 70 } 71 getCloseSessionsOnIpChangeOption()72 public OptionalBoolean getCloseSessionsOnIpChangeOption() { 73 return OptionalBoolean.fromBoolean( 74 getOrDefault(QUIC, "close_sessions_on_ip_change", null, Boolean.class)); 75 } 76 getMigrateSessionsOnNetworkChangeV2Option()77 public OptionalBoolean getMigrateSessionsOnNetworkChangeV2Option() { 78 return OptionalBoolean.fromBoolean( 79 getOrDefault(QUIC, "migrate_sessions_on_network_change_v2", null, Boolean.class)); 80 } 81 getMigrateSessionsEarlyV2()82 public OptionalBoolean getMigrateSessionsEarlyV2() { 83 return OptionalBoolean.fromBoolean( 84 getOrDefault(QUIC, "migrate_sessions_early_v2", null, Boolean.class)); 85 } 86 getDisableBidirectionalStreamsOption()87 public OptionalBoolean getDisableBidirectionalStreamsOption() { 88 return OptionalBoolean.fromBoolean( 89 getOrDefault(QUIC, "disable_bidirectional_streams", null, Boolean.class)); 90 } 91 92 @SuppressWarnings("GoodTime") // CronetStatsLog expects int getMaxTimeBeforeCryptoHandshakeSecondsOption()93 public int getMaxTimeBeforeCryptoHandshakeSecondsOption() { 94 return getOrDefault( 95 QUIC, "max_time_before_crypto_handshake_seconds", UNSET_INT_VALUE, Integer.class); 96 } 97 98 @SuppressWarnings("GoodTime") // CronetStatsLog expects int getMaxIdleTimeBeforeCryptoHandshakeSecondsOption()99 public int getMaxIdleTimeBeforeCryptoHandshakeSecondsOption() { 100 return getOrDefault( 101 QUIC, 102 "max_idle_time_before_crypto_handshake_seconds", 103 UNSET_INT_VALUE, 104 Integer.class); 105 } 106 getEnableSocketRecvOptimizationOption()107 public OptionalBoolean getEnableSocketRecvOptimizationOption() { 108 return OptionalBoolean.fromBoolean( 109 getOrDefault(QUIC, "enable_socket_recv_optimization", null, Boolean.class)); 110 } 111 getAsyncDnsEnableOption()112 public OptionalBoolean getAsyncDnsEnableOption() { 113 return OptionalBoolean.fromBoolean(getOrDefault(ASYNC_DNS, "enable", null, Boolean.class)); 114 } 115 getStaleDnsEnableOption()116 public OptionalBoolean getStaleDnsEnableOption() { 117 return OptionalBoolean.fromBoolean(getOrDefault(STALE_DNS, "enable", null, Boolean.class)); 118 } 119 120 @SuppressWarnings("GoodTime") // CronetStatsLog expects int getStaleDnsDelayMillisOption()121 public int getStaleDnsDelayMillisOption() { 122 return getOrDefault(STALE_DNS, "delay_ms", UNSET_INT_VALUE, Integer.class); 123 } 124 125 @SuppressWarnings("GoodTime") // CronetStatsLog expects int getStaleDnsMaxExpiredTimeMillisOption()126 public int getStaleDnsMaxExpiredTimeMillisOption() { 127 return getOrDefault(STALE_DNS, "max_expired_time_ms", UNSET_INT_VALUE, Integer.class); 128 } 129 getStaleDnsMaxStaleUsesOption()130 public int getStaleDnsMaxStaleUsesOption() { 131 return getOrDefault(STALE_DNS, "max_stale_uses", UNSET_INT_VALUE, Integer.class); 132 } 133 getStaleDnsAllowOtherNetworkOption()134 public OptionalBoolean getStaleDnsAllowOtherNetworkOption() { 135 return OptionalBoolean.fromBoolean( 136 getOrDefault(STALE_DNS, "allow_other_network", null, Boolean.class)); 137 } 138 getStaleDnsPersistToDiskOption()139 public OptionalBoolean getStaleDnsPersistToDiskOption() { 140 return OptionalBoolean.fromBoolean( 141 getOrDefault(STALE_DNS, "persist_to_disk", null, Boolean.class)); 142 } 143 144 @SuppressWarnings("GoodTime") // CronetStatsLog expects int getStaleDnsPersistDelayMillisOption()145 public int getStaleDnsPersistDelayMillisOption() { 146 return getOrDefault(STALE_DNS, "persist_delay_ms", UNSET_INT_VALUE, Integer.class); 147 } 148 getStaleDnsUseStaleOnNameNotResolvedOption()149 public OptionalBoolean getStaleDnsUseStaleOnNameNotResolvedOption() { 150 return OptionalBoolean.fromBoolean( 151 getOrDefault(STALE_DNS, "use_stale_on_name_not_resolved", null, Boolean.class)); 152 } 153 getDisableIpv6OnWifiOption()154 public OptionalBoolean getDisableIpv6OnWifiOption() { 155 return OptionalBoolean.fromBoolean( 156 getOrDefault("disable_ipv6_on_wifi", null, Boolean.class)); 157 } 158 159 /** 160 * Checks if an experimentalOption fieldTrial key exists, then gets the value of the child 161 * option. 162 * 163 * @param experimentalOptionFieldTrialName the super option name for a nested experimental 164 * option eg QUIC.connection_options where <code>QUIC</code> is the FieldTrialName and 165 * <code> 166 * connection_options</code> is the child option 167 * @param option the child option eg <code>connection_options</code> 168 * @param defaultValue the defaultValue if the option is null or empty 169 * @return the experimental option value 170 */ getOrDefault( String experimentalOptionFieldTrialName, String option, T defaultValue, Class<T> clazz)171 private <T> T getOrDefault( 172 String experimentalOptionFieldTrialName, 173 String option, 174 T defaultValue, 175 Class<T> clazz) { 176 // check if the field trial name exists 177 JSONObject options = null; 178 try { 179 options = mJson.getJSONObject(experimentalOptionFieldTrialName); 180 } catch (JSONException e) { 181 Log.d( 182 TAG, 183 String.format( 184 "Failed to get %s options: %s", 185 experimentalOptionFieldTrialName, e.getMessage())); 186 } 187 188 if (options == null) { 189 return defaultValue; 190 } 191 192 T value = defaultValue; 193 try { 194 value = clazz.cast(options.get(option)); 195 } catch (JSONException | ClassCastException e) { 196 Log.d(TAG, String.format("Failed to get %s options: %s", option, e.getMessage())); 197 } 198 return value; 199 } 200 getOrDefault(String option, T defaultValue, Class<T> clazz)201 private <T> T getOrDefault(String option, T defaultValue, Class<T> clazz) { 202 T value = defaultValue; 203 try { 204 value = clazz.cast(mJson.get(option)); 205 } catch (JSONException | ClassCastException e) { 206 Log.d(TAG, String.format("Failed to get %s options: %s", option, e.getMessage())); 207 } 208 return value; 209 } 210 211 /** 212 * Checks that the connection_options options are always valid and do not contain any PII. 213 * Removes any value that does not conform to a valid option. 214 */ parseExperimentalOptionsString(String str)215 private String parseExperimentalOptionsString(String str) { 216 if (isNullOrEmpty(str)) { 217 return str; 218 } 219 220 ArrayList<String> nStr = new ArrayList<>(); 221 for (String s : str.split(",", -1)) { 222 if (VALID_CONNECTION_OPTIONS.contains(s.toUpperCase(Locale.ROOT).trim())) { 223 nStr.add(s); 224 } 225 } 226 227 return String.join(",", nStr); 228 } 229 230 /** 231 * The generated CronetStatsLog class has an optionalBoolean(UNSET,TRUE,FALSE) variable for each 232 * of the experimental options. Since these values will always be the same for the options, we 233 * picked one of them and used it to create a private variable that we can use to make the code 234 * more readable. 235 */ 236 public static enum OptionalBoolean { 237 UNSET( 238 CronetStatsLog 239 .CRONET_ENGINE_CREATED__EXPERIMENTAL_OPTIONS_QUIC_STORE_SERVER_CONFIGS_IN_PROPERTIES__OPTIONAL_BOOLEAN_UNSET), 240 TRUE( 241 CronetStatsLog 242 .CRONET_ENGINE_CREATED__EXPERIMENTAL_OPTIONS_QUIC_STORE_SERVER_CONFIGS_IN_PROPERTIES__OPTIONAL_BOOLEAN_TRUE), 243 FALSE( 244 CronetStatsLog 245 .CRONET_ENGINE_CREATED__EXPERIMENTAL_OPTIONS_QUIC_STORE_SERVER_CONFIGS_IN_PROPERTIES__OPTIONAL_BOOLEAN_FALSE); 246 247 private final int mValue; 248 OptionalBoolean(int value)249 private OptionalBoolean(int value) { 250 this.mValue = value; 251 } 252 getValue()253 public int getValue() { 254 return mValue; 255 } 256 fromBoolean(Boolean value)257 public static OptionalBoolean fromBoolean(Boolean value) { 258 if (value == null) { 259 return UNSET; 260 } 261 262 return value ? TRUE : FALSE; 263 } 264 } 265 isNullOrEmpty(String str)266 private boolean isNullOrEmpty(String str) { 267 return str == null || str.isEmpty(); 268 } 269 270 // Source: 271 // //external/cronet:net/third_party/quiche/src/quiche/quic/core/crypto/crypto_protocol.h 272 public static final Set<String> VALID_CONNECTION_OPTIONS = 273 Set.of( 274 "CHLO", "SHLO", "SCFG", "REJ", "CETV", "PRST", "SCUP", "ALPN", "P256", "C255", 275 "AESG", "CC20", "QBIC", "AFCW", "IFW5", "IFW6", "IFW7", "IFW8", "IFW9", "IFWA", 276 "TBBR", "1RTT", "2RTT", "LRTT", "BBS1", "BBS2", "BBS3", "BBS4", "BBS5", "BBRR", 277 "BBR1", "BBR2", "BBR3", "BBR4", "BBR5", "BBR9", "BBRA", "BBRB", "BBRS", "BBQ1", 278 "BBQ2", "BBQ3", "BBQ5", "BBQ6", "BBQ7", "BBQ8", "BBQ9", "BBQ0", "RENO", "TPCC", 279 "BYTE", "IW03", "IW10", "IW20", "IW50", "B2ON", "B2NA", "B2NE", "B2RP", "B2LO", 280 "B2HR", "B2SL", "B2H2", "B2RC", "BSAO", "B2DL", "B201", "B202", "B203", "B204", 281 "B205", "B206", "B207", "NTLP", "1TLP", "1RTO", "NRTO", "TIME", "ATIM", "MIN1", 282 "MIN4", "MAD0", "MAD2", "MAD3", "1ACK", "AKD3", "AKDU", "AFFE", "AFF1", "AFF2", 283 "SSLR", "NPRR", "2RTO", "3RTO", "4RTO", "5RTO", "6RTO", "CBHD", "NBHD", "CONH", 284 "LFAK", "STMP", "EACK", "ILD0", "ILD1", "ILD2", "ILD3", "ILD4", "RUNT", "NSTP", 285 "NRTT", "1PTO", "2PTO", "6PTO", "7PTO", "8PTO", "PTOS", "PTOA", "PEB1", "PEB2", 286 "PVS1", "PAG1", "PAG2", "PSDA", "PLE1", "PLE2", "APTO", "ELDT", "RVCM", "TCID", 287 "MPTH", "NCMR", "DFER", "NPCO", "BWRE", "BWMX", "BWID", "BWI1", "BWRS", "BWS2", 288 "BWS3", "BWS4", "BWS5", "BWS6", "BWP0", "BWP1", "BWP2", "BWP3", "BWP4", "BWG4", 289 "BWG7", "BWG8", "BWS7", "BWM3", "BWM4", "ICW1", "DTOS", "FIDT", "3AFF", "10AF", 290 "MTUH", "MTUL", "NSLC", "NCHP", "NBPE", "X509", "X59R", "CHID", "VER ", "NONC", 291 "NONP", "KEXS", "AEAD", "COPT", "CLOP", "ICSL", "MIBS", "MIUS", "ADE ", "IRTT", 292 "TRTT", "SNI ", "PUBS", "SCID", "ORBT", "PDMD", "PROF", "CCRT", "EXPY", "STTL", 293 "SFCW", "CFCW", "UAID", "XLCT", "QLVE", "PDP1", "PDP2", "PDP3", "PDP5", "QNZ2", 294 "MAD", "IGNP", "SRWP", "ROWF", "ROWR", "GSR0", "GSR1", "GSR2", "GSR3", "NRES", 295 "INVC", "GWCH", "YTCH", "ACH0", "RREJ", "CADR", "ASAD", "SRST", "CIDK", "CIDS", 296 "RNON", "RSEQ", "PAD ", "EPID", "SNO0", "STK0", "CRT255", "CSCT"); 297 } 298