Skip to content

Commit

Permalink
Changes to allow reconnect attempt if server rejected request due to …
Browse files Browse the repository at this point in the history
…client <-> server time difference
  • Loading branch information
tylerjroach committed Nov 10, 2023
1 parent e586c5b commit 87e3b0e
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.amplifyframework.predictions.aws.models.liveness.ColorDisplayed
import com.amplifyframework.predictions.aws.models.liveness.FaceMovementAndLightClientChallenge
import com.amplifyframework.predictions.aws.models.liveness.FreshnessColor
import com.amplifyframework.predictions.aws.models.liveness.InitialFace
import com.amplifyframework.predictions.aws.models.liveness.InvalidSignatureException
import com.amplifyframework.predictions.aws.models.liveness.LivenessResponseStream
import com.amplifyframework.predictions.aws.models.liveness.SessionInformation
import com.amplifyframework.predictions.aws.models.liveness.TargetFace
Expand Down Expand Up @@ -78,11 +79,11 @@ internal class LivenessWebSocket(
private val signer = AWSV4Signer()
private var credentials: Credentials? = null

internal var offset = 0L
// The reported time difference between the server and client. Only set if diff is higher than 4 minutes
internal var timeDiffOffsetInMillis = 0L
internal enum class ConnectionState {
NORMAL,
ATTEMPT_RECONNECT,
TOO_MANY_RECONNECTS;
}
internal var reconnectState = ConnectionState.NORMAL

Expand Down Expand Up @@ -111,21 +112,13 @@ internal class LivenessWebSocket(

super.onOpen(webSocket, response)

// if offset is > 5 minutes, server will reject the request
if (kotlin.math.abs(tempOffset) < FIVE_MINUTES) {
reconnectState = ConnectionState.NORMAL
this@LivenessWebSocket.webSocket = webSocket
} else {
// server will close this websocket
if (reconnectState == ConnectionState.ATTEMPT_RECONNECT) {
// this is not the first try, report that failure back
reconnectState = ConnectionState.TOO_MANY_RECONNECTS
} else {
// this is the first try, don't report that failure back
reconnectState = ConnectionState.ATTEMPT_RECONNECT
offset = tempOffset
start()
}
this@LivenessWebSocket.webSocket = webSocket

// If offset is > 4 minutes, server may reject the request
// The real allowed diff from serer is < 5 but we check for 4 to add a buffer
if (!isTimeDiffSafe(tempOffset)) {
LOG.info("Server reported a time difference between client and server of > 4 minutes")
timeDiffOffsetInMillis = tempOffset
}
}

Expand Down Expand Up @@ -167,10 +160,22 @@ internal class LivenessWebSocket(
override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
LOG.debug("WebSocket onClosed")
super.onClosed(webSocket, code, reason)
if (reconnectState == ConnectionState.ATTEMPT_RECONNECT) {
// do nothing; we expected the server to close the connection
val recordedError = webSocketError
/*
If the server reports an invalid signature due to a time difference between the local clock and the
server clock, AND we haven't already tried to reconnect, then we should try to reconnect with an offset
*/
if (reconnectState == ConnectionState.NORMAL &&
!isTimeDiffSafe(timeDiffOffsetInMillis) &&
recordedError is PredictionsException &&
recordedError.cause is InvalidSignatureException
) {
LOG.info("The server rejected the connection due to a likely time difference. Attempting reconnect.")
reconnectState = ConnectionState.ATTEMPT_RECONNECT
webSocketError = null
start()
} else if (code != NORMAL_SOCKET_CLOSURE_STATUS_CODE && !clientStoppedSession) {
val faceLivenessException = webSocketError ?: PredictionsException(
val faceLivenessException = recordedError ?: PredictionsException(
"An error occurred during the face liveness check.",
reason
)
Expand Down Expand Up @@ -302,6 +307,18 @@ internal class LivenessWebSocket(
AccessDeniedException(
cause = livenessResponse.accessDeniedException
)
} else if (livenessResponse.unrecognizedClientException != null) {
PredictionsException(
"Unrecognized client",
livenessResponse.unrecognizedClientException,
"Please check your credentials"
)
} else if (livenessResponse.invalidSignatureException != null) {
PredictionsException(
"Invalid signature",
livenessResponse.invalidSignatureException,
"Please check your credentials"
)
} else {
PredictionsException(
"An unknown error occurred during the Liveness flow.",
Expand Down Expand Up @@ -467,12 +484,14 @@ internal class LivenessWebSocket(
}

fun adjustedDate(date: Long = Date().time): Long {
return date + offset
return date + timeDiffOffsetInMillis
}

private fun isTimeDiffSafe(diffInMillis: Long) = kotlin.math.abs(diffInMillis) < FOUR_MINUTES

companion object {
private const val NORMAL_SOCKET_CLOSURE_STATUS_CODE = 1000
private val FIVE_MINUTES = 1000 * 60 * 5
private const val FOUR_MINUTES = 1000 * 60 * 4
@VisibleForTesting val datePattern = "EEE, d MMM yyyy HH:mm:ss z"
private val LOG = Amplify.Logging.logger(CategoryType.PREDICTIONS, "amplify:aws-predictions")
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,6 @@ internal data class LivenessResponseStream(
@SerialName("ServiceUnavailableException") val serviceUnavailableException: ServiceUnavailableException? = null,
@SerialName("SessionNotFoundException") val sessionNotFoundException: SessionNotFoundException? = null,
@SerialName("AccessDeniedException") val accessDeniedException: AccessDeniedException? = null,
@SerialName("InvalidSignatureException") val invalidSignatureException: InvalidSignatureException? = null
@SerialName("InvalidSignatureException") val invalidSignatureException: InvalidSignatureException? = null,
@SerialName("UnrecognizedClientException") val unrecognizedClientException: UnrecognizedClientException? = null
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0
*
* or in the "license" file accompanying this file. This file 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.
*/
package com.amplifyframework.predictions.aws.models.liveness

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

/**
* Constructs a new UnrecognizedClientException with the specified error message.
*
* @param message Describes the error encountered.
*/
@Serializable
internal data class UnrecognizedClientException(
@SerialName("Message") override val message: String
) : Exception(message)

0 comments on commit 87e3b0e

Please sign in to comment.