Skip to content

Commit

Permalink
Merge pull request #16 from OSGP/feature/FDP-1961-PSK-change-flow
Browse files Browse the repository at this point in the history
FDP-1961: psk change flow
  • Loading branch information
jasperkamerling authored Apr 2, 2024
2 parents 96450aa + 4125874 commit ed1ff5d
Show file tree
Hide file tree
Showing 40 changed files with 778 additions and 228 deletions.
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<!--
SPDX-FileCopyrightText: Contributors to the GXF project
SPDX-License-Identifier: Apache-2.0
-->
# Sng Crest Device Simulator

The simulator acts as a coap device, sends alarm messages and is able to handling a PSK set command.

## Flow

1. The simulator sends a coap alarm message to the coap-http proxy, from a `while(true)` loop
in a separate virtual thread, started from the `Simulator.kt` class, that
extends `CommandLineRunner`.
2. If the response received contains a `PSK:SET` command, then:
1. Handle the PSK change:
1. The new key is saved with status `PENDING`.
2. Send a success message.
3. The pending key gets status `ACTIVE` and the previous active key gets status `INACTIVE`.
2. If an error occurs during the PSK change handling:
1. Send a failure message.
2. If a pending key was already saved, set the status of this key as `INVALID`.
3. The thread sleeps for `simulatorProperties.sleepDuration` seconds and then the flow starts
again.

1 change: 1 addition & 0 deletions application/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ dependencies {
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation(libs.mockitoKotlin)

// Generate test and integration test reports
jacocoAggregation(project(":application"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ package org.gxf.crestdevicesimulator

import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.dataformat.cbor.databind.CBORMapper
import org.assertj.core.api.Assertions.assertThat
import org.awaitility.Awaitility
import org.eclipse.californium.core.CoapServer
import org.eclipse.californium.elements.config.Configuration
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Value
Expand All @@ -20,12 +20,15 @@ import java.time.Duration

@SpringBootTest
class SimulatorIntegrationTest {

@Value("\${simulator.config.uri}")
private lateinit var uri: URI

private val mapper = ObjectMapper()
private lateinit var coapServer: CoapServer
private val coapResourceStub = CoapResourceStub()
private val expectedJsonNode = ObjectMapper().readTree(ClassPathResource("messages/kod-message.json").file)
private val expectedJsonNode =
mapper.readTree(ClassPathResource("messages/kod-message.json").file)

@BeforeEach
fun setup() {
Expand All @@ -37,9 +40,9 @@ class SimulatorIntegrationTest {

@Test
fun shouldSendCoapRequestToConfiguredEndpoint() {
Awaitility.await().atMost(Duration.ofSeconds(10)).untilAsserted {
Awaitility.await().atMost(Duration.ofSeconds(5)).untilAsserted {
val jsonNodeStub = CBORMapper().readTree(coapResourceStub.lastRequestPayload)
Assertions.assertEquals(expectedJsonNode, jsonNodeStub)
assertThat(jsonNodeStub).isEqualTo(expectedJsonNode)
}
}
}
8 changes: 7 additions & 1 deletion application/src/integrationTest/resources/application.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
#SPDX-FileCopyrightText: Contributors to the GXF project
#
#SPDX-License-Identifier: Apache-2.0
spring:
flyway:
enabled: false
Expand All @@ -13,7 +16,9 @@ spring:

simulator:
config:
message-path: "messages/kod-message.json"
scheduled-message: "classpath:messages/kod-message.json"
success-message: "classpath:messages/psk-change-success-message.json"
failure-message: "classpath:messages/psk-change-failure-message.json"
# Simulator will produce valid/invalid CBOR messages
produce-valid-cbor: true
# Uri of the target coap server
Expand All @@ -24,3 +29,4 @@ simulator:
psk-identity: 867787050253370
psk-key: ABCDEFGHIJKLMNOP
psk-secret: "123456"
sleep-duration: 100ms
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"ID": 867787050253370,
"MID": 1,
"IMSI": 460023210226023,
"EID": "89001012012341234012345678901224",
"ICCID": "89882280666074936745",
"TS": 1687522333,
"TSL": 1687522333,
"CON": "A",
"FW": 2100,
"TEL": 20416,
"cID": 7020155,
"PWR": 0,
"BAT": 3533,
"CSQ": 25,
"RSRQ": -210,
"RSRP": -99,
"SNR": 22,
"TRY": 1,
"MSI": 0,
"URC": [
"PSK:EQER",
{
"DL": "!PSK:SET"
}
],
"A": [
3,
16,
0,
0,
0,
0,
0,
0
],
"MEM": 7,
"UPT": 32,
"RLY": 0,
"T1": [
265
],
"H1": [
402
],
"D": 9,
"P1": [
12
],
"P2": [
12
],
"FMC": 0
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
{
"ID": 867787050253370,
"MID": 1,
"IMSI": 460023210226023,
"EID": "89001012012341234012345678901224",
"ICCID": "89882280666074936745",
"TS": 1687522333,
"TSL": 1687522333,
"CON": "A",
"FW": 2100,
"TEL": 20416,
"cID": 7020155,
"PWR": 0,
"BAT": 3533,
"CSQ": 25,
"RSRQ": -210,
"RSRP": -99,
"SNR": 22,
"TRY": 1,
"MSI": 0,
"URC": [
"PSK:SET",
{
"DL": "!PSK:SET"
}
],
"A": [
3,
16,
0,
0,
0,
0,
0,
0
],
"MEM": 7,
"UPT": 32,
"RLY": 0,
"T1": [
265
],
"H1": [
402
],
"D": 9,
"P1": [
12
],
"P2": [
12
],
"FMC": 0
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.runApplication
import org.springframework.scheduling.annotation.EnableScheduling


@EnableConfigurationProperties(SimulatorProperties::class)
@EnableScheduling
@SpringBootApplication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package org.gxf.crestdevicesimulator.configuration

import org.gxf.crestdevicesimulator.simulator.data.entity.PreSharedKey
import org.gxf.crestdevicesimulator.simulator.data.entity.PreSharedKeyStatus
import org.gxf.crestdevicesimulator.simulator.data.repository.PskRepository
import org.springframework.context.annotation.Bean

Expand All @@ -15,14 +16,23 @@ class CoapClientConfiguration(private val simulatorProperties: SimulatorProperti
@Bean
fun pskStore(): AdvancedSingleIdentityPskStore {
val store = AdvancedSingleIdentityPskStore(simulatorProperties.pskIdentity)
val savedKey = pskRepository.findById(simulatorProperties.pskIdentity)
val savedKey = pskRepository.findFirstByIdentityAndStatusOrderByRevisionDesc(
simulatorProperties.pskIdentity,
PreSharedKeyStatus.ACTIVE
)

if (savedKey.isEmpty) {
val initialPreSharedKey = PreSharedKey(simulatorProperties.pskIdentity, simulatorProperties.pskKey, simulatorProperties.pskSecret)
if (savedKey == null) {
val initialPreSharedKey = PreSharedKey(
simulatorProperties.pskIdentity,
0,
simulatorProperties.pskKey,
simulatorProperties.pskSecret,
PreSharedKeyStatus.ACTIVE
)
pskRepository.save(initialPreSharedKey)
store.key = simulatorProperties.pskKey
} else {
store.key = savedKey.get().preSharedKey
store.key = savedKey.preSharedKey
}

return store
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
package org.gxf.crestdevicesimulator.configuration

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.core.io.Resource
import java.net.URI
import java.time.Duration

@ConfigurationProperties(prefix = "simulator.config")
class SimulatorProperties(
val uri: URI,
val pskIdentity: String,
val pskKey: String,
val pskSecret: String,
val messagePath: String,
val sleepDuration: Duration,
val scheduledMessage: Resource,
val successMessage: Resource,
val failureMessage: Resource,
val produceValidCbor: Boolean,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,60 +4,41 @@

package org.gxf.crestdevicesimulator.simulator

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.ObjectMapper
import org.eclipse.californium.core.CoapClient
import io.github.oshai.kotlinlogging.KotlinLogging
import org.eclipse.californium.core.coap.MediaTypeRegistry
import org.eclipse.californium.core.coap.Request
import org.eclipse.californium.elements.exception.ConnectorException
import org.gxf.crestdevicesimulator.configuration.SimulatorProperties
import org.gxf.crestdevicesimulator.simulator.coap.CoapClientService
import org.gxf.crestdevicesimulator.simulator.response.ResponseHandler
import org.springframework.core.io.ClassPathResource
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Service
import java.io.IOException

@Service
import org.gxf.crestdevicesimulator.simulator.message.MessageHandler
import org.springframework.boot.CommandLineRunner
import org.springframework.core.io.Resource
import org.springframework.stereotype.Component

@Component
class Simulator(
private val simulatorProperties: SimulatorProperties,
private val coapClientService: CoapClientService,
private val responseHandler: ResponseHandler) {
private val simulatorProperties: SimulatorProperties,
private val messageHandler: MessageHandler,
private val mapper: ObjectMapper
) : CommandLineRunner {

private val logger = KotlinLogging.logger {}

init {
override fun run(args: Array<String>) {
logger.info { "Simulator config started with config: $simulatorProperties" }
}

@Scheduled(fixedDelay = 5000, initialDelay = 0)
fun sendPostMessage() {
val jsonNode = ObjectMapper().readTree(ClassPathResource(simulatorProperties.messagePath).file)
val payload = if (simulatorProperties.produceValidCbor) CborFactory.createValidCbor(jsonNode) else CborFactory.createInvalidCbor()
val request =
Request.newPost()
.apply {
options.setContentFormat(MediaTypeRegistry.APPLICATION_CBOR)
}.setPayload(payload)

logger.info { "SEND REQUEST $request" }

request(request)
}

private fun request(request: Request) {
var coapClient: CoapClient? = null
try {
coapClient = coapClientService.createCoapClient()
val response = coapClient.advanced(request)
responseHandler.handleResponse(response)
logger.info { "RESPONSE $response" }
} catch (e: ConnectorException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} finally {
if (coapClient != null) coapClientService.shutdownCoapClient(coapClient)
val message: JsonNode = createMessage(simulatorProperties.scheduledMessage)

// Start infinite message sending loop in separate thread
// This ensures Spring Boot can complete startup and doesn't block on the infinite loop
Thread.ofVirtual().start {
while (true) {
logger.info { "Sending scheduled alarm message" }
messageHandler.sendMessage(message)

logger.info { "Sleeping for: ${simulatorProperties.sleepDuration}" }
Thread.sleep(simulatorProperties.sleepDuration)
}
}
}

fun createMessage(resource: Resource): JsonNode =
mapper.readTree(resource.inputStream)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// SPDX-FileCopyrightText: Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdevicesimulator.simulator.coap

import org.eclipse.californium.core.CoapClient
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,18 @@
package org.gxf.crestdevicesimulator.simulator.data.entity

import jakarta.persistence.Entity
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated
import jakarta.persistence.Id
import jakarta.persistence.IdClass
import org.gxf.crestdevicesimulator.simulator.data.repository.PreSharedKeyCompositeKey

@Entity
class PreSharedKey(@Id val identity: String, var preSharedKey: String, var secret: String)
@IdClass(PreSharedKeyCompositeKey::class)
class PreSharedKey(
@Id val identity: String,
@Id val revision: Int,
var preSharedKey: String,
val secret: String,
@Enumerated(EnumType.STRING) var status: PreSharedKeyStatus
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-FileCopyrightText: Contributors to the GXF project
//
// SPDX-License-Identifier: Apache-2.0
package org.gxf.crestdevicesimulator.simulator.data.entity

enum class PreSharedKeyStatus {
ACTIVE,
INACTIVE,
PENDING,
INVALID
}
Loading

0 comments on commit ed1ff5d

Please sign in to comment.