diff options
author | Robbie Shade <rjshade@google.com> | 2015-08-13 16:12:26 -0400 |
---|---|---|
committer | Robbie Shade <rjshade@google.com> | 2015-08-13 16:12:26 -0400 |
commit | 37ad56b469836b7d4b3e9753913e37aae3ac46c7 (patch) | |
tree | d8d3cdf9f50f66ac567523872096f2f555a13b43 /src/objective-c | |
parent | 98981efe0d29626645abc2ff495737a078bab021 (diff) | |
parent | 118f65dc8c5df39872aff5f6bf269b16ce82c259 (diff) |
Merge remote-tracking branch 'upstream/master' into add_udp_server_2
Diffstat (limited to 'src/objective-c')
33 files changed, 928 insertions, 259 deletions
diff --git a/src/objective-c/GRPCClient/GRPCCall+OAuth2.h b/src/objective-c/GRPCClient/GRPCCall+OAuth2.h new file mode 100644 index 0000000000..2e379a7157 --- /dev/null +++ b/src/objective-c/GRPCClient/GRPCCall+OAuth2.h @@ -0,0 +1,49 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import "GRPCCall.h" + +// Helpers for setting and reading headers compatible with OAuth2. +@interface GRPCCall (OAuth2) + +// Setting this property is equivalent to setting "Bearer <passed token>" as the value of the +// request header with key "authorization" (the authorization header). Setting it to nil removes the +// authorization header from the request. +// The value obtained by getting the property is the OAuth2 bearer token if the authorization header +// of the request has the form "Bearer <token>", or nil otherwise. +@property(atomic, copy) NSString *oauth2AccessToken; + +// Returns the value (if any) of the "www-authenticate" response header (the challenge header). +@property(atomic, readonly) NSString *oauth2ChallengeHeader; + +@end diff --git a/src/objective-c/GRPCClient/GRPCCall+OAuth2.m b/src/objective-c/GRPCClient/GRPCCall+OAuth2.m new file mode 100644 index 0000000000..83b0de18e3 --- /dev/null +++ b/src/objective-c/GRPCClient/GRPCCall+OAuth2.m @@ -0,0 +1,63 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import "GRPCCall+OAuth2.h" + +static NSString * const kAuthorizationHeader = @"authorization"; +static NSString * const kBearerPrefix = @"Bearer "; +static NSString * const kChallengeHeader = @"www-authenticate"; + +@implementation GRPCCall (OAuth2) + +- (NSString *)oauth2AccessToken { + NSString *headerValue = self.requestHeaders[kAuthorizationHeader]; + if ([headerValue hasPrefix:kBearerPrefix]) { + return [headerValue substringFromIndex:kBearerPrefix.length]; + } else { + return nil; + } +} + +- (void)setOauth2AccessToken:(NSString *)token { + if (token) { + self.requestHeaders[kAuthorizationHeader] = [kBearerPrefix stringByAppendingString:token]; + } else { + [self.requestHeaders removeObjectForKey:kAuthorizationHeader]; + } +} + +- (NSString *)oauth2ChallengeHeader { + return self.responseHeaders[kChallengeHeader]; +} + +@end diff --git a/src/objective-c/GRPCClient/GRPCCall+Tests.h b/src/objective-c/GRPCClient/GRPCCall+Tests.h new file mode 100644 index 0000000000..cca1614606 --- /dev/null +++ b/src/objective-c/GRPCClient/GRPCCall+Tests.h @@ -0,0 +1,54 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import "GRPCCall.h" + +// Methods to let tune down the security of gRPC connections for specific hosts. These shouldn't be +// used in releases, but are sometimes needed for testing. +@interface GRPCCall (Tests) + +// Establish all SSL connections to the provided host using the passed SSL target name and the root +// certificates found in the file at |certsPath|. +// +// Must be called before any gRPC call to that host is made. It's illegal to pass the same host to +// more than one invocation of the methods of this category. ++ (void)useTestCertsPath:(NSString *)certsPath + testName:(NSString *)testName + forHost:(NSString *)host; + +// Establish all connections to the provided host using cleartext instead of SSL. +// +// Must be called before any gRPC call to that host is made. It's illegal to pass the same host to +// more than one invocation of the methods of this category. ++ (void)useInsecureConnectionsForHost:(NSString *)host; +@end diff --git a/src/objective-c/GRPCClient/GRPCCall+Tests.m b/src/objective-c/GRPCClient/GRPCCall+Tests.m new file mode 100644 index 0000000000..bade0b2920 --- /dev/null +++ b/src/objective-c/GRPCClient/GRPCCall+Tests.m @@ -0,0 +1,53 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import "GRPCCall+Tests.h" + +#import "private/GRPCHost.h" + +@implementation GRPCCall (Tests) + ++ (void)useTestCertsPath:(NSString *)certsPath + testName:(NSString *)testName + forHost:(NSString *)host { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:host]; + hostConfig.pathToCertificates = certsPath; + hostConfig.hostNameOverride = testName; +} + ++ (void)useInsecureConnectionsForHost:(NSString *)host { + GRPCHost *hostConfig = [GRPCHost hostWithAddress:host]; + hostConfig.secure = NO; +} + +@end diff --git a/src/objective-c/GRPCClient/GRPCCall.h b/src/objective-c/GRPCClient/GRPCCall.h index 4a8b7fff48..4eda499b1a 100644 --- a/src/objective-c/GRPCClient/GRPCCall.h +++ b/src/objective-c/GRPCClient/GRPCCall.h @@ -48,8 +48,10 @@ #import <Foundation/Foundation.h> #import <RxLibrary/GRXWriter.h> -// Key used in |NSError|'s |userInfo| dictionary to store the response metadata sent by the server. -extern id const kGRPCStatusMetadataKey; +// Keys used in |NSError|'s |userInfo| dictionary to store the response headers and trailers sent by +// the server. +extern id const kGRPCHeadersKey; +extern id const kGRPCTrailersKey; // Represents a single gRPC remote call. @interface GRPCCall : GRXWriter @@ -57,43 +59,49 @@ extern id const kGRPCStatusMetadataKey; // These HTTP headers will be passed to the server as part of this call. Each HTTP header is a // name-value pair with string names and either string or binary values. // -// The passed dictionary has to use NSString keys, corresponding to the header names. The -// value associated to each can be a NSString object or a NSData object. E.g.: +// The passed dictionary has to use NSString keys, corresponding to the header names. The value +// associated to each can be a NSString object or a NSData object. E.g.: // -// call.requestMetadata = @{@"Authorization": @"Bearer ..."}; +// call.requestHeaders = @{@"authorization": @"Bearer ..."}; // -// call.requestMetadata[@"SomeBinaryHeader"] = someData; +// call.requestHeaders[@"my-header-bin"] = someData; // -// After the call is started, modifying this won't have any effect. +// After the call is started, trying to modify this property is an error. // // For convenience, the property is initialized to an empty NSMutableDictionary, and the setter // accepts (and copies) both mutable and immutable dictionaries. -- (NSMutableDictionary *)requestMetadata; // nonatomic -- (void)setRequestMetadata:(NSDictionary *)requestMetadata; // nonatomic, copy +- (NSMutableDictionary *)requestHeaders; // nonatomic +- (void)setRequestHeaders:(NSDictionary *)requestHeaders; // nonatomic, copy -// This dictionary is populated with the HTTP headers received from the server. When the RPC ends, -// the HTTP trailers received are added to the dictionary too. It has the same structure as the -// request metadata dictionary. +// This dictionary is populated with the HTTP headers received from the server. This happens before +// any response message is received from the server. It has the same structure as the request +// headers dictionary: Keys are NSString header names; names ending with the suffix "-bin" have a +// NSData value; the others have a NSString value. // -// The first time this object calls |writeValue| on the writeable passed to |startWithWriteable|, -// the |responseMetadata| dictionary already contains the response headers. When it calls -// |writesFinishedWithError|, the dictionary contains both the response headers and trailers. -@property(atomic, readonly) NSDictionary *responseMetadata; +// The value of this property is nil until all response headers are received, and will change before +// any of -writeValue: or -writesFinishedWithError: are sent to the writeable. +@property(atomic, readonly) NSDictionary *responseHeaders; + +// Same as responseHeaders, but populated with the HTTP trailers received from the server before the +// call finishes. +// +// The value of this property is nil until all response trailers are received, and will change +// before -writesFinishedWithError: is sent to the writeable. +@property(atomic, readonly) NSDictionary *responseTrailers; // The request writer has to write NSData objects into the provided Writeable. The server will -// receive each of those separately and in order. -// A gRPC call might not complete until the request writer finishes. On the other hand, the -// request finishing doesn't necessarily make the call to finish, as the server might continue -// sending messages to the response side of the call indefinitely (depending on the semantics of -// the specific remote method called). +// receive each of those separately and in order as distinct messages. +// A gRPC call might not complete until the request writer finishes. On the other hand, the request +// finishing doesn't necessarily make the call to finish, as the server might continue sending +// messages to the response side of the call indefinitely (depending on the semantics of the +// specific remote method called). // To finish a call right away, invoke cancel. - (instancetype)initWithHost:(NSString *)host path:(NSString *)path requestsWriter:(GRXWriter *)requestsWriter NS_DESIGNATED_INITIALIZER; -// Finishes the request side of this call, notifies the server that the RPC -// should be cancelled, and finishes the response side of the call with an error -// of code CANCELED. +// Finishes the request side of this call, notifies the server that the RPC should be cancelled, and +// finishes the response side of the call with an error of code CANCELED. - (void)cancel; // TODO(jcanizales): Let specify a deadline. As a category of GRXWriter? diff --git a/src/objective-c/GRPCClient/GRPCCall.m b/src/objective-c/GRPCClient/GRPCCall.m index 9d9648ae28..ff5d1c5aaf 100644 --- a/src/objective-c/GRPCClient/GRPCCall.m +++ b/src/objective-c/GRPCClient/GRPCCall.m @@ -37,15 +37,18 @@ #include <grpc/support/time.h> #import <RxLibrary/GRXConcurrentWriteable.h> -#import "private/GRPCChannel.h" #import "private/GRPCWrappedCall.h" #import "private/NSData+GRPC.h" #import "private/NSDictionary+GRPC.h" #import "private/NSError+GRPC.h" -NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; +NSString * const kGRPCHeadersKey = @"io.grpc.HeadersKey"; +NSString * const kGRPCTrailersKey = @"io.grpc.TrailersKey"; @interface GRPCCall () <GRXWriteable> +// Make them read-write. +@property(atomic, strong) NSDictionary *responseHeaders; +@property(atomic, strong) NSDictionary *responseTrailers; @end // The following methods of a C gRPC call object aren't reentrant, and thus @@ -70,21 +73,27 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; GRPCWrappedCall *_wrappedCall; dispatch_once_t _callAlreadyInvoked; - GRPCChannel *_channel; - // The C gRPC library has less guarantees on the ordering of events than we // do. Particularly, in the face of errors, there's no ordering guarantee at // all. This wrapper over our actual writeable ensures thread-safety and // correct ordering. GRXConcurrentWriteable *_responseWriteable; + + // The network thread wants the requestWriter to resume (when the server is ready for more input), + // or to stop (on errors), concurrently with user threads that want to start it, pause it or stop + // it. Because a writer isn't thread-safe, we'll synchronize those operations on it. + // We don't use a dispatch queue for that purpose, because the writer can call writeValue: or + // writesFinishedWithError: on this GRPCCall as part of those operations. We want to be able to + // pause the writer immediately on writeValue:, so we need our locking to be recursive. GRXWriter *_requestWriter; // To create a retain cycle when a call is started, up until it finishes. See - // |startWithWriteable:| and |finishWithError:|. - GRPCCall *_self; + // |startWithWriteable:| and |finishWithError:|. This saves users from having to retain a + // reference to the call object if all they're interested in is the handler being executed when + // the response arrives. + GRPCCall *_retainSelf; - NSMutableDictionary *_requestMetadata; - NSMutableDictionary *_responseMetadata; + NSMutableDictionary *_requestHeaders; } @synthesize state = _state; @@ -105,46 +114,41 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; format:@"The requests writer can't be already started."]; } if ((self = [super init])) { - _channel = [GRPCChannel channelToHost:host]; - - _wrappedCall = [[GRPCWrappedCall alloc] initWithChannel:_channel - path:path - host:host]; + _wrappedCall = [[GRPCWrappedCall alloc] initWithHost:host path:path]; + if (!_wrappedCall) { + return nil; + } // Serial queue to invoke the non-reentrant methods of the grpc_call object. _callQueue = dispatch_queue_create("org.grpc.call", NULL); _requestWriter = requestWriter; - _requestMetadata = [NSMutableDictionary dictionary]; - _responseMetadata = [NSMutableDictionary dictionary]; + _requestHeaders = [NSMutableDictionary dictionary]; } return self; } #pragma mark Metadata -- (NSMutableDictionary *)requestMetadata { - return _requestMetadata; -} - -- (void)setRequestMetadata:(NSDictionary *)requestMetadata { - _requestMetadata = [NSMutableDictionary dictionaryWithDictionary:requestMetadata]; +- (NSMutableDictionary *)requestHeaders { + return _requestHeaders; } -- (NSDictionary *)responseMetadata { - return _responseMetadata; +- (void)setRequestHeaders:(NSDictionary *)requestHeaders { + _requestHeaders = [NSMutableDictionary dictionaryWithDictionary:requestHeaders]; } #pragma mark Finish - (void)finishWithError:(NSError *)errorOrNil { // If the call isn't retained anywhere else, it can be deallocated now. - _self = nil; + _retainSelf = nil; // If there were still request messages coming, stop them. - _requestWriter.state = GRXWriterStateFinished; - _requestWriter = nil; + @synchronized(_requestWriter) { + _requestWriter.state = GRXWriterStateFinished; + } if (errorOrNil) { [_responseWriteable cancelWithError:errorOrNil]; @@ -226,11 +230,10 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; #pragma mark Send headers -// TODO(jcanizales): Rename to commitHeaders. -- (void)sendHeaders:(NSDictionary *)metadata { +- (void)sendHeaders:(NSDictionary *)headers { // TODO(jcanizales): Add error handlers for async failures [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMetadata alloc] - initWithMetadata:metadata ?: @{} handler:nil]]]; + initWithMetadata:headers ?: @{} handler:nil]]]; } #pragma mark GRXWriteable implementation @@ -244,12 +247,14 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // Resume the request writer. GRPCCall *strongSelf = weakSelf; if (strongSelf) { - strongSelf->_requestWriter.state = GRXWriterStateStarted; + @synchronized(strongSelf->_requestWriter) { + strongSelf->_requestWriter.state = GRXWriterStateStarted; + } } }; - [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMessage alloc] - initWithMessage:message - handler:resumingHandler]] errorHandler:errorHandler]; + [_wrappedCall startBatchWithOperations:@[[[GRPCOpSendMessage alloc] initWithMessage:message + handler:resumingHandler]] + errorHandler:errorHandler]; } - (void)writeValue:(id)value { @@ -257,7 +262,9 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // Pause the input and only resume it when the C layer notifies us that writes // can proceed. - _requestWriter.state = GRXWriterStatePaused; + @synchronized(_requestWriter) { + _requestWriter.state = GRXWriterStatePaused; + } __weak GRPCCall *weakSelf = self; dispatch_async(_callQueue, ^{ @@ -277,7 +284,6 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; } - (void)writesFinishedWithError:(NSError *)errorOrNil { - _requestWriter = nil; if (errorOrNil) { [self cancel]; } else { @@ -296,42 +302,54 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // Both handlers will eventually be called, from the network queue. Writes can start immediately // after this. -// The first one (metadataHandler), when the response headers are received. +// The first one (headersHandler), when the response headers are received. // The second one (completionHandler), whenever the RPC finishes for any reason. -- (void)invokeCallWithMetadataHandler:(void(^)(NSDictionary *))metadataHandler +- (void)invokeCallWithHeadersHandler:(void(^)(NSDictionary *))headersHandler completionHandler:(void(^)(NSError *, NSDictionary *))completionHandler { // TODO(jcanizales): Add error handlers for async failures [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvMetadata alloc] - initWithHandler:metadataHandler]]]; + initWithHandler:headersHandler]]]; [_wrappedCall startBatchWithOperations:@[[[GRPCOpRecvStatus alloc] initWithHandler:completionHandler]]]; } - (void)invokeCall { __weak GRPCCall *weakSelf = self; - [self invokeCallWithMetadataHandler:^(NSDictionary *headers) { + [self invokeCallWithHeadersHandler:^(NSDictionary *headers) { // Response headers received. GRPCCall *strongSelf = weakSelf; if (strongSelf) { - [strongSelf->_responseMetadata addEntriesFromDictionary:headers]; + strongSelf.responseHeaders = headers; [strongSelf startNextRead]; } } completionHandler:^(NSError *error, NSDictionary *trailers) { GRPCCall *strongSelf = weakSelf; if (strongSelf) { - [strongSelf->_responseMetadata addEntriesFromDictionary:trailers]; + strongSelf.responseTrailers = trailers; if (error) { - NSMutableDictionary *userInfo = - [NSMutableDictionary dictionaryWithDictionary:error.userInfo]; - userInfo[kGRPCStatusMetadataKey] = strongSelf->_responseMetadata; + NSMutableDictionary *userInfo = [NSMutableDictionary dictionary]; + if (error.userInfo) { + [userInfo addEntriesFromDictionary:error.userInfo]; + } + userInfo[kGRPCTrailersKey] = strongSelf.responseTrailers; + // TODO(jcanizales): The C gRPC library doesn't guarantee that the headers block will be + // called before this one, so an error might end up with trailers but no headers. We + // shouldn't call finishWithError until ater both blocks are called. It is also when this is + // done that we can provide a merged view of response headers and trailers in a thread-safe + // way. + if (strongSelf.responseHeaders) { + userInfo[kGRPCHeadersKey] = strongSelf.responseHeaders; + } error = [NSError errorWithDomain:error.domain code:error.code userInfo:userInfo]; } [strongSelf finishWithError:error]; } }]; // Now that the RPC has been initiated, request writes can start. - [_requestWriter startWithWriteable:self]; + @synchronized(_requestWriter) { + [_requestWriter startWithWriteable:self]; + } } #pragma mark GRXWriter implementation @@ -342,10 +360,10 @@ NSString * const kGRPCStatusMetadataKey = @"io.grpc.StatusMetadataKey"; // before being autoreleased). // Care is taken not to retain self strongly in any of the blocks used in this implementation, so // that the life of the instance is determined by this retain cycle. - _self = self; + _retainSelf = self; _responseWriteable = [[GRXConcurrentWriteable alloc] initWithWriteable:writeable]; - [self sendHeaders:_requestMetadata]; + [self sendHeaders:_requestHeaders]; [self invokeCall]; } diff --git a/src/objective-c/GRPCClient/private/GRPCChannel.h b/src/objective-c/GRPCClient/private/GRPCChannel.h index bc6a47d469..2a7b701576 100644 --- a/src/objective-c/GRPCClient/private/GRPCChannel.h +++ b/src/objective-c/GRPCClient/private/GRPCChannel.h @@ -35,17 +35,12 @@ struct grpc_channel; -// Each separate instance of this class represents at least one TCP -// connection to the provided host. To create a grpc_call, pass the -// value of the unmanagedChannel property to grpc_channel_create_call. -// Release this object when the call is finished. +// Each separate instance of this class represents at least one TCP connection to the provided host. +// Create them using one of the subclasses |GRPCSecureChannel| and |GRPCUnsecuredChannel|. @interface GRPCChannel : NSObject @property(nonatomic, readonly) struct grpc_channel *unmanagedChannel; -// Convenience constructor to allow for reuse of connections. -+ (instancetype)channelToHost:(NSString *)host; - -- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; - +// This initializer takes ownership of the passed channel, and will destroy it when this object is +// deallocated. It's illegal to pass the same grpc_channel to two different GRPCChannel objects. - (instancetype)initWithChannel:(struct grpc_channel *)unmanagedChannel NS_DESIGNATED_INITIALIZER; @end diff --git a/src/objective-c/GRPCClient/private/GRPCChannel.m b/src/objective-c/GRPCClient/private/GRPCChannel.m index af4326332f..4366e63320 100644 --- a/src/objective-c/GRPCClient/private/GRPCChannel.m +++ b/src/objective-c/GRPCClient/private/GRPCChannel.m @@ -35,53 +35,17 @@ #include <grpc/grpc.h> -#import "GRPCSecureChannel.h" -#import "GRPCUnsecuredChannel.h" - @implementation GRPCChannel -+ (instancetype)channelToHost:(NSString *)host { - // TODO(mlumish): Investigate whether a cache with strong links is a good idea - static NSMutableDictionary *channelCache; - static dispatch_once_t cacheInitialization; - dispatch_once(&cacheInitialization, ^{ - channelCache = [NSMutableDictionary dictionary]; - }); - GRPCChannel *channel = channelCache[host]; - if (!channel) { - channel = [[self alloc] initWithHost:host]; - channelCache[host] = channel; - } - return channel; -} - - (instancetype)init { - return [self initWithHost:nil]; + return [self initWithChannel:NULL]; } -- (instancetype)initWithHost:(NSString *)host { - if (![host rangeOfString:@"://"].length) { - // No scheme provided; assume https. - host = [@"https://" stringByAppendingString:host]; - } - NSURL *hostURL = [NSURL URLWithString:host]; - if (!hostURL) { - [NSException raise:NSInvalidArgumentException format:@"Invalid URL: %@", host]; +// Designated initializer +- (instancetype)initWithChannel:(grpc_channel *)unmanagedChannel { + if (!unmanagedChannel) { + return nil; } - if ([hostURL.scheme isEqualToString:@"https"]) { - host = [@[hostURL.host, hostURL.port ?: @443] componentsJoinedByString:@":"]; - return [[GRPCSecureChannel alloc] initWithHost:host]; - } - if ([hostURL.scheme isEqualToString:@"http"]) { - host = [@[hostURL.host, hostURL.port ?: @80] componentsJoinedByString:@":"]; - return [[GRPCUnsecuredChannel alloc] initWithHost:host]; - } - [NSException raise:NSInvalidArgumentException - format:@"URL scheme %@ isn't supported.", hostURL.scheme]; - return nil; // silence warning. -} - -- (instancetype)initWithChannel:(struct grpc_channel *)unmanagedChannel { if ((self = [super init])) { _unmanagedChannel = unmanagedChannel; } @@ -89,12 +53,8 @@ } - (void)dealloc { - // _unmanagedChannel is NULL when deallocating an object of the base class (because the - // initializer returns a different object). - if (_unmanagedChannel) { - // TODO(jcanizales): Be sure to add a test with a server that closes the connection prematurely, - // as in the past that made this call to crash. - grpc_channel_destroy(_unmanagedChannel); - } + // TODO(jcanizales): Be sure to add a test with a server that closes the connection prematurely, + // as in the past that made this call to crash. + grpc_channel_destroy(_unmanagedChannel); } @end diff --git a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m index 12535c9616..696069c200 100644 --- a/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m +++ b/src/objective-c/GRPCClient/private/GRPCCompletionQueue.m @@ -38,8 +38,6 @@ @implementation GRPCCompletionQueue + (instancetype)completionQueue { - // TODO(jcanizales): Reuse completion queues to consume only one thread, - // instead of one per call. return [[self alloc] init]; } diff --git a/src/objective-c/GRPCClient/private/GRPCHost.h b/src/objective-c/GRPCClient/private/GRPCHost.h new file mode 100644 index 0000000000..f0bbd53023 --- /dev/null +++ b/src/objective-c/GRPCClient/private/GRPCHost.h @@ -0,0 +1,58 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import <Foundation/Foundation.h> + +@class GRPCCompletionQueue; +struct grpc_call; + +@interface GRPCHost : NSObject + +@property(nonatomic, readonly) NSString *address; + +// The following properties should only be modified for testing: + +@property(nonatomic, getter=isSecure) BOOL secure; + +@property(nonatomic, copy) NSString *pathToCertificates; +@property(nonatomic, copy) NSString *hostNameOverride; + +// Host objects initialized with the same address are the same. ++ (instancetype)hostWithAddress:(NSString *)address; +- (instancetype)initWithAddress:(NSString *)address NS_DESIGNATED_INITIALIZER; + +// Create a grpc_call object to the provided path on this host. +- (struct grpc_call *)unmanagedCallWithPath:(NSString *)path + completionQueue:(GRPCCompletionQueue *)queue; + +@end diff --git a/src/objective-c/GRPCClient/private/GRPCHost.m b/src/objective-c/GRPCClient/private/GRPCHost.m new file mode 100644 index 0000000000..d902f95b51 --- /dev/null +++ b/src/objective-c/GRPCClient/private/GRPCHost.m @@ -0,0 +1,126 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import "GRPCHost.h" + +#include <grpc/grpc.h> + +#import "GRPCChannel.h" +#import "GRPCCompletionQueue.h" +#import "GRPCSecureChannel.h" +#import "GRPCUnsecuredChannel.h" + +@interface GRPCHost () +// TODO(mlumish): Investigate whether caching channels with strong links is a good idea. +@property(nonatomic, strong) GRPCChannel *channel; +@end + +@implementation GRPCHost + ++ (instancetype)hostWithAddress:(NSString *)address { + return [[self alloc] initWithAddress:address]; +} + +- (instancetype)init { + return [self initWithAddress:nil]; +} + +// Default initializer. +- (instancetype)initWithAddress:(NSString *)address { + + // 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 && !hostURL.port) { + address = [hostURL.host stringByAppendingString:@":443"]; + } + + // Look up the GRPCHost in the cache. + static NSMutableDictionary *hostCache; + static dispatch_once_t cacheInitialization; + dispatch_once(&cacheInitialization, ^{ + hostCache = [NSMutableDictionary dictionary]; + }); + @synchronized(hostCache) { + GRPCHost *cachedHost = hostCache[address]; + if (cachedHost) { + return cachedHost; + } + + if ((self = [super init])) { + _address = address; + _secure = YES; + hostCache[address] = self; + } + } + return self; +} + +- (grpc_call *)unmanagedCallWithPath:(NSString *)path completionQueue:(GRPCCompletionQueue *)queue { + if (!queue || !path || !self.channel) { + return NULL; + } + return grpc_channel_create_call(self.channel.unmanagedChannel, + NULL, GRPC_PROPAGATE_DEFAULTS, + queue.unmanagedQueue, + path.UTF8String, + self.hostName.UTF8String, + gpr_inf_future(GPR_CLOCK_REALTIME)); +} + +- (GRPCChannel *)channel { + // Create it lazily, because we don't want to open a connection just because someone is + // configuring a host. + if (!_channel) { + if (_secure) { + _channel = [[GRPCSecureChannel alloc] initWithHost:_address + pathToCertificates:_pathToCertificates + hostNameOverride:_hostNameOverride]; + } else { + _channel = [[GRPCUnsecuredChannel alloc] initWithHost:_address]; + } + } + return _channel; +} + +- (NSString *)hostName { + // TODO(jcanizales): Default to nil instead of _address when Issue #2635 is clarified. + return _hostNameOverride ?: _address; +} + +// TODO(jcanizales): Don't let set |secure| to |NO| if |pathToCertificates| or |hostNameOverride| +// have been set. Don't let set either of the latter if |secure| has been set to |NO|. + +@end diff --git a/src/objective-c/GRPCClient/private/GRPCSecureChannel.h b/src/objective-c/GRPCClient/private/GRPCSecureChannel.h index d34ceaea0c..74257eb058 100644 --- a/src/objective-c/GRPCClient/private/GRPCSecureChannel.h +++ b/src/objective-c/GRPCClient/private/GRPCSecureChannel.h @@ -31,8 +31,23 @@ * */ +#include <grpc/grpc.h> + #import "GRPCChannel.h" +struct grpc_credentials; + @interface GRPCSecureChannel : GRPCChannel +- (instancetype)initWithHost:(NSString *)host; + +// Only in tests shouldn't pathToCertificates or hostNameOverride be nil. Passing nil for +// pathToCertificates results in using the default root certificates distributed with the library. +- (instancetype)initWithHost:(NSString *)host + pathToCertificates:(NSString *)path + hostNameOverride:(NSString *)hostNameOverride; +// The passed arguments aren't required to be valid beyond the invocation of this initializer. +- (instancetype)initWithHost:(NSString *)host + credentials:(struct grpc_credentials *)credentials + args:(grpc_channel_args *)args NS_DESIGNATED_INITIALIZER; @end diff --git a/src/objective-c/GRPCClient/private/GRPCSecureChannel.m b/src/objective-c/GRPCClient/private/GRPCSecureChannel.m index 43a8bd539e..0a54804bb2 100644 --- a/src/objective-c/GRPCClient/private/GRPCSecureChannel.m +++ b/src/objective-c/GRPCClient/private/GRPCSecureChannel.m @@ -33,28 +33,83 @@ #import "GRPCSecureChannel.h" -#import <grpc/grpc_security.h> +#include <grpc/grpc_security.h> + +// Returns NULL if the file at path couldn't be read. In that case, if errorPtr isn't NULL, +// *errorPtr will be an object describing what went wrong. +static grpc_credentials *CertificatesAtPath(NSString *path, NSError **errorPtr) { + // 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:errorPtr]; + NSData *contentInASCII = [contentInUTF8 dataUsingEncoding:NSASCIIStringEncoding + allowLossyConversion:YES]; + if (!contentInASCII.bytes) { + // Passing NULL to grpc_ssl_credentials_create produces behavior we don't want, so return. + return NULL; + } + return grpc_ssl_credentials_create(contentInASCII.bytes, NULL); +} @implementation GRPCSecureChannel - (instancetype)initWithHost:(NSString *)host { - static grpc_credentials *kCredentials; + return [self initWithHost:host pathToCertificates:nil hostNameOverride:nil]; +} + +- (instancetype)initWithHost:(NSString *)host + pathToCertificates:(NSString *)path + hostNameOverride:(NSString *)hostNameOverride { + // Load default SSL certificates once. + static grpc_credentials *kDefaultCertificates; 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 *certsPath = [bundle pathForResource:@"gRPCCertificates.bundle/roots" ofType:@"pem"]; - NSAssert(certsPath.length, - @"gRPCCertificates.bundle/roots.pem not found under %@. This file, with the root " - "certificates, is needed to establish TLS (HTTPS) connections.", bundle.bundlePath); - NSData *certsData = [NSData dataWithContentsOfFile:certsPath]; - NSAssert(certsData.length, @"No data read from %@", certsPath); - NSString *certsString = [[NSString alloc] initWithData:certsData encoding:NSUTF8StringEncoding]; - kCredentials = grpc_ssl_credentials_create(certsString.UTF8String, NULL); + NSString *path = [bundle pathForResource:defaultPath ofType:@"pem"]; + NSError *error; + kDefaultCertificates = CertificatesAtPath(path, &error); + NSAssert(kDefaultCertificates, @"Could not read %@/%@.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: %@", + bundle.bundlePath, defaultPath, error); }); - return (self = [super initWithChannel:grpc_secure_channel_create(kCredentials, - host.UTF8String, - NULL)]); + + //TODO(jcanizales): Add NSError** parameter to the initializer. + grpc_credentials *certificates = path ? CertificatesAtPath(path, NULL) : kDefaultCertificates; + if (!certificates) { + return nil; + } + + // Ritual to pass the SSL host name override to the C library. + grpc_channel_args channelArgs; + grpc_arg nameOverrideArg; + channelArgs.num_args = 1; + channelArgs.args = &nameOverrideArg; + nameOverrideArg.type = GRPC_ARG_STRING; + nameOverrideArg.key = GRPC_SSL_TARGET_NAME_OVERRIDE_ARG; + // Cast const away. Hope C gRPC doesn't modify it! + nameOverrideArg.value.string = (char *) hostNameOverride.UTF8String; + grpc_channel_args *args = hostNameOverride ? &channelArgs : NULL; + + return [self initWithHost:host credentials:certificates args:args]; +} + +- (instancetype)initWithHost:(NSString *)host + credentials:(grpc_credentials *)credentials + args:(grpc_channel_args *)args { + return (self = + [super initWithChannel:grpc_secure_channel_create(credentials, host.UTF8String, args)]); +} + +// TODO(jcanizales): GRPCSecureChannel and GRPCUnsecuredChannel are just convenience initializers +// for GRPCChannel. Move them into GRPCChannel, which will make the following unnecessary. +- (instancetype)initWithChannel:(grpc_channel *)unmanagedChannel { + [NSException raise:NSInternalInconsistencyException format:@"use another initializer"]; + return [self initWithHost:nil]; // silence warnings } @end diff --git a/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.h b/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.h index 9d89cfb541..8528be44c0 100644 --- a/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.h +++ b/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.h @@ -34,5 +34,5 @@ #import "GRPCChannel.h" @interface GRPCUnsecuredChannel : GRPCChannel - +- (instancetype)initWithHost:(NSString *)host NS_DESIGNATED_INITIALIZER; @end diff --git a/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.m b/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.m index 8518f78c5b..070a529629 100644 --- a/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.m +++ b/src/objective-c/GRPCClient/private/GRPCUnsecuredChannel.m @@ -41,4 +41,10 @@ return (self = [super initWithChannel:grpc_insecure_channel_create(host.UTF8String, NULL)]); } +// TODO(jcanizales): GRPCSecureChannel and GRPCUnsecuredChannel are just convenience initializers +// for GRPCChannel. Move them into GRPCChannel, which will make the following unnecessary. +- (instancetype)initWithChannel:(grpc_channel *)unmanagedChannel { + [NSException raise:NSInternalInconsistencyException format:@"use the other initializer"]; + return [self initWithHost:nil]; // silence warnings +} @end diff --git a/src/objective-c/GRPCClient/private/GRPCWrappedCall.h b/src/objective-c/GRPCClient/private/GRPCWrappedCall.h index 18f8bb5531..da11cbb761 100644 --- a/src/objective-c/GRPCClient/private/GRPCWrappedCall.h +++ b/src/objective-c/GRPCClient/private/GRPCWrappedCall.h @@ -81,11 +81,12 @@ @end +#pragma mark GRPCWrappedCall + @interface GRPCWrappedCall : NSObject -- (instancetype)initWithChannel:(GRPCChannel *)channel - path:(NSString *)path - host:(NSString *)host NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithHost:(NSString *)host + path:(NSString *)path NS_DESIGNATED_INITIALIZER; - (void)startBatchWithOperations:(NSArray *)ops errorHandler:(void(^)())errorHandler; diff --git a/src/objective-c/GRPCClient/private/GRPCWrappedCall.m b/src/objective-c/GRPCClient/private/GRPCWrappedCall.m index ecc512edca..951c051036 100644 --- a/src/objective-c/GRPCClient/private/GRPCWrappedCall.m +++ b/src/objective-c/GRPCClient/private/GRPCWrappedCall.m @@ -32,11 +32,14 @@ */ #import "GRPCWrappedCall.h" + #import <Foundation/Foundation.h> #include <grpc/grpc.h> #include <grpc/byte_buffer.h> #include <grpc/support/alloc.h> + #import "GRPCCompletionQueue.h" +#import "GRPCHost.h" #import "NSDictionary+GRPC.h" #import "NSData+GRPC.h" #import "NSError+GRPC.h" @@ -219,37 +222,36 @@ @end -@implementation GRPCWrappedCall{ - grpc_call *_call; +#pragma mark GRPCWrappedCall + +@implementation GRPCWrappedCall { GRPCCompletionQueue *_queue; + grpc_call *_call; } - (instancetype)init { - return [self initWithChannel:nil path:nil host:nil]; + return [self initWithHost:nil path:nil]; } -- (instancetype)initWithChannel:(GRPCChannel *)channel - path:(NSString *)path - host:(NSString *)host { - if (!channel || !path || !host) { +- (instancetype)initWithHost:(NSString *)host + path:(NSString *)path { + if (!path || !host) { [NSException raise:NSInvalidArgumentException - format:@"channel, method, and host cannot be nil."]; + format:@"path and host cannot be nil."]; } - + if (self = [super init]) { static dispatch_once_t initialization; dispatch_once(&initialization, ^{ grpc_init(); }); - + + // Each completion queue consumes one thread. There's a trade to be made between creating and + // consuming too many threads and having contention of multiple calls in a single completion + // queue. Currently we favor latency and use one per call. _queue = [GRPCCompletionQueue completionQueue]; - if (!_queue) { - return nil; - } - _call = grpc_channel_create_call( - channel.unmanagedChannel, NULL, GRPC_PROPAGATE_DEFAULTS, - _queue.unmanagedQueue, path.UTF8String, host.UTF8String, - gpr_inf_future(GPR_CLOCK_REALTIME)); + + _call = [[GRPCHost hostWithAddress:host] unmanagedCallWithPath:path completionQueue:_queue]; if (_call == NULL) { return nil; } diff --git a/src/objective-c/RxLibrary/GRXBufferedPipe.h b/src/objective-c/RxLibrary/GRXBufferedPipe.h index b6296e1ed7..ca94ce275f 100644 --- a/src/objective-c/RxLibrary/GRXBufferedPipe.h +++ b/src/objective-c/RxLibrary/GRXBufferedPipe.h @@ -36,13 +36,11 @@ #import "GRXWriteable.h" #import "GRXWriter.h" -// A buffered pipe is a Writeable that also acts as a Writer (to whichever other writeable is passed -// to -startWithWriteable:). +// A buffered pipe is a Writer that also acts as a Writeable. // Once it is started, whatever values are written into it (via -writeValue:) will be propagated // immediately, unless flow control prevents it. // If it is throttled and keeps receiving values, as well as if it receives values before being -// started, it will buffer them and propagate them in order as soon as its state becomes -// GRXWriterStateStarted. +// started, it will buffer them and propagate them in order as soon as its state becomes Started. // If it receives an error (via -writesFinishedWithError:), it will drop any buffered values and // propagate the error immediately. // @@ -51,6 +49,9 @@ // pipe will keep buffering all data written to it, your application could run out of memory and // crash. If you want to react to flow control signals to prevent that, instead of using this class // you can implement an object that conforms to GRXWriter. +// +// Thread-safety: +// The methods of an object of this class should not be called concurrently from different threads. @interface GRXBufferedPipe : GRXWriter<GRXWriteable> // Convenience constructor. diff --git a/src/objective-c/RxLibrary/GRXForwardingWriter.h b/src/objective-c/RxLibrary/GRXForwardingWriter.h index d004333d2b..f310832284 100644 --- a/src/objective-c/RxLibrary/GRXForwardingWriter.h +++ b/src/objective-c/RxLibrary/GRXForwardingWriter.h @@ -33,11 +33,17 @@ #import "GRXWriter.h" -// A "proxy" class that simply forwards values, completion, and errors from its -// input writer to its writeable. +// A "proxy" class that simply forwards values, completion, and errors from its input writer to its +// writeable. // It is useful as a superclass for pipes that act as a transformation of their // input writer, and for classes that represent objects with input and // output sequences of values, like an RPC. +// +// Thread-safety: +// All messages sent to this object need to be serialized. When it is started, the writer it wraps +// is started in the same thread. Manual state changes are propagated to the wrapped writer in the +// same thread too. Importantly, all messages the wrapped writer sends to its writeable need to be +// serialized with any message sent to this object. @interface GRXForwardingWriter : GRXWriter - (instancetype)initWithWriter:(GRXWriter *)writer NS_DESIGNATED_INITIALIZER; @end diff --git a/src/objective-c/RxLibrary/GRXForwardingWriter.m b/src/objective-c/RxLibrary/GRXForwardingWriter.m index 2342f51ab3..a72be9ace2 100644 --- a/src/objective-c/RxLibrary/GRXForwardingWriter.m +++ b/src/objective-c/RxLibrary/GRXForwardingWriter.m @@ -48,7 +48,11 @@ // Designated initializer - (instancetype)initWithWriter:(GRXWriter *)writer { if (!writer) { - [NSException raise:NSInvalidArgumentException format:@"writer can't be nil."]; + return nil; + } + if (writer.state != GRXWriterStateNotStarted) { + [NSException raise:NSInvalidArgumentException + format:@"The writer argument must not have already started."]; } if ((self = [super init])) { _writer = writer; diff --git a/src/objective-c/RxLibrary/GRXImmediateWriter.h b/src/objective-c/RxLibrary/GRXImmediateWriter.h index b171f0c760..3fcc259434 100644 --- a/src/objective-c/RxLibrary/GRXImmediateWriter.h +++ b/src/objective-c/RxLibrary/GRXImmediateWriter.h @@ -36,10 +36,17 @@ #import "GRXWriter.h" // Utility to construct GRXWriter instances from values that are immediately available when -// required. The returned writers all support pausing and early termination. +// required. // -// Unless the writeable callback pauses them or stops them early, these writers will do all their -// interactions with the writeable before the start method returns. +// Thread-safety: +// +// An object of this class shouldn't be messaged concurrently by more than one thread. It will start +// messaging the writeable before |startWithWriteable:| returns, in the same thread. That is the +// only place where the writer can be paused or stopped prematurely. +// +// If a paused writer of this class is resumed, it will start messaging the writeable, in the same +// thread, before |setState:| returns. Because the object can't be legally accessed concurrently, +// that's the only place where it can be paused again (or stopped). @interface GRXImmediateWriter : GRXWriter // Returns a writer that pulls values from the passed NSEnumerator instance and pushes them to diff --git a/src/objective-c/RxLibrary/GRXWriteable.h b/src/objective-c/RxLibrary/GRXWriteable.h index 216de30735..45613d6dd0 100644 --- a/src/objective-c/RxLibrary/GRXWriteable.h +++ b/src/objective-c/RxLibrary/GRXWriteable.h @@ -48,15 +48,15 @@ typedef void (^GRXValueHandler)(id value); typedef void (^GRXCompletionHandler)(NSError *errorOrNil); -typedef void (^GRXSingleValueHandler)(id value, NSError *errorOrNil); -typedef void (^GRXStreamHandler)(BOOL done, id value, NSError *error); +typedef void (^GRXSingleHandler)(id value, NSError *errorOrNil); +typedef void (^GRXEventHandler)(BOOL done, id value, NSError *error); // Utility to create objects that conform to the GRXWriteable protocol, from // blocks that handle each of the two methods of the protocol. @interface GRXWriteable : NSObject<GRXWriteable> -+ (instancetype)writeableWithSingleValueHandler:(GRXSingleValueHandler)handler; -+ (instancetype)writeableWithStreamHandler:(GRXStreamHandler)handler; ++ (instancetype)writeableWithSingleHandler:(GRXSingleHandler)handler; ++ (instancetype)writeableWithEventHandler:(GRXEventHandler)handler; - (instancetype)initWithValueHandler:(GRXValueHandler)valueHandler completionHandler:(GRXCompletionHandler)completionHandler diff --git a/src/objective-c/RxLibrary/GRXWriteable.m b/src/objective-c/RxLibrary/GRXWriteable.m index 63f7c3e7f3..2729d62b72 100644 --- a/src/objective-c/RxLibrary/GRXWriteable.m +++ b/src/objective-c/RxLibrary/GRXWriteable.m @@ -38,7 +38,7 @@ GRXCompletionHandler _completionHandler; } -+ (instancetype)writeableWithSingleValueHandler:(GRXSingleValueHandler)handler { ++ (instancetype)writeableWithSingleHandler:(GRXSingleHandler)handler { if (!handler) { return [[self alloc] init]; } @@ -51,7 +51,7 @@ }]; } -+ (instancetype)writeableWithStreamHandler:(GRXStreamHandler)handler { ++ (instancetype)writeableWithEventHandler:(GRXEventHandler)handler { if (!handler) { return [[self alloc] init]; } diff --git a/src/objective-c/RxLibrary/GRXWriter.h b/src/objective-c/RxLibrary/GRXWriter.h index 5d6e1a472a..b1c994aa38 100644 --- a/src/objective-c/RxLibrary/GRXWriter.h +++ b/src/objective-c/RxLibrary/GRXWriter.h @@ -35,84 +35,73 @@ #import "GRXWriteable.h" +// States of a writer. typedef NS_ENUM(NSInteger, GRXWriterState) { - // The writer has not yet been given a writeable to which it can push its - // values. To have an writer transition to the Started state, send it a - // startWithWriteable: message. + // The writer has not yet been given a writeable to which it can push its values. To have a writer + // transition to the Started state, send it a startWithWriteable: message. // - // An writer's state cannot be manually set to this value. + // A writer's state cannot be manually set to this value. GRXWriterStateNotStarted, // The writer might push values to the writeable at any moment. GRXWriterStateStarted, - // The writer is temporarily paused, and won't send any more values to the - // writeable unless its state is set back to Started. The writer might still - // transition to the Finished state at any moment, and is allowed to send - // writesFinishedWithError: to its writeable. - // - // Not all implementations of writer have to support pausing, and thus - // trying to set an writer's state to this value might have no effect. + // The writer is temporarily paused, and won't send any more values to the writeable unless its + // state is set back to Started. The writer might still transition to the Finished state at any + // moment, and is allowed to send writesFinishedWithError: to its writeable. GRXWriterStatePaused, // The writer has released its writeable and won't interact with it anymore. // - // One seldomly wants to set an writer's state to this value, as its - // writeable isn't notified with a writesFinishedWithError: message. Instead, sending - // finishWithError: to the writer will make it notify the writeable and then - // transition to this state. + // One seldomly wants to set a writer's state to this value, as its writeable isn't notified with + // a writesFinishedWithError: message. Instead, sending finishWithError: to the writer will make + // it notify the writeable and then transition to this state. GRXWriterStateFinished }; -// An object that conforms to this protocol can produce, on demand, a sequence -// of values. The sequence may be produced asynchronously, and it may consist of -// any number of elements, including none or an infinite number. +// An GRXWriter object can produce, on demand, a sequence of values. The sequence may be produced +// asynchronously, and it may consist of any number of elements, including none or an infinite +// number. +// +// GRXWriter is the active dual of NSEnumerator. The difference between them is thus whether the +// object plays an active or passive role during usage: A user of NSEnumerator pulls values off it, +// and passes the values to a writeable. A user of GRXWriter, though, just gives it a writeable, and +// the GRXWriter instance pushes values to the writeable. This makes this protocol suitable to +// represent a sequence of future values, as well as collections with internal iteration. // -// GRXWriter is the active dual of NSEnumerator. The difference between them -// is thus whether the object plays an active or passive role during usage: A -// user of NSEnumerator pulls values off it, and passes the values to a writeable. -// A user of GRXWriter, though, just gives it a writeable, and the -// GRXWriter instance pushes values to the writeable. This makes this protocol -// suitable to represent a sequence of future values, as well as collections -// with internal iteration. +// An instance of GRXWriter can start producing values after a writeable is passed to it. It can +// also be commanded to finish the sequence immediately (with an optional error). Finally, it can be +// asked to pause, and resumed later. All GRXWriter objects support pausing and early termination. // -// An instance of GRXWriter can start producing values after a writeable is -// passed to it. It can also be commanded to finish the sequence immediately -// (with an optional error). Finally, it can be asked to pause, but the -// conforming instance is not required to oblige. +// Thread-safety: // -// Unless otherwise indicated by a conforming class, no messages should be sent -// concurrently to a GRXWriter. I.e., conforming classes aren't required to -// be thread-safe. +// State transitions take immediate effect if the object is used from a single thread. Subclasses +// might offer stronger guarantees. +// +// Unless otherwise indicated by a conforming subclass, no messages should be sent concurrently to a +// GRXWriter. I.e., conforming classes aren't required to be thread-safe. @interface GRXWriter : NSObject -// This property can be used to query the current state of the writer, which -// determines how it might currently use its writeable. Some state transitions can -// be triggered by setting this property to the corresponding value, and that's -// useful for advanced use cases like pausing an writer. For more details, -// see the documentation of the enum. +// This property can be used to query the current state of the writer, which determines how it might +// currently use its writeable. Some state transitions can be triggered by setting this property to +// the corresponding value, and that's useful for advanced use cases like pausing an writer. For +// more details, see the documentation of the enum further down. @property(nonatomic) GRXWriterState state; -// Start sending messages to the writeable. Messages may be sent before the method -// returns, or they may be sent later in the future. See GRXWriteable.h for the -// different messages a writeable can receive. +// Transition to the Started state, and start sending messages to the writeable (a reference to it +// is retained). Messages to the writeable may be sent before the method returns, or they may be +// sent later in the future. See GRXWriteable.h for the different messages a writeable can receive. // -// If this writer draws its values from an external source (e.g. from the -// filesystem or from a server), calling this method will commonly trigger side -// effects (like network connections). +// If this writer draws its values from an external source (e.g. from the filesystem or from a +// server), calling this method will commonly trigger side effects (like network connections). // // This method might only be called on writers in the NotStarted state. - (void)startWithWriteable:(id<GRXWriteable>)writeable; -// Send writesFinishedWithError:errorOrNil immediately to the writeable, and don't send -// any more messages to it. -// -// This method might only be called on writers in the Started or Paused -// state. +// Send writesFinishedWithError:errorOrNil to the writeable. Then release the reference to it and +// transition to the Finished state. // -// TODO(jcanizales): Consider adding some guarantee about the immediacy of that -// stopping. I know I've relied on it in part of the code that uses this, but -// can't remember the details in the presence of concurrency. +// This method might only be called on writers in the Started or Paused state. - (void)finishWithError:(NSError *)errorOrNil; @end diff --git a/src/objective-c/tests/GRPCClientTests.m b/src/objective-c/tests/GRPCClientTests.m index 103e5ca3d4..06581e7599 100644 --- a/src/objective-c/tests/GRPCClientTests.m +++ b/src/objective-c/tests/GRPCClientTests.m @@ -35,6 +35,8 @@ #import <XCTest/XCTest.h> #import <GRPCClient/GRPCCall.h> +#import <GRPCClient/GRPCCall+OAuth2.h> +#import <GRPCClient/GRPCCall+Tests.h> #import <ProtoRPC/ProtoMethod.h> #import <RemoteTest/Messages.pbobjc.h> #import <RxLibrary/GRXWriteable.h> @@ -43,8 +45,7 @@ // These are a few tests similar to InteropTests, but which use the generic gRPC client (GRPCCall) // rather than a generated proto library on top of it. -// grpc-test.sandbox.google.com -static NSString * const kHostAddress = @"http://localhost:5050"; +static NSString * const kHostAddress = @"localhost:5050"; static NSString * const kPackage = @"grpc.testing"; static NSString * const kService = @"TestService"; @@ -58,6 +59,9 @@ static ProtoMethod *kUnaryCallMethod; @implementation GRPCClientTests - (void)setUp { + // Register test server as non-SSL. + [GRPCCall useInsecureConnectionsForHost:kHostAddress]; + // This method isn't implemented by the remote server. kInexistentMethod = [[ProtoMethod alloc] initWithPackage:kPackage service:kService @@ -110,7 +114,7 @@ static ProtoMethod *kUnaryCallMethod; [call startWithWriteable:responsesWriteable]; - [self waitForExpectationsWithTimeout:4 handler:nil]; + [self waitForExpectationsWithTimeout:8 handler:nil]; } - (void)testSimpleProtoRPC { @@ -142,7 +146,7 @@ static ProtoMethod *kUnaryCallMethod; [call startWithWriteable:responsesWriteable]; - [self waitForExpectationsWithTimeout:4 handler:nil]; + [self waitForExpectationsWithTimeout:8 handler:nil]; } - (void)testMetadata { @@ -157,18 +161,20 @@ static ProtoMethod *kUnaryCallMethod; path:kUnaryCallMethod.HTTPPath requestsWriter:requestsWriter]; - call.requestMetadata[@"Authorization"] = @"Bearer bogusToken"; + call.oauth2AccessToken = @"bogusToken"; id<GRXWriteable> responsesWriteable = [[GRXWriteable alloc] initWithValueHandler:^(NSData *value) { XCTFail(@"Received unexpected response: %@", value); } completionHandler:^(NSError *errorOrNil) { XCTAssertNotNil(errorOrNil, @"Finished without error!"); XCTAssertEqual(errorOrNil.code, 16, @"Finished with unexpected error: %@", errorOrNil); - XCTAssertEqualObjects(call.responseMetadata, errorOrNil.userInfo[kGRPCStatusMetadataKey], - @"Metadata in the NSError object and call object differ."); - NSString *challengeHeader = call.responseMetadata[@"www-authenticate"]; + XCTAssertEqualObjects(call.responseHeaders, errorOrNil.userInfo[kGRPCHeadersKey], + @"Headers in the NSError object and call object differ."); + XCTAssertEqualObjects(call.responseTrailers, errorOrNil.userInfo[kGRPCTrailersKey], + @"Trailers in the NSError object and call object differ."); + NSString *challengeHeader = call.oauth2ChallengeHeader; XCTAssertGreaterThan(challengeHeader.length, 0, - @"No challenge in response headers %@", call.responseMetadata); + @"No challenge in response headers %@", call.responseHeaders); [expectation fulfill]; }]; diff --git a/src/objective-c/tests/InteropTests.h b/src/objective-c/tests/InteropTests.h new file mode 100644 index 0000000000..1045c3d124 --- /dev/null +++ b/src/objective-c/tests/InteropTests.h @@ -0,0 +1,43 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +#import <XCTest/XCTest.h> + +// Implements tests as described here: +// https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md + +@interface InteropTests : XCTestCase +// Returns @"grpc-test.sandbox.google.com". +// Override in a subclass to perform the same tests against a different address. ++ (NSString *)host; +@end diff --git a/src/objective-c/tests/InteropTests.m b/src/objective-c/tests/InteropTests.m index b473d73422..1b63fe2059 100644 --- a/src/objective-c/tests/InteropTests.m +++ b/src/objective-c/tests/InteropTests.m @@ -31,11 +31,11 @@ * */ -#include <grpc/status.h> +#import "InteropTests.h" -#import <UIKit/UIKit.h> -#import <XCTest/XCTest.h> +#include <grpc/status.h> +#import <GRPCClient/GRPCCall+Tests.h> #import <ProtoRPC/ProtoRPC.h> #import <RemoteTest/Empty.pbobjc.h> #import <RemoteTest/Messages.pbobjc.h> @@ -76,21 +76,22 @@ } @end -@interface InteropTests : XCTestCase -@end +#pragma mark Tests + +static NSString * const kRemoteSSLHost = @"grpc-test.sandbox.google.com"; @implementation InteropTests { RMTTestService *_service; } -// grpc-test.sandbox.google.com ++ (NSString *)host { + return kRemoteSSLHost; +} - (void)setUp { - _service = [[RMTTestService alloc] initWithHost:@"http://localhost:5050"]; + _service = [[RMTTestService alloc] initWithHost:self.class.host]; } -// Tests as described here: https://github.com/grpc/grpc/blob/master/doc/interop-test-descriptions.md - - (void)testEmptyUnaryRPC { __weak XCTestExpectation *expectation = [self expectationWithDescription:@"EmptyUnary"]; @@ -127,7 +128,7 @@ [expectation fulfill]; }]; - [self waitForExpectationsWithTimeout:8 handler:nil]; + [self waitForExpectationsWithTimeout:16 handler:nil]; } - (void)testClientStreamingRPC { diff --git a/src/objective-c/tests/InteropTestsLocalCleartext.m b/src/objective-c/tests/InteropTestsLocalCleartext.m new file mode 100644 index 0000000000..2d7d3c4b2c --- /dev/null +++ b/src/objective-c/tests/InteropTestsLocalCleartext.m @@ -0,0 +1,59 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +// Repeat of the tests in InteropTests.m, but sending the RPCs to a local cleartext server instead +// of the remote SSL one. + +#import <GRPCClient/GRPCCall+Tests.h> + +#import "InteropTests.h" + +static NSString * const kLocalCleartextHost = @"localhost:5050"; + +@interface InteropTestsLocalCleartext : InteropTests +@end + +@implementation InteropTestsLocalCleartext + ++ (NSString *)host { + return kLocalCleartextHost; +} + +- (void)setUp { + // Register test server as non-SSL. + [GRPCCall useInsecureConnectionsForHost:kLocalCleartextHost]; + + [super setUp]; +} + +@end diff --git a/src/objective-c/tests/InteropTestsLocalSSL.m b/src/objective-c/tests/InteropTestsLocalSSL.m new file mode 100644 index 0000000000..f69f806dcf --- /dev/null +++ b/src/objective-c/tests/InteropTestsLocalSSL.m @@ -0,0 +1,62 @@ +/* + * + * Copyright 2015, Google Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +// Repeat of the tests in InteropTests.m, but sending the RPCs to a local SSL server instead of the +// remote one. + +#import <GRPCClient/GRPCCall+Tests.h> + +#import "InteropTests.h" + +static NSString * const kLocalSSLHost = @"localhost:5051"; + +@interface InteropTestsLocalSSL : InteropTests +@end + +@implementation InteropTestsLocalSSL + ++ (NSString *)host { + return kLocalSSLHost; +} + +- (void)setUp { + // Register test server certificates and name. + NSBundle *bundle = [NSBundle bundleForClass:self.class]; + NSString *certsPath = [bundle pathForResource:@"TestCertificates.bundle/test-certificates" + ofType:@"pem"]; + [GRPCCall useTestCertsPath:certsPath testName:@"foo.test.google.fr" forHost:kLocalSSLHost]; + + [super setUp]; +} + +@end diff --git a/src/objective-c/tests/RxLibraryUnitTests.m b/src/objective-c/tests/RxLibraryUnitTests.m index 5e3162875a..a67a4c6cd9 100644 --- a/src/objective-c/tests/RxLibraryUnitTests.m +++ b/src/objective-c/tests/RxLibraryUnitTests.m @@ -55,7 +55,7 @@ return [[self alloc] init]; } -- (GRXSingleValueHandler)block { +- (GRXSingleHandler)block { return ^(id value, NSError *errorOrNil) { ++_timesCalled; _value = value; @@ -71,13 +71,13 @@ #pragma mark Writeable -- (void)testWriteableSingleValueHandlerIsCalledForValue { +- (void)testWriteableSingleHandlerIsCalledForValue { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; id anyValue = @7; // If: - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; [writeable writeValue:anyValue]; // Then: @@ -86,13 +86,13 @@ XCTAssertEqualObjects(handler.errorOrNil, nil); } -- (void)testWriteableSingleValueHandlerIsCalledForError { +- (void)testWriteableSingleHandlerIsCalledForError { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; NSError *anyError = [NSError errorWithDomain:@"domain" code:7 userInfo:nil]; // If: - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; [writeable writesFinishedWithError:anyError]; // Then: @@ -106,7 +106,7 @@ - (void)testBufferedPipePropagatesValue { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; id anyValue = @7; // If: @@ -123,7 +123,7 @@ - (void)testBufferedPipePropagatesError { // Given: CapturingSingleValueHandler *handler = [CapturingSingleValueHandler handler]; - id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleValueHandler:handler.block]; + id<GRXWriteable> writeable = [GRXWriteable writeableWithSingleHandler:handler.block]; NSError *anyError = [NSError errorWithDomain:@"domain" code:7 userInfo:nil]; // If: diff --git a/src/objective-c/tests/TestCertificates.bundle/test-certificates.pem b/src/objective-c/tests/TestCertificates.bundle/test-certificates.pem new file mode 100644 index 0000000000..6c8511a73c --- /dev/null +++ b/src/objective-c/tests/TestCertificates.bundle/test-certificates.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICSjCCAbOgAwIBAgIJAJHGGR4dGioHMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNV +BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX +aWRnaXRzIFB0eSBMdGQxDzANBgNVBAMTBnRlc3RjYTAeFw0xNDExMTEyMjMxMjla +Fw0yNDExMDgyMjMxMjlaMFYxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0 +YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxDzANBgNVBAMT +BnRlc3RjYTCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAwEDfBV5MYdlHVHJ7 ++L4nxrZy7mBfAVXpOc5vMYztssUI7mL2/iYujiIXM+weZYNTEpLdjyJdu7R5gGUu +g1jSVK/EPHfc74O7AyZU34PNIP4Sh33N+/A5YexrNgJlPY+E3GdVYi4ldWJjgkAd +Qah2PH5ACLrIIC6tRka9hcaBlIECAwEAAaMgMB4wDAYDVR0TBAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAgQwDQYJKoZIhvcNAQELBQADgYEAHzC7jdYlzAVmddi/gdAeKPau +sPBG/C2HCWqHzpCUHcKuvMzDVkY/MP2o6JIW2DBbY64bO/FceExhjcykgaYtCH/m +oIU63+CFOTtR7otyQAWHqXa7q4SbCDlG7DyRFxqG0txPtGvy12lgldA2+RgcigQG +Dfcog5wrJytaQ6UA0wE= +-----END CERTIFICATE----- diff --git a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj index f13fb8288b..3a1c3d940a 100644 --- a/src/objective-c/tests/Tests.xcodeproj/project.pbxproj +++ b/src/objective-c/tests/Tests.xcodeproj/project.pbxproj @@ -13,6 +13,9 @@ 63423F511B151B77006CF63C /* RxLibraryUnitTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 63423F501B151B77006CF63C /* RxLibraryUnitTests.m */; }; 635697CD1B14FC11007A7283 /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 635697CC1B14FC11007A7283 /* Tests.m */; }; 635ED2EC1B1A3BC400FDE5C3 /* InteropTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */; }; + 63715F561B780C020029CB0B /* InteropTestsLocalCleartext.m in Sources */ = {isa = PBXBuildFile; fileRef = 63715F551B780C020029CB0B /* InteropTestsLocalCleartext.m */; }; + 63E240CE1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m in Sources */ = {isa = PBXBuildFile; fileRef = 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */; }; + 63E240D01B6C63DC005F3B0E /* TestCertificates.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 63E240CF1B6C63DC005F3B0E /* TestCertificates.bundle */; }; 7D8A186224D39101F90230F6 /* libPods.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 35F2B6BF3BAE8F0DC4AFD76E /* libPods.a */; }; /* End PBXBuildFile section */ @@ -49,6 +52,10 @@ 635697CC1B14FC11007A7283 /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = "<group>"; }; 635697D81B14FC11007A7283 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTests.m; sourceTree = "<group>"; }; + 63715F551B780C020029CB0B /* InteropTestsLocalCleartext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTestsLocalCleartext.m; sourceTree = "<group>"; }; + 63E240CC1B6C4D3A005F3B0E /* InteropTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = InteropTests.h; sourceTree = "<group>"; }; + 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = InteropTestsLocalSSL.m; sourceTree = "<group>"; }; + 63E240CF1B6C63DC005F3B0E /* TestCertificates.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = TestCertificates.bundle; sourceTree = "<group>"; }; FF7B5489BCFE40111D768DD0 /* Pods.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = Pods.debug.xcconfig; path = "Pods/Target Support Files/Pods/Pods.debug.xcconfig"; sourceTree = "<group>"; }; /* End PBXFileReference section */ @@ -93,6 +100,7 @@ isa = PBXGroup; children = ( 635697C91B14FC11007A7283 /* Tests */, + 63E240CF1B6C63DC005F3B0E /* TestCertificates.bundle */, 635697C81B14FC11007A7283 /* Products */, 51E4650F34F854F41FF053B3 /* Pods */, 136D535E19727099B941D7B1 /* Frameworks */, @@ -112,9 +120,12 @@ isa = PBXGroup; children = ( 6312AE4D1B1BF49B00341DEE /* GRPCClientTests.m */, - 63175DFE1B1B9FAF00027841 /* LocalClearTextTests.m */, + 63E240CC1B6C4D3A005F3B0E /* InteropTests.h */, 635ED2EB1B1A3BC400FDE5C3 /* InteropTests.m */, + 63E240CD1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m */, + 63715F551B780C020029CB0B /* InteropTestsLocalCleartext.m */, 63423F501B151B77006CF63C /* RxLibraryUnitTests.m */, + 63175DFE1B1B9FAF00027841 /* LocalClearTextTests.m */, 635697CC1B14FC11007A7283 /* Tests.m */, 635697D71B14FC11007A7283 /* Supporting Files */, ); @@ -209,6 +220,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 63E240D01B6C63DC005F3B0E /* TestCertificates.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -252,8 +264,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 63715F561B780C020029CB0B /* InteropTestsLocalCleartext.m in Sources */, 63175DFF1B1B9FAF00027841 /* LocalClearTextTests.m in Sources */, 63423F511B151B77006CF63C /* RxLibraryUnitTests.m in Sources */, + 63E240CE1B6C4E2B005F3B0E /* InteropTestsLocalSSL.m in Sources */, 6312AE4E1B1BF49B00341DEE /* GRPCClientTests.m in Sources */, 635ED2EC1B1A3BC400FDE5C3 /* InteropTests.m in Sources */, ); diff --git a/src/objective-c/tests/run_tests.sh b/src/objective-c/tests/run_tests.sh index b13c0f0633..7b133c1782 100755 --- a/src/objective-c/tests/run_tests.sh +++ b/src/objective-c/tests/run_tests.sh @@ -37,7 +37,8 @@ cd $(dirname $0) # Run the tests server. ../../../bins/$CONFIG/interop_server --port=5050 & -# Kill it when this script exits. +../../../bins/$CONFIG/interop_server --port=5051 --enable_ssl & +# Kill them when this script exits. trap 'kill -9 `jobs -p`' EXIT # xcodebuild is very verbose. We filter its output and tell Bash to fail if any |