diff --git a/CHANGELOG.md b/CHANGELOG.md index ae68662..8919899 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/gradle.properties b/gradle.properties index a13fc55..84a5139 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,2 @@ -version=0.7.5 +version=0.8.0 kotlin.code.style=official \ No newline at end of file diff --git a/hmkit-fleet/src/main/kotlin/HMKitFleet.kt b/hmkit-fleet/src/main/kotlin/HMKitFleet.kt index 3a52a95..5f38ab5 100644 --- a/hmkit-fleet/src/main/kotlin/HMKitFleet.kt +++ b/hmkit-fleet/src/main/kotlin/HMKitFleet.kt @@ -180,6 +180,24 @@ object HMKitFleet { koin.get().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> = GlobalScope.future { + koin.get().sendCommandV05(command, vehicleAccess.accessCertificate) + } + /** * Revoke the vehicle clearance. After this, the [VehicleAccess] object is invalid. * diff --git a/hmkit-fleet/src/main/kotlin/network/AccessCertificateRequests.kt b/hmkit-fleet/src/main/kotlin/network/AccessCertificateRequests.kt index 7ea6b05..4f90380 100644 --- a/hmkit-fleet/src/main/kotlin/network/AccessCertificateRequests.kt +++ b/hmkit-fleet/src/main/kotlin/network/AccessCertificateRequests.kt @@ -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) diff --git a/hmkit-fleet/src/main/kotlin/network/AuthTokenRequests.kt b/hmkit-fleet/src/main/kotlin/network/AuthTokenRequests.kt index 441fd66..7b79cd5 100644 --- a/hmkit-fleet/src/main/kotlin/network/AuthTokenRequests.kt +++ b/hmkit-fleet/src/main/kotlin/network/AuthTokenRequests.kt @@ -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) diff --git a/hmkit-fleet/src/main/kotlin/network/ClearanceRequests.kt b/hmkit-fleet/src/main/kotlin/network/ClearanceRequests.kt index 0b0472d..ad4e69d 100644 --- a/hmkit-fleet/src/main/kotlin/network/ClearanceRequests.kt +++ b/hmkit-fleet/src/main/kotlin/network/ClearanceRequests.kt @@ -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(statusElement) + jsonIg.decodeFromJsonElement(statusElement) if (status.vin == vin) { return Response(status, null) } @@ -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(statusElement) + val status = jsonIg.decodeFromJsonElement(statusElement) status } @@ -130,7 +130,7 @@ internal class ClearanceRequests( val response = call.await() return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody -> - val status = Json.decodeFromString(responseBody) + val status = jsonIg.decodeFromString(responseBody) Response(status) } } @@ -154,7 +154,7 @@ internal class ClearanceRequests( val response = call.await() return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody -> - val status = Json.decodeFromString(responseBody) + val status = jsonIg.decodeFromString(responseBody) Response(status) } } @@ -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) } } diff --git a/hmkit-fleet/src/main/kotlin/network/Requests.kt b/hmkit-fleet/src/main/kotlin/network/Requests.kt index 78d3b1b..7ff1ef7 100644 --- a/hmkit-fleet/src/main/kotlin/network/Requests.kt +++ b/hmkit-fleet/src/main/kotlin/network/Requests.kt @@ -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 tryParseResponse( response: Response, expectedResponseCode: Int, @@ -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(body) - bodyInPrettyPrint = format.encodeToString(jsonElement) + val jsonElement = jsonIgPr.decodeFromString(body) + bodyInPrettyPrint = jsonIgPr.encodeToString(jsonElement) } logger.debug( @@ -85,14 +92,14 @@ internal open class Requests( } fun parseError(responseBody: String): com.highmobility.hmkitfleet.network.Response { - 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(errors.first()) + jsonIg.decodeFromJsonElement(errors.first()) Response(null, error) } else { val error = Error( @@ -104,7 +111,7 @@ internal open class Requests( } } else if (json is JsonArray) { if (json.size > 0) { - val error = Json.decodeFromJsonElement(json.first()) + val error = jsonIg.decodeFromJsonElement(json.first()) return Response(null, error) } } diff --git a/hmkit-fleet/src/main/kotlin/network/TelematicsRequests.kt b/hmkit-fleet/src/main/kotlin/network/TelematicsRequests.kt index 37894dc..434b6c4 100644 --- a/hmkit-fleet/src/main/kotlin/network/TelematicsRequests.kt +++ b/hmkit-fleet/src/main/kotlin/network/TelematicsRequests.kt @@ -65,6 +65,37 @@ internal class TelematicsRequests( return postCommand(encryptedCommand, accessCertificate) } + suspend fun sendCommandV05( + command: Bytes, + accessCertificate: AccessCertificate + ): Response { + 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 { val request = Request.Builder() .url("${baseUrl}/nonces") @@ -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) } @@ -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(responseBody) + val telematicsResponse = jsonIg.decodeFromString(responseBody) // Server only returns encrypted data if status is OK val decryptedData = if (telematicsResponse.status == TelematicsCommandResponse.Status.OK) { @@ -137,7 +168,7 @@ 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))) @@ -145,4 +176,35 @@ internal class TelematicsRequests( return responseObject } + + private suspend fun postCommandV05( + encryptedCommand: Bytes, + accessCertificate: AccessCertificate, + ): Response { + 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) + } + } } \ No newline at end of file diff --git a/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt b/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt index 11bf7e6..375d098 100644 --- a/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt +++ b/hmkit-fleet/src/main/kotlin/network/UtilityRequests.kt @@ -70,7 +70,7 @@ internal class UtilityRequests( val response = call.await() return tryParseResponse(response, HttpURLConnection.HTTP_OK) { responseBody -> - val eligibilityStatus = Json.decodeFromString(responseBody) + val eligibilityStatus = jsonIg.decodeFromString(responseBody) if (eligibilityStatus.vin != vin) logger.warn("VIN in response does not match VIN in request") Response(eligibilityStatus, null) } @@ -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 } } \ No newline at end of file diff --git a/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/TelematicsRequestsTest.kt b/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/TelematicsRequestsTest.kt index 9dd49ff..a81d32a 100644 --- a/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/TelematicsRequestsTest.kt +++ b/hmkit-fleet/src/test/kotlin/com/highmobility/hmkitfleet/network/TelematicsRequestsTest.kt @@ -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( """