blob: ab78bb8ec9edf9e597b3f9ae40005ee9d3ddd39d [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/variations/ios_chrome_variations_seed_fetcher.h"
#import "base/metrics/histogram_functions.h"
#import "base/notreached.h"
#import "base/strings/string_util.h"
#import "base/strings/sys_string_conversions.h"
#import "base/time/time.h"
#import "build/branding_buildflags.h"
#import "components/variations/seed_response.h"
#import "components/variations/variations_switches.h"
#import "components/variations/variations_url_constants.h"
#import "components/version_info/version_info.h"
#import "ios/chrome/browser/variations/ios_chrome_variations_seed_store.h"
#import "ios/chrome/common/channel_info.h"
#import "net/http/http_status_code.h"
#import "ios/chrome/browser/variations/ios_chrome_variations_seed_store+private.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
namespace {
// Maximum time allowed to fetch the seed before the request is cancelled.
const base::TimeDelta kRequestTimeout = base::Seconds(2);
// Histogram names for seed fetch time and result.
const char kSeedFetchResultHistogram[] =
"IOS.Variations.FirstRun.SeedFetchResult";
const char kSeedFetchTimeHistogram[] = "IOS.Variations.FirstRun.SeedFetchTime";
// Whether a current request for variations seed is being made; this variable
// exists that only one instance of the manager updates the global seed at one
// time.
static BOOL g_seed_fetching_in_progress = NO;
} // namespace
@interface IOSChromeVariationsSeedFetcher () {
// The variations server domain name.
std::string _variationsDomain;
// The forced channel string retrieved from the command line.
std::string _forcedChannel;
}
// Whether the current binary should fetch Finch seed for experiment purpose.
@property(nonatomic, assign) BOOL fetchingEnabled;
// The URL of the variations server, including query parameters that identifies
// the request initiator.
@property(nonatomic, readonly) NSURL* variationsUrl;
// The timestamp when the current seed request starts. This is used for metric
// reporting, and will be reset to null value when the request finishes.
@property(nonatomic, assign) base::Time startTimeOfOngoingSeedRequest;
@end
@implementation IOSChromeVariationsSeedFetcher
#pragma mark - Public
- (instancetype)init {
self = [super init];
if (self) {
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
_fetchingEnabled = YES;
#else
_fetchingEnabled = NO;
#endif
_variationsDomain = variations::kDefaultServerUrl;
_forcedChannel = std::string();
[self applySwitchesFromArguments:[[NSProcessInfo processInfo] arguments]];
}
return self;
}
- (void)startSeedFetch {
// Set up a serial queue to to avoid concurrent read/write to static data.
static dispatch_once_t onceToken;
static dispatch_queue_t queue = nil;
#if BUILDFLAG(GOOGLE_CHROME_BRANDING)
const char* label = "com.google.chrome.first_run_variations_seed_manager";
#else
const char* label = "org.chromium.first_run_variations_seed_manager";
#endif
dispatch_once(&onceToken, ^{
queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL);
});
// Adds the task of fetching the seed to the static serial queue, and return
// from `startSeedFetch` immediately. Note that the block will retain `self`.
dispatch_async(queue, ^{
[self startSeedFetchHelper];
});
}
#pragma mark - Accessors
- (NSURL*)variationsUrl {
// Setting "osname", "milestone" and "channel" as parameters. Dogfood
// experimenting is not supported on Chrome iOS, therefore we do not need the
// "restrict" parameter.
std::string queryString =
"?osname=ios&milestone=" + version_info::GetMajorVersionNumber();
std::string channel = _forcedChannel;
if (channel.empty() && GetChannel() != version_info::Channel::UNKNOWN) {
channel = GetChannelString();
}
if (!channel.empty()) {
queryString += "&channel=" + channel;
}
return [NSURL
URLWithString:base::SysUTF8ToNSString(_variationsDomain + queryString)];
}
#pragma mark - Private
// Parse custom values from the command line and apply them to the seed manager.
- (void)applySwitchesFromArguments:(NSArray<NSString*>*)arguments {
std::string url_switch =
"--" + std::string(variations::switches::kVariationsServerURL) + "=";
std::string channel_switch =
"--" + std::string(variations::switches::kFakeVariationsChannel) + "=";
for (NSString* a in arguments) {
std::string arg = base::SysNSStringToUTF8(a);
if (base::StartsWith(arg, url_switch)) {
_variationsDomain = arg.substr(url_switch.size());
if (!self.fetchingEnabled && !_variationsDomain.empty()) {
self.fetchingEnabled = YES;
}
} else if (base::StartsWith(arg, channel_switch)) {
_forcedChannel = arg.substr(channel_switch.size());
}
}
}
// Helper method for `startSeedFetch` that initiates an HTTPS request to the
// Finch server in the static serial queue.
- (void)startSeedFetchHelper {
DCHECK(!g_seed_fetching_in_progress)
<< "SeedFetch started while already in progress";
// Stops executing if seed fetching is disabled.
if (!self.fetchingEnabled) {
[self notifyDelegateSeedFetchResult:NO];
return;
}
g_seed_fetching_in_progress = YES;
NSMutableURLRequest* request = [NSMutableURLRequest
requestWithURL:self.variationsUrl
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:kRequestTimeout.InSecondsF()];
// Pass only "gzip" as an accepted format. Do not pass delta compression
// ("x-bm"), as it is not applicable on first run (since there is no
// existing seed).
[request setValue:@"gzip" forHTTPHeaderField:@"A-IM"];
NSURLSessionDataTask* task = [[NSURLSession sharedSession]
dataTaskWithRequest:request
completionHandler:^(NSData* data, NSURLResponse* response,
NSError* error) {
[self onSeedRequestCompletedWithData:data
response:(NSHTTPURLResponse*)response
error:error];
}];
self.startTimeOfOngoingSeedRequest = base::Time::Now();
[task resume];
}
// Method that generates the seed using the HTTPS response sent back from the
// Finch server, stores them in the shared seed, and records relevant metrics.
- (void)onSeedRequestCompletedWithData:(NSData*)data
response:(NSHTTPURLResponse*)httpResponse
error:(NSError*)error {
// Normally net::HTTP_NOT_MODIFIED should be considered as a
// successful response, but it is not expected when the request does
// not contain "If-None-Match" header.
BOOL success = error == nil && httpResponse.statusCode == net::HTTP_OK;
IOSSeedFetchException exception = IOSSeedFetchException::kNotApplicable;
if (success) {
base::UmaHistogramTimes(
kSeedFetchTimeHistogram,
base::Time::Now() - self.startTimeOfOngoingSeedRequest);
std::unique_ptr<variations::SeedResponse> seed =
[self seedResponseForHTTPResponse:httpResponse data:data];
if (seed) {
[IOSChromeVariationsSeedStore updateSharedSeed:std::move(seed)];
} else {
// Currently, only the IM header is mandatory to create a first run seed,
// and is the only possible reason that a seed is downloaded but not
// created.
exception = IOSSeedFetchException::kInvalidIMHeader;
success = NO;
}
} else if (error.code == NSURLErrorTimedOut) {
exception = IOSSeedFetchException::kHTTPSRequestTimeout;
} else if (error.code == NSURLErrorBadURL ||
error.code == NSURLErrorDNSLookupFailed ||
error.code == NSURLErrorCannotFindHost) {
exception = IOSSeedFetchException::kHTTPSRequestBadUrl;
}
self.startTimeOfOngoingSeedRequest = base::Time();
g_seed_fetching_in_progress = NO;
// Log seed fetch result on UMA and notify delegate.
int seedFetchResultValue = exception == IOSSeedFetchException::kNotApplicable
? static_cast<int>(httpResponse.statusCode)
: static_cast<int>(exception);
base::UmaHistogramSparse(kSeedFetchResultHistogram, seedFetchResultValue);
[self notifyDelegateSeedFetchResult:success];
}
// Generates and returns the SeedResponse by parsing the HTTP response returned
// by the variations server. Returns `nil` if the HTTP response is invalid.
- (std::unique_ptr<variations::SeedResponse>)
seedResponseForHTTPResponse:(NSHTTPURLResponse*)httpResponse
data:(NSData*)data {
NSString* signature =
[httpResponse valueForHTTPHeaderField:@"X-Seed-Signature"];
NSString* country = [httpResponse valueForHTTPHeaderField:@"X-Country"];
// Returned seed should have been gzip compressed.
NSCharacterSet* whitespace = [NSCharacterSet whitespaceCharacterSet];
NSPredicate* nonEmpty = [NSPredicate
predicateWithBlock:^BOOL(NSString* im, NSDictionary* bindings) {
return [[im stringByTrimmingCharactersInSet:whitespace] length] > 0;
}];
NSArray<NSString*>* instanceManipulations = [[[httpResponse
valueForHTTPHeaderField:@"IM"] componentsSeparatedByString:@","]
filteredArrayUsingPredicate:nonEmpty];
// Only gzip compressed data is supported on first run seed fetching with
// "gzip" specified in the request.
if ([instanceManipulations count] == 1 &&
[[instanceManipulations[0] stringByTrimmingCharactersInSet:whitespace]
isEqualToString:@"gzip"]) {
auto seed = std::make_unique<variations::SeedResponse>();
if (data) {
// "data" is binary, for which protobuf uses strings.
seed->data = std::string(reinterpret_cast<const char*>([data bytes]),
[data length]);
}
seed->signature = base::SysNSStringToUTF8(signature);
seed->country = base::SysNSStringToUTF8(country);
seed->date = base::Time::Now();
seed->is_gzip_compressed = YES;
return seed;
}
return nullptr;
}
// Notifies the delegate of the seed fetching result. Since the seed fetch
// request is sent on the background instead of the main queue, this method
// should explicitly dispatch the result back on the main queue.
- (void)notifyDelegateSeedFetchResult:(BOOL)result {
__weak IOSChromeVariationsSeedFetcher* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf.delegate didFetchSeedSuccess:result];
});
}
// Invoked by the testing code to reset the fetching status after each test. DO
// NOT INVOKE IN PRODUCTION CODE.
+ (void)resetFetchingStatusForTesting {
g_seed_fetching_in_progress = NO;
}
@end