FSTDatastore.m 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  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. #import "FIRFirestoreErrors.h"
  20. #import "Firestore/Source/API/FIRFirestore+Internal.h"
  21. #import "Firestore/Source/API/FIRFirestoreVersion.h"
  22. #import "Firestore/Source/Auth/FSTCredentialsProvider.h"
  23. #import "Firestore/Source/Core/FSTDatabaseInfo.h"
  24. #import "Firestore/Source/Local/FSTLocalStore.h"
  25. #import "Firestore/Source/Model/FSTDatabaseID.h"
  26. #import "Firestore/Source/Model/FSTDocument.h"
  27. #import "Firestore/Source/Model/FSTDocumentKey.h"
  28. #import "Firestore/Source/Model/FSTMutation.h"
  29. #import "Firestore/Source/Remote/FSTSerializerBeta.h"
  30. #import "Firestore/Source/Remote/FSTStream.h"
  31. #import "Firestore/Source/Util/FSTAssert.h"
  32. #import "Firestore/Source/Util/FSTDispatchQueue.h"
  33. #import "Firestore/Source/Util/FSTLogger.h"
  34. #import "Firestore/Protos/objc/google/firestore/v1beta1/Firestore.pbrpc.h"
  35. NS_ASSUME_NONNULL_BEGIN
  36. // GRPC does not publicly declare a means of disabling SSL, which we need for testing. Firestore
  37. // directly exposes an sslEnabled setting so this is required to plumb that through. Note that our
  38. // own tests depend on this working so we'll know if this changes upstream.
  39. @interface GRPCHost
  40. + (nullable instancetype)hostWithAddress:(NSString *)address;
  41. @property(nonatomic, getter=isSecure) BOOL secure;
  42. @end
  43. static NSString *const kXGoogAPIClientHeader = @"x-goog-api-client";
  44. static NSString *const kGoogleCloudResourcePrefix = @"google-cloud-resource-prefix";
  45. /** Function typedef used to create RPCs. */
  46. typedef GRPCProtoCall * (^RPCFactory)(void);
  47. #pragma mark - FSTDatastore
  48. @interface FSTDatastore ()
  49. /** The GRPC service for Firestore. */
  50. @property(nonatomic, strong, readonly) GCFSFirestore *service;
  51. @property(nonatomic, strong, readonly) FSTDispatchQueue *workerDispatchQueue;
  52. /** An object for getting an auth token before each request. */
  53. @property(nonatomic, strong, readonly) id<FSTCredentialsProvider> credentials;
  54. @property(nonatomic, strong, readonly) FSTSerializerBeta *serializer;
  55. @end
  56. @implementation FSTDatastore
  57. + (instancetype)datastoreWithDatabase:(FSTDatabaseInfo *)databaseInfo
  58. workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
  59. credentials:(id<FSTCredentialsProvider>)credentials {
  60. return [[FSTDatastore alloc] initWithDatabaseInfo:databaseInfo
  61. workerDispatchQueue:workerDispatchQueue
  62. credentials:credentials];
  63. }
  64. - (instancetype)initWithDatabaseInfo:(FSTDatabaseInfo *)databaseInfo
  65. workerDispatchQueue:(FSTDispatchQueue *)workerDispatchQueue
  66. credentials:(id<FSTCredentialsProvider>)credentials {
  67. if (self = [super init]) {
  68. _databaseInfo = databaseInfo;
  69. if (!databaseInfo.isSSLEnabled) {
  70. GRPCHost *hostConfig = [GRPCHost hostWithAddress:databaseInfo.host];
  71. hostConfig.secure = NO;
  72. }
  73. _service = [GCFSFirestore serviceWithHost:databaseInfo.host];
  74. _workerDispatchQueue = workerDispatchQueue;
  75. _credentials = credentials;
  76. _serializer = [[FSTSerializerBeta alloc] initWithDatabaseID:databaseInfo.databaseID];
  77. }
  78. return self;
  79. }
  80. - (NSString *)description {
  81. return [NSString stringWithFormat:@"<FSTDatastore: %@>", self.databaseInfo];
  82. }
  83. /**
  84. * Converts the error to an error within the domain FIRFirestoreErrorDomain.
  85. */
  86. + (NSError *)firestoreErrorForError:(NSError *)error {
  87. if (!error) {
  88. return error;
  89. } else if ([error.domain isEqualToString:FIRFirestoreErrorDomain]) {
  90. return error;
  91. } else if ([error.domain isEqualToString:kGRPCErrorDomain]) {
  92. FSTAssert(error.code >= GRPCErrorCodeCancelled && error.code <= GRPCErrorCodeUnauthenticated,
  93. @"Unknown GRPC error code: %ld", (long)error.code);
  94. return
  95. [NSError errorWithDomain:FIRFirestoreErrorDomain code:error.code userInfo:error.userInfo];
  96. } else {
  97. return [NSError errorWithDomain:FIRFirestoreErrorDomain
  98. code:FIRFirestoreErrorCodeUnknown
  99. userInfo:@{NSUnderlyingErrorKey : error}];
  100. }
  101. }
  102. + (BOOL)isAbortedError:(NSError *)error {
  103. FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain],
  104. @"isAbortedError: only works with errors emitted by FSTDatastore.");
  105. return error.code == FIRFirestoreErrorCodeAborted;
  106. }
  107. + (BOOL)isPermanentWriteError:(NSError *)error {
  108. FSTAssert([error.domain isEqualToString:FIRFirestoreErrorDomain],
  109. @"isPerminanteWriteError: only works with errors emitted by FSTDatastore.");
  110. switch (error.code) {
  111. case FIRFirestoreErrorCodeCancelled:
  112. case FIRFirestoreErrorCodeUnknown:
  113. case FIRFirestoreErrorCodeDeadlineExceeded:
  114. case FIRFirestoreErrorCodeResourceExhausted:
  115. case FIRFirestoreErrorCodeInternal:
  116. case FIRFirestoreErrorCodeUnavailable:
  117. case FIRFirestoreErrorCodeUnauthenticated:
  118. // Unauthenticated means something went wrong with our token and we need
  119. // to retry with new credentials which will happen automatically.
  120. // TODO(b/37325376): Give up after second unauthenticated error.
  121. return NO;
  122. case FIRFirestoreErrorCodeInvalidArgument:
  123. case FIRFirestoreErrorCodeNotFound:
  124. case FIRFirestoreErrorCodeAlreadyExists:
  125. case FIRFirestoreErrorCodePermissionDenied:
  126. case FIRFirestoreErrorCodeFailedPrecondition:
  127. case FIRFirestoreErrorCodeAborted:
  128. // Aborted might be retried in some scenarios, but that is dependant on
  129. // the context and should handled individually by the calling code.
  130. // See https://cloud.google.com/apis/design/errors
  131. case FIRFirestoreErrorCodeOutOfRange:
  132. case FIRFirestoreErrorCodeUnimplemented:
  133. case FIRFirestoreErrorCodeDataLoss:
  134. default:
  135. return YES;
  136. }
  137. }
  138. /** Returns the string to be used as x-goog-api-client header value. */
  139. + (NSString *)googAPIClientHeaderValue {
  140. // TODO(dimond): This should ideally also include the grpc version, however, gRPC defines the
  141. // version as a macro, so it would be hardcoded based on version we have at compile time of
  142. // the Firestore library, rather than the version available at runtime/at compile time by the
  143. // user of the library.
  144. return [NSString stringWithFormat:@"gl-objc/ fire/%s grpc/", FirebaseFirestoreVersionString];
  145. }
  146. /** Returns the string to be used as google-cloud-resource-prefix header value. */
  147. + (NSString *)googleCloudResourcePrefixForDatabaseID:(FSTDatabaseID *)databaseID {
  148. return [NSString
  149. stringWithFormat:@"projects/%@/databases/%@", databaseID.projectID, databaseID.databaseID];
  150. }
  151. /**
  152. * Takes a dictionary of (HTTP) response headers and returns the set of whitelisted headers
  153. * (for logging purposes).
  154. */
  155. + (NSDictionary<NSString *, NSString *> *)extractWhiteListedHeaders:
  156. (NSDictionary<NSString *, NSString *> *)headers {
  157. NSMutableDictionary<NSString *, NSString *> *whiteListedHeaders =
  158. [NSMutableDictionary dictionary];
  159. NSArray<NSString *> *whiteList = @[
  160. @"date", @"x-google-backends", @"x-google-netmon-label", @"x-google-service",
  161. @"x-google-gfe-request-trace"
  162. ];
  163. [headers
  164. enumerateKeysAndObjectsUsingBlock:^(NSString *headerName, NSString *headerValue, BOOL *stop) {
  165. if ([whiteList containsObject:[headerName lowercaseString]]) {
  166. whiteListedHeaders[headerName] = headerValue;
  167. }
  168. }];
  169. return whiteListedHeaders;
  170. }
  171. /** Logs the (whitelisted) headers returned for an GRPCProtoCall RPC. */
  172. + (void)logHeadersForRPC:(GRPCProtoCall *)rpc RPCName:(NSString *)rpcName {
  173. if ([FIRFirestore isLoggingEnabled]) {
  174. FSTLog(@"RPC %@ returned headers (whitelisted): %@", rpcName,
  175. [FSTDatastore extractWhiteListedHeaders:rpc.responseHeaders]);
  176. }
  177. }
  178. - (void)commitMutations:(NSArray<FSTMutation *> *)mutations
  179. completion:(FSTVoidErrorBlock)completion {
  180. GCFSCommitRequest *request = [GCFSCommitRequest message];
  181. request.database = [self.serializer encodedDatabaseID];
  182. NSMutableArray<GCFSWrite *> *mutationProtos = [NSMutableArray array];
  183. for (FSTMutation *mutation in mutations) {
  184. [mutationProtos addObject:[self.serializer encodedMutation:mutation]];
  185. }
  186. request.writesArray = mutationProtos;
  187. RPCFactory rpcFactory = ^GRPCProtoCall * {
  188. __block GRPCProtoCall *rpc = [self.service
  189. RPCToCommitWithRequest:request
  190. handler:^(GCFSCommitResponse *response, NSError *_Nullable error) {
  191. error = [FSTDatastore firestoreErrorForError:error];
  192. [self.workerDispatchQueue dispatchAsync:^{
  193. FSTLog(@"RPC CommitRequest completed. Error: %@", error);
  194. [FSTDatastore logHeadersForRPC:rpc RPCName:@"CommitRequest"];
  195. completion(error);
  196. }];
  197. }];
  198. return rpc;
  199. };
  200. [self invokeRPCWithFactory:rpcFactory errorHandler:completion];
  201. }
  202. - (void)lookupDocuments:(NSArray<FSTDocumentKey *> *)keys
  203. completion:(FSTVoidMaybeDocumentArrayErrorBlock)completion {
  204. GCFSBatchGetDocumentsRequest *request = [GCFSBatchGetDocumentsRequest message];
  205. request.database = [self.serializer encodedDatabaseID];
  206. for (FSTDocumentKey *key in keys) {
  207. [request.documentsArray addObject:[self.serializer encodedDocumentKey:key]];
  208. }
  209. __block FSTMaybeDocumentDictionary *results =
  210. [FSTMaybeDocumentDictionary maybeDocumentDictionary];
  211. RPCFactory rpcFactory = ^GRPCProtoCall * {
  212. __block GRPCProtoCall *rpc = [self.service
  213. RPCToBatchGetDocumentsWithRequest:request
  214. eventHandler:^(BOOL done,
  215. GCFSBatchGetDocumentsResponse *_Nullable response,
  216. NSError *_Nullable error) {
  217. error = [FSTDatastore firestoreErrorForError:error];
  218. [self.workerDispatchQueue dispatchAsync:^{
  219. if (error) {
  220. FSTLog(@"RPC BatchGetDocuments completed. Error: %@", error);
  221. [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"];
  222. completion(nil, error);
  223. return;
  224. }
  225. if (!done) {
  226. // Streaming response, accumulate result
  227. FSTMaybeDocument *doc =
  228. [self.serializer decodedMaybeDocumentFromBatch:response];
  229. results = [results dictionaryBySettingObject:doc forKey:doc.key];
  230. } else {
  231. // Streaming response is done, call completion
  232. FSTLog(@"RPC BatchGetDocuments completed successfully.");
  233. [FSTDatastore logHeadersForRPC:rpc RPCName:@"BatchGetDocuments"];
  234. FSTAssert(!response, @"Got response after done.");
  235. NSMutableArray<FSTMaybeDocument *> *docs =
  236. [NSMutableArray arrayWithCapacity:keys.count];
  237. for (FSTDocumentKey *key in keys) {
  238. [docs addObject:results[key]];
  239. }
  240. completion(docs, nil);
  241. }
  242. }];
  243. }];
  244. return rpc;
  245. };
  246. [self invokeRPCWithFactory:rpcFactory
  247. errorHandler:^(NSError *_Nonnull error) {
  248. error = [FSTDatastore firestoreErrorForError:error];
  249. completion(nil, error);
  250. }];
  251. }
  252. - (void)invokeRPCWithFactory:(GRPCProtoCall * (^)(void))rpcFactory
  253. errorHandler:(FSTVoidErrorBlock)errorHandler {
  254. // TODO(mikelehen): We should force a refresh if the previous RPC failed due to an expired token,
  255. // but I'm not sure how to detect that right now. http://b/32762461
  256. [self.credentials
  257. getTokenForcingRefresh:NO
  258. completion:^(FSTGetTokenResult *_Nullable result, NSError *_Nullable error) {
  259. error = [FSTDatastore firestoreErrorForError:error];
  260. [self.workerDispatchQueue dispatchAsyncAllowingSameQueue:^{
  261. if (error) {
  262. errorHandler(error);
  263. } else {
  264. GRPCProtoCall *rpc = rpcFactory();
  265. [FSTDatastore prepareHeadersForRPC:rpc
  266. databaseID:self.databaseInfo.databaseID
  267. token:result.token];
  268. [rpc start];
  269. }
  270. }];
  271. }];
  272. }
  273. - (FSTWatchStream *)createWatchStream {
  274. return [[FSTWatchStream alloc] initWithDatabase:_databaseInfo
  275. workerDispatchQueue:_workerDispatchQueue
  276. credentials:_credentials
  277. serializer:_serializer];
  278. }
  279. - (FSTWriteStream *)createWriteStream {
  280. return [[FSTWriteStream alloc] initWithDatabase:_databaseInfo
  281. workerDispatchQueue:_workerDispatchQueue
  282. credentials:_credentials
  283. serializer:_serializer];
  284. }
  285. /** Adds headers to the RPC including any OAuth access token if provided .*/
  286. + (void)prepareHeadersForRPC:(GRPCCall *)rpc
  287. databaseID:(FSTDatabaseID *)databaseID
  288. token:(nullable NSString *)token {
  289. rpc.oauth2AccessToken = token;
  290. rpc.requestHeaders[kXGoogAPIClientHeader] = [FSTDatastore googAPIClientHeaderValue];
  291. // This header is used to improve routing and project isolation by the backend.
  292. rpc.requestHeaders[kGoogleCloudResourcePrefix] =
  293. [FSTDatastore googleCloudResourcePrefixForDatabaseID:databaseID];
  294. }
  295. @end
  296. NS_ASSUME_NONNULL_END