mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
517 lines
18 KiB
Objective-C
517 lines
18 KiB
Objective-C
/*
|
|
* 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.
|
|
*/
|
|
|
|
// Targeted compilation is ONLY for testing. UIKit is weak-linked in actual
|
|
// release build.
|
|
|
|
#import <Foundation/Foundation.h>
|
|
|
|
#import "FirebaseCore/Extension/FirebaseCoreInternal.h"
|
|
#import "FirebaseDatabase/Sources/Api/Private/FIRDatabase_Private.h"
|
|
#import "FirebaseDatabase/Sources/Constants/FConstants.h"
|
|
#import "FirebaseDatabase/Sources/Public/FirebaseDatabase/FIRDatabaseReference.h"
|
|
#import "FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h"
|
|
#import "FirebaseDatabase/Sources/Utilities/FStringUtilities.h"
|
|
|
|
#if TARGET_OS_IOS || TARGET_OS_TV || \
|
|
(defined(TARGET_OS_VISION) && TARGET_OS_VISION)
|
|
#import <UIKit/UIKit.h>
|
|
#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) &&
|
|
// TARGET_OS_VISION)
|
|
|
|
#if TARGET_OS_WATCH
|
|
#import <Network/Network.h>
|
|
#import <WatchKit/WatchKit.h>
|
|
#endif // TARGET_OS_WATCH
|
|
|
|
static NSString *const kAppCheckTokenHeader = @"X-Firebase-AppCheck";
|
|
static NSString *const kUserAgentHeader = @"User-Agent";
|
|
static NSString *const kGoogleAppIDHeader = @"X-Firebase-GMPID";
|
|
|
|
@interface FWebSocketConnection () {
|
|
NSMutableString *frame;
|
|
BOOL everConnected;
|
|
BOOL isClosed;
|
|
NSTimer *keepAlive;
|
|
}
|
|
|
|
- (void)shutdown;
|
|
- (void)onClosed;
|
|
- (void)closeIfNeverConnected;
|
|
|
|
#if TARGET_OS_WATCH
|
|
@property(nonatomic, strong) NSURLSessionWebSocketTask *webSocketTask;
|
|
#else
|
|
@property(nonatomic, strong) FSRWebSocket *webSocket;
|
|
#endif // TARGET_OS_WATCH
|
|
@property(nonatomic, strong) NSNumber *connectionId;
|
|
@property(nonatomic, readwrite) int totalFrames;
|
|
@property(nonatomic, readonly) BOOL buffering;
|
|
@property(nonatomic, readonly) NSString *userAgent;
|
|
@property(nonatomic) dispatch_queue_t dispatchQueue;
|
|
|
|
- (void)nop:(NSTimer *)timer;
|
|
|
|
@end
|
|
|
|
@implementation FWebSocketConnection
|
|
|
|
@synthesize delegate;
|
|
#if !TARGET_OS_WATCH
|
|
@synthesize webSocket;
|
|
#endif // !TARGET_OS_WATCH
|
|
@synthesize connectionId;
|
|
|
|
- (instancetype)initWith:(FRepoInfo *)repoInfo
|
|
andQueue:(dispatch_queue_t)queue
|
|
googleAppID:(NSString *)googleAppID
|
|
lastSessionID:(NSString *)lastSessionID
|
|
appCheckToken:(nullable NSString *)appCheckToken {
|
|
self = [super init];
|
|
if (self) {
|
|
everConnected = NO;
|
|
isClosed = NO;
|
|
self.connectionId = [FUtilities LUIDGenerator];
|
|
self.totalFrames = 0;
|
|
self.dispatchQueue = queue;
|
|
frame = nil;
|
|
|
|
NSString *userAgent = [self userAgent];
|
|
NSString *connectionURL =
|
|
[repoInfo connectionURLWithLastSessionID:lastSessionID];
|
|
|
|
FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@",
|
|
self.connectionId, connectionURL, userAgent);
|
|
|
|
NSURLRequest *req = [[self class] createRequestWithURL:connectionURL
|
|
userAgent:userAgent
|
|
googleAppID:googleAppID
|
|
appCheckToken:appCheckToken];
|
|
#if TARGET_OS_WATCH
|
|
// Regular NSURLSession websocket.
|
|
NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];
|
|
opQueue.underlyingQueue = queue;
|
|
NSURLSession *session = [NSURLSession
|
|
sessionWithConfiguration:[NSURLSessionConfiguration
|
|
defaultSessionConfiguration]
|
|
delegate:self
|
|
delegateQueue:opQueue];
|
|
NSURLSessionWebSocketTask *task =
|
|
[session webSocketTaskWithRequest:req];
|
|
self.webSocketTask = task;
|
|
|
|
[[NSNotificationCenter defaultCenter]
|
|
addObserverForName:WKApplicationWillResignActiveNotification
|
|
object:nil
|
|
queue:opQueue
|
|
usingBlock:^(NSNotification *_Nonnull note) {
|
|
FFLog(@"I-RDB083015",
|
|
@"Received watchOS background notification, "
|
|
@"closing web socket.");
|
|
[self onClosed];
|
|
}];
|
|
#else
|
|
// TODO(mmaksym): Remove googleAppID and userAgent from FSRWebSocket as
|
|
// they are passed via NSURLRequest.
|
|
self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req
|
|
queue:queue
|
|
googleAppID:googleAppID
|
|
andUserAgent:userAgent];
|
|
[self.webSocket setDelegateDispatchQueue:queue];
|
|
self.webSocket.delegate = self;
|
|
#endif // TARGET_OS_WATCH
|
|
}
|
|
return self;
|
|
}
|
|
|
|
- (NSString *)userAgent {
|
|
NSString *systemVersion;
|
|
NSString *deviceName;
|
|
BOOL hasUiDeviceClass = NO;
|
|
|
|
// Targeted compilation is ONLY for testing. UIKit is weak-linked in actual
|
|
// release build.
|
|
#if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
|
|
Class uiDeviceClass = NSClassFromString(@"UIDevice");
|
|
if (uiDeviceClass) {
|
|
systemVersion = [uiDeviceClass currentDevice].systemVersion;
|
|
deviceName = [uiDeviceClass currentDevice].model;
|
|
hasUiDeviceClass = YES;
|
|
}
|
|
#endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) &&
|
|
// TARGET_OS_VISION)
|
|
|
|
if (!hasUiDeviceClass) {
|
|
NSDictionary *systemVersionDictionary = [NSDictionary
|
|
dictionaryWithContentsOfFile:
|
|
@"/System/Library/CoreServices/SystemVersion.plist"];
|
|
systemVersion =
|
|
[systemVersionDictionary objectForKey:@"ProductVersion"];
|
|
deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
|
|
}
|
|
|
|
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
|
|
|
|
// Sanitize '/'s in deviceName and bundleIdentifier for stats
|
|
deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
|
|
bundleIdentifier =
|
|
[FStringUtilities sanitizedForUserAgent:bundleIdentifier];
|
|
|
|
// Firebase/5/<semver>_<build date>_<git hash>/<os version>/{device model /
|
|
// os (Mac OS X, iPhone, etc.}_<bundle id>
|
|
NSString *ua = [NSString
|
|
stringWithFormat:@"Firebase/%@/%@/%@/%@_%@", kWebsocketProtocolVersion,
|
|
[FIRDatabase buildVersion], systemVersion, deviceName,
|
|
bundleIdentifier];
|
|
return ua;
|
|
}
|
|
|
|
- (BOOL)buffering {
|
|
return frame != nil;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Public FWebSocketConnection methods
|
|
|
|
- (void)open {
|
|
FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.",
|
|
self.connectionId);
|
|
assert(delegate);
|
|
everConnected = NO;
|
|
// TODO Assert url
|
|
#if TARGET_OS_WATCH
|
|
[self.webSocketTask resume];
|
|
// We need to request data from the web socket in order for it to start
|
|
// sending data.
|
|
[self receiveWebSocketData];
|
|
#else
|
|
[self.webSocket open];
|
|
#endif // TARGET_OS_WATCH
|
|
dispatch_time_t when = dispatch_time(
|
|
DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
|
|
dispatch_after(when, self.dispatchQueue, ^{
|
|
[self closeIfNeverConnected];
|
|
});
|
|
}
|
|
|
|
- (void)close {
|
|
FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.",
|
|
self.connectionId);
|
|
isClosed = YES;
|
|
#if TARGET_OS_WATCH
|
|
[self.webSocketTask
|
|
cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure
|
|
reason:nil];
|
|
#else
|
|
[self.webSocket close];
|
|
#endif // TARGET_OS_WATCH
|
|
}
|
|
|
|
- (void)start {
|
|
// Start is a no-op for websockets.
|
|
}
|
|
|
|
- (void)send:(NSDictionary *)dictionary {
|
|
|
|
[self resetKeepAlive];
|
|
|
|
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
|
|
options:kNilOptions
|
|
error:nil];
|
|
|
|
NSString *data = [[NSString alloc] initWithData:jsonData
|
|
encoding:NSUTF8StringEncoding];
|
|
|
|
NSArray *dataSegs = [FUtilities splitString:data
|
|
intoMaxSize:kWebsocketMaxFrameSize];
|
|
|
|
// First send the header so the server knows how many segments are
|
|
// forthcoming
|
|
if (dataSegs.count > 1) {
|
|
NSString *formattedData =
|
|
[NSString stringWithFormat:@"%u", (unsigned int)dataSegs.count];
|
|
[self sendStringToWebSocket:formattedData];
|
|
}
|
|
|
|
// Then, actually send the segments.
|
|
for (NSString *segment in dataSegs) {
|
|
[self sendStringToWebSocket:segment];
|
|
}
|
|
}
|
|
|
|
- (void)nop:(NSTimer *)timer {
|
|
if (!isClosed) {
|
|
FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
|
|
// Note: the backend is expecting a string "0" here, not any special
|
|
// ping/pong from build in websocket APIs.
|
|
[self sendStringToWebSocket:@"0"];
|
|
} else {
|
|
FFLog(@"I-RDB083005",
|
|
@"(wsc:%@) No more websocket; invalidating nop timer.",
|
|
self.connectionId);
|
|
[timer invalidate];
|
|
}
|
|
}
|
|
|
|
- (void)handleNewFrameCount:(int)numFrames {
|
|
self.totalFrames = numFrames;
|
|
frame = [[NSMutableString alloc] initWithString:@""];
|
|
FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d",
|
|
self.connectionId, self.totalFrames);
|
|
}
|
|
|
|
- (NSString *)extractFrameCount:(NSString *)message {
|
|
if ([message length] <= 4) {
|
|
int frameCount = [message intValue];
|
|
if (frameCount > 0) {
|
|
[self handleNewFrameCount:frameCount];
|
|
return nil;
|
|
}
|
|
}
|
|
[self handleNewFrameCount:1];
|
|
return message;
|
|
}
|
|
|
|
- (void)appendFrame:(NSString *)message {
|
|
[frame appendString:message];
|
|
self.totalFrames = self.totalFrames - 1;
|
|
|
|
if (self.totalFrames == 0) {
|
|
// Call delegate and pass an immutable version of the frame
|
|
NSDictionary *json = [NSJSONSerialization
|
|
JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
|
|
options:kNilOptions
|
|
error:nil];
|
|
frame = nil;
|
|
FFLog(@"I-RDB083007",
|
|
@"(wsc:%@) handleIncomingFrame sending complete frame: %d",
|
|
self.connectionId, self.totalFrames);
|
|
|
|
@autoreleasepool {
|
|
[self.delegate onMessage:self withMessage:json];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void)handleIncomingFrame:(NSString *)message {
|
|
// The realtime protocol only uses text frames. SocketRocket delivers an
|
|
// NSData for a binary frame (and the watchOS task delivers a nil string),
|
|
// neither of which responds to the NSString selectors used below, so a
|
|
// server sending such a frame would crash the client. Ignore anything that
|
|
// is not a text frame.
|
|
if (![message isKindOfClass:[NSString class]]) {
|
|
FFWarn(@"I-RDB083021",
|
|
@"(wsc:%@) Ignoring non-text frame received on stream.",
|
|
self.connectionId);
|
|
return;
|
|
}
|
|
[self resetKeepAlive];
|
|
if (self.buffering) {
|
|
[self appendFrame:message];
|
|
} else {
|
|
NSString *remaining = [self extractFrameCount:message];
|
|
if (remaining) {
|
|
[self appendFrame:remaining];
|
|
}
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark URLSessionWebSocketDelegate watchOS implementation
|
|
#if TARGET_OS_WATCH
|
|
|
|
- (void)URLSession:(NSURLSession *)session
|
|
webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask
|
|
didOpenWithProtocol:(NSString *)protocol {
|
|
[self webSocketDidOpen];
|
|
}
|
|
|
|
- (void)URLSession:(NSURLSession *)session
|
|
webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask
|
|
didCloseWithCode:(NSURLSessionWebSocketCloseCode)closeCode
|
|
reason:(NSData *)reason {
|
|
FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@",
|
|
self.connectionId, (long)closeCode, reason);
|
|
[self onClosed];
|
|
}
|
|
|
|
- (void)receiveWebSocketData {
|
|
__weak __auto_type weakSelf = self;
|
|
[self.webSocketTask receiveMessageWithCompletionHandler:^(
|
|
NSURLSessionWebSocketMessage *_Nullable message,
|
|
NSError *_Nullable error) {
|
|
__auto_type strongSelf = weakSelf;
|
|
if (strongSelf == nil) {
|
|
return;
|
|
}
|
|
|
|
if (message) {
|
|
[strongSelf handleIncomingFrame:message.string];
|
|
} else if (error && !strongSelf->isClosed) {
|
|
FFWarn(@"I-RDB083020",
|
|
@"Error received from web socket, closing the connection. %@",
|
|
error);
|
|
[strongSelf shutdown];
|
|
return;
|
|
}
|
|
|
|
[strongSelf receiveWebSocketData];
|
|
}];
|
|
}
|
|
|
|
#else
|
|
|
|
#pragma mark SRWebSocketDelegate implementation
|
|
|
|
- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message {
|
|
[self handleIncomingFrame:message];
|
|
}
|
|
|
|
- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error {
|
|
FFLog(@"I-RDB083010", @"(wsc:%@) didFailWithError didFailWithError: %@",
|
|
self.connectionId, [error description]);
|
|
[self onClosed];
|
|
}
|
|
|
|
- (void)webSocket:(FSRWebSocket *)webSocket
|
|
didCloseWithCode:(NSInteger)code
|
|
reason:(NSString *)reason
|
|
wasClean:(BOOL)wasClean {
|
|
FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@",
|
|
self.connectionId, (long)code, reason);
|
|
[self onClosed];
|
|
}
|
|
|
|
#endif // TARGET_OS_WATCH
|
|
|
|
// Common to both SRWebSocketDelegate and URLSessionWebSocketDelegate.
|
|
|
|
- (void)webSocketDidOpen {
|
|
FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
|
|
|
|
everConnected = YES;
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self->keepAlive =
|
|
[NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
|
|
target:self
|
|
selector:@selector(nop:)
|
|
userInfo:nil
|
|
repeats:YES];
|
|
FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off",
|
|
self.connectionId);
|
|
});
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Private methods
|
|
|
|
/** Sends a string through the open web socket. */
|
|
- (void)sendStringToWebSocket:(NSString *)string {
|
|
#if TARGET_OS_WATCH
|
|
// Use built-in URLSessionWebSocket functionality.
|
|
[self.webSocketTask sendMessage:[[NSURLSessionWebSocketMessage alloc]
|
|
initWithString:string]
|
|
completionHandler:^(NSError *_Nullable error) {
|
|
if (error) {
|
|
FFWarn(@"I-RDB083016",
|
|
@"Error sending web socket data: %@.", error);
|
|
return;
|
|
}
|
|
}];
|
|
#else
|
|
// Use existing SocketRocket implementation.
|
|
[self.webSocket send:string];
|
|
#endif // TARGET_OS_WATCH
|
|
}
|
|
|
|
/**
|
|
* Note that the close / onClosed / shutdown cycle here is a little different
|
|
* from the javascript client. In order to properly handle deallocation, no
|
|
* close-related action is taken at a higher level until we have received
|
|
* notification from the websocket itself that it is closed. Otherwise, we end
|
|
* up deallocating this class and the FConnection class before the websocket has
|
|
* a change to call some of its delegate methods. So, since close is the
|
|
* external close handler, we just set a flag saying not to call our own
|
|
* delegate method and close the websocket. That will trigger a callback into
|
|
* this class that can then do things like clean up the keepalive timer.
|
|
*/
|
|
|
|
- (void)closeIfNeverConnected {
|
|
if (!everConnected) {
|
|
FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect",
|
|
self.connectionId);
|
|
#if TARGET_OS_WATCH
|
|
[self.webSocketTask
|
|
cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNoStatusReceived
|
|
reason:nil];
|
|
#else
|
|
[self.webSocket close];
|
|
#endif // TARGET_OS_WATCH
|
|
}
|
|
}
|
|
|
|
- (void)shutdown {
|
|
isClosed = YES;
|
|
|
|
// Call delegate methods
|
|
[self.delegate onDisconnect:self wasEverConnected:everConnected];
|
|
}
|
|
|
|
- (void)onClosed {
|
|
if (!isClosed) {
|
|
FFLog(@"I-RDB083013", @"Websocket is closing itself");
|
|
[self shutdown];
|
|
}
|
|
#if TARGET_OS_WATCH
|
|
self.webSocketTask = nil;
|
|
#else
|
|
self.webSocket = nil;
|
|
#endif // TARGET_OS_WATCH
|
|
if (keepAlive.isValid) {
|
|
[keepAlive invalidate];
|
|
}
|
|
}
|
|
|
|
- (void)resetKeepAlive {
|
|
NSDate *newTime =
|
|
[NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
|
|
// Calling setFireDate is actually kinda' expensive, so wait at least 5
|
|
// seconds before updating it.
|
|
if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
|
|
FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@",
|
|
self.connectionId, newTime, [keepAlive fireDate]);
|
|
[keepAlive setFireDate:newTime];
|
|
}
|
|
}
|
|
|
|
+ (NSURLRequest *)createRequestWithURL:(NSString *)connectionURL
|
|
userAgent:(NSString *)userAgent
|
|
googleAppID:(NSString *)googleAppID
|
|
appCheckToken:(nullable NSString *)appCheckToken {
|
|
|
|
NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
|
|
initWithURL:[[NSURL alloc] initWithString:connectionURL]];
|
|
|
|
[request setValue:appCheckToken forHTTPHeaderField:kAppCheckTokenHeader];
|
|
[request setValue:userAgent forHTTPHeaderField:kUserAgentHeader];
|
|
[request setValue:googleAppID forHTTPHeaderField:kGoogleAppIDHeader];
|
|
|
|
return [request copy];
|
|
}
|
|
|
|
@end
|