1/* 2 * 3 * Copyright 2015 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 */ 18 19#import "GRPCHost.h" 20 21#import <GRPCClient/GRPCCall.h> 22#include <grpc/grpc.h> 23#include <grpc/grpc_security.h> 24#ifdef GRPC_COMPILE_WITH_CRONET 25#import <GRPCClient/GRPCCall+ChannelArg.h> 26#import <GRPCClient/GRPCCall+Cronet.h> 27#endif 28 29#import "GRPCChannel.h" 30#import "GRPCCompletionQueue.h" 31#import "GRPCConnectivityMonitor.h" 32#import "NSDictionary+GRPC.h" 33#import "version.h" 34 35NS_ASSUME_NONNULL_BEGIN 36 37extern const char *kCFStreamVarName; 38 39static NSMutableDictionary *kHostCache; 40 41@implementation GRPCHost { 42 // TODO(mlumish): Investigate whether caching channels with strong links is a good idea. 43 GRPCChannel *_channel; 44} 45 46+ (nullable instancetype)hostWithAddress:(NSString *)address { 47 return [[self alloc] initWithAddress:address]; 48} 49 50- (void)dealloc { 51 if (_channelCreds != nil) { 52 grpc_channel_credentials_release(_channelCreds); 53 } 54 // Connectivity monitor is not required for CFStream 55 char *enableCFStream = getenv(kCFStreamVarName); 56 if (enableCFStream == nil || enableCFStream[0] != '1') { 57 [GRPCConnectivityMonitor unregisterObserver:self]; 58 } 59} 60 61// Default initializer. 62- (nullable instancetype)initWithAddress:(NSString *)address { 63 if (!address) { 64 return nil; 65 } 66 67 // To provide a default port, we try to interpret the address. If it's just a host name without 68 // scheme and without port, we'll use port 443. If it has a scheme, we pass it untouched to the C 69 // gRPC library. 70 // TODO(jcanizales): Add unit tests for the types of addresses we want to let pass untouched. 71 NSURL *hostURL = [NSURL URLWithString:[@"https://" stringByAppendingString:address]]; 72 if (hostURL.host && !hostURL.port) { 73 address = [hostURL.host stringByAppendingString:@":443"]; 74 } 75 76 // Look up the GRPCHost in the cache. 77 static dispatch_once_t cacheInitialization; 78 dispatch_once(&cacheInitialization, ^{ 79 kHostCache = [NSMutableDictionary dictionary]; 80 }); 81 @synchronized(kHostCache) { 82 GRPCHost *cachedHost = kHostCache[address]; 83 if (cachedHost) { 84 return cachedHost; 85 } 86 87 if ((self = [super init])) { 88 _address = address; 89 _secure = YES; 90 kHostCache[address] = self; 91 _compressAlgorithm = GRPC_COMPRESS_NONE; 92 _retryEnabled = YES; 93 } 94 95 // Connectivity monitor is not required for CFStream 96 char *enableCFStream = getenv(kCFStreamVarName); 97 if (enableCFStream == nil || enableCFStream[0] != '1') { 98 [GRPCConnectivityMonitor registerObserver:self selector:@selector(connectivityChange:)]; 99 } 100 } 101 return self; 102} 103 104+ (void)flushChannelCache { 105 @synchronized(kHostCache) { 106 [kHostCache enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, GRPCHost *_Nonnull host, 107 BOOL *_Nonnull stop) { 108 [host disconnect]; 109 }]; 110 } 111} 112 113+ (void)resetAllHostSettings { 114 @synchronized(kHostCache) { 115 kHostCache = [NSMutableDictionary dictionary]; 116 } 117} 118 119- (nullable grpc_call *)unmanagedCallWithPath:(NSString *)path 120 serverName:(NSString *)serverName 121 timeout:(NSTimeInterval)timeout 122 completionQueue:(GRPCCompletionQueue *)queue { 123 // The __block attribute is to allow channel take refcount inside @synchronized block. Without 124 // this attribute, retain of channel object happens after objc_sync_exit in release builds, which 125 // may result in channel released before used. See grpc/#15033. 126 __block GRPCChannel *channel; 127 // This is racing -[GRPCHost disconnect]. 128 @synchronized(self) { 129 if (!_channel) { 130 _channel = [self newChannel]; 131 } 132 channel = _channel; 133 } 134 return [channel unmanagedCallWithPath:path 135 serverName:serverName 136 timeout:timeout 137 completionQueue:queue]; 138} 139 140- (NSData *)nullTerminatedDataWithString:(NSString *)string { 141 // dataUsingEncoding: does not return a null-terminated string. 142 NSData *data = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; 143 NSMutableData *nullTerminated = [NSMutableData dataWithData:data]; 144 [nullTerminated appendBytes:"\0" length:1]; 145 return nullTerminated; 146} 147 148- (BOOL)setTLSPEMRootCerts:(nullable NSString *)pemRootCerts 149 withPrivateKey:(nullable NSString *)pemPrivateKey 150 withCertChain:(nullable NSString *)pemCertChain 151 error:(NSError **)errorPtr { 152 static NSData *kDefaultRootsASCII; 153 static NSError *kDefaultRootsError; 154 static dispatch_once_t loading; 155 dispatch_once(&loading, ^{ 156 NSString *defaultPath = @"gRPCCertificates.bundle/roots"; // .pem 157 // Do not use NSBundle.mainBundle, as it's nil for tests of library projects. 158 NSBundle *bundle = [NSBundle bundleForClass:self.class]; 159 NSString *path = [bundle pathForResource:defaultPath ofType:@"pem"]; 160 NSError *error; 161 // Files in PEM format can have non-ASCII characters in their comments (e.g. for the name of the 162 // issuer). Load them as UTF8 and produce an ASCII equivalent. 163 NSString *contentInUTF8 = 164 [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:&error]; 165 if (contentInUTF8 == nil) { 166 kDefaultRootsError = error; 167 return; 168 } 169 kDefaultRootsASCII = [self nullTerminatedDataWithString:contentInUTF8]; 170 }); 171 172 NSData *rootsASCII; 173 if (pemRootCerts != nil) { 174 rootsASCII = [self nullTerminatedDataWithString:pemRootCerts]; 175 } else { 176 if (kDefaultRootsASCII == nil) { 177 if (errorPtr) { 178 *errorPtr = kDefaultRootsError; 179 } 180 NSAssert( 181 kDefaultRootsASCII, 182 @"Could not read gRPCCertificates.bundle/roots.pem. This file, " 183 "with the root certificates, is needed to establish secure (TLS) connections. " 184 "Because the file is distributed with the gRPC library, this error is usually a sign " 185 "that the library wasn't configured correctly for your project. Error: %@", 186 kDefaultRootsError); 187 return NO; 188 } 189 rootsASCII = kDefaultRootsASCII; 190 } 191 192 grpc_channel_credentials *creds; 193 if (pemPrivateKey == nil && pemCertChain == nil) { 194 creds = grpc_ssl_credentials_create(rootsASCII.bytes, NULL, NULL, NULL); 195 } else { 196 grpc_ssl_pem_key_cert_pair key_cert_pair; 197 NSData *privateKeyASCII = [self nullTerminatedDataWithString:pemPrivateKey]; 198 NSData *certChainASCII = [self nullTerminatedDataWithString:pemCertChain]; 199 key_cert_pair.private_key = privateKeyASCII.bytes; 200 key_cert_pair.cert_chain = certChainASCII.bytes; 201 creds = grpc_ssl_credentials_create(rootsASCII.bytes, &key_cert_pair, NULL, NULL); 202 } 203 204 @synchronized(self) { 205 if (_channelCreds != nil) { 206 grpc_channel_credentials_release(_channelCreds); 207 } 208 _channelCreds = creds; 209 } 210 211 return YES; 212} 213 214- (NSDictionary *)channelArgsUsingCronet:(BOOL)useCronet { 215 NSMutableDictionary *args = [NSMutableDictionary dictionary]; 216 217 // TODO(jcanizales): Add OS and device information (see 218 // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md#user-agents ). 219 NSString *userAgent = @"grpc-objc/" GRPC_OBJC_VERSION_STRING; 220 if (_userAgentPrefix) { 221 userAgent = [_userAgentPrefix stringByAppendingFormat:@" %@", userAgent]; 222 } 223 args[@GRPC_ARG_PRIMARY_USER_AGENT_STRING] = userAgent; 224 225 if (_secure && _hostNameOverride) { 226 args[@GRPC_SSL_TARGET_NAME_OVERRIDE_ARG] = _hostNameOverride; 227 } 228 229 if (_responseSizeLimitOverride) { 230 args[@GRPC_ARG_MAX_RECEIVE_MESSAGE_LENGTH] = _responseSizeLimitOverride; 231 } 232 233 if (_compressAlgorithm != GRPC_COMPRESS_NONE) { 234 args[@GRPC_COMPRESSION_CHANNEL_DEFAULT_ALGORITHM] = [NSNumber numberWithInt:_compressAlgorithm]; 235 } 236 237 if (_keepaliveInterval != 0) { 238 args[@GRPC_ARG_KEEPALIVE_TIME_MS] = [NSNumber numberWithInt:_keepaliveInterval]; 239 args[@GRPC_ARG_KEEPALIVE_TIMEOUT_MS] = [NSNumber numberWithInt:_keepaliveTimeout]; 240 } 241 242 id logContext = self.logContext; 243 if (logContext != nil) { 244 args[@GRPC_ARG_MOBILE_LOG_CONTEXT] = logContext; 245 } 246 247 if (useCronet) { 248 args[@GRPC_ARG_DISABLE_CLIENT_AUTHORITY_FILTER] = [NSNumber numberWithInt:1]; 249 } 250 251 if (_retryEnabled == NO) { 252 args[@GRPC_ARG_ENABLE_RETRIES] = [NSNumber numberWithInt:0]; 253 } 254 255 if (_minConnectTimeout > 0) { 256 args[@GRPC_ARG_MIN_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_minConnectTimeout]; 257 } 258 if (_initialConnectBackoff > 0) { 259 args[@GRPC_ARG_INITIAL_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_initialConnectBackoff]; 260 } 261 if (_maxConnectBackoff > 0) { 262 args[@GRPC_ARG_MAX_RECONNECT_BACKOFF_MS] = [NSNumber numberWithInt:_maxConnectBackoff]; 263 } 264 265 return args; 266} 267 268- (GRPCChannel *)newChannel { 269 BOOL useCronet = NO; 270#ifdef GRPC_COMPILE_WITH_CRONET 271 useCronet = [GRPCCall isUsingCronet]; 272#endif 273 NSDictionary *args = [self channelArgsUsingCronet:useCronet]; 274 if (_secure) { 275 GRPCChannel *channel; 276 @synchronized(self) { 277 if (_channelCreds == nil) { 278 [self setTLSPEMRootCerts:nil withPrivateKey:nil withCertChain:nil error:nil]; 279 } 280#ifdef GRPC_COMPILE_WITH_CRONET 281 if (useCronet) { 282 channel = [GRPCChannel secureCronetChannelWithHost:_address channelArgs:args]; 283 } else 284#endif 285 { 286 channel = 287 [GRPCChannel secureChannelWithHost:_address credentials:_channelCreds channelArgs:args]; 288 } 289 } 290 return channel; 291 } else { 292 return [GRPCChannel insecureChannelWithHost:_address channelArgs:args]; 293 } 294} 295 296- (NSString *)hostName { 297 // TODO(jcanizales): Default to nil instead of _address when Issue #2635 is clarified. 298 return _hostNameOverride ?: _address; 299} 300 301- (void)disconnect { 302 // This is racing -[GRPCHost unmanagedCallWithPath:completionQueue:]. 303 @synchronized(self) { 304 _channel = nil; 305 } 306} 307 308// Flushes the host cache when connectivity status changes or when connection switch between Wifi 309// and Cellular data, so that a new call will use a new channel. Otherwise, a new call will still 310// use the cached channel which is no longer available and will cause gRPC to hang. 311- (void)connectivityChange:(NSNotification *)note { 312 [self disconnect]; 313} 314 315@end 316 317NS_ASSUME_NONNULL_END 318