| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- /*
- * 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.
- */
- // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual
- // release build.
- #import <Foundation/Foundation.h>
- #import "FConstants.h"
- #import "FIRDatabaseReference.h"
- #import "FIRDatabase_Private.h"
- #import "FStringUtilities.h"
- #import "FWebSocketConnection.h"
- #import "FirebaseCore/Sources/Private/FirebaseCoreInternal.h"
- #if TARGET_OS_IOS || TARGET_OS_TV
- #import <UIKit/UIKit.h>
- #endif
- @interface FWebSocketConnection () {
- NSMutableString *frame;
- BOOL everConnected;
- BOOL isClosed;
- NSTimer *keepAlive;
- }
- - (void)shutdown;
- - (void)onClosed;
- - (void)closeIfNeverConnected;
- @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;
- - (void)nop:(NSTimer *)timer;
- @end
- @implementation FWebSocketConnection
- @synthesize delegate;
- @synthesize webSocket;
- @synthesize connectionId;
- - (id)initWith:(FRepoInfo *)repoInfo
- andQueue:(dispatch_queue_t)queue
- googleAppID:(NSString *)googleAppID
- lastSessionID:(NSString *)lastSessionID {
- self = [super init];
- if (self) {
- everConnected = NO;
- isClosed = NO;
- self.connectionId = [FUtilities LUIDGenerator];
- self.totalFrames = 0;
- self.dispatchQueue = queue;
- frame = nil;
- NSString *connectionUrl =
- [repoInfo connectionURLWithLastSessionID:lastSessionID];
- NSString *ua = [self userAgent];
- FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@",
- self.connectionId, connectionUrl, ua);
- NSURLRequest *req = [[NSURLRequest alloc]
- initWithURL:[[NSURL alloc] initWithString:connectionUrl]];
- self.webSocket = [[FSRWebSocket alloc] initWithURLRequest:req
- queue:queue
- googleAppID:googleAppID
- andUserAgent:ua];
- [self.webSocket setDelegateDispatchQueue:queue];
- self.webSocket.delegate = self;
- }
- return self;
- }
- - (NSString *)userAgent {
- NSString *systemVersion;
- NSString *deviceName;
- BOOL hasUiDeviceClass = NO;
- // Targetted compilation is ONLY for testing. UIKit is weak-linked in actual
- // release build.
- #if TARGET_OS_IOS || TARGET_OS_TV
- Class uiDeviceClass = NSClassFromString(@"UIDevice");
- if (uiDeviceClass) {
- systemVersion = [uiDeviceClass currentDevice].systemVersion;
- deviceName = [uiDeviceClass currentDevice].model;
- hasUiDeviceClass = YES;
- }
- #endif
- 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
- [self.webSocket open];
- 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;
- [self.webSocket close];
- }
- - (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) {
- [self.webSocket
- send:[NSString
- stringWithFormat:@"%u", (unsigned int)dataSegs.count]];
- }
- // Then, actually send the segments.
- for (NSString *segment in dataSegs) {
- [self.webSocket send:segment];
- }
- }
- - (void)nop:(NSTimer *)timer {
- if (!isClosed) {
- FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
- [self.webSocket send:@"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 {
- [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
- - (void)webSocket:(FSRWebSocket *)webSocket didReceiveMessage:(id)message {
- [self handleIncomingFrame:message];
- }
- - (void)webSocketDidOpen:(FSRWebSocket *)webSocket {
- 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);
- });
- }
- - (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];
- }
- #pragma mark -
- #pragma mark Private methods
- /**
- * 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);
- [self.webSocket close];
- }
- }
- - (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];
- }
- self.webSocket = nil;
- 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];
- }
- }
- @end
|