| /* |
| * |
| * 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 |