Home | History | Annotate | Download | only in private
      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 
     35 NS_ASSUME_NONNULL_BEGIN
     36 
     37 extern const char *kCFStreamVarName;
     38 
     39 static 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 
    317 NS_ASSUME_NONNULL_END
    318