Files
firebase-ios-sdk/Firebase/Database/Realtime/FWebSocketConnection.m
T

346 lines
11 KiB
Objective-C
Raw Normal View History

2017-05-15 12:27:07 -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.
*/
2019-08-05 13:27:47 -07:00
// Targetted compilation is ONLY for testing. UIKit is weak-linked in actual
// release build.
2017-05-15 12:27:07 -07:00
#import <Foundation/Foundation.h>
2017-05-15 12:27:07 -07:00
#import "FConstants.h"
#import "FIRDatabaseReference.h"
#import "FIRDatabase_Private.h"
2019-08-05 13:27:47 -07:00
#import "FStringUtilities.h"
#import "FWebSocketConnection.h"
#import <FirebaseCore/FIRLogger.h>
2017-05-15 12:27:07 -07:00
2018-01-03 12:12:22 -08:00
#if TARGET_OS_IOS || TARGET_OS_TV
2017-05-15 12:27:07 -07:00
#import <UIKit/UIKit.h>
#endif
@interface FWebSocketConnection () {
2019-08-05 13:27:47 -07:00
NSMutableString *frame;
2017-05-15 12:27:07 -07:00
BOOL everConnected;
BOOL isClosed;
2019-08-05 13:27:47 -07:00
NSTimer *keepAlive;
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
- (void)shutdown;
- (void)onClosed;
- (void)closeIfNeverConnected;
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
@property(nonatomic, strong) FSRWebSocket *webSocket;
@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;
2017-05-15 12:27:07 -07:00
- (void)nop:(NSTimer *)timer;
@end
@implementation FWebSocketConnection
@synthesize delegate;
@synthesize webSocket;
@synthesize connectionId;
2019-08-05 13:27:47 -07:00
- (id)initWith:(FRepoInfo *)repoInfo
andQueue:(dispatch_queue_t)queue
lastSessionID:(NSString *)lastSessionID {
2017-05-15 12:27:07 -07:00
self = [super init];
if (self) {
everConnected = NO;
isClosed = NO;
self.connectionId = [FUtilities LUIDGenerator];
self.totalFrames = 0;
self.dispatchQueue = queue;
frame = nil;
2017-06-30 10:47:37 -07:00
2019-08-05 13:27:47 -07:00
NSString *connectionUrl =
[repoInfo connectionURLWithLastSessionID:lastSessionID];
NSString *ua = [self userAgent];
FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@",
self.connectionId, connectionUrl, ua);
2017-06-30 10:47:37 -07:00
2019-08-05 13:27:47 -07:00
NSURLRequest *req = [[NSURLRequest alloc]
initWithURL:[[NSURL alloc] initWithString:connectionUrl]];
self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req
queue:queue
andUserAgent:ua];
2017-05-15 12:27:07 -07:00
[self.webSocket setDelegateDispatchQueue:queue];
self.webSocket.delegate = self;
}
return self;
}
2019-08-05 13:27:47 -07:00
- (NSString *)userAgent {
NSString *systemVersion;
NSString *deviceName;
2017-05-15 12:27:07 -07:00
BOOL hasUiDeviceClass = NO;
2017-06-30 10:47:37 -07:00
2019-08-05 13:27:47 -07:00
// Targetted compilation is ONLY for testing. UIKit is weak-linked in actual
// release build.
#if TARGET_OS_IOS || TARGET_OS_TV
2017-05-15 12:27:07 -07:00
Class uiDeviceClass = NSClassFromString(@"UIDevice");
if (uiDeviceClass) {
systemVersion = [uiDeviceClass currentDevice].systemVersion;
deviceName = [uiDeviceClass currentDevice].model;
hasUiDeviceClass = YES;
}
2019-08-05 13:27:47 -07:00
#endif
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
if (!hasUiDeviceClass) {
2019-08-05 13:27:47 -07:00
NSDictionary *systemVersionDictionary = [NSDictionary
dictionaryWithContentsOfFile:
@"/System/Library/CoreServices/SystemVersion.plist"];
systemVersion =
[systemVersionDictionary objectForKey:@"ProductVersion"];
2017-05-15 12:27:07 -07:00
deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
}
2019-08-05 13:27:47 -07:00
NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
2017-05-15 12:27:07 -07:00
// Sanitize '/'s in deviceName and bundleIdentifier for stats
deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
2019-08-05 13:27:47 -07:00
bundleIdentifier =
[FStringUtilities sanitizedForUserAgent:bundleIdentifier];
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
// 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];
2017-05-15 12:27:07 -07:00
return ua;
}
2019-08-05 13:27:47 -07:00
- (BOOL)buffering {
2017-05-15 12:27:07 -07:00
return frame != nil;
}
#pragma mark -
#pragma mark Public FWebSocketConnection methods
2019-08-05 13:27:47 -07:00
- (void)open {
FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.",
self.connectionId);
2017-05-15 12:27:07 -07:00
assert(delegate);
everConnected = NO;
// TODO Assert url
[self.webSocket open];
2019-08-05 13:27:47 -07:00
dispatch_time_t when = dispatch_time(
DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
2017-05-15 12:27:07 -07:00
dispatch_after(when, self.dispatchQueue, ^{
2019-08-05 13:27:47 -07:00
[self closeIfNeverConnected];
2017-05-15 12:27:07 -07:00
});
}
2019-08-05 13:27:47 -07:00
- (void)close {
FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.",
self.connectionId);
2017-05-15 12:27:07 -07:00
isClosed = YES;
[self.webSocket close];
}
2019-08-05 13:27:47 -07:00
- (void)start {
2017-05-15 12:27:07 -07:00
// Start is a no-op for websockets.
}
2019-08-05 13:27:47 -07:00
- (void)send:(NSDictionary *)dictionary {
2017-05-15 12:27:07 -07:00
[self resetKeepAlive];
2019-08-05 13:27:47 -07:00
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
options:kNilOptions
error:nil];
2017-06-30 10:47:37 -07:00
2019-08-05 13:27:47 -07:00
NSString *data = [[NSString alloc] initWithData:jsonData
2017-05-15 12:27:07 -07:00
encoding:NSUTF8StringEncoding];
2017-06-30 10:47:37 -07:00
2019-08-05 13:27:47 -07:00
NSArray *dataSegs = [FUtilities splitString:data
intoMaxSize:kWebsocketMaxFrameSize];
2017-05-15 12:27:07 -07:00
2019-08-05 13:27:47 -07:00
// First send the header so the server knows how many segments are
// forthcoming
2017-05-15 12:27:07 -07:00
if (dataSegs.count > 1) {
2019-08-05 13:27:47 -07:00
[self.webSocket
send:[NSString
stringWithFormat:@"%u", (unsigned int)dataSegs.count]];
2017-05-15 12:27:07 -07:00
}
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
// Then, actually send the segments.
2019-08-05 13:27:47 -07:00
for (NSString *segment in dataSegs) {
2017-05-15 12:27:07 -07:00
[self.webSocket send:segment];
}
}
2019-08-05 13:27:47 -07:00
- (void)nop:(NSTimer *)timer {
if (!isClosed) {
2017-05-15 12:27:07 -07:00
FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
2017-06-30 10:47:37 -07:00
[self.webSocket send:@"0"];
2019-08-05 13:27:47 -07:00
} else {
FFLog(@"I-RDB083005",
@"(wsc:%@) No more websocket; invalidating nop timer.",
self.connectionId);
2017-05-15 12:27:07 -07:00
[timer invalidate];
}
}
2019-08-05 13:27:47 -07:00
- (void)handleNewFrameCount:(int)numFrames {
2017-05-15 12:27:07 -07:00
self.totalFrames = numFrames;
frame = [[NSMutableString alloc] initWithString:@""];
2019-08-05 13:27:47 -07:00
FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d",
self.connectionId, self.totalFrames);
2017-05-15 12:27:07 -07:00
}
2019-08-05 13:27:47 -07:00
- (NSString *)extractFrameCount:(NSString *)message {
2017-05-15 12:27:07 -07:00
if ([message length] <= 4) {
int frameCount = [message intValue];
if (frameCount > 0) {
[self handleNewFrameCount:frameCount];
return nil;
}
}
[self handleNewFrameCount:1];
return message;
}
2019-08-05 13:27:47 -07:00
- (void)appendFrame:(NSString *)message {
2017-05-15 12:27:07 -07:00
[frame appendString:message];
self.totalFrames = self.totalFrames - 1;
if (self.totalFrames == 0) {
// Call delegate and pass an immutable version of the frame
2019-08-05 13:27:47 -07:00
NSDictionary *json = [NSJSONSerialization
JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
options:kNilOptions
error:nil];
2017-05-15 12:27:07 -07:00
frame = nil;
2019-08-05 13:27:47 -07:00
FFLog(@"I-RDB083007",
@"(wsc:%@) handleIncomingFrame sending complete frame: %d",
self.connectionId, self.totalFrames);
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
@autoreleasepool {
[self.delegate onMessage:self withMessage:json];
}
}
}
2019-08-05 13:27:47 -07:00
- (void)handleIncomingFrame:(NSString *)message {
2017-05-15 12:27:07 -07:00
[self resetKeepAlive];
if (self.buffering) {
[self appendFrame:message];
} else {
NSString *remaining = [self extractFrameCount:message];
if (remaining) {
[self appendFrame:remaining];
}
}
}
#pragma mark -
#pragma mark SRWebSocketDelegate implementation
2019-08-05 13:27:47 -07:00
- (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message {
2017-05-15 12:27:07 -07:00
[self handleIncomingFrame:message];
}
2019-08-05 13:27:47 -07:00
- (void)webSocketDidOpen:(FSRWebSocket *)webSocket {
2017-05-15 12:27:07 -07:00
FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
everConnected = YES;
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
dispatch_async(dispatch_get_main_queue(), ^{
2019-08-05 13:27:47 -07:00
self->keepAlive =
[NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
target:self
selector:@selector(nop:)
userInfo:nil
repeats:YES];
FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off",
self.connectionId);
2017-05-15 12:27:07 -07:00
});
}
2019-08-05 13:27:47 -07:00
- (void)webSocket:(FSRWebSocket *)webSocket didFailWithError:(NSError *)error {
FFLog(@"I-RDB083010", @"(wsc:%@) didFailWithError didFailWithError: %@",
self.connectionId, [error description]);
2017-05-15 12:27:07 -07:00
[self onClosed];
}
2019-08-05 13:27:47 -07:00
- (void)webSocket:(FSRWebSocket *)webSocket
didCloseWithCode:(NSInteger)code
reason:(NSString *)reason
wasClean:(BOOL)wasClean {
FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@",
self.connectionId, (long)code, reason);
2017-05-15 12:27:07 -07:00
[self onClosed];
}
#pragma mark -
#pragma mark Private methods
/**
2019-08-05 13:27:47 -07:00
* 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.
2017-05-15 12:27:07 -07:00
*/
2019-08-05 13:27:47 -07:00
- (void)closeIfNeverConnected {
2017-05-15 12:27:07 -07:00
if (!everConnected) {
2019-08-05 13:27:47 -07:00
FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect",
self.connectionId);
2017-05-15 12:27:07 -07:00
[self.webSocket close];
}
}
2019-08-05 13:27:47 -07:00
- (void)shutdown {
2017-05-15 12:27:07 -07:00
isClosed = YES;
2017-06-30 10:47:37 -07:00
2017-05-15 12:27:07 -07:00
// Call delegate methods
[self.delegate onDisconnect:self wasEverConnected:everConnected];
}
2019-08-05 13:27:47 -07:00
- (void)onClosed {
2017-05-15 12:27:07 -07:00
if (!isClosed) {
FFLog(@"I-RDB083013", @"Websocket is closing itself");
[self shutdown];
}
self.webSocket = nil;
if (keepAlive.isValid) {
[keepAlive invalidate];
}
}
2019-08-05 13:27:47 -07:00
- (void)resetKeepAlive {
NSDate *newTime =
[NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
// Calling setFireDate is actually kinda' expensive, so wait at least 5
// seconds before updating it.
2017-05-15 12:27:07 -07:00
if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
2019-08-05 13:27:47 -07:00
FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@",
self.connectionId, newTime, [keepAlive fireDate]);
2017-05-15 12:27:07 -07:00
[keepAlive setFireDate:newTime];
}
}
@end