/* * * Copyright 2015 gRPC authors. * * 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. * */ #import "GRPCHost.h" #import <GRPCClient/GRPCCall.h> #include <grpc/grpc.h> #include <grpc/grpc_security.h> #ifdef GRPC_COMPILE_WITH_CRONET #import <GRPCClient/GRPCCall+ChannelArg.h> #import <GRPCClient/GRPCCall+Cronet.h> #endif #import "GRPCChannel.h" #import "GRPCCompletionQueue.h" #import "GRPCConnectivityMonitor.h" #import "NSDictionary+GRPC.h" #import "version.h" NS_ASSUME_NONNULL_BEGIN extern const char *kCFStreamVarName; static NSMutableDictionary *kHostCache; @implementation GRPCHost { // TODO(mlumish): Investigate whether caching channels with strong links is a good idea. GRPCChannel *_channel; } + (nullable instancetype)hostWithAddress:(NSString *)address { return [[self alloc] initWithAddress:address]; } - (void)dealloc { if (_channelCreds != nil) { grpc_channel_credentials_release(_channelCreds); } // Connectivity monitor is not required for CFStream char *enableCFStream = getenv(kCFStreamVarName); if (enableCFStream == nil || enableCFStream[0] != '1') { [GRPCConnectivityMonitor unregisterObserver:self]; } } // Default initializer. - (nullable instancetype)initWithAddress:(NSString *)address { if (!address) { return nil; } // To provide a default port, we try to interpret the address. If it's just a host name without // scheme and without port, we'll use port 443. If it has a scheme, we pass it untouched to the C // gRPC library. // TODO(jcanizales): Add unit tests for the types of addresses we want to let pass untouched. NSURL *hostURL = [NSURL URLWithString:[@"https://" stringByAppendingString:address]]; if (hostURL.host && !hostURL.port) { address = [hostURL.host stringByAppendingString:@":443"]; } // Look up the GRPCHost in the cache. static dispatch_once_t cacheInitialization; dispatch_once(&cacheInitialization, ^{ kHostCache = [NSMutableDictionary dictionary]; }); @synchronized(kHostCache) { GRPCHost *cachedHost = kHostCache[address]; if (cachedHost) { return cachedHost; } if ((self = [super init])) { _address = address; _secure = YES; kHostCache[address] = self; _compressAlgorithm = GRPC_COMPRESS_NONE; _retryEnabled = YES; } // Connectivity monitor is not required for CFStream char *enableCFStream = getenv(kCFStreamVarName); if (enableCFStream == nil || enableCFStream[0] != '1') { [GRPCConnectivityMonitor registerObserver:self selector:@selector(connectivityChange:)]; } } return self; } + (void)flushChannelCache { @synchronized(kHostCache) { [kHostCache enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, GRPCHost *_Nonnull host, BOOL *_Nonnull stop) { [host disconnect]; }]; } } + (void)resetAllHostSettings { @synchronized(kHostCache) { kHostCache = [NSMutableDictionary dictionary]; } } - (nullable grpc_call *)unmanagedCallWithPath:(NSString *)path serverName:(NSString *)serverName timeout:(NSTimeInterval)timeout completionQueue:(GRPCCompletionQueue *)queue { // The __block attribute is to allow channel take refcount inside @synchronized block. Without // this attribute, retain of channel object happens after objc_sync_exit in release builds, which // may result in channel released before used. See grpc/#15033. __block GRPCChannel *channel; // This is racing -[GRPCHost disconnect]. @synchronized(self) { if (!_channel) { _channel = [self newChannel]; } channel = _channel; } return [channel unmanagedCallWithPath:path serverName:serverName timeout:timeout completionQueue:queue]; } - (NSData *)nullTerminatedDataWithString:(NSString *)string { // dataUsingEncoding: does not return a null-terminated string. NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; NSMutableData *nullTerminated = [NSMutableData dataWithData:data]; [nullTerminated appendBytes:"\0" length:1]; return nullTerminated; } - (BOOL)setTLSPEMRootCerts:(nullable NSString *)pemRootCerts withPrivateKey:(nullable NSString *)pemPrivateKey withCertChain:(nullable NSString *)pemCertChain error:(NSError **)errorPtr { static NSData *kDefaultRootsASCII; static NSError *kDefaultRootsError; static dispatch_once_t loading; dispatch_once(&loading, ^{ NSString *defaultPath = @"gRPCCertificates.bundle/roots"; // .pem // Do not use NSBundle.mainBundle, as it's nil for tests of library projects. NSBundle *bundle = [NSBundle bundleForClass:self.class]; NSString *path = [bundle pathForResource:defaultPath ofType:@"pem"]; NSError *error; // Files in PEM format can have non-ASCII characters in their comments (e.g. for the name of the // issuer). Load them as UTF8 and produce an ASCII equivalent. NSString *contentInUTF8 = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; if (contentInUTF8 == nil) { kDefaultRootsError = error; return; } kDefaultRootsASCII = [self nullTerminatedDataWithString:contentInUTF8]; }); NSData *rootsASCII; if (pemRootCerts != nil) { rootsASCII = [self nullTerminatedDataWithString:pemRootCerts]; } else { if (kDefaultRootsASCII == nil) { if (errorPtr) { *errorPtr = kDefaultRootsError; } NSAssert( kDefaultRootsASCII, @"Could not read gRPCCertificates.bundle/roots.pem. This file, " "with the root certificates, is needed to establish secure (TLS) connections. " "Because the file is distributed with the gRPC library, this error is usually a sign " "that the library wasn't configured correctly for your project. Error: %@", kDefaultRootsError); return NO; } rootsASCII = kDefaultRootsASCII; } grpc_channel_credentials *creds; if (pemPrivateKey == nil && pemCertChain == nil) { creds = grpc_ssl_credentials_create(rootsASCII.bytes, NULL, NULL, NULL); } else { grpc_ssl_pem_key_cert_pair key_cert_pair; NSData *privateKeyASCII = [self nullTerminatedDataWithString:pemPrivateKey]; NSData *certChainASCII = [self nullTerminatedDataWithString:pemCertChain]; key_cert_pair.private_key = privateKeyASCII.bytes; key_cert_pair.cert_chain = certChainASCII.bytes; creds = grpc_ssl_credentials_create(rootsASCII.bytes, &key_cert_pair, NULL, NULL); } @synchronized(self) { if (_channelCreds != nil) { grpc_channel_credentials_release(_channelCreds); } _channelCreds = creds; } return YES; } - (NSDictionary *)channelArgsUsingCronet:(BOOL)useCronet { NSMutableDictionary *args = [NSMutableDictionary dictionary]; // TODO(jcanizales): Add OS and device information (see // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents ). NSString *userAgent = @"grpc-objc/" GRPC_OBJC_VERSION_STRING; if (_userAgentPrefix) { userAgent = [_userAgentPrefix stringByAppendingFormat:@" %@", userAgent]; } args[@GRPC_ARG_PRIMARY_USER_AGENT_STRING] = userAgent; if (_secure && _hostNameOverride) { args[@GRPC_SSL_TARGET_NAME_OVERRIDE_ARG] = _hostNameOverride; } if (_responseSizeLimitOverride) { args[@GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH] = _responseSizeLimitOverride; } if (_compressAlgorithm != GRPC_COMPRESS_NONE) { args[@GRPC_COMPRESSION_CHANNEL_DEFAULT_ALGORITHM] = [NSNumber numberWithInt:_compressAlgorithm]; } if (_keepaliveInterval != 0) { args[@GRPC_ARG_KEEPALIVE_TIME_MS] = [NSNumber numberWithInt:_keepaliveInterval]; args[@GRPC_ARG_KEEPALIVE_TIMEOUT_MS] = [NSNumber numberWithInt:_keepaliveTimeout]; } id logContext = self.logContext; if (logContext != nil) { args[@GRPC_ARG_MOBILE_LOG_CONTEXT] = logContext; } if (useCronet) { args[@GRPC_ARG_DISABLE_CLIENT_AUTHORITY_FILTER] = [NSNumber numberWithInt:1]; } if (_retryEnabled == NO) { args[@GRPC_ARG_ENABLE_RETRIES] = [NSNumber numberWithInt:0]; } if (_minConnectTimeout > 0) { args[@GRPC_ARG_MIN_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_minConnectTimeout]; } if (_initialConnectBackoff > 0) { args[@GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_initialConnectBackoff]; } if (_maxConnectBackoff > 0) { args[@GRPC_ARG_MAX_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_maxConnectBackoff]; } return args; } - (GRPCChannel *)newChannel { BOOL useCronet = NO; #ifdef GRPC_COMPILE_WITH_CRONET useCronet = [GRPCCall isUsingCronet]; #endif NSDictionary *args = [self channelArgsUsingCronet:useCronet]; if (_secure) { GRPCChannel *channel; @synchronized(self) { if (_channelCreds == nil) { [self setTLSPEMRootCerts:nil withPrivateKey:nil withCertChain:nil error:nil]; } #ifdef GRPC_COMPILE_WITH_CRONET if (useCronet) { channel = [GRPCChannel secureCronetChannelWithHost:_address channelArgs:args]; } else #endif { channel = [GRPCChannel secureChannelWithHost:_address credentials:_channelCreds channelArgs:args]; } } return channel; } else { return [GRPCChannel insecureChannelWithHost:_address channelArgs:args]; } } - (NSString *)hostName { // TODO(jcanizales): Default to nil instead of _address when Issue #2635 is clarified. return _hostNameOverride ?: _address; } - (void)disconnect { // This is racing -[GRPCHost unmanagedCallWithPath:completionQueue:]. @synchronized(self) { _channel = nil; } } // Flushes the host cache when connectivity status changes or when connection switch between Wifi // and Cellular data, so that a new call will use a new channel. Otherwise, a new call will still // use the cached channel which is no longer available and will cause gRPC to hang. - (void)connectivityChange:(NSNotification *)note { [self disconnect]; } @end NS_ASSUME_NONNULL_END