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