diff --git a/Source/ARTRealtime.m b/Source/ARTRealtime.m index 04871ef2c..261f57a8c 100644 --- a/Source/ARTRealtime.m +++ b/Source/ARTRealtime.m @@ -292,7 +292,7 @@ - (void)transitionSideEffects:(ARTConnectionStateChange *)stateChange { [_transport connect]; } - if (self.connection.state != ARTRealtimeFailed && self.connection.state != ARTRealtimeClosed) { + if (self.connection.state != ARTRealtimeFailed && self.connection.state != ARTRealtimeClosed && self.connection.state != ARTRealtimeDisconnected) { [_reachability listenForHost:[_transport host] callback:^(BOOL reachable) { if (reachable) { switch (_connection.state) { @@ -915,7 +915,7 @@ - (void)realtimeTransportClosed:(id)transport { [self transition:ARTRealtimeClosed]; } -- (void)realtimeTransportDisconnected:(id)transport { +- (void)realtimeTransportDisconnected:(id)transport withError:(ARTRealtimeTransportError *)error { if (transport != self.transport) { // Old connection return; @@ -924,7 +924,7 @@ - (void)realtimeTransportDisconnected:(id)transport { if (self.connection.state == ARTRealtimeClosing) { [self transition:ARTRealtimeClosed]; } else { - [self transition:ARTRealtimeDisconnected]; + [self transition:ARTRealtimeDisconnected withErrorInfo:[ARTErrorInfo createWithNSError:error.error]]; } } diff --git a/Source/ARTRealtimeTransport.h b/Source/ARTRealtimeTransport.h index 322a774c0..f3397cfdc 100644 --- a/Source/ARTRealtimeTransport.h +++ b/Source/ARTRealtimeTransport.h @@ -57,7 +57,7 @@ typedef NS_ENUM(NSUInteger, ARTRealtimeTransportState) { - (void)realtimeTransportUnavailable:(id)transport; - (void)realtimeTransportClosed:(id)transport; -- (void)realtimeTransportDisconnected:(id)transport; +- (void)realtimeTransportDisconnected:(id)transport withError:(art_nullable ARTRealtimeTransportError *)error; - (void)realtimeTransportNeverConnected:(id)transport; - (void)realtimeTransportRefused:(id)transport; - (void)realtimeTransportTooBig:(id)transport; diff --git a/Source/ARTRest.m b/Source/ARTRest.m index 49918e98b..d90bc84c0 100644 --- a/Source/ARTRest.m +++ b/Source/ARTRest.m @@ -34,6 +34,12 @@ #import "ARTDefault.h" #import "ARTGCD.h" +@interface ARTRest () { + __block NSUInteger _tokenErrorRetries; +} + +@end + @implementation ARTRest - (instancetype)initWithOptions:(ARTClientOptions *)options { @@ -68,6 +74,7 @@ - (instancetype)initWithOptions:(ARTClientOptions *)options { }; _defaultEncoding = (_options.useBinaryProtocol ? [msgPackEncoder mimeType] : [jsonEncoder mimeType]); _fallbackCount = 0; + _tokenErrorRetries = 0; _auth = [[ARTAuth alloc] init:self withOptions:_options]; _channels = [[ARTRestChannels alloc] initWithRest:self]; @@ -97,9 +104,15 @@ - (void)executeRequest:(NSMutableURLRequest *)request withAuthOption:(ARTAuthent [self executeRequest:request completion:callback]; break; case ARTAuthenticationOn: + _tokenErrorRetries = 0; [self executeRequestWithAuthentication:request withMethod:self.auth.method force:NO completion:callback]; break; case ARTAuthenticationNewToken: + _tokenErrorRetries = 0; + [self executeRequestWithAuthentication:request withMethod:self.auth.method force:YES completion:callback]; + break; + case ARTAuthenticationTokenRetry: + _tokenErrorRetries = _tokenErrorRetries + 1; [self executeRequestWithAuthentication:request withMethod:self.auth.method force:YES completion:callback]; break; case ARTAuthenticationUseBasic: @@ -167,8 +180,11 @@ - (void)executeRequest:(NSMutableURLRequest *)request completion:(void (^)(NSHTT if ([self shouldRenewToken:&dataError]) { [self.logger debug:__FILE__ line:__LINE__ message:@"RS:%p retry request %@", self, request]; // Make a single attempt to reissue the token and resend the request - [self executeRequest:request withAuthOption:ARTAuthenticationNewToken completion:callback]; - return; + if (_tokenErrorRetries < 1) { + [self executeRequest:request withAuthOption:ARTAuthenticationTokenRetry completion:callback]; + return; + } + error = dataError; } else { // Return error with HTTP StatusCode if ARTErrorStatusCode does not exist if (!dataError) { diff --git a/Source/ARTTypes.h b/Source/ARTTypes.h index e416d4086..6cfb1f7b5 100644 --- a/Source/ARTTypes.h +++ b/Source/ARTTypes.h @@ -25,7 +25,8 @@ typedef NS_ENUM(NSUInteger, ARTAuthentication) { ARTAuthenticationOff, ARTAuthenticationOn, ARTAuthenticationUseBasic, - ARTAuthenticationNewToken + ARTAuthenticationNewToken, + ARTAuthenticationTokenRetry }; typedef NS_ENUM(NSUInteger, ARTAuthMethod) { diff --git a/Source/ARTWebSocketTransport.m b/Source/ARTWebSocketTransport.m index 1cdb5db9b..017b81852 100644 --- a/Source/ARTWebSocketTransport.m +++ b/Source/ARTWebSocketTransport.m @@ -117,7 +117,14 @@ - (void)connectForcingNewToken:(BOOL)forceNewToken { if (error) { [[weakSelf logger] error:@"R:%p WS:%p ARTWebSocketTransport: token auth failed with %@", _delegate, self, error.description]; - [[weakSelf delegate] realtimeTransportFailed:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; + if (error.code == 40102 /*incompatible credentials*/) { + // RSA15c + [[weakSelf delegate] realtimeTransportFailed:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; + } + else { + // RSA4b + [[weakSelf delegate] realtimeTransportDisconnected:weakSelf withError:[[ARTRealtimeTransportError alloc] initWithError:error type:ARTRealtimeTransportErrorTypeAuth url:self.websocketURL]]; + } return; } @@ -301,7 +308,7 @@ - (void)webSocket:(SRWebSocket *)webSocket didCloseWithCode:(NSInteger)code reas case ARTWsGoingAway: case ARTWsAbnormalClose: // Connectivity issue - [s.delegate realtimeTransportDisconnected:s]; + [s.delegate realtimeTransportDisconnected:s withError:nil]; break; case ARTWsRefuse: case ARTWsPolicyValidation: diff --git a/Spec/Auth.swift b/Spec/Auth.swift index 81fdd4d22..f9662a1d1 100644 --- a/Spec/Auth.swift +++ b/Spec/Auth.swift @@ -245,6 +245,89 @@ class Auth : QuickSpec { } } } + + // RSA4b + it("in REST, if the token creation failed or the subsequent request with the new token failed due to a token error, then the request should result in an error") { + let options = AblyTests.commonAppSetup() + options.useTokenAuth = true + + let rest = ARTRest(options: options) + rest.httpExecutor = testHTTPExecutor + + let channel = rest.channels.get("test") + + testHTTPExecutor.afterRequest = { _ in + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(40141, description: "token revoked") + } + + testHTTPExecutor.simulateIncomingServerErrorOnNextRequest(40141, description: "token revoked") + waitUntil(timeout: testTimeout) { done in + channel.publish("message", data: nil) { error in + guard let error = error else { + fail("Error is nil"); done(); return + } + expect(error.code).to(equal(40141)) + done() + } + } + + // First request and a second attempt + expect(testHTTPExecutor.requests).to(haveCount(2)) + } + + // RSA4b + it("in Realtime, if the token creation failed then the connection should move to the DISCONNECTED state and reports the error") { + let options = AblyTests.commonAppSetup() + options.authCallback = { tokenParams, completion in + completion(nil, NSError(domain: NSURLErrorDomain, code: -1003, userInfo: [NSLocalizedDescriptionKey: "A server with the specified hostname could not be found."])) + } + options.autoConnect = false + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Failed) { _ in + fail("Should not reach Failed state"); done(); return + } + realtime.connection.once(.Disconnected) { stateChange in + guard let errorInfo = stateChange?.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.message).to(contain("server with the specified hostname could not be found")) + done() + } + realtime.connect() + } + } + + // RSA4b + it("in Realtime, if the connection fails due to a terminal token error, then the connection should move to the FAILED state and reports the error") { + let options = AblyTests.commonAppSetup() + options.authCallback = { tokenParams, completion in + let token = getTestToken() + let invalidToken = String(token.characters.reverse()) + completion(invalidToken, nil) + } + options.autoConnect = false + + let realtime = ARTRealtime(options: options) + defer { realtime.dispose(); realtime.close() } + + waitUntil(timeout: testTimeout) { done in + realtime.connection.once(.Failed) { stateChange in + guard let errorInfo = stateChange?.reason else { + fail("ErrorInfo is nil"); done(); return + } + expect(errorInfo.message).to(contain("No application found with id")) + done() + } + realtime.connection.once(.Disconnected) { _ in + fail("Should not reach Disconnected state"); done(); return + } + realtime.connect() + } + } } // RSA14