FSTDatastore.mm 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. /*
  2. * Copyright 2017 Google
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #import "Firestore/Source/Remote/FSTDatastore.h"
  17. #import <GRPCClient/GRPCCall+OAuth2.h>
  18. #import <ProtoRPC/ProtoRPC.h>
  19. #include <map>
  20. #include <memory>
  21. #include <vector>
  22. #import "FIRFirestoreErrors.h"
  23. #import "Firestore/Source/API/FIRFirestore+Internal.h"
  24. #import "Firestore/Source/API/FIRFirestoreVersion.h"
  25. #import "Firestore/Source/Local/FSTLocalStore.h"
  26. #import "Firestore/Source/Model/FSTDocument.h"
  27. #import "Firestore/Source/Model/FSTMutation.h"
  28. #import "Firestore/Source/Remote/FSTSerializerBeta.h"
  29. #import "Firestore/Source/Remote/FSTStream.h"
  30. #import "Firestore/Source/Util/FSTDispatchQueue.h"
  31. #import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h"
  32. #include "Firestore/core/src/firebase/firestore/auth/credentials_provider.h"
  33. #include "Firestore/core/src/firebase/firestore/auth/token.h"
  34. #include "Firestore/core/src/firebase/firestore/core/database_info.h"
  35. #include "Firestore/core/src/firebase/firestore/model/database_id.h"
  36. #include "Firestore/core/src/firebase/firestore/model/document_key.h"
  37. #include "Firestore/core/src/firebase/firestore/util/error_apple.h"
  38. #include "Firestore/core/src/firebase/firestore/util/hard_assert.h"
  39. #include "Firestore/core/src/firebase/firestore/util/log.h"
  40. #include "Firestore/core/src/firebase/firestore/util/string_apple.h"
  41. namespace util = firebase::firestore::util;
  42. using firebase::firestore::auth::CredentialsProvider;
  43. using firebase::firestore::auth::Token;
  44. using firebase::firestore::core::DatabaseInfo;
  45. using firebase::firestore::model::DatabaseId;
  46. using firebase::firestore::model::DocumentKey;
  47. NS_ASSUME_NONNULL_BEGIN
  48. // GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore
  49. // directly exposes an sslEnabled setting so this is required to plumb that through. Note that our
  50. // own tests depend on this working so we'll know if this changes upstream.
  51. @interface GRPCHost
  52. + (nullable instancetype)hostWithAddress:(NSString *)address;
  53. @property(nonatomic, getter=isSecure) BOOL secure;
  54. @end
  55. static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client";
  56. static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix";
  57. /** Function typedef used to create RPCs. */
  58. typedef GRPCProtoCall * (^RPCFactory)(void);
  59. #pragma mark - FSTDatastore
  60. @interface FSTDatastore ()
  61. /** The GRPC service for Firestore. */
  62. @property(nonatomic, strong, readonly) GCFSFirestore *service;
  63. @property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
  64. /**
  65. * An object for getting an auth token before each request. Does not own the CredentialsProvider
  66. * instance.
  67. */
  68. @property(nonatomic, assign, readonly) CredentialsProvider *credentials;
  69. @property(nonatomic, strong, readonly) FSTSerializerBeta *serializer;
  70. @end
  71. @implementation FSTDatastore
  72. + (instancetype)datastoreWithDatabase:(const DatabaseInfo *)databaseInfo
  73. workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
  74. credentials:(CredentialsProvider *)credentials {
  75. return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo
  76. workerDispatchQueue:workerDispatchQueue
  77. credentials:credentials];
  78. }
  79. - (instancetype)initWithDatabaseInfo:(const DatabaseInfo *)databaseInfo
  80. workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
  81. credentials:(CredentialsProvider *)credentials {
  82. if (self = [super init]) {
  83. _databaseInfo = databaseInfo;
  84. NSString *host = util::WrapNSString(databaseInfo->host());
  85. if (!databaseInfo->ssl_enabled()) {
  86. GRPCHost *hostConfig = [GRPCHost hostWithAddress:host];
  87. hostConfig.secure = NO;
  88. }
  89. _service = [GCFSFirestore serviceWithHost:host];
  90. _workerDispatchQueue = workerDispatchQueue;
  91. _credentials = credentials;
  92. _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:&databaseInfo->database_id()];
  93. }
  94. return self;
  95. }
  96. - (NSString *)description {
  97. return [NSString stringWithFormat:@"<FSTDatastore: <DatabaseInfo: database_id:%s host:%s>>",
  98. self.databaseInfo->database_id().database_id().c_str(),
  99. self.databaseInfo->host().c_str()];
  100. }
  101. /**
  102. * Converts the error to an error within the domain FIRFirestoreErrorDomain.
  103. */
  104. + (NSError *)firestoreErrorForError:(NSError *)error {
  105. if (!error) {
  106. return error;
  107. } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) {
  108. return error;
  109. } else if ([error.domain isEqualToString:kGRPCErrorDomain]) {
  110. HARD_ASSERT(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated,
  111. "Unknown GRPC error code: %s", error.code);
  112. return
  113. [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo];
  114. } else {
  115. return [NSError errorWithDomain:FIRFirestoreErrorDomain
  116. code:FIRFirestoreErrorCodeUnknown
  117. userInfo:@{NSUnderlyingErrorKey : error}];
  118. }
  119. }
  120. + (BOOL)isAbortedError:(NSError *)error {
  121. HARD_ASSERT([error.domain isEqualToString:FIRFirestoreErrorDomain],
  122. "isAbortedError: only works with errors emitted by FSTDatastore.");
  123. return error.code == FIRFirestoreErrorCodeAborted;
  124. }
  125. + (BOOL)isPermanentWriteError:(NSError *)error {
  126. HARD_ASSERT([error.domain isEqualToString:FIRFirestoreErrorDomain],
  127. "isPerminanteWriteError: only works with errors emitted by FSTDatastore.");
  128. switch (error.code) {
  129. case FIRFirestoreErrorCodeCancelled:
  130. case FIRFirestoreErrorCodeUnknown:
  131. case FIRFirestoreErrorCodeDeadlineExceeded:
  132. case FIRFirestoreErrorCodeResourceExhausted:
  133. case FIRFirestoreErrorCodeInternal:
  134. case FIRFirestoreErrorCodeUnavailable:
  135. case FIRFirestoreErrorCodeUnauthenticated:
  136. // Unauthenticated means something went wrong with our token and we need
  137. // to retry with new credentials which will happen automatically.
  138. // TODO(b/37325376): Give up after second unauthenticated error.
  139. return NO;
  140. case FIRFirestoreErrorCodeInvalidArgument:
  141. case FIRFirestoreErrorCodeNotFound:
  142. case FIRFirestoreErrorCodeAlreadyExists:
  143. case FIRFirestoreErrorCodePermissionDenied:
  144. case FIRFirestoreErrorCodeFailedPrecondition:
  145. case FIRFirestoreErrorCodeAborted:
  146. // Aborted might be retried in some scenarios, but that is dependant on
  147. // the context and should handled individually by the calling code.
  148. // See https://cloud.google.com/apis/design/errors
  149. case FIRFirestoreErrorCodeOutOfRange:
  150. case FIRFirestoreErrorCodeUnimplemented:
  151. case FIRFirestoreErrorCodeDataLoss:
  152. default:
  153. return YES;
  154. }
  155. }
  156. /** Returns the string to be used as x-goog-api-client header value. */
  157. + (NSString *)googAPIClientHeaderValue {
  158. // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the
  159. // version as a macro, so it would be hardcoded based on version we have at compile time of
  160. // the Firestore library, rather than the version available at runtime/at compile time by the
  161. // user of the library.
  162. return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FIRFirestoreVersionString];
  163. }
  164. /** Returns the string to be used as google-cloud-resource-prefix header value. */
  165. + (NSString *)googleCloudResourcePrefixForDatabaseID:(const DatabaseId *)databaseID {
  166. return [NSString stringWithFormat:@"projects/%s/databases/%s", databaseID->project_id().c_str(),
  167. databaseID->database_id().c_str()];
  168. }
  169. /**
  170. * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers
  171. * (for logging purposes).
  172. */
  173. + (NSDictionary<NSString *, NSString *> *)extractWhiteListedHeaders:
  174. (NSDictionary<NSString *, NSString *> *)headers {
  175. NSMutableDictionary<NSString *, NSString *> *whiteListedHeaders =
  176. [NSMutableDictionary dictionary];
  177. NSArray<NSString *> *whiteList = @[
  178. @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service",
  179. @"x-google-gfe-request-trace"
  180. ];
  181. [headers
  182. enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) {
  183. if ([whiteList containsObject:[headerName lowercaseString]]) {
  184. whiteListedHeaders[headerName] = headerValue;
  185. }
  186. }];
  187. return whiteListedHeaders;
  188. }
  189. /** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */
  190. + (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName {
  191. if ([FIRFirestore isLoggingEnabled]) {
  192. LOG_DEBUG("RPC %s returned headers (whitelisted): %s", rpcName,
  193. [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]);
  194. }
  195. }
  196. - (void)commitMutations:(NSArray<FSTMutation *> *)mutations
  197. completion:(FSTVoidErrorBlock)completion {
  198. GCFSCommitRequest *request = [GCFSCommitRequest message];
  199. request.database = [self.serializer encodedDatabaseID];
  200. NSMutableArray<GCFSWrite *> *mutationProtos = [NSMutableArray array];
  201. for (FSTMutation *mutation in mutations) {
  202. [mutationProtos addObject:[self.serializer encodedMutation:mutation]];
  203. }
  204. request.writesArray = mutationProtos;
  205. RPCFactory rpcFactory = ^GRPCProtoCall * {
  206. __block GRPCProtoCall *rpc = [self.service
  207. RPCToCommitWithRequest:request
  208. handler:^(GCFSCommitResponse *response, NSError *_Nullable error) {
  209. error = [FSTDatastore firestoreErrorForError:error];
  210. [self.workerDispatchQueue dispatchAsync:^{
  211. LOG_DEBUG("RPC CommitRequest completed. Error: %s", error);
  212. [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"];
  213. completion(error);
  214. }];
  215. }];
  216. return rpc;
  217. };
  218. [self invokeRPCWithFactory:rpcFactory errorHandler:completion];
  219. }
  220. - (void)lookupDocuments:(const std::vector<DocumentKey> &)keys
  221. completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion {
  222. GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message];
  223. request.database = [self.serializer encodedDatabaseID];
  224. for (const DocumentKey &key : keys) {
  225. [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]];
  226. }
  227. struct Closure {
  228. std::map<DocumentKey, FSTMaybeDocument *> results;
  229. };
  230. __block std::shared_ptr<Closure> closure = std::make_shared<Closure>(Closure{});
  231. RPCFactory rpcFactory = ^GRPCProtoCall * {
  232. __block GRPCProtoCall *rpc = [self.service
  233. RPCToBatchGetDocumentsWithRequest:request
  234. eventHandler:^(BOOL done,
  235. GCFSBatchGetDocumentsResponse *_Nullable response,
  236. NSError *_Nullable error) {
  237. error = [FSTDatastore firestoreErrorForError:error];
  238. [self.workerDispatchQueue dispatchAsync:^{
  239. if (error) {
  240. LOG_DEBUG("RPC BatchGetDocuments completed. Error: %s", error);
  241. [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"];
  242. completion(nil, error);
  243. return;
  244. }
  245. if (!done) {
  246. // Streaming response, accumulate result
  247. FSTMaybeDocument *doc =
  248. [self.serializer decodedMaybeDocumentFromBatch:response];
  249. closure->results.insert({doc.key, doc});
  250. } else {
  251. // Streaming response is done, call completion
  252. LOG_DEBUG("RPC BatchGetDocuments completed successfully.");
  253. [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"];
  254. HARD_ASSERT(!response, "Got response after done.");
  255. NSMutableArray<FSTMaybeDocument *> *docs =
  256. [NSMutableArray arrayWithCapacity:closure->results.size()];
  257. for (auto &&entry : closure->results) {
  258. FSTMaybeDocument *doc = entry.second;
  259. [docs addObject:doc];
  260. }
  261. completion(docs, nil);
  262. }
  263. }];
  264. }];
  265. return rpc;
  266. };
  267. [self invokeRPCWithFactory:rpcFactory
  268. errorHandler:^(NSError *_Nonnull error) {
  269. error = [FSTDatastore firestoreErrorForError:error];
  270. completion(nil, error);
  271. }];
  272. }
  273. - (void)invokeRPCWithFactory:(GRPCProtoCall * (^)(void))rpcFactory
  274. errorHandler:(FSTVoidErrorBlock)errorHandler {
  275. // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token,
  276. // but I'm not sure how to detect that right now. http://b/32762461
  277. _credentials->GetToken(
  278. /*force_refresh=*/false, [self, rpcFactory, errorHandler](util::StatusOr<Token> result) {
  279. [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{
  280. if (!result.ok()) {
  281. errorHandler(util::MakeNSError(result.status()));
  282. } else {
  283. GRPCProtoCall *rpc = rpcFactory();
  284. const Token &token = result.ValueOrDie();
  285. [FSTDatastore
  286. prepareHeadersForRPC:rpc
  287. databaseID:&self.databaseInfo->database_id()
  288. token:(token.user().is_authenticated() ? token.token()
  289. : absl::string_view())];
  290. [rpc start];
  291. }
  292. }];
  293. });
  294. }
  295. - (FSTWatchStream *)createWatchStream {
  296. return [[FSTWatchStream alloc] initWithDatabase:_databaseInfo
  297. workerDispatchQueue:_workerDispatchQueue
  298. credentials:_credentials
  299. serializer:_serializer];
  300. }
  301. - (FSTWriteStream *)createWriteStream {
  302. return [[FSTWriteStream alloc] initWithDatabase:_databaseInfo
  303. workerDispatchQueue:_workerDispatchQueue
  304. credentials:_credentials
  305. serializer:_serializer];
  306. }
  307. /** Adds headers to the RPC including any OAuth access token if provided .*/
  308. + (void)prepareHeadersForRPC:(GRPCCall *)rpc
  309. databaseID:(const DatabaseId *)databaseID
  310. token:(const absl::string_view)token {
  311. rpc.oauth2AccessToken = token.data() == nullptr ? nil : util::WrapNSString(token);
  312. rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue];
  313. // This header is used to improve routing and project isolation by the backend.
  314. rpc.requestHeaders[kGoogleCloudResourcePrefix] =
  315. [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID];
  316. }
  317. @end
  318. NS_ASSUME_NONNULL_END