Skip to content

Commit

Permalink
Merge pull request #36 from highmobility/v0-ignore-keys
Browse files Browse the repository at this point in the history
V0 ignore keys
  • Loading branch information
tonisives authored Jan 31, 2024
2 parents c2c5918 + c9195e1 commit 12f20fc
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 25 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [0.8.0] - 2024-1-31

- Fix `Json {} ignoreUnknownKeys` error
- Add deprecated `sendCommandV05` method to support 0.5.0 API

## [0.7.5] - 2023-11-29

### Added
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
version=0.7.5
version=0.8.0
kotlin.code.style=official
18 changes: 18 additions & 0 deletions hmkit-fleet/src/main/kotlin/HMKitFleet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,24 @@ object HMKitFleet {
koin.get<TelematicsRequests>().sendCommand(command, vehicleAccess.accessCertificate)
}

/**
* Send a telematics command to the vehicle.
*
* This is a legacy method that returns the response as a [Bytes] object. If possible, use the normal [sendCommand]
* method that returns the correct error format.
*
* @param vehicleAccess The vehicle access object returned in [getVehicleAccess]
* @param command The command that is sent to the vehicle.
* @return The response command from the vehicle.
*/
@Deprecated("Please use sendCommand instead")
fun sendCommandV05(
command: Bytes,
vehicleAccess: VehicleAccess
): CompletableFuture<Response<Bytes>> = GlobalScope.future {
koin.get<TelematicsRequests>().sendCommandV05(command, vehicleAccess.accessCertificate)
}

/**
* Revoke the vehicle clearance. After this, the [VehicleAccess] object is invalid.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ internal class AccessCertificateRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_CREATED) { body ->
val jsonResponse = Json.parseToJsonElement(body) as JsonObject
val jsonResponse = jsonIg.parseToJsonElement(body) as JsonObject
val certBytes = jsonResponse.jsonObject[apiDeviceCertKey]?.jsonPrimitive?.content
val cert = AccessCertificate(certBytes)
Response(cert, null)
Expand Down
2 changes: 1 addition & 1 deletion hmkit-fleet/src/main/kotlin/network/AuthTokenRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ internal class AuthTokenRequests(

return try {
if (response.code == HttpURLConnection.HTTP_CREATED) {
cache.authToken = Json.decodeFromString(responseBody)
cache.authToken = jsonIg.decodeFromString(responseBody)
Response(cache.authToken)
} else {
parseError(responseBody)
Expand Down
18 changes: 9 additions & 9 deletions hmkit-fleet/src/main/kotlin/network/ClearanceRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,11 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val jsonElement = Json.parseToJsonElement(responseBody) as JsonObject
val jsonElement = jsonIg.parseToJsonElement(responseBody) as JsonObject
val statuses = jsonElement["vehicles"] as JsonArray
for (statusElement in statuses) {
val status =
Json.decodeFromJsonElement<RequestClearanceResponse>(statusElement)
jsonIg.decodeFromJsonElement<RequestClearanceResponse>(statusElement)
if (status.vin == vin) {
return Response(status, null)
}
Expand All @@ -100,11 +100,11 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val statuses = Json.parseToJsonElement(responseBody) as JsonArray
val statuses = jsonIg.parseToJsonElement(responseBody) as JsonArray

val builder = Array(statuses.size) {
val statusElement = statuses[it]
val status = Json.decodeFromJsonElement<ClearanceStatus>(statusElement)
val status = jsonIg.decodeFromJsonElement<ClearanceStatus>(statusElement)
status
}

Expand All @@ -130,7 +130,7 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val status = Json.decodeFromString<ClearanceStatus>(responseBody)
val status = jsonIg.decodeFromString<ClearanceStatus>(responseBody)
Response(status)
}
}
Expand All @@ -154,7 +154,7 @@ internal class ClearanceRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val status = Json.decodeFromString<RequestClearanceResponse>(responseBody)
val status = jsonIg.decodeFromString<RequestClearanceResponse>(responseBody)
Response(status)
}
}
Expand All @@ -166,14 +166,14 @@ internal class ClearanceRequests(
): RequestBody {
val vehicle = buildJsonObject {
put("vin", vin)
put("brand", Json.encodeToJsonElement(brand))
put("brand", jsonIg.encodeToJsonElement(brand))
if (controlMeasures != null) {
putJsonObject("control_measures") {
for (controlMeasure in controlMeasures) {
// polymorphism adds type key to child controlmeasure classes. remove with filter
val json = Json.encodeToJsonElement(controlMeasure)
val json = jsonIg.encodeToJsonElement(controlMeasure)
val valuesWithoutType = json.jsonObject.filterNot { it.key == "type" }
val jsonTrimmed = Json.encodeToJsonElement(valuesWithoutType)
val jsonTrimmed = jsonIg.encodeToJsonElement(valuesWithoutType)
put("odometer", jsonTrimmed)
}
}
Expand Down
21 changes: 14 additions & 7 deletions hmkit-fleet/src/main/kotlin/network/Requests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ internal open class Requests(
val mediaType = "application/json; charset=utf-8".toMediaType()
val baseHeaders = Headers.Builder().add("Content-Type", "application/json").build()

protected val jsonIg = Json {
ignoreUnknownKeys = true
}

private val jsonIgPr = Json {
ignoreUnknownKeys = true
prettyPrint = true
}

inline fun <T> tryParseResponse(
response: Response,
expectedResponseCode: Int,
Expand All @@ -61,14 +70,12 @@ internal open class Requests(
}

fun printRequest(request: Request) {
val format = Json { prettyPrint = true }

// parse into json, so can log it out with pretty print
val body = request.bodyAsString()
var bodyInPrettyPrint = ""
if (!body.isNullOrBlank()) {
val jsonElement = format.decodeFromString<JsonElement>(body)
bodyInPrettyPrint = format.encodeToString(jsonElement)
val jsonElement = jsonIgPr.decodeFromString<JsonElement>(body)
bodyInPrettyPrint = jsonIgPr.encodeToString(jsonElement)
}

logger.debug(
Expand All @@ -85,14 +92,14 @@ internal open class Requests(
}

fun <T> parseError(responseBody: String): com.highmobility.hmkitfleet.network.Response<T> {
val json = Json.parseToJsonElement(responseBody)
val json = jsonIg.parseToJsonElement(responseBody)
if (json is JsonObject) {
// there are 3 error formats
val errors = json["errors"] as? JsonArray

return if (errors != null && errors.size > 0) {
val error =
Json.decodeFromJsonElement<Error>(errors.first())
jsonIg.decodeFromJsonElement<Error>(errors.first())
Response(null, error)
} else {
val error = Error(
Expand All @@ -104,7 +111,7 @@ internal open class Requests(
}
} else if (json is JsonArray) {
if (json.size > 0) {
val error = Json.decodeFromJsonElement<Error>(json.first())
val error = jsonIg.decodeFromJsonElement<Error>(json.first())
return Response(null, error)
}
}
Expand Down
68 changes: 65 additions & 3 deletions hmkit-fleet/src/main/kotlin/network/TelematicsRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,37 @@ internal class TelematicsRequests(
return postCommand(encryptedCommand, accessCertificate)
}

suspend fun sendCommandV05(
command: Bytes,
accessCertificate: AccessCertificate
): Response<Bytes> {
val nonce = getNonce()

if (nonce.error != null) return Response(null, nonce.error)

val encryptedCommand =
crypto.createTelematicsContainer(
command,
privateKey,
certificate.serial,
accessCertificate,
Bytes(nonce.response!!)
)

val encryptedCommandResponse = postCommandV05(encryptedCommand, accessCertificate)

if (encryptedCommandResponse.error != null) return encryptedCommandResponse

val decryptedResponseCommand = crypto.getPayloadFromTelematicsContainer(
encryptedCommandResponse.response!!,
privateKey,
accessCertificate,
)

return Response(decryptedResponseCommand)
}


private suspend fun getNonce(): Response<String> {
val request = Request.Builder()
.url("${baseUrl}/nonces")
Expand All @@ -83,7 +114,7 @@ internal class TelematicsRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_CREATED) { body ->
val jsonResponse = Json.parseToJsonElement(body) as JsonObject
val jsonResponse = jsonIg.parseToJsonElement(body) as JsonObject
val nonce = jsonResponse.jsonObject["nonce"]?.jsonPrimitive?.content
Response(nonce, null)
}
Expand Down Expand Up @@ -114,7 +145,7 @@ internal class TelematicsRequests(

val responseObject = try {
if (response.code == 200 || response.code == 400 || response.code == 404 || response.code == 408) {
val telematicsResponse = Json.decodeFromString<TelematicsCommandResponse>(responseBody)
val telematicsResponse = jsonIg.decodeFromString<TelematicsCommandResponse>(responseBody)

// Server only returns encrypted data if status is OK
val decryptedData = if (telematicsResponse.status == TelematicsCommandResponse.Status.OK) {
Expand All @@ -137,12 +168,43 @@ internal class TelematicsRequests(
} else {
// try to parse the normal server error format.
// it will throw and will be caught if server returned unknown format
TelematicsResponse(errors = Json.decodeFromString(responseBody))
TelematicsResponse(errors = jsonIg.decodeFromString(responseBody))
}
} catch (e: Exception) {
TelematicsResponse(errors = listOf(Error(title = "Unknown server response", detail = e.message)))
}

return responseObject
}

private suspend fun postCommandV05(
encryptedCommand: Bytes,
accessCertificate: AccessCertificate,
): Response<Bytes> {
val request = Request.Builder()
.url("${baseUrl}/telematics_commands")
.headers(baseHeaders)
.post(
requestBody(
mapOf(
"serial_number" to accessCertificate.gainerSerial.hex,
"issuer" to certificate.issuer.hex,
"data" to encryptedCommand.base64
)
)
)
.build()

printRequest(request)

val call = client.newCall(request)
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { body ->
val jsonResponse = jsonIg.parseToJsonElement(body) as JsonObject
val encryptedResponseCommand =
jsonResponse.jsonObject["response_data"]?.jsonPrimitive?.content
Response(Bytes(encryptedResponseCommand), null)
}
}
}
6 changes: 3 additions & 3 deletions hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ internal class UtilityRequests(
val response = call.await()

return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody ->
val eligibilityStatus = Json.decodeFromString<EligibilityStatus>(responseBody)
val eligibilityStatus = jsonIg.decodeFromString<EligibilityStatus>(responseBody)
if (eligibilityStatus.vin != vin) logger.warn("VIN in response does not match VIN in request")
Response(eligibilityStatus, null)
}
Expand All @@ -82,10 +82,10 @@ internal class UtilityRequests(
): RequestBody {
val vehicle = buildJsonObject {
put("vin", vin)
put("brand", Json.encodeToJsonElement(brand))
put("brand", jsonIg.encodeToJsonElement(brand))
}

val body = Json.encodeToString(vehicle).toRequestBody(mediaType)
val body = jsonIg.encodeToString(vehicle).toRequestBody(mediaType)
return body
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,56 @@ internal class TelematicsRequestsTest : BaseTest() {
assert(response.response?.responseData!! == decryptedReceivedCommand)
}

@Test
fun createAccessTokenV05() {
mockNonceResponse()
mockTelematicsResponse()

val baseUrl: HttpUrl = mockWebServer.url("")

val telematicsRequests = TelematicsRequests(
client, mockLogger, baseUrl.toString(), privateKey, certificate, crypto
)

val response = runBlocking {
telematicsRequests.sendCommandV05(Diagnostics.GetState(), mockAccessCert)
}

verify {
crypto.createTelematicsContainer(
Diagnostics.GetState(), privateKey, certificate.serial, mockAccessCert, nonce
)
}

// first request is nonce
val nonceRequest: RecordedRequest = mockWebServer.takeRequest()
assert(nonceRequest.path!!.endsWith("/nonces"))

// verify request
val nonceRequestBody = Json.parseToJsonElement(nonceRequest.body.readUtf8()) as JsonObject
assert(nonceRequestBody["serial_number"]!!.jsonPrimitive.contentOrNull == certificate.serial.hex)

// second request is telematics command
val commandRequest: RecordedRequest = mockWebServer.takeRequest()
assert(commandRequest.path!!.endsWith("/telematics_commands"))

// verify request
val jsonBody = Json.parseToJsonElement(commandRequest.body.readUtf8()) as JsonObject
assert(jsonBody["serial_number"]!!.jsonPrimitive.contentOrNull == certificate.serial.hex)
assert(jsonBody["issuer"]!!.jsonPrimitive.contentOrNull == certificate.issuer.hex)
assert(jsonBody["data"]!!.jsonPrimitive.contentOrNull == encryptedSentCommand.base64)

// verify command decrypted
verify {
crypto.getPayloadFromTelematicsContainer(
encryptedReceivedCommand, privateKey, mockAccessCert
)
}

// verify final telematics command response
assert(response.response!! == decryptedReceivedCommand)
}

private fun mockTelematicsResponse() {
val mockResponse = MockResponse().setResponseCode(HttpURLConnection.HTTP_OK).setBody(
"""
Expand Down

0 comments on commit 12f20fc

Please sign in to comment.