2018-03-20 11:59:03 -07:00
|
|
|
// 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.
|
|
|
|
|
|
2020-08-03 16:28:14 -07:00
|
|
|
#import "Functions/FirebaseFunctions/Public/FirebaseFunctions/FIRFunctions.h"
|
2020-06-19 12:14:22 -07:00
|
|
|
#import "Functions/FirebaseFunctions/FIRFunctions+Internal.h"
|
2018-03-20 11:59:03 -07:00
|
|
|
|
2020-06-09 05:21:28 -07:00
|
|
|
#import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
|
2020-09-03 14:45:56 -04:00
|
|
|
#import "FirebaseMessaging/Sources/Interop/FIRMessagingInterop.h"
|
2020-06-09 06:09:27 -07:00
|
|
|
#import "Interop/Auth/Public/FIRAuthInterop.h"
|
2018-12-06 13:30:08 -05:00
|
|
|
|
2020-06-19 12:14:22 -07:00
|
|
|
#import "Functions/FirebaseFunctions/FIRHTTPSCallable+Internal.h"
|
|
|
|
|
#import "Functions/FirebaseFunctions/FUNContext.h"
|
|
|
|
|
#import "Functions/FirebaseFunctions/FUNError.h"
|
|
|
|
|
#import "Functions/FirebaseFunctions/FUNSerializer.h"
|
|
|
|
|
#import "Functions/FirebaseFunctions/FUNUsageValidation.h"
|
2020-08-03 16:28:14 -07:00
|
|
|
#import "Functions/FirebaseFunctions/Public/FirebaseFunctions/FIRError.h"
|
|
|
|
|
#import "Functions/FirebaseFunctions/Public/FirebaseFunctions/FIRHTTPSCallable.h"
|
2018-03-20 11:59:03 -07:00
|
|
|
|
2020-06-19 12:14:22 -07:00
|
|
|
#if SWIFT_PACKAGE
|
|
|
|
|
@import GTMSessionFetcherCore;
|
|
|
|
|
#else
|
2019-04-08 22:38:51 -04:00
|
|
|
#import <GTMSessionFetcher/GTMSessionFetcherService.h>
|
2020-06-19 12:14:22 -07:00
|
|
|
#endif
|
2018-03-20 11:59:03 -07:00
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
|
|
2020-09-03 14:45:56 -04:00
|
|
|
NSString *const kFUNFCMTokenHeader = @"Firebase-Instance-ID-Token";
|
2018-12-06 13:30:08 -05:00
|
|
|
NSString *const kFUNDefaultRegion = @"us-central1";
|
2018-03-20 11:59:03 -07:00
|
|
|
|
2018-12-06 13:30:08 -05:00
|
|
|
/// Empty protocol to register Functions as a component with Core.
|
|
|
|
|
@protocol FIRFunctionsInstanceProvider
|
|
|
|
|
@end
|
|
|
|
|
|
2018-12-13 08:36:08 -08:00
|
|
|
@interface FIRFunctions () <FIRLibrary, FIRFunctionsInstanceProvider> {
|
2018-03-20 11:59:03 -07:00
|
|
|
// The network client to use for http requests.
|
|
|
|
|
GTMSessionFetcherService *_fetcherService;
|
|
|
|
|
// The projectID to use for all function references.
|
2018-12-06 13:30:08 -05:00
|
|
|
NSString *_projectID;
|
2018-03-20 11:59:03 -07:00
|
|
|
// The region to use for all function references.
|
|
|
|
|
NSString *_region;
|
2020-10-21 09:50:30 -04:00
|
|
|
// The custom domain to use for all functions references (optional).
|
|
|
|
|
NSString *_customDomain;
|
2018-03-20 11:59:03 -07:00
|
|
|
// A serializer to encode/decode data and return values.
|
|
|
|
|
FUNSerializer *_serializer;
|
|
|
|
|
// A factory for getting the metadata to include with function calls.
|
|
|
|
|
FUNContextProvider *_contextProvider;
|
2018-06-21 15:25:13 -04:00
|
|
|
// For testing only. If this is set, functions will be called against it instead of Firebase.
|
|
|
|
|
NSString *_emulatorOrigin;
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
2018-12-06 13:30:08 -05:00
|
|
|
// Re-declare this initializer here in order to attribute it as the designated initializer.
|
|
|
|
|
- (instancetype)initWithProjectID:(NSString *)projectID
|
|
|
|
|
region:(NSString *)region
|
2020-10-21 09:50:30 -04:00
|
|
|
customDomain:(nullable NSString *)customDomain
|
2020-09-03 14:45:56 -04:00
|
|
|
auth:(nullable id<FIRAuthInterop>)auth
|
|
|
|
|
messaging:(nullable id<FIRMessagingInterop>)messaging
|
|
|
|
|
NS_DESIGNATED_INITIALIZER;
|
2018-03-20 11:59:03 -07:00
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
@implementation FIRFunctions
|
|
|
|
|
|
2018-12-06 13:30:08 -05:00
|
|
|
+ (void)load {
|
2020-10-02 12:26:22 -07:00
|
|
|
[FIRApp registerInternalLibrary:(Class<FIRLibrary>)self withName:@"fire-fun"];
|
2018-12-06 13:30:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (NSArray<FIRComponent *> *)componentsToRegister {
|
|
|
|
|
FIRComponentCreationBlock creationBlock =
|
|
|
|
|
^id _Nullable(FIRComponentContainer *container, BOOL *isCacheable) {
|
|
|
|
|
*isCacheable = YES;
|
|
|
|
|
return [self functionsForApp:container.app];
|
|
|
|
|
};
|
2019-01-02 09:27:31 -08:00
|
|
|
FIRDependency *auth = [FIRDependency dependencyWithProtocol:@protocol(FIRAuthInterop)
|
|
|
|
|
isRequired:NO];
|
2018-12-06 13:30:08 -05:00
|
|
|
FIRComponent *internalProvider =
|
|
|
|
|
[FIRComponent componentWithProtocol:@protocol(FIRFunctionsInstanceProvider)
|
|
|
|
|
instantiationTiming:FIRInstantiationTimingLazy
|
|
|
|
|
dependencies:@[ auth ]
|
|
|
|
|
creationBlock:creationBlock];
|
|
|
|
|
return @[ internalProvider ];
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:59:03 -07:00
|
|
|
+ (instancetype)functions {
|
2020-10-21 09:50:30 -04:00
|
|
|
return [[self alloc] initWithApp:[FIRApp defaultApp] region:kFUNDefaultRegion customDomain:nil];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (instancetype)functionsForApp:(FIRApp *)app {
|
2020-10-21 09:50:30 -04:00
|
|
|
return [[self alloc] initWithApp:app region:kFUNDefaultRegion customDomain:nil];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (instancetype)functionsForRegion:(NSString *)region {
|
2020-10-21 09:50:30 -04:00
|
|
|
return [[self alloc] initWithApp:[FIRApp defaultApp] region:region customDomain:nil];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (instancetype)functionsForCustomDomain:(NSString *)customDomain {
|
|
|
|
|
return [[self alloc] initWithApp:[FIRApp defaultApp]
|
|
|
|
|
region:kFUNDefaultRegion
|
|
|
|
|
customDomain:customDomain];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
+ (instancetype)functionsForApp:(FIRApp *)app region:(NSString *)region {
|
2020-10-21 09:50:30 -04:00
|
|
|
return [[self alloc] initWithApp:app region:region customDomain:nil];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
2020-10-21 09:50:30 -04:00
|
|
|
+ (instancetype)functionsForApp:(FIRApp *)app customDomain:(NSString *)customDomain {
|
|
|
|
|
return [[self alloc] initWithApp:app region:kFUNDefaultRegion customDomain:customDomain];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithApp:(FIRApp *)app
|
|
|
|
|
region:(NSString *)region
|
|
|
|
|
customDomain:(nullable NSString *)customDomain {
|
2018-12-06 13:30:08 -05:00
|
|
|
return [self initWithProjectID:app.options.projectID
|
|
|
|
|
region:region
|
2020-10-21 09:50:30 -04:00
|
|
|
customDomain:customDomain
|
2020-09-03 14:45:56 -04:00
|
|
|
auth:FIR_COMPONENT(FIRAuthInterop, app.container)
|
|
|
|
|
messaging:FIR_COMPONENT(FIRMessagingInterop, app.container)];
|
2018-12-06 13:30:08 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (instancetype)initWithProjectID:(NSString *)projectID
|
|
|
|
|
region:(NSString *)region
|
2020-10-21 09:50:30 -04:00
|
|
|
customDomain:(nullable NSString *)customDomain
|
2020-09-03 14:45:56 -04:00
|
|
|
auth:(nullable id<FIRAuthInterop>)auth
|
|
|
|
|
messaging:(nullable id<FIRMessagingInterop>)messaging {
|
2018-03-20 11:59:03 -07:00
|
|
|
self = [super init];
|
|
|
|
|
if (self) {
|
|
|
|
|
if (!region) {
|
|
|
|
|
FUNThrowInvalidArgument(@"FIRFunctions region cannot be nil.");
|
|
|
|
|
}
|
|
|
|
|
_fetcherService = [[GTMSessionFetcherService alloc] init];
|
2018-12-06 13:30:08 -05:00
|
|
|
_projectID = [projectID copy];
|
2018-03-20 11:59:03 -07:00
|
|
|
_region = [region copy];
|
2020-10-21 09:50:30 -04:00
|
|
|
_customDomain = [customDomain copy];
|
2018-03-20 11:59:03 -07:00
|
|
|
_serializer = [[FUNSerializer alloc] init];
|
2020-09-03 14:45:56 -04:00
|
|
|
_contextProvider = [[FUNContextProvider alloc] initWithAuth:auth messaging:messaging];
|
2018-06-21 15:25:13 -04:00
|
|
|
_emulatorOrigin = nil;
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
return self;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)useLocalhost {
|
2020-11-06 10:37:06 -08:00
|
|
|
[self useEmulatorWithHost:@"http://localhost" port:5005];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)useEmulatorWithHost:(NSString *)host port:(NSInteger)port {
|
|
|
|
|
NSAssert(host.length > 0, @"Cannot connect to nil or empty host");
|
|
|
|
|
NSString *origin = [NSString stringWithFormat:@"%@:%li", host, (long)port];
|
|
|
|
|
_emulatorOrigin = origin;
|
2018-06-21 15:25:13 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)useFunctionsEmulatorOrigin:(NSString *)origin {
|
|
|
|
|
_emulatorOrigin = origin;
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (NSString *)URLWithName:(NSString *)name {
|
|
|
|
|
if (!name) {
|
|
|
|
|
FUNThrowInvalidArgument(@"FIRFunctions function name cannot be nil.");
|
|
|
|
|
}
|
2018-12-06 13:30:08 -05:00
|
|
|
if (!_projectID) {
|
2018-03-20 11:59:03 -07:00
|
|
|
FUNThrowInvalidArgument(@"FIRFunctions app projectID cannot be nil.");
|
|
|
|
|
}
|
2018-06-21 15:25:13 -04:00
|
|
|
if (_emulatorOrigin) {
|
2018-12-06 13:30:08 -05:00
|
|
|
return [NSString stringWithFormat:@"%@/%@/%@/%@", _emulatorOrigin, _projectID, _region, name];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
2020-10-21 09:50:30 -04:00
|
|
|
if (_customDomain) {
|
|
|
|
|
return [NSString stringWithFormat:@"%@/%@", _customDomain, name];
|
|
|
|
|
}
|
2018-03-20 11:59:03 -07:00
|
|
|
return
|
2018-12-06 13:30:08 -05:00
|
|
|
[NSString stringWithFormat:@"https://%@-%@.cloudfunctions.net/%@", _region, _projectID, name];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)callFunction:(NSString *)name
|
|
|
|
|
withObject:(nullable id)data
|
2019-02-25 10:10:20 -08:00
|
|
|
timeout:(NSTimeInterval)timeout
|
2018-03-20 11:59:03 -07:00
|
|
|
completion:(void (^)(FIRHTTPSCallableResult *_Nullable result,
|
|
|
|
|
NSError *_Nullable error))completion {
|
|
|
|
|
[_contextProvider getContext:^(FUNContext *_Nullable context, NSError *_Nullable error) {
|
|
|
|
|
if (error) {
|
|
|
|
|
if (completion) {
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
2019-02-25 10:10:20 -08:00
|
|
|
return [self callFunction:name
|
|
|
|
|
withObject:data
|
|
|
|
|
timeout:timeout
|
|
|
|
|
context:context
|
|
|
|
|
completion:completion];
|
2018-03-20 11:59:03 -07:00
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (void)callFunction:(NSString *)name
|
|
|
|
|
withObject:(nullable id)data
|
2019-02-25 10:10:20 -08:00
|
|
|
timeout:(NSTimeInterval)timeout
|
2018-03-20 11:59:03 -07:00
|
|
|
context:(FUNContext *)context
|
|
|
|
|
completion:(void (^)(FIRHTTPSCallableResult *_Nullable result,
|
|
|
|
|
NSError *_Nullable error))completion {
|
2019-02-25 10:10:20 -08:00
|
|
|
NSURL *url = [NSURL URLWithString:[self URLWithName:name]];
|
|
|
|
|
NSURLRequest *request = [NSURLRequest requestWithURL:url
|
|
|
|
|
cachePolicy:NSURLRequestUseProtocolCachePolicy
|
|
|
|
|
timeoutInterval:timeout];
|
|
|
|
|
GTMSessionFetcher *fetcher = [_fetcherService fetcherWithRequest:request];
|
2018-03-20 11:59:03 -07:00
|
|
|
|
|
|
|
|
NSMutableDictionary *body = [NSMutableDictionary dictionary];
|
|
|
|
|
// Encode the data in the body.
|
|
|
|
|
if (!data) {
|
|
|
|
|
data = [NSNull null];
|
|
|
|
|
}
|
|
|
|
|
id encoded = [_serializer encode:data];
|
|
|
|
|
if (!encoded) {
|
|
|
|
|
FUNThrowInvalidArgument(@"FIRFunctions data encoded as nil. This should not happen.");
|
|
|
|
|
}
|
|
|
|
|
body[@"data"] = encoded;
|
|
|
|
|
|
|
|
|
|
NSError *error = nil;
|
|
|
|
|
NSData *payload = [NSJSONSerialization dataWithJSONObject:body options:0 error:&error];
|
|
|
|
|
if (error) {
|
|
|
|
|
if (completion) {
|
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
fetcher.bodyData = payload;
|
|
|
|
|
|
|
|
|
|
// Set the headers.
|
|
|
|
|
[fetcher setRequestValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
|
|
|
|
|
if (context.authToken) {
|
|
|
|
|
NSString *value = [NSString stringWithFormat:@"Bearer %@", context.authToken];
|
|
|
|
|
[fetcher setRequestValue:value forHTTPHeaderField:@"Authorization"];
|
|
|
|
|
}
|
2020-09-03 14:45:56 -04:00
|
|
|
if (context.FCMToken) {
|
|
|
|
|
[fetcher setRequestValue:context.FCMToken forHTTPHeaderField:kFUNFCMTokenHeader];
|
2018-03-20 11:59:03 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Override normal security rules if this is a local test.
|
2018-06-21 15:25:13 -04:00
|
|
|
if (_emulatorOrigin) {
|
2018-03-20 11:59:03 -07:00
|
|
|
fetcher.allowLocalhostRequest = YES;
|
|
|
|
|
fetcher.allowedInsecureSchemes = @[ @"http" ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FUNSerializer *serializer = _serializer;
|
|
|
|
|
[fetcher beginFetchWithCompletionHandler:^(NSData *_Nullable data, NSError *_Nullable error) {
|
|
|
|
|
// If there was an HTTP error, convert it to our own error domain.
|
|
|
|
|
if (error) {
|
|
|
|
|
if ([error.domain isEqualToString:kGTMSessionFetcherStatusDomain]) {
|
|
|
|
|
error = FUNErrorForResponse(error.code, data, serializer);
|
|
|
|
|
}
|
2019-02-25 10:10:20 -08:00
|
|
|
if ([error.domain isEqualToString:NSURLErrorDomain]) {
|
|
|
|
|
if (error.code == NSURLErrorTimedOut) {
|
|
|
|
|
error = FUNErrorForCode(FIRFunctionsErrorCodeDeadlineExceeded);
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-20 11:59:03 -07:00
|
|
|
} else {
|
|
|
|
|
// If there wasn't an HTTP error, see if there was an error in the body.
|
|
|
|
|
error = FUNErrorForResponse(200, data, serializer);
|
|
|
|
|
}
|
|
|
|
|
// If there was an error, report it to the user and stop.
|
|
|
|
|
if (error) {
|
|
|
|
|
if (completion) {
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
id responseJSON = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
|
|
|
|
|
if (error) {
|
|
|
|
|
if (completion) {
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (![responseJSON isKindOfClass:[NSDictionary class]]) {
|
|
|
|
|
NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"Response was not a dictionary."};
|
|
|
|
|
error = [NSError errorWithDomain:FIRFunctionsErrorDomain
|
|
|
|
|
code:FIRFunctionsErrorCodeInternal
|
|
|
|
|
userInfo:userInfo];
|
|
|
|
|
if (completion) {
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
id dataJSON = responseJSON[@"data"];
|
|
|
|
|
// TODO(klimt): Allow "result" instead of "data" for now, for backwards compatibility.
|
|
|
|
|
if (!dataJSON) {
|
|
|
|
|
dataJSON = responseJSON[@"result"];
|
|
|
|
|
}
|
|
|
|
|
if (!dataJSON) {
|
2019-04-17 17:01:09 -07:00
|
|
|
NSDictionary *userInfo = @{NSLocalizedDescriptionKey : @"Response is missing data field."};
|
2018-03-20 11:59:03 -07:00
|
|
|
error = [NSError errorWithDomain:FIRFunctionsErrorDomain
|
|
|
|
|
code:FIRFunctionsErrorCodeInternal
|
|
|
|
|
userInfo:userInfo];
|
|
|
|
|
if (completion) {
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
id resultData = [serializer decode:dataJSON error:&error];
|
|
|
|
|
if (error) {
|
|
|
|
|
if (completion) {
|
|
|
|
|
completion(nil, error);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
id result = [[FIRHTTPSCallableResult alloc] initWithData:resultData];
|
|
|
|
|
if (completion) {
|
|
|
|
|
// If there's no result field, this will return nil, which is fine.
|
|
|
|
|
completion(result, nil);
|
|
|
|
|
}
|
|
|
|
|
}];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
- (FIRHTTPSCallable *)HTTPSCallableWithName:(NSString *)name {
|
|
|
|
|
return [[FIRHTTPSCallable alloc] initWithFunctions:self name:name];
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-06 10:37:06 -08:00
|
|
|
- (nullable NSString *)emulatorOrigin {
|
|
|
|
|
return _emulatorOrigin;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-20 11:59:03 -07:00
|
|
|
@end
|
|
|
|
|
|
|
|
|
|
NS_ASSUME_NONNULL_END
|