/* * Copyright 2017 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #import "Firestore/Source/Remote/FSTDatastore.h" #import #import #include #include #include #import "FIRFirestoreErrors.h" #import "Firestore/Source/API/FIRFirestore+Internal.h" #import "Firestore/Source/API/FIRFirestoreVersion.h" #import "Firestore/Source/Local/FSTLocalStore.h" #import "Firestore/Source/Model/FSTDocument.h" #import "Firestore/Source/Model/FSTMutation.h" #import "Firestore/Source/Remote/FSTSerializerBeta.h" #import "Firestore/Source/Remote/FSTStream.h" #import "Firestore/Source/Util/FSTDispatchQueue.h" #import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h" #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h" #include "Firestore/core/src/firebase/firestore/auth/token.h" #include "Firestore/core/src/firebase/firestore/core/database_info.h" #include "Firestore/core/src/firebase/firestore/model/database_id.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/util/error_apple.h" #include "Firestore/core/src/firebase/firestore/util/hard_assert.h" #include "Firestore/core/src/firebase/firestore/util/log.h" #include "Firestore/core/src/firebase/firestore/util/string_apple.h" namespace util = firebase::firestore::util; using firebase::firestore::auth::CredentialsProvider; using firebase::firestore::auth::Token; using firebase::firestore::core::DatabaseInfo; using firebase::firestore::model::DatabaseId; using firebase::firestore::model::DocumentKey; NS_ASSUME_NONNULL_BEGIN // GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore // directly exposes an sslEnabled setting so this is required to plumb that through. Note that our // own tests depend on this working so we'll know if this changes upstream. @interface GRPCHost + (nullable instancetype)hostWithAddress:(NSString *)address; @property(nonatomic, getter=isSecure) BOOL secure; @end static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client"; static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix"; /** Function typedef used to create RPCs. */ typedef GRPCProtoCall * (^RPCFactory)(void); #pragma mark - FSTDatastore @interface FSTDatastore () /** The GRPC service for Firestore. */ @property(nonatomic, strong, readonly) GCFSFirestore *service; @property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue; /** * An object for getting an auth token before each request. Does not own the CredentialsProvider * instance. */ @property(nonatomic, assign, readonly) CredentialsProvider *credentials; @property(nonatomic, strong, readonly) FSTSerializerBeta *serializer; @end @implementation FSTDatastore + (instancetype)datastoreWithDatabase:(const DatabaseInfo *)databaseInfo workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue credentials:(CredentialsProvider *)credentials { return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo workerDispatchQueue:workerDispatchQueue credentials:credentials]; } - (instancetype)initWithDatabaseInfo:(const DatabaseInfo *)databaseInfo workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue credentials:(CredentialsProvider *)credentials { if (self = [super init]) { _databaseInfo = databaseInfo; NSString *host = util::WrapNSString(databaseInfo->host()); if (!databaseInfo->ssl_enabled()) { GRPCHost *hostConfig = [GRPCHost hostWithAddress:host]; hostConfig.secure = NO; } _service = [GCFSFirestore serviceWithHost:host]; _workerDispatchQueue = workerDispatchQueue; _credentials = credentials; _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:&databaseInfo->database_id()]; } return self; } - (NSString *)description { return [NSString stringWithFormat:@">", self.databaseInfo->database_id().database_id().c_str(), self.databaseInfo->host().c_str()]; } /** * Converts the error to an error within the domain FIRFirestoreErrorDomain. */ + (NSError *)firestoreErrorForError:(NSError *)error { if (!error) { return error; } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) { return error; } else if ([error.domain isEqualToString:kGRPCErrorDomain]) { HARD_ASSERT(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated, "Unknown GRPC error code: %s", error.code); return [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo]; } else { return [NSError errorWithDomain:FIRFirestoreErrorDomain code:FIRFirestoreErrorCodeUnknown userInfo:@{NSUnderlyingErrorKey : error}]; } } + (BOOL)isAbortedError:(NSError *)error { HARD_ASSERT([error.domain isEqualToString:FIRFirestoreErrorDomain], "isAbortedError: only works with errors emitted by FSTDatastore."); return error.code == FIRFirestoreErrorCodeAborted; } + (BOOL)isPermanentWriteError:(NSError *)error { HARD_ASSERT([error.domain isEqualToString:FIRFirestoreErrorDomain], "isPerminanteWriteError: only works with errors emitted by FSTDatastore."); switch (error.code) { case FIRFirestoreErrorCodeCancelled: case FIRFirestoreErrorCodeUnknown: case FIRFirestoreErrorCodeDeadlineExceeded: case FIRFirestoreErrorCodeResourceExhausted: case FIRFirestoreErrorCodeInternal: case FIRFirestoreErrorCodeUnavailable: // Unauthenticated means something went wrong with our token and we need to retry with new // credentials which will happen automatically. case FIRFirestoreErrorCodeUnauthenticated: return NO; case FIRFirestoreErrorCodeInvalidArgument: case FIRFirestoreErrorCodeNotFound: case FIRFirestoreErrorCodeAlreadyExists: case FIRFirestoreErrorCodePermissionDenied: case FIRFirestoreErrorCodeFailedPrecondition: case FIRFirestoreErrorCodeAborted: // Aborted might be retried in some scenarios, but that is dependant on // the context and should handled individually by the calling code. // See https://cloud.google.com/apis/design/errors case FIRFirestoreErrorCodeOutOfRange: case FIRFirestoreErrorCodeUnimplemented: case FIRFirestoreErrorCodeDataLoss: return YES; default: return YES; } } /** Returns the string to be used as x-goog-api-client header value. */ + (NSString *)googAPIClientHeaderValue { // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the // version as a macro, so it would be hardcoded based on version we have at compile time of // the Firestore library, rather than the version available at runtime/at compile time by the // user of the library. return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FIRFirestoreVersionString]; } /** Returns the string to be used as google-cloud-resource-prefix header value. */ + (NSString *)googleCloudResourcePrefixForDatabaseID:(const DatabaseId *)databaseID { return [NSString stringWithFormat:@"projects/%s/databases/%s", databaseID->project_id().c_str(), databaseID->database_id().c_str()]; } /** * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers * (for logging purposes). */ + (NSDictionary *)extractWhiteListedHeaders: (NSDictionary *)headers { NSMutableDictionary *whiteListedHeaders = [NSMutableDictionary dictionary]; NSArray *whiteList = @[ @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service", @"x-google-gfe-request-trace" ]; [headers enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) { if ([whiteList containsObject:[headerName lowercaseString]]) { whiteListedHeaders[headerName] = headerValue; } }]; return whiteListedHeaders; } /** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */ + (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName { if ([FIRFirestore isLoggingEnabled]) { LOG_DEBUG("RPC %s returned headers (whitelisted): %s", rpcName, [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]); } } - (void)commitMutations:(NSArray *)mutations completion:(FSTVoidErrorBlock)completion { GCFSCommitRequest *request = [GCFSCommitRequest message]; request.database = [self.serializer encodedDatabaseID]; NSMutableArray *mutationProtos = [NSMutableArray array]; for (FSTMutation *mutation in mutations) { [mutationProtos addObject:[self.serializer encodedMutation:mutation]]; } request.writesArray = mutationProtos; RPCFactory rpcFactory = ^GRPCProtoCall * { __block GRPCProtoCall *rpc = [self.service RPCToCommitWithRequest:request handler:^(GCFSCommitResponse *response, NSError *_Nullable error) { error = [FSTDatastore firestoreErrorForError:error]; [self.workerDispatchQueue dispatchAsync:^{ if (error != nil && error.code == FIRFirestoreErrorCodeUnauthenticated) { self->_credentials->InvalidateToken(); } LOG_DEBUG("RPC CommitRequest completed. Error: %s", error); [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"]; completion(error); }]; }]; return rpc; }; [self invokeRPCWithFactory:rpcFactory errorHandler:completion]; } - (void)lookupDocuments:(const std::vector &)keys completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion { GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message]; request.database = [self.serializer encodedDatabaseID]; for (const DocumentKey &key : keys) { [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]]; } struct Closure { std::map results; }; __block std::shared_ptr closure = std::make_shared(Closure{}); RPCFactory rpcFactory = ^GRPCProtoCall * { __block GRPCProtoCall *rpc = [self.service RPCToBatchGetDocumentsWithRequest:request eventHandler:^(BOOL done, GCFSBatchGetDocumentsResponse *_Nullable response, NSError *_Nullable error) { error = [FSTDatastore firestoreErrorForError:error]; [self.workerDispatchQueue dispatchAsync:^{ if (error) { LOG_DEBUG("RPC BatchGetDocuments completed. Error: %s", error); if (error.code == FIRFirestoreErrorCodeUnauthenticated) { self->_credentials->InvalidateToken(); } [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; completion(nil, error); return; } if (!done) { // Streaming response, accumulate result FSTMaybeDocument *doc = [self.serializer decodedMaybeDocumentFromBatch:response]; closure->results.insert({doc.key, doc}); } else { // Streaming response is done, call completion LOG_DEBUG("RPC BatchGetDocuments completed successfully."); [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"]; HARD_ASSERT(!response, "Got response after done."); NSMutableArray *docs = [NSMutableArray arrayWithCapacity:closure->results.size()]; for (auto &&entry : closure->results) { FSTMaybeDocument *doc = entry.second; [docs addObject:doc]; } completion(docs, nil); } }]; }]; return rpc; }; [self invokeRPCWithFactory:rpcFactory errorHandler:^(NSError *_Nonnull error) { error = [FSTDatastore firestoreErrorForError:error]; completion(nil, error); }]; } - (void)invokeRPCWithFactory:(GRPCProtoCall * (^)(void))rpcFactory errorHandler:(FSTVoidErrorBlock)errorHandler { _credentials->GetToken([self, rpcFactory, errorHandler](util::StatusOr result) { [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{ if (!result.ok()) { errorHandler(util::MakeNSError(result.status())); } else { GRPCProtoCall *rpc = rpcFactory(); const Token &token = result.ValueOrDie(); [FSTDatastore prepareHeadersForRPC:rpc databaseID:&self.databaseInfo->database_id() token:(token.user().is_authenticated() ? token.token() : absl::string_view())]; [rpc start]; } }]; }); } - (FSTWatchStream *)createWatchStream { return [[FSTWatchStream alloc] initWithDatabase:_databaseInfo workerDispatchQueue:_workerDispatchQueue credentials:_credentials serializer:_serializer]; } - (FSTWriteStream *)createWriteStream { return [[FSTWriteStream alloc] initWithDatabase:_databaseInfo workerDispatchQueue:_workerDispatchQueue credentials:_credentials serializer:_serializer]; } /** Adds headers to the RPC including any OAuth access token if provided .*/ + (void)prepareHeadersForRPC:(GRPCCall *)rpc databaseID:(const DatabaseId *)databaseID token:(const absl::string_view)token { rpc.oauth2AccessToken = token.data() == nullptr ? nil : util::WrapNSString(token); rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue]; // This header is used to improve routing and project isolation by the backend. rpc.requestHeaders[kGoogleCloudResourcePrefix] = [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID]; } @end NS_ASSUME_NONNULL_END