| // Copyright 2023 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package org.chromium.net.telemetry; |
| |
| import android.util.Log; |
| |
| import org.json.JSONException; |
| import org.json.JSONObject; |
| import org.json.JSONTokener; |
| |
| import java.util.ArrayList; |
| import java.util.Locale; |
| import java.util.Set; |
| |
| /** Parses the experimentalOptions string */ |
| public final class ExperimentalOptions { |
| private static final String TAG = ExperimentalOptions.class.getSimpleName(); |
| |
| public static final int UNSET_INT_VALUE = -1; |
| |
| // Declare static experimental options field trial names |
| private static final String QUIC = "QUIC"; |
| private static final String ASYNC_DNS = "AsyncDNS"; |
| private static final String STALE_DNS = "StaleDNS"; |
| |
| // The JSONObject created from the experimentalOptions String |
| private JSONObject mJson = new JSONObject(); |
| |
| public ExperimentalOptions(String experimentalOptions) { |
| if (!isNullOrEmpty(experimentalOptions)) { |
| try { |
| mJson = (JSONObject) new JSONTokener(experimentalOptions).nextValue(); |
| } catch (JSONException | ClassCastException e) { |
| Log.d( |
| TAG, |
| String.format( |
| "Experimental options could not be parsed, using default values." |
| + " Error: %s", |
| e.getMessage())); |
| } |
| } |
| } |
| |
| public String getConnectionOptionsOption() { |
| return parseExperimentalOptionsString( |
| getOrDefault(QUIC, "connection_options", null, String.class)); |
| } |
| |
| public OptionalBoolean getStoreServerConfigsInPropertiesOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "store_server_configs_in_properties", null, Boolean.class)); |
| } |
| |
| public int getMaxServerConfigsStoredInPropertiesOption() { |
| return getOrDefault( |
| QUIC, "max_server_configs_stored_in_properties", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| @SuppressWarnings("GoodTime") // CronetStatsLog expects int |
| public int getIdleConnectionTimeoutSecondsOption() { |
| return getOrDefault( |
| QUIC, "idle_connection_timeout_seconds", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| public OptionalBoolean getGoawaySessionsOnIpChangeOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "goaway_sessions_on_ip_change", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getCloseSessionsOnIpChangeOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "close_sessions_on_ip_change", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getMigrateSessionsOnNetworkChangeV2Option() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "migrate_sessions_on_network_change_v2", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getMigrateSessionsEarlyV2() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "migrate_sessions_early_v2", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getDisableBidirectionalStreamsOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "disable_bidirectional_streams", null, Boolean.class)); |
| } |
| |
| @SuppressWarnings("GoodTime") // CronetStatsLog expects int |
| public int getMaxTimeBeforeCryptoHandshakeSecondsOption() { |
| return getOrDefault( |
| QUIC, "max_time_before_crypto_handshake_seconds", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| @SuppressWarnings("GoodTime") // CronetStatsLog expects int |
| public int getMaxIdleTimeBeforeCryptoHandshakeSecondsOption() { |
| return getOrDefault( |
| QUIC, |
| "max_idle_time_before_crypto_handshake_seconds", |
| UNSET_INT_VALUE, |
| Integer.class); |
| } |
| |
| public OptionalBoolean getEnableSocketRecvOptimizationOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(QUIC, "enable_socket_recv_optimization", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getAsyncDnsEnableOption() { |
| return OptionalBoolean.fromBoolean(getOrDefault(ASYNC_DNS, "enable", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getStaleDnsEnableOption() { |
| return OptionalBoolean.fromBoolean(getOrDefault(STALE_DNS, "enable", null, Boolean.class)); |
| } |
| |
| @SuppressWarnings("GoodTime") // CronetStatsLog expects int |
| public int getStaleDnsDelayMillisOption() { |
| return getOrDefault(STALE_DNS, "delay_ms", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| @SuppressWarnings("GoodTime") // CronetStatsLog expects int |
| public int getStaleDnsMaxExpiredTimeMillisOption() { |
| return getOrDefault(STALE_DNS, "max_expired_time_ms", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| public int getStaleDnsMaxStaleUsesOption() { |
| return getOrDefault(STALE_DNS, "max_stale_uses", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| public OptionalBoolean getStaleDnsAllowOtherNetworkOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(STALE_DNS, "allow_other_network", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getStaleDnsPersistToDiskOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(STALE_DNS, "persist_to_disk", null, Boolean.class)); |
| } |
| |
| @SuppressWarnings("GoodTime") // CronetStatsLog expects int |
| public int getStaleDnsPersistDelayMillisOption() { |
| return getOrDefault(STALE_DNS, "persist_delay_ms", UNSET_INT_VALUE, Integer.class); |
| } |
| |
| public OptionalBoolean getStaleDnsUseStaleOnNameNotResolvedOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault(STALE_DNS, "use_stale_on_name_not_resolved", null, Boolean.class)); |
| } |
| |
| public OptionalBoolean getDisableIpv6OnWifiOption() { |
| return OptionalBoolean.fromBoolean( |
| getOrDefault("disable_ipv6_on_wifi", null, Boolean.class)); |
| } |
| |
| /** |
| * Checks if an experimentalOption fieldTrial key exists, then gets the value of the child |
| * option. |
| * |
| * @param experimentalOptionFieldTrialName the super option name for a nested experimental |
| * option eg QUIC.connection_options where <code>QUIC</code> is the FieldTrialName and |
| * <code> |
| * connection_options</code> is the child option |
| * @param option the child option eg <code>connection_options</code> |
| * @param defaultValue the defaultValue if the option is null or empty |
| * @return the experimental option value |
| */ |
| private <T> T getOrDefault( |
| String experimentalOptionFieldTrialName, |
| String option, |
| T defaultValue, |
| Class<T> clazz) { |
| // check if the field trial name exists |
| JSONObject options = null; |
| try { |
| options = mJson.getJSONObject(experimentalOptionFieldTrialName); |
| } catch (JSONException e) { |
| Log.d( |
| TAG, |
| String.format( |
| "Failed to get %s options: %s", |
| experimentalOptionFieldTrialName, e.getMessage())); |
| } |
| |
| if (options == null) { |
| return defaultValue; |
| } |
| |
| T value = defaultValue; |
| try { |
| value = clazz.cast(options.get(option)); |
| } catch (JSONException | ClassCastException e) { |
| Log.d(TAG, String.format("Failed to get %s options: %s", option, e.getMessage())); |
| } |
| return value; |
| } |
| |
| private <T> T getOrDefault(String option, T defaultValue, Class<T> clazz) { |
| T value = defaultValue; |
| try { |
| value = clazz.cast(mJson.get(option)); |
| } catch (JSONException | ClassCastException e) { |
| Log.d(TAG, String.format("Failed to get %s options: %s", option, e.getMessage())); |
| } |
| return value; |
| } |
| |
| /** |
| * Checks that the connection_options options are always valid and do not contain any PII. |
| * Removes any value that does not conform to a valid option. |
| */ |
| private String parseExperimentalOptionsString(String str) { |
| if (isNullOrEmpty(str)) { |
| return str; |
| } |
| |
| ArrayList<String> nStr = new ArrayList<>(); |
| for (String s : str.split(",", -1)) { |
| if (VALID_CONNECTION_OPTIONS.contains(s.toUpperCase(Locale.ROOT).trim())) { |
| nStr.add(s); |
| } |
| } |
| |
| return String.join(",", nStr); |
| } |
| |
| /** |
| * The generated CronetStatsLog class has an optionalBoolean(UNSET,TRUE,FALSE) variable for each |
| * of the experimental options. Since these values will always be the same for the options, we |
| * picked one of them and used it to create a private variable that we can use to make the code |
| * more readable. |
| */ |
| public static enum OptionalBoolean { |
| UNSET( |
| CronetStatsLog |
| .CRONET_ENGINE_CREATED__EXPERIMENTAL_OPTIONS_QUIC_STORE_SERVER_CONFIGS_IN_PROPERTIES__OPTIONAL_BOOLEAN_UNSET), |
| TRUE( |
| CronetStatsLog |
| .CRONET_ENGINE_CREATED__EXPERIMENTAL_OPTIONS_QUIC_STORE_SERVER_CONFIGS_IN_PROPERTIES__OPTIONAL_BOOLEAN_TRUE), |
| FALSE( |
| CronetStatsLog |
| .CRONET_ENGINE_CREATED__EXPERIMENTAL_OPTIONS_QUIC_STORE_SERVER_CONFIGS_IN_PROPERTIES__OPTIONAL_BOOLEAN_FALSE); |
| |
| private final int mValue; |
| |
| private OptionalBoolean(int value) { |
| this.mValue = value; |
| } |
| |
| public int getValue() { |
| return mValue; |
| } |
| |
| public static OptionalBoolean fromBoolean(Boolean value) { |
| if (value == null) { |
| return UNSET; |
| } |
| |
| return value ? TRUE : FALSE; |
| } |
| } |
| |
| private boolean isNullOrEmpty(String str) { |
| return str == null || str.isEmpty(); |
| } |
| |
| // Source: |
| // //external/cronet:net/third_party/quiche/src/quiche/quic/core/crypto/crypto_protocol.h |
| public static final Set<String> VALID_CONNECTION_OPTIONS = |
| Set.of( |
| "CHLO", "SHLO", "SCFG", "REJ", "CETV", "PRST", "SCUP", "ALPN", "P256", "C255", |
| "AESG", "CC20", "QBIC", "AFCW", "IFW5", "IFW6", "IFW7", "IFW8", "IFW9", "IFWA", |
| "TBBR", "1RTT", "2RTT", "LRTT", "BBS1", "BBS2", "BBS3", "BBS4", "BBS5", "BBRR", |
| "BBR1", "BBR2", "BBR3", "BBR4", "BBR5", "BBR9", "BBRA", "BBRB", "BBRS", "BBQ1", |
| "BBQ2", "BBQ3", "BBQ5", "BBQ6", "BBQ7", "BBQ8", "BBQ9", "BBQ0", "RENO", "TPCC", |
| "BYTE", "IW03", "IW10", "IW20", "IW50", "B2ON", "B2NA", "B2NE", "B2RP", "B2LO", |
| "B2HR", "B2SL", "B2H2", "B2RC", "BSAO", "B2DL", "B201", "B202", "B203", "B204", |
| "B205", "B206", "B207", "NTLP", "1TLP", "1RTO", "NRTO", "TIME", "ATIM", "MIN1", |
| "MIN4", "MAD0", "MAD2", "MAD3", "1ACK", "AKD3", "AKDU", "AFFE", "AFF1", "AFF2", |
| "SSLR", "NPRR", "2RTO", "3RTO", "4RTO", "5RTO", "6RTO", "CBHD", "NBHD", "CONH", |
| "LFAK", "STMP", "EACK", "ILD0", "ILD1", "ILD2", "ILD3", "ILD4", "RUNT", "NSTP", |
| "NRTT", "1PTO", "2PTO", "6PTO", "7PTO", "8PTO", "PTOS", "PTOA", "PEB1", "PEB2", |
| "PVS1", "PAG1", "PAG2", "PSDA", "PLE1", "PLE2", "APTO", "ELDT", "RVCM", "TCID", |
| "MPTH", "NCMR", "DFER", "NPCO", "BWRE", "BWMX", "BWID", "BWI1", "BWRS", "BWS2", |
| "BWS3", "BWS4", "BWS5", "BWS6", "BWP0", "BWP1", "BWP2", "BWP3", "BWP4", "BWG4", |
| "BWG7", "BWG8", "BWS7", "BWM3", "BWM4", "ICW1", "DTOS", "FIDT", "3AFF", "10AF", |
| "MTUH", "MTUL", "NSLC", "NCHP", "NBPE", "X509", "X59R", "CHID", "VER ", "NONC", |
| "NONP", "KEXS", "AEAD", "COPT", "CLOP", "ICSL", "MIBS", "MIUS", "ADE ", "IRTT", |
| "TRTT", "SNI ", "PUBS", "SCID", "ORBT", "PDMD", "PROF", "CCRT", "EXPY", "STTL", |
| "SFCW", "CFCW", "UAID", "XLCT", "QLVE", "PDP1", "PDP2", "PDP3", "PDP5", "QNZ2", |
| "MAD", "IGNP", "SRWP", "ROWF", "ROWR", "GSR0", "GSR1", "GSR2", "GSR3", "NRES", |
| "INVC", "GWCH", "YTCH", "ACH0", "RREJ", "CADR", "ASAD", "SRST", "CIDK", "CIDS", |
| "RNON", "RSEQ", "PAD ", "EPID", "SNO0", "STK0", "CRT255", "CSCT"); |
| } |