FWebSocketConnection.m 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477
  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. // Targeted compilation is ONLY for testing. UIKit is weak-linked in actual
  17. // release build.
  18. #import <Foundation/Foundation.h>
  19. #import "FirebaseCore/Extension/FirebaseCoreInternal.h"
  20. #import "FirebaseDatabase/Sources/Api/Private/FIRDatabase_Private.h"
  21. #import "FirebaseDatabase/Sources/Constants/FConstants.h"
  22. #import "FirebaseDatabase/Sources/Public/FirebaseDatabase/FIRDatabaseReference.h"
  23. #import "FirebaseDatabase/Sources/Realtime/FWebSocketConnection.h"
  24. #import "FirebaseDatabase/Sources/Utilities/FStringUtilities.h"
  25. #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
  26. #import <UIKit/UIKit.h>
  27. #elif TARGET_OS_WATCH
  28. #import <WatchKit/WatchKit.h>
  29. #elif TARGET_OS_OSX
  30. #import <AppKit/NSApplication.h>
  31. #endif
  32. #import <Network/Network.h>
  33. static NSString *const kAppCheckTokenHeader = @"X-Firebase-AppCheck";
  34. static NSString *const kUserAgentHeader = @"User-Agent";
  35. static NSString *const kGoogleAppIDHeader = @"X-Firebase-GMPID";
  36. @interface FWebSocketConnection () {
  37. NSMutableString *frame;
  38. BOOL everConnected;
  39. BOOL isClosed;
  40. NSTimer *keepAlive;
  41. }
  42. - (void)shutdown;
  43. - (void)onClosed;
  44. - (void)closeIfNeverConnected;
  45. @property(nonatomic, strong)
  46. NSURLSessionWebSocketTask *webSocketTask API_AVAILABLE(
  47. macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));
  48. @property(nonatomic, strong) NSNumber *connectionId;
  49. @property(nonatomic, readwrite) int totalFrames;
  50. @property(nonatomic, readonly) BOOL buffering;
  51. @property(nonatomic, readonly) NSString *userAgent;
  52. @property(nonatomic) dispatch_queue_t dispatchQueue;
  53. - (void)nop:(NSTimer *)timer;
  54. @end
  55. @implementation FWebSocketConnection
  56. @synthesize delegate;
  57. @synthesize connectionId;
  58. - (instancetype)initWith:(FRepoInfo *)repoInfo
  59. andQueue:(dispatch_queue_t)queue
  60. googleAppID:(NSString *)googleAppID
  61. lastSessionID:(NSString *)lastSessionID
  62. appCheckToken:(nullable NSString *)appCheckToken {
  63. self = [super init];
  64. if (self) {
  65. everConnected = NO;
  66. isClosed = NO;
  67. self.connectionId = [FUtilities LUIDGenerator];
  68. self.totalFrames = 0;
  69. self.dispatchQueue = queue;
  70. frame = nil;
  71. NSString *userAgent = [self userAgent];
  72. NSString *connectionURL =
  73. [repoInfo connectionURLWithLastSessionID:lastSessionID];
  74. FFLog(@"I-RDB083001", @"(wsc:%@) Connecting to: %@ as %@",
  75. self.connectionId, connectionURL, userAgent);
  76. NSURLRequest *req = [[self class] createRequestWithURL:connectionURL
  77. userAgent:userAgent
  78. googleAppID:googleAppID
  79. appCheckToken:appCheckToken];
  80. if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0,
  81. watchOS 6.0, *)) {
  82. // Regular NSURLSession websocket.
  83. NSOperationQueue *opQueue = [[NSOperationQueue alloc] init];
  84. opQueue.underlyingQueue = queue;
  85. NSURLSession *session = [NSURLSession
  86. sessionWithConfiguration:[NSURLSessionConfiguration
  87. defaultSessionConfiguration]
  88. delegate:self
  89. delegateQueue:opQueue];
  90. NSURLSessionWebSocketTask *task =
  91. [session webSocketTaskWithRequest:req];
  92. self.webSocketTask = task;
  93. #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION || TARGET_OS_MACCATALYST
  94. NSString *resignName = UIApplicationWillResignActiveNotification;
  95. #elif TARGET_OS_OSX
  96. NSString *resignName = NSApplicationWillResignActiveNotification;
  97. #elif TARGET_OS_WATCH
  98. NSString *resignName = WKApplicationWillResignActiveNotification;
  99. #elif
  100. #error("missing platform")
  101. #endif
  102. [[NSNotificationCenter defaultCenter]
  103. addObserverForName:resignName
  104. object:nil
  105. queue:opQueue
  106. usingBlock:^(NSNotification *_Nonnull note) {
  107. FFLog(@"I-RDB083015",
  108. @"Received notification that application "
  109. @"will resign, "
  110. @"closing web socket.");
  111. [self onClosed];
  112. }];
  113. }
  114. }
  115. return self;
  116. }
  117. - (NSString *)userAgent {
  118. NSString *systemVersion;
  119. NSString *deviceName;
  120. BOOL hasUiDeviceClass = NO;
  121. // Targeted compilation is ONLY for testing. UIKit is weak-linked in actual
  122. // release build.
  123. #if TARGET_OS_IOS || TARGET_OS_TV || TARGET_OS_VISION
  124. Class uiDeviceClass = NSClassFromString(@"UIDevice");
  125. if (uiDeviceClass) {
  126. systemVersion = [uiDeviceClass currentDevice].systemVersion;
  127. deviceName = [uiDeviceClass currentDevice].model;
  128. hasUiDeviceClass = YES;
  129. }
  130. #endif // TARGET_OS_IOS || TARGET_OS_TV || (defined(TARGET_OS_VISION) &&
  131. // TARGET_OS_VISION)
  132. if (!hasUiDeviceClass) {
  133. NSDictionary *systemVersionDictionary = [NSDictionary
  134. dictionaryWithContentsOfFile:
  135. @"/System/Library/CoreServices/SystemVersion.plist"];
  136. systemVersion =
  137. [systemVersionDictionary objectForKey:@"ProductVersion"];
  138. deviceName = [systemVersionDictionary objectForKey:@"ProductName"];
  139. }
  140. NSString *bundleIdentifier = [[NSBundle mainBundle] bundleIdentifier];
  141. // Sanitize '/'s in deviceName and bundleIdentifier for stats
  142. deviceName = [FStringUtilities sanitizedForUserAgent:deviceName];
  143. bundleIdentifier =
  144. [FStringUtilities sanitizedForUserAgent:bundleIdentifier];
  145. // Firebase/5/<semver>_<build date>_<git hash>/<os version>/{device model /
  146. // os (Mac OS X, iPhone, etc.}_<bundle id>
  147. NSString *ua = [NSString
  148. stringWithFormat:@"Firebase/%@/%@/%@/%@_%@", kWebsocketProtocolVersion,
  149. [FIRDatabase buildVersion], systemVersion, deviceName,
  150. bundleIdentifier];
  151. return ua;
  152. }
  153. - (BOOL)buffering {
  154. return frame != nil;
  155. }
  156. #pragma mark -
  157. #pragma mark Public FWebSocketConnection methods
  158. - (void)open {
  159. FFLog(@"I-RDB083002", @"(wsc:%@) FWebSocketConnection open.",
  160. self.connectionId);
  161. assert(delegate);
  162. everConnected = NO;
  163. // TODO Assert url
  164. if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0,
  165. watchOS 6.0, *)) {
  166. [self.webSocketTask resume];
  167. // We need to request data from the web socket in order for it to start
  168. // sending data.
  169. [self receiveWebSocketData];
  170. }
  171. dispatch_time_t when = dispatch_time(
  172. DISPATCH_TIME_NOW, kWebsocketConnectTimeout * NSEC_PER_SEC);
  173. dispatch_after(when, self.dispatchQueue, ^{
  174. [self closeIfNeverConnected];
  175. });
  176. }
  177. - (void)close {
  178. FFLog(@"I-RDB083003", @"(wsc:%@) FWebSocketConnection is being closed.",
  179. self.connectionId);
  180. isClosed = YES;
  181. if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0,
  182. watchOS 6.0, *)) {
  183. [self.webSocketTask
  184. cancelWithCloseCode:NSURLSessionWebSocketCloseCodeNormalClosure
  185. reason:nil];
  186. }
  187. }
  188. - (void)start {
  189. // Start is a no-op for websockets.
  190. }
  191. - (void)send:(NSDictionary *)dictionary {
  192. [self resetKeepAlive];
  193. NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dictionary
  194. options:kNilOptions
  195. error:nil];
  196. NSString *data = [[NSString alloc] initWithData:jsonData
  197. encoding:NSUTF8StringEncoding];
  198. NSArray *dataSegs = [FUtilities splitString:data
  199. intoMaxSize:kWebsocketMaxFrameSize];
  200. // First send the header so the server knows how many segments are
  201. // forthcoming
  202. if (dataSegs.count > 1) {
  203. NSString *formattedData =
  204. [NSString stringWithFormat:@"%u", (unsigned int)dataSegs.count];
  205. [self sendStringToWebSocket:formattedData];
  206. }
  207. // Then, actually send the segments.
  208. for (NSString *segment in dataSegs) {
  209. [self sendStringToWebSocket:segment];
  210. }
  211. }
  212. - (void)nop:(NSTimer *)timer {
  213. if (!isClosed) {
  214. FFLog(@"I-RDB083004", @"(wsc:%@) nop", self.connectionId);
  215. // Note: the backend is expecting a string "0" here, not any special
  216. // ping/pong from build in websocket APIs.
  217. [self sendStringToWebSocket:@"0"];
  218. } else {
  219. FFLog(@"I-RDB083005",
  220. @"(wsc:%@) No more websocket; invalidating nop timer.",
  221. self.connectionId);
  222. [timer invalidate];
  223. }
  224. }
  225. - (void)handleNewFrameCount:(int)numFrames {
  226. self.totalFrames = numFrames;
  227. frame = [[NSMutableString alloc] initWithString:@""];
  228. FFLog(@"I-RDB083006", @"(wsc:%@) handleNewFrameCount: %d",
  229. self.connectionId, self.totalFrames);
  230. }
  231. - (NSString *)extractFrameCount:(NSString *)message {
  232. if ([message length] <= 4) {
  233. int frameCount = [message intValue];
  234. if (frameCount > 0) {
  235. [self handleNewFrameCount:frameCount];
  236. return nil;
  237. }
  238. }
  239. [self handleNewFrameCount:1];
  240. return message;
  241. }
  242. - (void)appendFrame:(NSString *)message {
  243. [frame appendString:message];
  244. self.totalFrames = self.totalFrames - 1;
  245. if (self.totalFrames == 0) {
  246. // Call delegate and pass an immutable version of the frame
  247. NSDictionary *json = [NSJSONSerialization
  248. JSONObjectWithData:[frame dataUsingEncoding:NSUTF8StringEncoding]
  249. options:kNilOptions
  250. error:nil];
  251. frame = nil;
  252. FFLog(@"I-RDB083007",
  253. @"(wsc:%@) handleIncomingFrame sending complete frame: %d",
  254. self.connectionId, self.totalFrames);
  255. @autoreleasepool {
  256. [self.delegate onMessage:self withMessage:json];
  257. }
  258. }
  259. }
  260. - (void)handleIncomingFrame:(NSString *)message {
  261. [self resetKeepAlive];
  262. if (self.buffering) {
  263. [self appendFrame:message];
  264. } else {
  265. NSString *remaining = [self extractFrameCount:message];
  266. if (remaining) {
  267. [self appendFrame:remaining];
  268. }
  269. }
  270. }
  271. #pragma mark -
  272. #pragma mark URLSessionWebSocketDelegate implementation
  273. - (void)URLSession:(NSURLSession *)session
  274. webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask
  275. didOpenWithProtocol:(NSString *)protocol
  276. API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) {
  277. [self webSocketDidOpen];
  278. }
  279. - (void)URLSession:(NSURLSession *)session
  280. webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask
  281. didCloseWithCode:(NSURLSessionWebSocketCloseCode)closeCode
  282. reason:(NSData *)reason
  283. API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0)) {
  284. FFLog(@"I-RDB083011", @"(wsc:%@) didCloseWithCode: %ld %@",
  285. self.connectionId, (long)closeCode, reason);
  286. [self onClosed];
  287. }
  288. - (void)receiveWebSocketData API_AVAILABLE(macos(10.15), ios(13.0),
  289. watchos(6.0), tvos(13.0)) {
  290. __weak __auto_type weakSelf = self;
  291. [self.webSocketTask receiveMessageWithCompletionHandler:^(
  292. NSURLSessionWebSocketMessage *_Nullable message,
  293. NSError *_Nullable error) {
  294. __auto_type strongSelf = weakSelf;
  295. if (strongSelf == nil) {
  296. return;
  297. }
  298. if (message) {
  299. [strongSelf handleIncomingFrame:message.string];
  300. } else if (error && !strongSelf->isClosed) {
  301. FFWarn(@"I-RDB083020",
  302. @"Error received from web socket, closing the connection. %@",
  303. error);
  304. [strongSelf shutdown];
  305. return;
  306. }
  307. [strongSelf receiveWebSocketData];
  308. }];
  309. }
  310. // Common to both SRWebSocketDelegate and URLSessionWebSocketDelegate.
  311. - (void)webSocketDidOpen {
  312. FFLog(@"I-RDB083008", @"(wsc:%@) webSocketDidOpen", self.connectionId);
  313. everConnected = YES;
  314. dispatch_async(dispatch_get_main_queue(), ^{
  315. self->keepAlive =
  316. [NSTimer scheduledTimerWithTimeInterval:kWebsocketKeepaliveInterval
  317. target:self
  318. selector:@selector(nop:)
  319. userInfo:nil
  320. repeats:YES];
  321. FFLog(@"I-RDB083009", @"(wsc:%@) nop timer kicked off",
  322. self.connectionId);
  323. });
  324. }
  325. #pragma mark -
  326. #pragma mark Private methods
  327. /** Sends a string through the open web socket. */
  328. - (void)sendStringToWebSocket:(NSString *)string {
  329. if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0,
  330. watchOS 6.0, *)) {
  331. // Use built-in URLSessionWebSocket functionality.
  332. [self.webSocketTask
  333. sendMessage:[[NSURLSessionWebSocketMessage alloc]
  334. initWithString:string]
  335. completionHandler:^(NSError *_Nullable error) {
  336. if (error) {
  337. FFWarn(@"I-RDB083016", @"Error sending web socket data: %@.",
  338. error);
  339. return;
  340. }
  341. }];
  342. }
  343. }
  344. /**
  345. * Note that the close / onClosed / shutdown cycle here is a little different
  346. * from the javascript client. In order to properly handle deallocation, no
  347. * close-related action is taken at a higher level until we have received
  348. * notification from the websocket itself that it is closed. Otherwise, we end
  349. * up deallocating this class and the FConnection class before the websocket has
  350. * a change to call some of its delegate methods. So, since close is the
  351. * external close handler, we just set a flag saying not to call our own
  352. * delegate method and close the websocket. That will trigger a callback into
  353. * this class that can then do things like clean up the keepalive timer.
  354. */
  355. - (void)closeIfNeverConnected {
  356. if (!everConnected) {
  357. FFLog(@"I-RDB083012", @"(wsc:%@) Websocket timed out on connect",
  358. self.connectionId);
  359. if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0,
  360. watchOS 6.0, *)) {
  361. [self.webSocketTask
  362. cancelWithCloseCode:
  363. NSURLSessionWebSocketCloseCodeNoStatusReceived
  364. reason:nil];
  365. }
  366. }
  367. }
  368. - (void)shutdown {
  369. isClosed = YES;
  370. // Call delegate methods
  371. [self.delegate onDisconnect:self wasEverConnected:everConnected];
  372. }
  373. - (void)onClosed {
  374. if (!isClosed) {
  375. FFLog(@"I-RDB083013", @"Websocket is closing itself");
  376. [self shutdown];
  377. }
  378. if (@available(iOS 13.0, macOS 10.15, macCatalyst 13.1, tvOS 13.0,
  379. watchOS 6.0, *)) {
  380. self.webSocketTask = nil;
  381. }
  382. if (keepAlive.isValid) {
  383. [keepAlive invalidate];
  384. }
  385. }
  386. - (void)resetKeepAlive {
  387. NSDate *newTime =
  388. [NSDate dateWithTimeIntervalSinceNow:kWebsocketKeepaliveInterval];
  389. // Calling setFireDate is actually kinda' expensive, so wait at least 5
  390. // seconds before updating it.
  391. if ([newTime timeIntervalSinceDate:keepAlive.fireDate] > 5) {
  392. FFLog(@"I-RDB083014", @"(wsc:%@) resetting keepalive, to %@ ; old: %@",
  393. self.connectionId, newTime, [keepAlive fireDate]);
  394. [keepAlive setFireDate:newTime];
  395. }
  396. }
  397. + (NSURLRequest *)createRequestWithURL:(NSString *)connectionURL
  398. userAgent:(NSString *)userAgent
  399. googleAppID:(NSString *)googleAppID
  400. appCheckToken:(nullable NSString *)appCheckToken {
  401. NSMutableURLRequest *request = [[NSMutableURLRequest alloc]
  402. initWithURL:[[NSURL alloc] initWithString:connectionURL]];
  403. [request setValue:appCheckToken forHTTPHeaderField:kAppCheckTokenHeader];
  404. [request setValue:userAgent forHTTPHeaderField:kUserAgentHeader];
  405. [request setValue:googleAppID forHTTPHeaderField:kGoogleAppIDHeader];
  406. return [request copy];
  407. }
  408. @end