From 560b9130fa9f34288ee18cdc539ce03200458955 Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sat, 5 Feb 2022 01:33:18 +0100 Subject: [PATCH 01/10] start work on replacing library --- doip-library/build.gradle.kts | 23 ++++ doip-library/src/main/kotlin/DoipEntities.kt | 68 ++++++++++ .../src/main/kotlin/DoipMessageParser.kt | 122 ++++++++++++++++++ doip-library/src/main/kotlin/DoipMessages.kt | 116 +++++++++++++++++ .../src/test/kotlin/DoipMessageParserTest.kt | 72 +++++++++++ doip-sim-ecu-dsl/build.gradle.kts | 1 + settings.gradle.kts | 1 + 7 files changed, 403 insertions(+) create mode 100644 doip-library/build.gradle.kts create mode 100644 doip-library/src/main/kotlin/DoipEntities.kt create mode 100644 doip-library/src/main/kotlin/DoipMessageParser.kt create mode 100644 doip-library/src/main/kotlin/DoipMessages.kt create mode 100644 doip-library/src/test/kotlin/DoipMessageParserTest.kt diff --git a/doip-library/build.gradle.kts b/doip-library/build.gradle.kts new file mode 100644 index 0000000..f5bc9c9 --- /dev/null +++ b/doip-library/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + kotlin("jvm") // Standalone: Add version + `java-library` +} + +repositories { + mavenCentral() +} + +val ktorVersion = "1.6.7" + +dependencies { + implementation(kotlin("stdlib")) + +// implementation("io.ktor:ktor-server-core:$ktorVersion") +// implementation("io.ktor:ktor-server-netty:$ktorVersion") + implementation("io.ktor:ktor-network:$ktorVersion") +// implementation("ch.qos.logback:logback-classic:1.2.5") + + testImplementation(kotlin("test")) + testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0") + testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") +} diff --git a/doip-library/src/main/kotlin/DoipEntities.kt b/doip-library/src/main/kotlin/DoipEntities.kt new file mode 100644 index 0000000..cda12bf --- /dev/null +++ b/doip-library/src/main/kotlin/DoipEntities.kt @@ -0,0 +1,68 @@ +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.util.network.* +import io.ktor.utils.io.* +import io.ktor.utils.io.core.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import java.net.InetSocketAddress +import kotlin.concurrent.thread + +class DoipEntities { + fun start() { + thread(name = "UDP-RECV") { + runBlocking { + val socket = + aSocket(ActorSelectorManager(Dispatchers.IO)).udp().bind(InetSocketAddress("0.0.0.0", 13400)) { + this.broadcast = true +// socket.joinGroup(multicastAddress) + } + + while (!socket.isClosed) { + val datagram = socket.receive() + if (datagram.address.port != 13400) { + val data = datagram.packet.readText() + println(data) + socket.send( + Datagram( + ByteReadPacket(byteArrayOf(1, 2, 3)), + InetSocketAddress("255.255.255.255", 13400) + ) + ) + } + } + } + } + + thread(name = "TCP_RECV") { + runBlocking { + val serverSocket = + aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind(InetSocketAddress("0.0.0.0", 13400)) + while (!serverSocket.isClosed) { + val socket = serverSocket.accept() + launch { + val input = socket.openReadChannel() + val output = socket.openWriteChannel(autoFlush = true) + try { + while (!socket.isClosed) { + val line = input.readUTF8Line() ?: break + + println("${socket.remoteAddress}: $line") + output.writeStringUtf8("$line\r\n") + } + } catch (e: Throwable) { + e.printStackTrace() + } finally { + withContext(Dispatchers.IO) { + socket.close() + } + } + } + } + + } + } + } +} diff --git a/doip-library/src/main/kotlin/DoipMessageParser.kt b/doip-library/src/main/kotlin/DoipMessageParser.kt new file mode 100644 index 0000000..d90e338 --- /dev/null +++ b/doip-library/src/main/kotlin/DoipMessageParser.kt @@ -0,0 +1,122 @@ +import java.nio.ByteBuffer +import java.nio.ByteOrder +import kotlin.experimental.xor + +class IncorrectPatternFormat(message: String) : RuntimeException(message) +class HeaderTooShort(message: String) : RuntimeException(message) +class InvalidPayloadLength(message: String) : RuntimeException(message) +class UnknownPayloadType(message: String) : RuntimeException(message) + +fun doipMessage(payloadType: Short, vararg data: Byte): ByteArray { + val bb = ByteBuffer.allocate(8 + data.size) + bb.put(0x02) + bb.put(0xFD.toByte()) + bb.putShort(payloadType) + bb.putInt(data.size) + bb.put(data) + return bb.array() +} + +object DoipMessageParser { + private fun checkSyncPattern(data: ByteBuffer): Boolean { + if (data[0] != 0x02.toByte() || + data[1] != 0x02.toByte() xor 0xFF.toByte() + ) { + return false + } + return true + } + + private fun checkPayloadLength( + expectedLength: Int, + payloadLength: Int, + buffer: ByteBuffer, + block: ByteBuffer.() -> T + ): DoipUdpMessage { + if (buffer.limit() - 8 != payloadLength || expectedLength != payloadLength) { + throw InvalidPayloadLength("Payload isn't the right size ($payloadLength ${buffer.limit() - 8})") + } + return block(buffer) as DoipUdpMessage + } + + fun parseUDP(ba: ByteArray): DoipUdpMessage { + val data = ByteBuffer.wrap(ba) + data.rewind() + data.order(ByteOrder.BIG_ENDIAN) + + // Check header length + if (data.limit() < 8) { + throw HeaderTooShort("DoIP UDP message too short for interpretation") + } + if (!checkSyncPattern(data)) { + throw IncorrectPatternFormat("Sync Pattern isn't valid") + } + val payloadType = data.getShort(2) + val payloadLength = data.getInt(4) + + return when (payloadType) { + TYPE_HEADER_NACK -> checkPayloadLength(1, payloadLength, data) { DoipUdpHeaderNegAck(data[8]) } + + TYPE_UDP_VIR -> checkPayloadLength(0, payloadLength, data) { DoipUdpVehicleInformationRequest() } + + TYPE_UDP_VIR_EID -> checkPayloadLength( + 6, + payloadLength, + data + ) { DoipUdpVehicleInformationRequestWithEid(data.sliceArray(8, 6)) } + + TYPE_UDP_VIR_VIN -> checkPayloadLength( + 17, + payloadLength, + data + ) { DoipUdpVehicleInformationRequestWithVIN(data.sliceArray(8, 17)) } + + TYPE_UDP_VAM -> checkPayloadLength( + 33, + payloadLength, + data + ) { + DoipUdpVehicleVehicleAnnouncmentMessage( + vin = data.sliceArray(8, 17), + logicalAddress = data.getShort(25), + eid = data.sliceArray(27, 6), + gid = data.sliceArray(33, 6), + furtherActionRequired = data.get(39), + syncStatus = data.get(40) + ) + } + + TYPE_UDP_ENTITY_STATUS_REQ -> checkPayloadLength(0, payloadLength, data) { DoipUdpEntityStatusRequest() } + + TYPE_UDP_ENTITY_STATUS_RES -> checkPayloadLength(7, payloadLength, data) { + DoipUdpEntityStatusResponse( + nodeType = data.get(8), + numberOfSockets = data.get(9), + currentNumberOfSockets = data.get(10), + maxDataSize = data.getInt(11) + ) + } + + TYPE_UDP_DIAG_POWER_MODE_REQ -> checkPayloadLength( + 0, + payloadLength, + data + ) { DoipUdpDiagnosticPowerModeRequest() } + + TYPE_UDP_DIAG_POWER_MODE_RES -> checkPayloadLength( + 1, + payloadLength, + data + ) { DoipUdpDiagnosticPowerModeResponse(data.get(8)) } + + else -> throw UnknownPayloadType("$payloadType is an unknown payload type") + } + } +} + +fun ByteBuffer.sliceArray(index: Int, length: Int): ByteArray { + val ba = ByteArray(length) + this.get(index, ba, 0, length) + return ba +} + diff --git a/doip-library/src/main/kotlin/DoipMessages.kt b/doip-library/src/main/kotlin/DoipMessages.kt new file mode 100644 index 0000000..ec31031 --- /dev/null +++ b/doip-library/src/main/kotlin/DoipMessages.kt @@ -0,0 +1,116 @@ +import java.lang.IllegalArgumentException + +const val TYPE_HEADER_NACK: Short = 0x0000 +const val TYPE_UDP_VIR: Short = 0x0001 +const val TYPE_UDP_VIR_EID: Short = 0x0002 +const val TYPE_UDP_VIR_VIN: Short = 0x0003 +const val TYPE_UDP_VAM: Short = 0x0004 +const val TYPE_TCP_ROUTING_REQ: Short = 0x0005 +const val TYPE_TCP_ROUTING_RES: Short = 0x0006 +const val TYPE_TCP_ALIVE_REQ: Short = 0x0007 +const val TYPE_TCP_ALIVE_RES: Short = 0x0008 + +const val TYPE_UDP_ENTITY_STATUS_REQ: Short = 0x4001 +const val TYPE_UDP_ENTITY_STATUS_RES: Short = 0x4002 +const val TYPE_UDP_DIAG_POWER_MODE_REQ: Short = 0x4003 +const val TYPE_UDP_DIAG_POWER_MODE_RES: Short = 0x4004 + +const val TYPE_TCP_DIAG_MESSAGE: Short = 0x8001.toShort() +const val TYPE_TCP_DIAG_MESSAGE_POS_ACK: Short = 0x8002.toShort() +const val TYPE_TCP_DIAG_MESSAGE_NEG_ACK: Short = 0x8003.toShort() + +open class DoipMessage + +open class DoipUdpMessage : DoipMessage() + +class DoipUdpHeaderNegAck(val code: Byte) : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_HEADER_NACK, code) } +} + +class DoipUdpVehicleInformationRequest : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_VIR) } +} + +class DoipUdpVehicleInformationRequestWithEid(val eid: ByteArray) : DoipUdpMessage() { + init { + if (eid.size != 6) { + throw IllegalArgumentException("eid must be 6 bytes") + } + } + val message by lazy { doipMessage(TYPE_UDP_VIR_EID, *eid) } +} + +class DoipUdpVehicleInformationRequestWithVIN(val vin: ByteArray) : DoipUdpMessage() { + init { + if (vin.size != 17) { + throw IllegalArgumentException("vin must be 17 bytes") + } + } + val message by lazy { doipMessage(TYPE_UDP_VIR_VIN, *vin) } +} + +class DoipUdpVehicleVehicleAnnouncmentMessage( + val vin: ByteArray, + val logicalAddress: Short, + val gid: ByteArray, + val eid: ByteArray, + val furtherActionRequired: Byte, + val syncStatus: Byte +) : DoipUdpMessage() { + init { + if (vin.size != 17) { + throw IllegalArgumentException("vin must be 17 bytes") + } + if (gid.size != 6) { + throw IllegalArgumentException("gid must be 6 bytes") + } + if (eid.size != 6) { + throw IllegalArgumentException("eid must be 6 bytes") + } + } + + val message by lazy { + doipMessage( + TYPE_UDP_VAM, + *vin, + (logicalAddress.toInt() shr 8).toByte(), + logicalAddress.toByte(), + *eid, + *gid, + furtherActionRequired, + syncStatus + ) + } +} + +class DoipUdpEntityStatusRequest : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_ENTITY_STATUS_REQ) } +} + +class DoipUdpEntityStatusResponse( + val nodeType: Byte, + val numberOfSockets: Byte, + val currentNumberOfSockets: Byte, + val maxDataSize: Int +) : DoipUdpMessage() { + val message by lazy { + doipMessage( + TYPE_UDP_ENTITY_STATUS_RES, + nodeType, + numberOfSockets, + currentNumberOfSockets, + *maxDataSize.toByteArray() + ) + } +} + +class DoipUdpDiagnosticPowerModeRequest : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_REQ) } +} + +class DoipUdpDiagnosticPowerModeResponse(val diagPowerMode: Byte) : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_RES, diagPowerMode) } +} + +fun Int.toByteArray(): ByteArray = + byteArrayOf((this and 0xFF000000.toInt() shr 24).toByte(), (this and 0xFF0000 shr 16).toByte(), (this and 0xFF00 shr 8).toByte(), (this and 0xFF).toByte()) diff --git a/doip-library/src/test/kotlin/DoipMessageParserTest.kt b/doip-library/src/test/kotlin/DoipMessageParserTest.kt new file mode 100644 index 0000000..ebe1147 --- /dev/null +++ b/doip-library/src/test/kotlin/DoipMessageParserTest.kt @@ -0,0 +1,72 @@ +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Test +import kotlin.test.assertIs + +class DoipMessageParserTest { + @Test + fun test() { + val eid = byteArrayOf(0x10, 0x20, 0x30, 0x40, 0x50, 0x60) + val gid = byteArrayOf(0x60, 0x50, 0x40, 0x30, 0x20, 0x10) + val vin = "01234567891234567".encodeToByteArray() + + val negAckMsg = doipMessage(0x0000, 0x10) + val negAck = DoipMessageParser.parseUDP(negAckMsg) + assertIs(negAck) + assertThat(negAck.code).isEqualTo(0x10) + assertThat(negAck.message).isEqualTo(negAckMsg) + + val virMsg = doipMessage(0x0001) + val vir = DoipMessageParser.parseUDP(virMsg) + assertIs(vir) + assertThat(vir.message).isEqualTo(virMsg) + + val virEIDMsg = doipMessage(0x0002, *eid) + val virEid = DoipMessageParser.parseUDP(virEIDMsg) + assertIs(virEid) + assertThat(virEid.eid).isEqualTo(eid) + assertThat(virEid.message).isEqualTo(virEIDMsg) + + val virVINMsg = doipMessage(0x0003, *vin) + val virVIN = DoipMessageParser.parseUDP(virVINMsg) + assertIs(virVIN) + assertThat(virVIN.vin).isEqualTo(vin) + assertThat(virVIN.message).isEqualTo(virVINMsg) + + val vamMsg = doipMessage(0x0004, *vin, 0x10, 0x01, *eid, *gid, 0x02, 0x03) + val vam = DoipMessageParser.parseUDP(vamMsg) + assertIs(vam) + assertThat(vam.vin).isEqualTo(vin) + assertThat(vam.logicalAddress).isEqualTo(0x1001) + assertThat(vam.gid).isEqualTo(gid) + assertThat(vam.eid).isEqualTo(eid) + assertThat(vam.furtherActionRequired).isEqualTo(0x02) + assertThat(vam.syncStatus).isEqualTo(0x03) + assertThat(vam.message).isEqualTo(vamMsg) + + val esReqMsg = doipMessage(0x4001) + val esReq = DoipMessageParser.parseUDP(esReqMsg) + assertIs(esReq) + assertThat(esReq.message).isEqualTo(esReqMsg) + + val esResMsg = doipMessage(0x4002, 0x01, 0x02, 0x03, 0xff.toByte(), 0x00, 0xff.toByte(), 0x00) + val esRes = DoipMessageParser.parseUDP(esResMsg) + assertIs(esRes) + assertThat(esRes.nodeType).isEqualTo(0x01) + assertThat(esRes.numberOfSockets).isEqualTo(0x02) + assertThat(esRes.currentNumberOfSockets).isEqualTo(0x03) + assertThat(esRes.maxDataSize).isEqualTo(0xff00ff00.toInt()) + assertThat(esRes.message).isEqualTo(esResMsg) + + val pmReqMsg = doipMessage(0x4003) + val pmReq = DoipMessageParser.parseUDP(pmReqMsg) + assertIs(pmReq) + assertThat(pmReq.message).isEqualTo(pmReqMsg) + + val pmResMsg = doipMessage(0x4004, 0x01) + val pmRes = DoipMessageParser.parseUDP(pmResMsg) + assertIs(pmRes) + assertThat(pmRes.diagPowerMode).isEqualTo(0x01) + assertThat(pmRes.message).isEqualTo(pmResMsg) + } +} diff --git a/doip-sim-ecu-dsl/build.gradle.kts b/doip-sim-ecu-dsl/build.gradle.kts index d5da583..2ee17f7 100644 --- a/doip-sim-ecu-dsl/build.gradle.kts +++ b/doip-sim-ecu-dsl/build.gradle.kts @@ -14,6 +14,7 @@ repositories { } dependencies { + implementation(project(":doip-library")) implementation(kotlin("stdlib")) api("com.github.doip-sim-ecu:doip-simulation:1.4.8") diff --git a/settings.gradle.kts b/settings.gradle.kts index d8bad5a..2f122a6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,4 +9,5 @@ pluginManagement { rootProject.name = "doip-sim-ecu-dsl" +include("doip-library") include("doip-sim-ecu-dsl") From 992aeb4ec0163bd059c2a6f3ab67deb7e0ade9cf Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sat, 5 Feb 2022 11:27:33 +0100 Subject: [PATCH 02/10] more work on replacing library --- .../kotlin/DefaultDoipUdpMessageHandler.kt | 82 ++++++++++++ doip-library/src/main/kotlin/DoipEntities.kt | 68 ---------- doip-library/src/main/kotlin/DoipGateway.kt | 109 ++++++++++++++++ .../src/main/kotlin/DoipMessageParser.kt | 122 ------------------ doip-library/src/main/kotlin/DoipMessages.kt | 5 +- .../src/main/kotlin/DoipUDPMessageParser.kt | 108 ++++++++++++++++ .../src/main/kotlin/DoipUdpMessageHandler.kt | 120 +++++++++++++++++ doip-library/src/main/kotlin/Utils.kt | 36 ++++++ ...serTest.kt => DoipUDPMessageParserTest.kt} | 24 ++-- doip-library/src/test/kotlin/UtilsKtTest.kt | 45 +++++++ 10 files changed, 513 insertions(+), 206 deletions(-) create mode 100644 doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt delete mode 100644 doip-library/src/main/kotlin/DoipEntities.kt create mode 100644 doip-library/src/main/kotlin/DoipGateway.kt delete mode 100644 doip-library/src/main/kotlin/DoipMessageParser.kt create mode 100644 doip-library/src/main/kotlin/DoipUDPMessageParser.kt create mode 100644 doip-library/src/main/kotlin/DoipUdpMessageHandler.kt create mode 100644 doip-library/src/main/kotlin/Utils.kt rename doip-library/src/test/kotlin/{DoipMessageParserTest.kt => DoipUDPMessageParserTest.kt} (79%) create mode 100644 doip-library/src/test/kotlin/UtilsKtTest.kt diff --git a/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt b/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt new file mode 100644 index 0000000..153b167 --- /dev/null +++ b/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt @@ -0,0 +1,82 @@ +import io.ktor.network.sockets.* +import io.ktor.util.network.* +import io.ktor.utils.io.core.* +import kotlinx.coroutines.channels.SendChannel + +open class DefaultDoipUdpMessageHandler( + protected val config: DoipGatewayConfig +) : DoipUdpMessageHandler { + + companion object { + fun generateVamByGatewayConfig(config: DoipGatewayConfig): DoipUdpVehicleAnnouncementMessage = + DoipUdpVehicleAnnouncementMessage(config.vin, config.logicalAddress, config.gid, config.eid, 0, 0) + } + + suspend fun sendVamResponse( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + ) { + sendChannel.send( + Datagram( + packet = ByteReadPacket(generateVamByGatewayConfig(config).message), + address = sourceAddress + ) + ) + } + + override suspend fun handleUdpVehicleInformationRequest( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleInformationRequest + ) { + sendVamResponse(sendChannel, sourceAddress) + } + + override suspend fun handleUdpVehicleInformationRequestWithEid( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleInformationRequestWithEid + ) { + sendVamResponse(sendChannel, sourceAddress) + } + + override suspend fun handleUdpVehicleInformationRequestWithVIN( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleInformationRequestWithVIN + ) { + sendVamResponse(sendChannel, sourceAddress) + } + + override suspend fun handleUdpEntityStatusRequest( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpEntityStatusRequest + ) { + sendChannel.send( + Datagram( + packet = ByteReadPacket( + DoipUdpEntityStatusResponse(0, 255.toByte(), 0, 0xFFFF) + .message + ), + address = sourceAddress + ) + ) + } + + override suspend fun handleUdpDiagnosticPowerModeRequest( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpDiagnosticPowerModeRequest + ) { + sendChannel.send( + Datagram( + packet = ByteReadPacket( + DoipUdpDiagnosticPowerModeResponse(0) + .message + ), + address = sourceAddress + ) + ) + } +} diff --git a/doip-library/src/main/kotlin/DoipEntities.kt b/doip-library/src/main/kotlin/DoipEntities.kt deleted file mode 100644 index cda12bf..0000000 --- a/doip-library/src/main/kotlin/DoipEntities.kt +++ /dev/null @@ -1,68 +0,0 @@ -import io.ktor.network.selector.* -import io.ktor.network.sockets.* -import io.ktor.util.network.* -import io.ktor.utils.io.* -import io.ktor.utils.io.core.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import java.net.InetSocketAddress -import kotlin.concurrent.thread - -class DoipEntities { - fun start() { - thread(name = "UDP-RECV") { - runBlocking { - val socket = - aSocket(ActorSelectorManager(Dispatchers.IO)).udp().bind(InetSocketAddress("0.0.0.0", 13400)) { - this.broadcast = true -// socket.joinGroup(multicastAddress) - } - - while (!socket.isClosed) { - val datagram = socket.receive() - if (datagram.address.port != 13400) { - val data = datagram.packet.readText() - println(data) - socket.send( - Datagram( - ByteReadPacket(byteArrayOf(1, 2, 3)), - InetSocketAddress("255.255.255.255", 13400) - ) - ) - } - } - } - } - - thread(name = "TCP_RECV") { - runBlocking { - val serverSocket = - aSocket(ActorSelectorManager(Dispatchers.IO)).tcp().bind(InetSocketAddress("0.0.0.0", 13400)) - while (!serverSocket.isClosed) { - val socket = serverSocket.accept() - launch { - val input = socket.openReadChannel() - val output = socket.openWriteChannel(autoFlush = true) - try { - while (!socket.isClosed) { - val line = input.readUTF8Line() ?: break - - println("${socket.remoteAddress}: $line") - output.writeStringUtf8("$line\r\n") - } - } catch (e: Throwable) { - e.printStackTrace() - } finally { - withContext(Dispatchers.IO) { - socket.close() - } - } - } - } - - } - } - } -} diff --git a/doip-library/src/main/kotlin/DoipGateway.kt b/doip-library/src/main/kotlin/DoipGateway.kt new file mode 100644 index 0000000..b6d37f0 --- /dev/null +++ b/doip-library/src/main/kotlin/DoipGateway.kt @@ -0,0 +1,109 @@ +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.utils.io.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import java.net.InetAddress +import java.net.InetSocketAddress +import kotlin.concurrent.thread +import kotlin.concurrent.timer + +typealias GID = ByteArray +typealias EID = ByteArray +typealias VIN = ByteArray + +open class DoipGatewayConfig( + val name: String, + val logicalAddress: Short, + val gid: GID, + val eid: EID, + val vin: VIN, + val maxDataSize: UInt = 0xFFFF.toUInt(), + val localAddress: InetAddress = InetAddress.getByName("0.0.0.0"), + val localPort: Int = 13400, + val broadcastEnabled: Boolean = true, + val broadcastAddress: InetAddress = InetAddress.getByName("255.255.255.255"), + // TODO tlsPort +) { + init { + if (name.isEmpty()) { + throw IllegalArgumentException("name must be not empty") + } + if (gid.size != 6) { + throw IllegalArgumentException("gid must be 6 bytes") + } + if (eid.size != 6) { + throw IllegalArgumentException("eid must be 6 bytes") + } + if (vin.size != 17) { + throw IllegalArgumentException("vin must be 17 bytes") + } + } +} + +open class DoipGateway( + val config: DoipGatewayConfig, +) { + private val logger = LoggerFactory.getLogger(DoipGateway::class.java) + + protected fun createDoipUdpMessageHandler(): DoipUdpMessageHandler = + DefaultDoipUdpMessageHandler( + config + ) + + fun start() { + thread(name = "UDP-RECV") { + runBlocking { + val socket = + aSocket(ActorSelectorManager(Dispatchers.IO)) + .udp() + .bind(InetSocketAddress(config.localAddress, config.localPort)) { + this.broadcast = true +// socket.joinGroup(multicastAddress) + } + val udpMessageHandler = createDoipUdpMessageHandler() + while (!socket.isClosed) { + val datagram = socket.receive() + val message = DoipUDPMessageParser.parseUDP(datagram.packet) + MDC.put("ecuName", config.name) + udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) + } + } + } + + thread(name = "TCP-RECV") { + runBlocking { + val serverSocket = + aSocket(ActorSelectorManager(Dispatchers.IO)) + .tcp() + .bind(InetSocketAddress(config.localAddress, config.localPort)) + while (!serverSocket.isClosed) { + val socket = serverSocket.accept() + launch { + val input = socket.openReadChannel() + val output = socket.openWriteChannel(autoFlush = true) + try { + while (!socket.isClosed) { + val line = input.readUTF8Line() ?: break + + println("${socket.remoteAddress}: $line") + output.writeStringUtf8("$line\r\n") + } + } catch (e: Throwable) { + e.printStackTrace() + } finally { + withContext(Dispatchers.IO) { + socket.close() + } + } + } + } + + } + } + } +} diff --git a/doip-library/src/main/kotlin/DoipMessageParser.kt b/doip-library/src/main/kotlin/DoipMessageParser.kt deleted file mode 100644 index d90e338..0000000 --- a/doip-library/src/main/kotlin/DoipMessageParser.kt +++ /dev/null @@ -1,122 +0,0 @@ -import java.nio.ByteBuffer -import java.nio.ByteOrder -import kotlin.experimental.xor - -class IncorrectPatternFormat(message: String) : RuntimeException(message) -class HeaderTooShort(message: String) : RuntimeException(message) -class InvalidPayloadLength(message: String) : RuntimeException(message) -class UnknownPayloadType(message: String) : RuntimeException(message) - -fun doipMessage(payloadType: Short, vararg data: Byte): ByteArray { - val bb = ByteBuffer.allocate(8 + data.size) - bb.put(0x02) - bb.put(0xFD.toByte()) - bb.putShort(payloadType) - bb.putInt(data.size) - bb.put(data) - return bb.array() -} - -object DoipMessageParser { - private fun checkSyncPattern(data: ByteBuffer): Boolean { - if (data[0] != 0x02.toByte() || - data[1] != 0x02.toByte() xor 0xFF.toByte() - ) { - return false - } - return true - } - - private fun checkPayloadLength( - expectedLength: Int, - payloadLength: Int, - buffer: ByteBuffer, - block: ByteBuffer.() -> T - ): DoipUdpMessage { - if (buffer.limit() - 8 != payloadLength || expectedLength != payloadLength) { - throw InvalidPayloadLength("Payload isn't the right size ($payloadLength ${buffer.limit() - 8})") - } - return block(buffer) as DoipUdpMessage - } - - fun parseUDP(ba: ByteArray): DoipUdpMessage { - val data = ByteBuffer.wrap(ba) - data.rewind() - data.order(ByteOrder.BIG_ENDIAN) - - // Check header length - if (data.limit() < 8) { - throw HeaderTooShort("DoIP UDP message too short for interpretation") - } - if (!checkSyncPattern(data)) { - throw IncorrectPatternFormat("Sync Pattern isn't valid") - } - val payloadType = data.getShort(2) - val payloadLength = data.getInt(4) - - return when (payloadType) { - TYPE_HEADER_NACK -> checkPayloadLength(1, payloadLength, data) { DoipUdpHeaderNegAck(data[8]) } - - TYPE_UDP_VIR -> checkPayloadLength(0, payloadLength, data) { DoipUdpVehicleInformationRequest() } - - TYPE_UDP_VIR_EID -> checkPayloadLength( - 6, - payloadLength, - data - ) { DoipUdpVehicleInformationRequestWithEid(data.sliceArray(8, 6)) } - - TYPE_UDP_VIR_VIN -> checkPayloadLength( - 17, - payloadLength, - data - ) { DoipUdpVehicleInformationRequestWithVIN(data.sliceArray(8, 17)) } - - TYPE_UDP_VAM -> checkPayloadLength( - 33, - payloadLength, - data - ) { - DoipUdpVehicleVehicleAnnouncmentMessage( - vin = data.sliceArray(8, 17), - logicalAddress = data.getShort(25), - eid = data.sliceArray(27, 6), - gid = data.sliceArray(33, 6), - furtherActionRequired = data.get(39), - syncStatus = data.get(40) - ) - } - - TYPE_UDP_ENTITY_STATUS_REQ -> checkPayloadLength(0, payloadLength, data) { DoipUdpEntityStatusRequest() } - - TYPE_UDP_ENTITY_STATUS_RES -> checkPayloadLength(7, payloadLength, data) { - DoipUdpEntityStatusResponse( - nodeType = data.get(8), - numberOfSockets = data.get(9), - currentNumberOfSockets = data.get(10), - maxDataSize = data.getInt(11) - ) - } - - TYPE_UDP_DIAG_POWER_MODE_REQ -> checkPayloadLength( - 0, - payloadLength, - data - ) { DoipUdpDiagnosticPowerModeRequest() } - - TYPE_UDP_DIAG_POWER_MODE_RES -> checkPayloadLength( - 1, - payloadLength, - data - ) { DoipUdpDiagnosticPowerModeResponse(data.get(8)) } - - else -> throw UnknownPayloadType("$payloadType is an unknown payload type") - } - } -} - -fun ByteBuffer.sliceArray(index: Int, length: Int): ByteArray { - val ba = ByteArray(length) - this.get(index, ba, 0, length) - return ba -} - diff --git a/doip-library/src/main/kotlin/DoipMessages.kt b/doip-library/src/main/kotlin/DoipMessages.kt index ec31031..bafb16e 100644 --- a/doip-library/src/main/kotlin/DoipMessages.kt +++ b/doip-library/src/main/kotlin/DoipMessages.kt @@ -49,7 +49,7 @@ class DoipUdpVehicleInformationRequestWithVIN(val vin: ByteArray) : DoipUdpMessa val message by lazy { doipMessage(TYPE_UDP_VIR_VIN, *vin) } } -class DoipUdpVehicleVehicleAnnouncmentMessage( +class DoipUdpVehicleAnnouncementMessage( val vin: ByteArray, val logicalAddress: Short, val gid: ByteArray, @@ -111,6 +111,3 @@ class DoipUdpDiagnosticPowerModeRequest : DoipUdpMessage() { class DoipUdpDiagnosticPowerModeResponse(val diagPowerMode: Byte) : DoipUdpMessage() { val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_RES, diagPowerMode) } } - -fun Int.toByteArray(): ByteArray = - byteArrayOf((this and 0xFF000000.toInt() shr 24).toByte(), (this and 0xFF0000 shr 16).toByte(), (this and 0xFF00 shr 8).toByte(), (this and 0xFF).toByte()) diff --git a/doip-library/src/main/kotlin/DoipUDPMessageParser.kt b/doip-library/src/main/kotlin/DoipUDPMessageParser.kt new file mode 100644 index 0000000..6f9d05a --- /dev/null +++ b/doip-library/src/main/kotlin/DoipUDPMessageParser.kt @@ -0,0 +1,108 @@ +import io.ktor.utils.io.core.* +import java.nio.ByteBuffer +import kotlin.experimental.xor + +class IncorrectPatternFormat(message: String) : RuntimeException(message) +class HeaderTooShort(message: String) : RuntimeException(message) +class InvalidPayloadLength(message: String) : RuntimeException(message) +class UnknownPayloadType(message: String) : RuntimeException(message) + +object DoipUDPMessageParser { + private fun checkSyncPattern(protocolVersion: Byte, inverseProtocolVersion: Byte): Boolean { + if (protocolVersion != 0x02.toByte() || + inverseProtocolVersion != 0x02.toByte() xor 0xFF.toByte() + ) { + return false + } + return true + } + + private fun checkPayloadLength( + expectedLength: Int, + payloadLength: Int, + brp: ByteReadPacket, + block: () -> T + ): DoipUdpMessage { + if (brp.remaining.toInt() != payloadLength || expectedLength != payloadLength) { + throw InvalidPayloadLength("Payload isn't the right size ($payloadLength vs. ${brp.remaining})") + } + return block() as DoipUdpMessage + } + + fun parseUDP(ba: ByteArray): DoipUdpMessage = + parseUDP(ByteReadPacket(ba)) + + fun parseUDP(brp: ByteReadPacket): DoipUdpMessage { + // Check header length + if (brp.remaining < 8) { + throw HeaderTooShort("DoIP UDP message too short for interpretation") + } + + val protocolVersion = brp.readByte() + val inverseProtocolVersion = brp.readByte() + if (!checkSyncPattern(protocolVersion, inverseProtocolVersion)) { + throw IncorrectPatternFormat("Sync Pattern isn't valid") + } + val payloadType = brp.readShort() + val payloadLength = brp.readInt() + + return when (payloadType) { + TYPE_HEADER_NACK -> checkPayloadLength(1, payloadLength, brp) { DoipUdpHeaderNegAck(brp.readByte()) } + + TYPE_UDP_VIR -> checkPayloadLength(0, payloadLength, brp) { DoipUdpVehicleInformationRequest() } + + TYPE_UDP_VIR_EID -> checkPayloadLength( + 6, + payloadLength, + brp, + ) { DoipUdpVehicleInformationRequestWithEid(brp.readBytes(6)) } + + TYPE_UDP_VIR_VIN -> checkPayloadLength( + 17, + payloadLength, + brp + ) { DoipUdpVehicleInformationRequestWithVIN(brp.readBytes(17)) } + + TYPE_UDP_VAM -> checkPayloadLength( + 33, + payloadLength, + brp + ) { + DoipUdpVehicleAnnouncementMessage( + vin = brp.readBytes(17), + logicalAddress = brp.readShort(), + eid = brp.readBytes(6), + gid = brp.readBytes(6), + furtherActionRequired = brp.readByte(), + syncStatus = brp.readByte() + ) + } + + TYPE_UDP_ENTITY_STATUS_REQ -> checkPayloadLength(0, payloadLength, brp) { DoipUdpEntityStatusRequest() } + + TYPE_UDP_ENTITY_STATUS_RES -> checkPayloadLength(7, payloadLength, brp) { + DoipUdpEntityStatusResponse( + nodeType = brp.readByte(), + numberOfSockets = brp.readByte(), + currentNumberOfSockets = brp.readByte(), + maxDataSize = brp.readInt() + ) + } + + TYPE_UDP_DIAG_POWER_MODE_REQ -> checkPayloadLength( + 0, + payloadLength, + brp + ) { DoipUdpDiagnosticPowerModeRequest() } + + TYPE_UDP_DIAG_POWER_MODE_RES -> checkPayloadLength( + 1, + payloadLength, + brp + ) { DoipUdpDiagnosticPowerModeResponse(brp.readByte()) } + + else -> throw UnknownPayloadType("$payloadType is an unknown payload type") + } + } +} + diff --git a/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt b/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt new file mode 100644 index 0000000..7134be8 --- /dev/null +++ b/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt @@ -0,0 +1,120 @@ +import io.ktor.network.sockets.* +import io.ktor.util.network.* +import kotlinx.coroutines.channels.SendChannel + +interface DoipUdpMessageHandler { + suspend fun handleUdpMessage( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpMessage + ) { + when (message) { + is DoipUdpHeaderNegAck -> { + handleUdpHeaderNegAck(sendChannel, sourceAddress, message) + } + is DoipUdpVehicleInformationRequest -> { + handleUdpVehicleInformationRequest(sendChannel, sourceAddress, message) + } + is DoipUdpVehicleInformationRequestWithEid -> { + handleUdpVehicleInformationRequestWithEid(sendChannel, sourceAddress, message) + } + is DoipUdpVehicleInformationRequestWithVIN -> { + handleUdpVehicleInformationRequestWithVIN(sendChannel, sourceAddress, message) + } + is DoipUdpVehicleAnnouncementMessage -> { + handleUdpVehicleAnnouncementMessage(sendChannel, sourceAddress, message) + } + is DoipUdpEntityStatusRequest -> { + handleUdpEntityStatusRequest(sendChannel, sourceAddress, message) + } + is DoipUdpEntityStatusResponse -> { + handleUdpEntityStatusResponse(sendChannel, sourceAddress, message) + } + is DoipUdpDiagnosticPowerModeRequest -> { + handleUdpDiagnosticPowerModeRequest(sendChannel, sourceAddress, message) + } + is DoipUdpDiagnosticPowerModeResponse -> { + handleUdpDiagnosticPowerModeResponse(sendChannel, sourceAddress, message) + } + else -> { + handleUnknownDoipUdpMessage(sendChannel, sourceAddress, message) + } + } + } + + suspend fun handleUdpHeaderNegAck( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpHeaderNegAck + ) { + } + + suspend fun handleUdpVehicleInformationRequest( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleInformationRequest + ) { + } + + suspend fun handleUdpVehicleInformationRequestWithEid( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleInformationRequestWithEid + ) { + } + + suspend fun handleUdpVehicleInformationRequestWithVIN( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleInformationRequestWithVIN + ) { + } + + suspend fun handleUdpVehicleAnnouncementMessage( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpVehicleAnnouncementMessage + ) { + } + + suspend fun handleUdpEntityStatusRequest( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpEntityStatusRequest + ) { + + } + + suspend fun handleUdpEntityStatusResponse( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpEntityStatusResponse + ) { + + } + + suspend fun handleUdpDiagnosticPowerModeRequest( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpDiagnosticPowerModeRequest + ) { + + } + + suspend fun handleUdpDiagnosticPowerModeResponse( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpDiagnosticPowerModeResponse + ) { + + } + + suspend fun handleUnknownDoipUdpMessage( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + message: DoipUdpMessage + ) { + + } + +} diff --git a/doip-library/src/main/kotlin/Utils.kt b/doip-library/src/main/kotlin/Utils.kt new file mode 100644 index 0000000..b02066c --- /dev/null +++ b/doip-library/src/main/kotlin/Utils.kt @@ -0,0 +1,36 @@ +import java.nio.ByteBuffer + +/** + * Convert an int to a big-endian bytearray + */ +fun Int.toByteArray(): ByteArray = + byteArrayOf((this and 0xFF000000.toInt() shr 24).toByte(), (this and 0xFF0000 shr 16).toByte(), (this and 0xFF00 shr 8).toByte(), (this and 0xFF).toByte()) + +/** + * Convert a short to a big-endian bytearray + */ +fun Short.toByteArray(): ByteArray = + byteArrayOf((this.toInt() and 0xFF00 shr 8).toByte(), this.toByte()) + +/** + * Returns an array out of a bytebuffer, with absolute index position and length + */ +fun ByteBuffer.sliceArray(index: Int, length: Int): ByteArray { + val ba = ByteArray(length) + this.get(index, ba, 0, length) + return ba +} + +/** + * Convenience function to create a doip message + */ +fun doipMessage(payloadType: Short, vararg data: Byte): ByteArray { + val bb = ByteBuffer.allocate(8 + data.size) + bb.put(0x02) + bb.put(0xFD.toByte()) + bb.putShort(payloadType) + bb.putInt(data.size) + bb.put(data) + return bb.array() +} + diff --git a/doip-library/src/test/kotlin/DoipMessageParserTest.kt b/doip-library/src/test/kotlin/DoipUDPMessageParserTest.kt similarity index 79% rename from doip-library/src/test/kotlin/DoipMessageParserTest.kt rename to doip-library/src/test/kotlin/DoipUDPMessageParserTest.kt index ebe1147..84d7989 100644 --- a/doip-library/src/test/kotlin/DoipMessageParserTest.kt +++ b/doip-library/src/test/kotlin/DoipUDPMessageParserTest.kt @@ -3,39 +3,39 @@ import assertk.assertions.isEqualTo import org.junit.Test import kotlin.test.assertIs -class DoipMessageParserTest { +class DoipUDPMessageParserTest { @Test - fun test() { + fun `test udp messages`() { val eid = byteArrayOf(0x10, 0x20, 0x30, 0x40, 0x50, 0x60) val gid = byteArrayOf(0x60, 0x50, 0x40, 0x30, 0x20, 0x10) val vin = "01234567891234567".encodeToByteArray() val negAckMsg = doipMessage(0x0000, 0x10) - val negAck = DoipMessageParser.parseUDP(negAckMsg) + val negAck = DoipUDPMessageParser.parseUDP(negAckMsg) assertIs(negAck) assertThat(negAck.code).isEqualTo(0x10) assertThat(negAck.message).isEqualTo(negAckMsg) val virMsg = doipMessage(0x0001) - val vir = DoipMessageParser.parseUDP(virMsg) + val vir = DoipUDPMessageParser.parseUDP(virMsg) assertIs(vir) assertThat(vir.message).isEqualTo(virMsg) val virEIDMsg = doipMessage(0x0002, *eid) - val virEid = DoipMessageParser.parseUDP(virEIDMsg) + val virEid = DoipUDPMessageParser.parseUDP(virEIDMsg) assertIs(virEid) assertThat(virEid.eid).isEqualTo(eid) assertThat(virEid.message).isEqualTo(virEIDMsg) val virVINMsg = doipMessage(0x0003, *vin) - val virVIN = DoipMessageParser.parseUDP(virVINMsg) + val virVIN = DoipUDPMessageParser.parseUDP(virVINMsg) assertIs(virVIN) assertThat(virVIN.vin).isEqualTo(vin) assertThat(virVIN.message).isEqualTo(virVINMsg) val vamMsg = doipMessage(0x0004, *vin, 0x10, 0x01, *eid, *gid, 0x02, 0x03) - val vam = DoipMessageParser.parseUDP(vamMsg) - assertIs(vam) + val vam = DoipUDPMessageParser.parseUDP(vamMsg) + assertIs(vam) assertThat(vam.vin).isEqualTo(vin) assertThat(vam.logicalAddress).isEqualTo(0x1001) assertThat(vam.gid).isEqualTo(gid) @@ -45,12 +45,12 @@ class DoipMessageParserTest { assertThat(vam.message).isEqualTo(vamMsg) val esReqMsg = doipMessage(0x4001) - val esReq = DoipMessageParser.parseUDP(esReqMsg) + val esReq = DoipUDPMessageParser.parseUDP(esReqMsg) assertIs(esReq) assertThat(esReq.message).isEqualTo(esReqMsg) val esResMsg = doipMessage(0x4002, 0x01, 0x02, 0x03, 0xff.toByte(), 0x00, 0xff.toByte(), 0x00) - val esRes = DoipMessageParser.parseUDP(esResMsg) + val esRes = DoipUDPMessageParser.parseUDP(esResMsg) assertIs(esRes) assertThat(esRes.nodeType).isEqualTo(0x01) assertThat(esRes.numberOfSockets).isEqualTo(0x02) @@ -59,12 +59,12 @@ class DoipMessageParserTest { assertThat(esRes.message).isEqualTo(esResMsg) val pmReqMsg = doipMessage(0x4003) - val pmReq = DoipMessageParser.parseUDP(pmReqMsg) + val pmReq = DoipUDPMessageParser.parseUDP(pmReqMsg) assertIs(pmReq) assertThat(pmReq.message).isEqualTo(pmReqMsg) val pmResMsg = doipMessage(0x4004, 0x01) - val pmRes = DoipMessageParser.parseUDP(pmResMsg) + val pmRes = DoipUDPMessageParser.parseUDP(pmResMsg) assertIs(pmRes) assertThat(pmRes.diagPowerMode).isEqualTo(0x01) assertThat(pmRes.message).isEqualTo(pmResMsg) diff --git a/doip-library/src/test/kotlin/UtilsKtTest.kt b/doip-library/src/test/kotlin/UtilsKtTest.kt new file mode 100644 index 0000000..9df468a --- /dev/null +++ b/doip-library/src/test/kotlin/UtilsKtTest.kt @@ -0,0 +1,45 @@ +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.Test +import java.nio.ByteBuffer + +class UtilsKtTest { + @Test + fun `test short to ByteArray`() { + assertThat(0.toShort().toByteArray()).isEqualTo(byteArrayOf(0x00, 0x00)) + assertThat(1.toShort().toByteArray()).isEqualTo(byteArrayOf(0x00, 0x01)) + assertThat(32768.toShort().toByteArray()).isEqualTo(byteArrayOf(0x80.toByte(), 0x00)) + assertThat(65535.toShort().toByteArray()).isEqualTo(byteArrayOf(0xff.toByte(), 0xff.toByte())) + assertThat((-1).toShort().toByteArray()).isEqualTo(byteArrayOf(0xff.toByte(), 0xff.toByte())) + assertThat(Short.MAX_VALUE.toByteArray()).isEqualTo(byteArrayOf(0x7f, 0xff.toByte())) + assertThat(Short.MIN_VALUE.toByteArray()).isEqualTo(byteArrayOf(0x80.toByte(), 0x00.toByte())) + } + + @Test + fun `test int to ByteArray`() { + assertThat(0.toByteArray()).isEqualTo(byteArrayOf(0x00, 0x00, 0x00, 0x00)) + assertThat(255.toByteArray()).isEqualTo(byteArrayOf(0x00, 0x00, 0x00, 0xff.toByte())) + assertThat(Integer.MIN_VALUE.toByteArray()).isEqualTo(byteArrayOf(0x80.toByte(), 0x00.toByte(), 0x00.toByte(), 0x00.toByte())) + assertThat(Integer.MAX_VALUE.toByteArray()).isEqualTo(byteArrayOf(0x7f.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())) + assertThat((-1).toByteArray()).isEqualTo(byteArrayOf(0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())) + assertThat(0xFFFFFFFF.toInt().toByteArray()).isEqualTo(byteArrayOf(0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())) + } + + @Test + fun testByteBufferSlicedArray() { + fun genByteBuffer(length: Int): ByteBuffer { + val bb = ByteBuffer.allocate(length) + for (i in 0 until length) { + bb.put((i % 256).toByte()) + } + return bb + } + assertThat(ByteBuffer.wrap(byteArrayOf()).sliceArray(0, 0)).isEqualTo(byteArrayOf()) + val bb = genByteBuffer(512) + assertThat(bb.sliceArray(3, 3)).isEqualTo(byteArrayOf(3, 4, 5)) + assertThat(bb.sliceArray(0, 0)).isEqualTo(byteArrayOf()) + assertThat(bb.sliceArray(0, 1)).isEqualTo(byteArrayOf(0)) + assertThat(bb.sliceArray(2, 3)).isEqualTo(byteArrayOf(2, 3, 4)) + assertThat(bb.sliceArray(255, 3)).isEqualTo(byteArrayOf(0xff.toByte(), 0x00, 0x01)) + } +} From 399c1fa7d8f7d1c84afed63b0604f26294bd1a58 Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sat, 5 Feb 2022 22:07:52 +0100 Subject: [PATCH 03/10] more work on replacing library --- doip-library/build.gradle.kts | 2 +- .../kotlin/DefaultDoipUdpMessageHandler.kt | 14 +- doip-library/src/main/kotlin/DoipEntity.kt | 219 ++++++++++++++ doip-library/src/main/kotlin/DoipGateway.kt | 109 ------- doip-library/src/main/kotlin/DoipMessages.kt | 92 ------ .../src/main/kotlin/DoipTcpMessages.kt | 283 ++++++++++++++++++ .../src/main/kotlin/DoipUdpMessageHandler.kt | 16 + ...ssageParser.kt => DoipUdpMessageParser.kt} | 13 +- .../src/main/kotlin/DoipUdpMessages.kt | 98 ++++++ doip-library/src/main/kotlin/EcuConfig.kt | 5 + doip-library/src/main/kotlin/SimulatedEcu.kt | 29 ++ doip-library/src/main/kotlin/UdsMessage.kt | 32 ++ ...serTest.kt => DoipUdpMessageParserTest.kt} | 20 +- doip-sim-ecu-dsl/build.gradle.kts | 4 - doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt | 12 +- doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt | 55 +--- .../src/main/kotlin/SimGateway.kt | 63 ++-- .../main/kotlin/helper/LoggerExtensions.kt | 6 +- .../src/main/kotlin/helper/Timer.kt | 7 +- .../src/test/kotlin/SimDslTest.kt | 11 +- .../src/test/kotlin/SimEcuTest.kt | 76 +++-- 21 files changed, 812 insertions(+), 354 deletions(-) create mode 100644 doip-library/src/main/kotlin/DoipEntity.kt delete mode 100644 doip-library/src/main/kotlin/DoipGateway.kt create mode 100644 doip-library/src/main/kotlin/DoipTcpMessages.kt rename doip-library/src/main/kotlin/{DoipUDPMessageParser.kt => DoipUdpMessageParser.kt} (89%) create mode 100644 doip-library/src/main/kotlin/DoipUdpMessages.kt create mode 100644 doip-library/src/main/kotlin/EcuConfig.kt create mode 100644 doip-library/src/main/kotlin/SimulatedEcu.kt create mode 100644 doip-library/src/main/kotlin/UdsMessage.kt rename doip-library/src/test/kotlin/{DoipUDPMessageParserTest.kt => DoipUdpMessageParserTest.kt} (82%) diff --git a/doip-library/build.gradle.kts b/doip-library/build.gradle.kts index f5bc9c9..04b156c 100644 --- a/doip-library/build.gradle.kts +++ b/doip-library/build.gradle.kts @@ -14,7 +14,7 @@ dependencies { // implementation("io.ktor:ktor-server-core:$ktorVersion") // implementation("io.ktor:ktor-server-netty:$ktorVersion") - implementation("io.ktor:ktor-network:$ktorVersion") + api("io.ktor:ktor-network:$ktorVersion") // implementation("ch.qos.logback:logback-classic:1.2.5") testImplementation(kotlin("test")) diff --git a/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt b/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt index 153b167..0cafe21 100644 --- a/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt +++ b/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt @@ -4,11 +4,11 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.channels.SendChannel open class DefaultDoipUdpMessageHandler( - protected val config: DoipGatewayConfig + protected val config: DoipEntityConfig ) : DoipUdpMessageHandler { companion object { - fun generateVamByGatewayConfig(config: DoipGatewayConfig): DoipUdpVehicleAnnouncementMessage = + fun generateVamByEntityConfig(config: DoipEntityConfig): DoipUdpVehicleAnnouncementMessage = DoipUdpVehicleAnnouncementMessage(config.vin, config.logicalAddress, config.gid, config.eid, 0, 0) } @@ -18,7 +18,7 @@ open class DefaultDoipUdpMessageHandler( ) { sendChannel.send( Datagram( - packet = ByteReadPacket(generateVamByGatewayConfig(config).message), + packet = ByteReadPacket(generateVamByEntityConfig(config).message), address = sourceAddress ) ) @@ -37,7 +37,9 @@ open class DefaultDoipUdpMessageHandler( sourceAddress: NetworkAddress, message: DoipUdpVehicleInformationRequestWithEid ) { - sendVamResponse(sendChannel, sourceAddress) + if (config.eid.contentEquals(message.eid)) { + sendVamResponse(sendChannel, sourceAddress) + } } override suspend fun handleUdpVehicleInformationRequestWithVIN( @@ -45,7 +47,9 @@ open class DefaultDoipUdpMessageHandler( sourceAddress: NetworkAddress, message: DoipUdpVehicleInformationRequestWithVIN ) { - sendVamResponse(sendChannel, sourceAddress) + if (config.vin.contentEquals(message.vin)) { + sendVamResponse(sendChannel, sourceAddress) + } } override suspend fun handleUdpEntityStatusRequest( diff --git a/doip-library/src/main/kotlin/DoipEntity.kt b/doip-library/src/main/kotlin/DoipEntity.kt new file mode 100644 index 0000000..a01504a --- /dev/null +++ b/doip-library/src/main/kotlin/DoipEntity.kt @@ -0,0 +1,219 @@ +import io.ktor.network.selector.* +import io.ktor.network.sockets.* +import io.ktor.utils.io.* +import io.ktor.utils.io.core.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.SendChannel +import org.slf4j.LoggerFactory +import org.slf4j.MDC +import java.net.InetAddress +import java.net.InetSocketAddress +import kotlin.concurrent.fixedRateTimer +import kotlin.concurrent.thread + +typealias GID = ByteArray +typealias EID = ByteArray +typealias VIN = ByteArray + +open class DoipEntityConfig( + val name: String, + val logicalAddress: Short, + val gid: GID, + val eid: EID, + val vin: VIN, + val maxDataSize: Int = 0xFFFF, + val localAddress: InetAddress = InetAddress.getByName("0.0.0.0"), + val localPort: Int = 13400, + val broadcastEnabled: Boolean = true, + val broadcastAddress: InetAddress = InetAddress.getByName("255.255.255.255"), + // TODO tlsEnabled, tlsPort, certificate chain? + val ecuConfigList: MutableList = mutableListOf() +) { + init { + if (name.isEmpty()) { + throw IllegalArgumentException("name must be not empty") + } + if (gid.size != 6) { + throw IllegalArgumentException("gid must be 6 bytes") + } + if (eid.size != 6) { + throw IllegalArgumentException("eid must be 6 bytes") + } + if (vin.size != 17) { + throw IllegalArgumentException("vin must be 17 bytes") + } + } +} + +open class DoipEntity( + val config: DoipEntityConfig, +) : DiagnosticMessageHandler { + protected val logger = LoggerFactory.getLogger(DoipEntity::class.java) + + protected var targetEcusByPhysical: Map + protected var targetEcusByFunctional: MutableMap> + + protected var vamSentCounter = 0 + + init { + targetEcusByPhysical = this.config.ecuConfigList.associate { Pair(it.physicalAddress, createEcu(it)) } + targetEcusByFunctional = mutableMapOf() + targetEcusByPhysical.forEach { + val list = targetEcusByFunctional[it.key] + if (list == null) { + targetEcusByFunctional[it.key] = mutableListOf(it.value) + } else { + list.add(it.value) + } + } + } + + protected open fun createEcu(config: EcuConfig): SimulatedEcu = + SimulatedEcu(config) + + protected open fun createDoipUdpMessageHandler(): DoipUdpMessageHandler = + DefaultDoipUdpMessageHandler( + config + ) + + protected open fun createDoipTcpMessageHandler(socket: Socket): DoipTcpConnectionMessageHandler = + DefaultDoipTcpConnectionMessageHandler( + socket = socket, + logicalAddress = config.logicalAddress, + maxPayloadLength = config.maxDataSize - 8, + diagMessageHandler = this + ) + + protected suspend fun startVamTimer(sendChannel: SendChannel) { + if (config.broadcastEnabled) { + fixedRateTimer("${config.name}-VAM", daemon = true, initialDelay = 500, period = 500) { + if (vamSentCounter >= 3) { + this.cancel() + return@fixedRateTimer + } + val vam = DefaultDoipUdpMessageHandler.generateVamByEntityConfig(config) + runBlocking(Dispatchers.IO) { + sendChannel.send( + Datagram( + packet = ByteReadPacket(vam.message), + address = InetSocketAddress(config.broadcastAddress, 13400) + ) + ) + } + + vamSentCounter++ + } + } + } + + protected open suspend fun sendResponse(request: DoipTcpDiagMessage, output: ByteWriteChannel, data: ByteArray) { + if (data.isEmpty()) { + return + } + val response = DoipTcpDiagMessage( + sourceAddress = request.targetAddress, + targetAddress = request.sourceAddress, + payload = data + ) + output.writeFully(response.message) + } + + override fun existsTargetAddress(targetAddress: Short): Boolean = + targetEcusByPhysical.containsKey(targetAddress) || targetEcusByFunctional.containsKey(targetAddress) + + override suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) { + val ecu = targetEcusByPhysical[diagMessage.targetAddress] + ecu?.run { onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.PHYSICAL, output)) } + + val ecus = targetEcusByFunctional[diagMessage.targetAddress] + ecus?.forEach { + it.onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.FUNCTIONAL, output)) + } + } + + fun start() { + thread(name = "UDP-RECV") { + runBlocking { + val socket = + aSocket(ActorSelectorManager(Dispatchers.IO)) + .udp() + .bind(InetSocketAddress(config.localAddress, config.localPort)) { + this.broadcast = true +// socket.joinGroup(multicastAddress) + } + startVamTimer(socket.outgoing) + val udpMessageHandler = createDoipUdpMessageHandler() + while (!socket.isClosed) { + val datagram = socket.receive() + try { + MDC.put("ecuName", config.name) + val message = udpMessageHandler.parseMessage(datagram) + udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) + } catch (e: HeaderNegAckException) { + val code = when (e) { + is IncorrectPatternFormat -> DoipUdpHeaderNegAck.NACK_INCORRECT_PATTERN_FORMAT + is HeaderTooShort -> DoipUdpHeaderNegAck.NACK_INCORRECT_PATTERN_FORMAT + is InvalidPayloadLength -> DoipUdpHeaderNegAck.NACK_INVALID_PAYLOAD_LENGTH + is UnknownPayloadType -> DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE + else -> { + e.printStackTrace(); DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE + } // TODO log message + } + udpMessageHandler.respondHeaderNegAck( + socket.outgoing, + datagram.address, + code + ) + } catch (e: Exception) { + // TODO log + e.printStackTrace() + } + } + } + } + + thread(name = "TCP-RECV") { + runBlocking { + val serverSocket = + aSocket(ActorSelectorManager(Dispatchers.IO)) + .tcp() + .bind(InetSocketAddress(config.localAddress, config.localPort)) + while (!serverSocket.isClosed) { + val socket = serverSocket.accept() + launch { + val tcpMessageReceiver = createDoipTcpMessageHandler(socket) + val input = socket.openReadChannel() + val output = socket.openWriteChannel(autoFlush = tcpMessageReceiver.isAutoFlushEnabled()) + try { + while (!socket.isClosed) { + try { + val message = tcpMessageReceiver.receiveTcpData(input) + tcpMessageReceiver.handleTcpMessage(message, output) + } catch (e: HeaderNegAckException) { + e.printStackTrace() + val response = + DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message + output.writeFully(response, 0, response.size) + output.flush() + } catch (e: Exception) { + e.printStackTrace() + val response = + DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message + output.writeFully(response, 0, response.size) + output.flush() + } + } + } catch (e: Throwable) { + e.printStackTrace() + } finally { + withContext(Dispatchers.IO) { + socket.close() + } + } + } + } + + } + } + } +} diff --git a/doip-library/src/main/kotlin/DoipGateway.kt b/doip-library/src/main/kotlin/DoipGateway.kt deleted file mode 100644 index b6d37f0..0000000 --- a/doip-library/src/main/kotlin/DoipGateway.kt +++ /dev/null @@ -1,109 +0,0 @@ -import io.ktor.network.selector.* -import io.ktor.network.sockets.* -import io.ktor.utils.io.* -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import org.slf4j.LoggerFactory -import org.slf4j.MDC -import java.net.InetAddress -import java.net.InetSocketAddress -import kotlin.concurrent.thread -import kotlin.concurrent.timer - -typealias GID = ByteArray -typealias EID = ByteArray -typealias VIN = ByteArray - -open class DoipGatewayConfig( - val name: String, - val logicalAddress: Short, - val gid: GID, - val eid: EID, - val vin: VIN, - val maxDataSize: UInt = 0xFFFF.toUInt(), - val localAddress: InetAddress = InetAddress.getByName("0.0.0.0"), - val localPort: Int = 13400, - val broadcastEnabled: Boolean = true, - val broadcastAddress: InetAddress = InetAddress.getByName("255.255.255.255"), - // TODO tlsPort -) { - init { - if (name.isEmpty()) { - throw IllegalArgumentException("name must be not empty") - } - if (gid.size != 6) { - throw IllegalArgumentException("gid must be 6 bytes") - } - if (eid.size != 6) { - throw IllegalArgumentException("eid must be 6 bytes") - } - if (vin.size != 17) { - throw IllegalArgumentException("vin must be 17 bytes") - } - } -} - -open class DoipGateway( - val config: DoipGatewayConfig, -) { - private val logger = LoggerFactory.getLogger(DoipGateway::class.java) - - protected fun createDoipUdpMessageHandler(): DoipUdpMessageHandler = - DefaultDoipUdpMessageHandler( - config - ) - - fun start() { - thread(name = "UDP-RECV") { - runBlocking { - val socket = - aSocket(ActorSelectorManager(Dispatchers.IO)) - .udp() - .bind(InetSocketAddress(config.localAddress, config.localPort)) { - this.broadcast = true -// socket.joinGroup(multicastAddress) - } - val udpMessageHandler = createDoipUdpMessageHandler() - while (!socket.isClosed) { - val datagram = socket.receive() - val message = DoipUDPMessageParser.parseUDP(datagram.packet) - MDC.put("ecuName", config.name) - udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) - } - } - } - - thread(name = "TCP-RECV") { - runBlocking { - val serverSocket = - aSocket(ActorSelectorManager(Dispatchers.IO)) - .tcp() - .bind(InetSocketAddress(config.localAddress, config.localPort)) - while (!serverSocket.isClosed) { - val socket = serverSocket.accept() - launch { - val input = socket.openReadChannel() - val output = socket.openWriteChannel(autoFlush = true) - try { - while (!socket.isClosed) { - val line = input.readUTF8Line() ?: break - - println("${socket.remoteAddress}: $line") - output.writeStringUtf8("$line\r\n") - } - } catch (e: Throwable) { - e.printStackTrace() - } finally { - withContext(Dispatchers.IO) { - socket.close() - } - } - } - } - - } - } - } -} diff --git a/doip-library/src/main/kotlin/DoipMessages.kt b/doip-library/src/main/kotlin/DoipMessages.kt index bafb16e..b0ee407 100644 --- a/doip-library/src/main/kotlin/DoipMessages.kt +++ b/doip-library/src/main/kotlin/DoipMessages.kt @@ -1,5 +1,3 @@ -import java.lang.IllegalArgumentException - const val TYPE_HEADER_NACK: Short = 0x0000 const val TYPE_UDP_VIR: Short = 0x0001 const val TYPE_UDP_VIR_EID: Short = 0x0002 @@ -21,93 +19,3 @@ const val TYPE_TCP_DIAG_MESSAGE_NEG_ACK: Short = 0x8003.toShort() open class DoipMessage -open class DoipUdpMessage : DoipMessage() - -class DoipUdpHeaderNegAck(val code: Byte) : DoipUdpMessage() { - val message by lazy { doipMessage(TYPE_HEADER_NACK, code) } -} - -class DoipUdpVehicleInformationRequest : DoipUdpMessage() { - val message by lazy { doipMessage(TYPE_UDP_VIR) } -} - -class DoipUdpVehicleInformationRequestWithEid(val eid: ByteArray) : DoipUdpMessage() { - init { - if (eid.size != 6) { - throw IllegalArgumentException("eid must be 6 bytes") - } - } - val message by lazy { doipMessage(TYPE_UDP_VIR_EID, *eid) } -} - -class DoipUdpVehicleInformationRequestWithVIN(val vin: ByteArray) : DoipUdpMessage() { - init { - if (vin.size != 17) { - throw IllegalArgumentException("vin must be 17 bytes") - } - } - val message by lazy { doipMessage(TYPE_UDP_VIR_VIN, *vin) } -} - -class DoipUdpVehicleAnnouncementMessage( - val vin: ByteArray, - val logicalAddress: Short, - val gid: ByteArray, - val eid: ByteArray, - val furtherActionRequired: Byte, - val syncStatus: Byte -) : DoipUdpMessage() { - init { - if (vin.size != 17) { - throw IllegalArgumentException("vin must be 17 bytes") - } - if (gid.size != 6) { - throw IllegalArgumentException("gid must be 6 bytes") - } - if (eid.size != 6) { - throw IllegalArgumentException("eid must be 6 bytes") - } - } - - val message by lazy { - doipMessage( - TYPE_UDP_VAM, - *vin, - (logicalAddress.toInt() shr 8).toByte(), - logicalAddress.toByte(), - *eid, - *gid, - furtherActionRequired, - syncStatus - ) - } -} - -class DoipUdpEntityStatusRequest : DoipUdpMessage() { - val message by lazy { doipMessage(TYPE_UDP_ENTITY_STATUS_REQ) } -} - -class DoipUdpEntityStatusResponse( - val nodeType: Byte, - val numberOfSockets: Byte, - val currentNumberOfSockets: Byte, - val maxDataSize: Int -) : DoipUdpMessage() { - val message by lazy { - doipMessage( - TYPE_UDP_ENTITY_STATUS_RES, - nodeType, - numberOfSockets, - currentNumberOfSockets, - *maxDataSize.toByteArray() - ) - } -} - -class DoipUdpDiagnosticPowerModeRequest : DoipUdpMessage() { - val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_REQ) } -} - -class DoipUdpDiagnosticPowerModeResponse(val diagPowerMode: Byte) : DoipUdpMessage() { - val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_RES, diagPowerMode) } -} diff --git a/doip-library/src/main/kotlin/DoipTcpMessages.kt b/doip-library/src/main/kotlin/DoipTcpMessages.kt new file mode 100644 index 0000000..d98810c --- /dev/null +++ b/doip-library/src/main/kotlin/DoipTcpMessages.kt @@ -0,0 +1,283 @@ +import io.ktor.network.sockets.* +import io.ktor.utils.io.* +import kotlin.experimental.xor + +open class DoipTcpMessage + +interface DoipTcpConnectionMessageHandler { + suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage + suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) + suspend fun isAutoFlushEnabled(): Boolean +} + +class DoipTcpHeaderNegAck( + val code: Byte +) : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_HEADER_NACK, code) } +} + +class DoipTcpRoutingActivationRequest( + val sourceAddress: Short, + val activationType: Byte, + val oemData: Int = -1 +) : DoipTcpMessage() { + val message by lazy { + if (oemData != -1) + doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType, *oemData.toByteArray()) + else + doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType) + } +} + +class DoipTcpRoutingActivationResponse( + val testerAddress: Short, + val entityAddress: Short, + val responseCode: Byte +) : DoipTcpMessage() { + @Suppress("unused") + companion object { + const val RC_ERROR_UNKNOWN_SOURCE_ADDRESS: Byte = 0x00 + const val RC_ERROR_TCP_DATA_SOCKETS_EXHAUSED: Byte = 0x01 + const val RC_ERROR_DIFFERENT_SOURCE_ADDRESS: Byte = 0x02 + const val RC_ERROR_SOURCE_ADDRESS_ALREADY_ACTIVE: Byte = 0x03 + const val RC_ERROR_AUTHENTICATION_MISSING: Byte = 0x04 + const val RC_ERROR_CONFIRMATION_REJECTED: Byte = 0x05 + const val RC_ERROR_UNSUPPORTED_ACTIVATION_TYPE: Byte = 0x06 + const val RC_OK: Byte = 0x10 + const val RC_OK_REQUIRES_CONFIRMATION: Byte = 0x11 + } + val message by lazy { + doipMessage( + TYPE_TCP_ROUTING_RES, *testerAddress.toByteArray(), *entityAddress.toByteArray(), responseCode + ) + } +} + +class DoipTcpAliveCheckRequest : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_TCP_ALIVE_REQ) } +} + +class DoipTcpAliveCheckResponse(val sourceAddress: Short) : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_TCP_ALIVE_RES) } +} + +class DoipTcpDiagMessage( + val sourceAddress: Short, + val targetAddress: Short, + val payload: ByteArray +) : DoipTcpMessage() { + val message by lazy { + doipMessage( + TYPE_TCP_DIAG_MESSAGE, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), *payload + ) + } +} + +class DoipTcpDiagMessagePosAck( + val sourceAddress: Short, + val targetAddress: Short, + val ackCode: Byte, + val payload: ByteArray = ByteArray(0) +) : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_TCP_DIAG_MESSAGE_POS_ACK, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), ackCode, *payload) } +} + +class DoipTcpDiagMessageNegAck( + val sourceAddress: Short, + val targetAddress: Short, + val nackCode: Byte, + val payload: ByteArray = ByteArray(0) +) : DoipTcpMessage() { + @Suppress("unused") + companion object { + const val NACK_CODE_INVALID_SOURCE_ADDRESS: Byte = 0x02 + const val NACK_CODE_UNKNOWN_TARGET_ADDRESS: Byte = 0x03 + const val NACK_CODE_DIAGNOSTIC_MESSAGE_TOO_LARGE: Byte = 0x04 + const val NACK_CODE_OUT_OF_MEMORY: Byte = 0x05 + const val NACK_CODE_TARGET_UNREACHABLE: Byte = 0x06 + const val NACK_CODE_UNKNOWN_NETWORK: Byte = 0x07 + const val NACK_CODE_TRANSPORT_PROTOCOL_ERROR: Byte = 0x08 + } + val message by lazy { doipMessage(TYPE_TCP_DIAG_MESSAGE_NEG_ACK, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), nackCode, *payload) } +} + +private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int) { + if (condition) { + this.discardExact(n.toLong()) + } +} + +interface DiagnosticMessageHandler { + fun existsTargetAddress(targetAddress: Short): Boolean + suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) +} + +open class DefaultDoipTcpConnectionMessageHandler( + val socket: Socket, + val logicalAddress: Short, + val maxPayloadLength: Int, + val diagMessageHandler: DiagnosticMessageHandler) : + DoipTcpConnectionMessageHandler { + + override suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage { + val protocolVersion = brc.readByte() + val inverseProtocolVersion = brc.readByte() + if (protocolVersion != 0x02.toByte() || protocolVersion != (inverseProtocolVersion xor 0xFF.toByte())) { + throw IncorrectPatternFormat("Invalid header") + } + val payloadType = brc.readShort() + val payloadLength = brc.readInt() + if (payloadLength > maxPayloadLength) { + throw InvalidPayloadLength("Payload longer than maximum allowed length") + } + when (payloadType) { + TYPE_HEADER_NACK -> { + val code = brc.readByte() + brc.discardIf(payloadLength > 1, payloadLength - 1) + return DoipTcpHeaderNegAck(code) + } + TYPE_TCP_ROUTING_REQ -> { + val sourceAddress = brc.readShort() + val activationType = brc.readByte() + val oemData = if (payloadLength == 11) brc.readInt() else -1 + brc.discardIf(payloadLength > 11, payloadLength - 11) + return DoipTcpRoutingActivationRequest(sourceAddress, activationType, oemData) + } + TYPE_TCP_ROUTING_RES -> { + val testerAddress = brc.readShort() + val entityAddress = brc.readShort() + val responseCode = brc.readByte() + brc.discardIf(payloadLength > 5, payloadLength - 5) + return DoipTcpRoutingActivationResponse( + testerAddress = testerAddress, entityAddress = entityAddress, responseCode = responseCode + ) + } + TYPE_TCP_ALIVE_REQ -> { + return DoipTcpAliveCheckRequest() + } + TYPE_TCP_ALIVE_RES -> { + val sourceAddress = brc.readShort() + return DoipTcpAliveCheckResponse(sourceAddress) + } + TYPE_TCP_DIAG_MESSAGE -> { + val sourceAddress = brc.readShort() + val targetAddress = brc.readShort() + val payload = ByteArray(payloadLength - 4) + brc.readFully(payload, 0, payload.size) + return DoipTcpDiagMessage( + sourceAddress, targetAddress, payload + ) + } + TYPE_TCP_DIAG_MESSAGE_POS_ACK -> { + val sourceAddress = brc.readShort() + val targetAddress = brc.readShort() + val ackCode = brc.readByte() + val payload = ByteArray(payloadLength - 5) + brc.readFully(payload, 0, payload.size) + return DoipTcpDiagMessagePosAck( + sourceAddress = sourceAddress, + targetAddress = targetAddress, + ackCode = ackCode, + payload = payload + ) + } + TYPE_TCP_DIAG_MESSAGE_NEG_ACK -> { + val sourceAddress = brc.readShort() + val targetAddress = brc.readShort() + val ackCode = brc.readByte() + val payload = ByteArray(payloadLength - 5) + brc.readFully(payload, 0, payload.size) + return DoipTcpDiagMessageNegAck( + sourceAddress = sourceAddress, + targetAddress = targetAddress, + nackCode = ackCode, + payload = payload + ) + } + else -> throw UnknownPayloadType("Unknown payload type $payloadType") + } + } + + override suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) { + when (message) { + is DoipTcpHeaderNegAck -> handleTcpHeaderNegAck(message, output) + is DoipTcpRoutingActivationRequest -> handleTcpRoutingActivationRequest(message, output) + is DoipTcpRoutingActivationResponse -> handleTcpRoutingActivationResponse(message, output) + is DoipTcpAliveCheckRequest -> handleTcpAliveCheckRequest(message, output) + is DoipTcpAliveCheckResponse -> handleTcpAliveCheckResponse(message, output) + is DoipTcpDiagMessage -> handleTcpDiagMessage(message, output) + is DoipTcpDiagMessagePosAck -> handleTcpDiagMessagePosAck(message, output) + is DoipTcpDiagMessageNegAck -> handleTcpDiagMessageNegAck(message, output) + } + output.flush() + } + + override suspend fun isAutoFlushEnabled(): Boolean = false + + protected open suspend fun handleTcpHeaderNegAck(message: DoipTcpHeaderNegAck, output: ByteWriteChannel) { + // No implementation + } + + protected open suspend fun handleTcpRoutingActivationRequest(message: DoipTcpRoutingActivationRequest, output: ByteWriteChannel) { + if (message.activationType != 0x00.toByte() && message.activationType != 0x01.toByte()) { + output.writeFully( + DoipTcpRoutingActivationResponse( + message.sourceAddress, + logicalAddress, + DoipTcpRoutingActivationResponse.RC_ERROR_UNSUPPORTED_ACTIVATION_TYPE + ).message + ) + } else { + output.writeFully( + DoipTcpRoutingActivationResponse( + message.sourceAddress, + logicalAddress, + DoipTcpRoutingActivationResponse.RC_OK + ).message + ) + } + } + + protected open suspend fun handleTcpRoutingActivationResponse(message: DoipTcpRoutingActivationResponse, output: ByteWriteChannel) { + // No implementation + } + + protected open suspend fun handleTcpAliveCheckRequest(message: DoipTcpAliveCheckRequest, output: ByteWriteChannel) { + output.writeFully(DoipTcpAliveCheckResponse(logicalAddress).message) + } + + protected open suspend fun handleTcpAliveCheckResponse(message: DoipTcpAliveCheckResponse, output: ByteWriteChannel) { + // No implementation + } + + protected open suspend fun handleTcpDiagMessage(message: DoipTcpDiagMessage, output: ByteWriteChannel) { + if (diagMessageHandler.existsTargetAddress(message.targetAddress)) { + // Acknowledge message + val ack = DoipTcpDiagMessagePosAck( + message.targetAddress, + message.sourceAddress, + 0x00 + ) + output.writeFully(ack.message) + + diagMessageHandler.onIncomingDiagMessage(message, output) + } else { + // Reject message with unknown target address + val reject = DoipTcpDiagMessageNegAck( + message.targetAddress, + message.sourceAddress, + DoipTcpDiagMessageNegAck.NACK_CODE_UNKNOWN_TARGET_ADDRESS + ) + output.writeFully(reject.message) + } + } + + protected open suspend fun handleTcpDiagMessagePosAck(message: DoipTcpDiagMessagePosAck, output: ByteWriteChannel) { + // No implementation + } + + protected open suspend fun handleTcpDiagMessageNegAck(message: DoipTcpDiagMessageNegAck, output: ByteWriteChannel) { + // No implementation + } + +} diff --git a/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt b/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt index 7134be8..3b9b089 100644 --- a/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt +++ b/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt @@ -1,5 +1,6 @@ import io.ktor.network.sockets.* import io.ktor.util.network.* +import io.ktor.utils.io.core.* import kotlinx.coroutines.channels.SendChannel interface DoipUdpMessageHandler { @@ -117,4 +118,19 @@ interface DoipUdpMessageHandler { } + suspend fun respondHeaderNegAck( + sendChannel: SendChannel, + sourceAddress: NetworkAddress, + code: Byte + ) { + sendChannel.send( + Datagram( + packet = ByteReadPacket(DoipUdpHeaderNegAck(code).message), + address = sourceAddress + ) + ) + } + + suspend fun parseMessage(datagram: Datagram): DoipUdpMessage = + DoipUdpMessageParser.parseUDP(datagram.packet) } diff --git a/doip-library/src/main/kotlin/DoipUDPMessageParser.kt b/doip-library/src/main/kotlin/DoipUdpMessageParser.kt similarity index 89% rename from doip-library/src/main/kotlin/DoipUDPMessageParser.kt rename to doip-library/src/main/kotlin/DoipUdpMessageParser.kt index 6f9d05a..eae88f0 100644 --- a/doip-library/src/main/kotlin/DoipUDPMessageParser.kt +++ b/doip-library/src/main/kotlin/DoipUdpMessageParser.kt @@ -1,13 +1,14 @@ import io.ktor.utils.io.core.* -import java.nio.ByteBuffer import kotlin.experimental.xor -class IncorrectPatternFormat(message: String) : RuntimeException(message) -class HeaderTooShort(message: String) : RuntimeException(message) -class InvalidPayloadLength(message: String) : RuntimeException(message) -class UnknownPayloadType(message: String) : RuntimeException(message) +open class HeaderNegAckException(message: String) : RuntimeException(message) -object DoipUDPMessageParser { +class IncorrectPatternFormat(message: String) : HeaderNegAckException(message) +class HeaderTooShort(message: String) : HeaderNegAckException(message) +class InvalidPayloadLength(message: String) : HeaderNegAckException(message) +class UnknownPayloadType(message: String) : HeaderNegAckException(message) + +object DoipUdpMessageParser { private fun checkSyncPattern(protocolVersion: Byte, inverseProtocolVersion: Byte): Boolean { if (protocolVersion != 0x02.toByte() || inverseProtocolVersion != 0x02.toByte() xor 0xFF.toByte() diff --git a/doip-library/src/main/kotlin/DoipUdpMessages.kt b/doip-library/src/main/kotlin/DoipUdpMessages.kt new file mode 100644 index 0000000..2753885 --- /dev/null +++ b/doip-library/src/main/kotlin/DoipUdpMessages.kt @@ -0,0 +1,98 @@ +open class DoipUdpMessage : DoipMessage() + +class DoipUdpHeaderNegAck(val code: Byte) : DoipUdpMessage() { + companion object { + const val NACK_INCORRECT_PATTERN_FORMAT: Byte = 0 + const val NACK_UNKNOWN_PAYLOAD_TYPE: Byte = 1 + const val NACK_MESSAGE_TOO_LARGE: Byte = 2 + const val NACK_OUT_OF_MEMORY: Byte = 3 + const val NACK_INVALID_PAYLOAD_LENGTH: Byte = 4 + } + + val message by lazy { doipMessage(TYPE_HEADER_NACK, code) } +} + +class DoipUdpVehicleInformationRequest : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_VIR) } +} + +class DoipUdpVehicleInformationRequestWithEid(val eid: EID) : DoipUdpMessage() { + init { + if (eid.size != 6) { + throw IllegalArgumentException("eid must be 6 bytes") + } + } + val message by lazy { doipMessage(TYPE_UDP_VIR_EID, *eid) } +} + +class DoipUdpVehicleInformationRequestWithVIN(val vin: VIN) : DoipUdpMessage() { + init { + if (vin.size != 17) { + throw IllegalArgumentException("vin must be 17 bytes") + } + } + val message by lazy { doipMessage(TYPE_UDP_VIR_VIN, *vin) } +} + +class DoipUdpVehicleAnnouncementMessage( + val vin: VIN, + val logicalAddress: Short, + val gid: GID, + val eid: EID, + val furtherActionRequired: Byte, + val syncStatus: Byte +) : DoipUdpMessage() { + init { + if (vin.size != 17) { + throw IllegalArgumentException("vin must be 17 bytes") + } + if (gid.size != 6) { + throw IllegalArgumentException("gid must be 6 bytes") + } + if (eid.size != 6) { + throw IllegalArgumentException("eid must be 6 bytes") + } + } + + val message by lazy { + doipMessage( + TYPE_UDP_VAM, + *vin, + (logicalAddress.toInt() shr 8).toByte(), + logicalAddress.toByte(), + *eid, + *gid, + furtherActionRequired, + syncStatus + ) + } +} + +class DoipUdpEntityStatusRequest : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_ENTITY_STATUS_REQ) } +} + +class DoipUdpEntityStatusResponse( + val nodeType: Byte, + val numberOfSockets: Byte, + val currentNumberOfSockets: Byte, + val maxDataSize: Int +) : DoipUdpMessage() { + val message by lazy { + doipMessage( + TYPE_UDP_ENTITY_STATUS_RES, + nodeType, + numberOfSockets, + currentNumberOfSockets, + *maxDataSize.toByteArray() + ) + } +} + +class DoipUdpDiagnosticPowerModeRequest : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_REQ) } +} + +class DoipUdpDiagnosticPowerModeResponse(val diagPowerMode: Byte) : DoipUdpMessage() { + val message by lazy { doipMessage(TYPE_UDP_DIAG_POWER_MODE_RES, diagPowerMode) } +} diff --git a/doip-library/src/main/kotlin/EcuConfig.kt b/doip-library/src/main/kotlin/EcuConfig.kt new file mode 100644 index 0000000..be51c0d --- /dev/null +++ b/doip-library/src/main/kotlin/EcuConfig.kt @@ -0,0 +1,5 @@ +open class EcuConfig( + val name: String, + val physicalAddress: Short, + val functionalAddress: Short, +) diff --git a/doip-library/src/main/kotlin/SimulatedEcu.kt b/doip-library/src/main/kotlin/SimulatedEcu.kt new file mode 100644 index 0000000..da7bc0a --- /dev/null +++ b/doip-library/src/main/kotlin/SimulatedEcu.kt @@ -0,0 +1,29 @@ +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.util.concurrent.atomic.AtomicBoolean + +open class SimulatedEcu(val config: EcuConfig) { + protected val logger: Logger = LoggerFactory.getLogger(SimulatedEcu::class.java) + + private val isBusy: AtomicBoolean = AtomicBoolean(false) + + open fun handleRequest(request: UdsMessage) { + } + + open fun handleRequestIfBusy(request: UdsMessage) { + // Busy NRC + request.respond(byteArrayOf(0x7f, request.message[0], 0x21)) + } + + open fun onIncomingUdsMessage(request: UdsMessage) { + return if (isBusy.compareAndSet(false, true)) { + try { + handleRequest(request) + } finally { + isBusy.set(false) + } + } else { + handleRequestIfBusy(request) + } + } +} diff --git a/doip-library/src/main/kotlin/UdsMessage.kt b/doip-library/src/main/kotlin/UdsMessage.kt new file mode 100644 index 0000000..27d031c --- /dev/null +++ b/doip-library/src/main/kotlin/UdsMessage.kt @@ -0,0 +1,32 @@ +import io.ktor.utils.io.* +import kotlinx.coroutines.* + +open class UdsMessage( + val sourceAddress: Short, + val targetAddress: Short, + val targetAddressType: Int, + val message: ByteArray, + val output: ByteWriteChannel +) { + companion object { + const val PHYSICAL = 0 + const val FUNCTIONAL = 1 + } + + fun respond(data: ByteArray) { + val response = DoipTcpDiagMessage(targetAddress, sourceAddress, data) + + runBlocking { + output.writeFully(response.message) + } + } +} + +fun DoipTcpDiagMessage.toUdsMessage(addressType: Int, output: ByteWriteChannel): UdsMessage = + UdsMessage( + sourceAddress = this.sourceAddress, + targetAddress = this.targetAddress, + targetAddressType = addressType, + message = this.payload, + output = output, + ) diff --git a/doip-library/src/test/kotlin/DoipUDPMessageParserTest.kt b/doip-library/src/test/kotlin/DoipUdpMessageParserTest.kt similarity index 82% rename from doip-library/src/test/kotlin/DoipUDPMessageParserTest.kt rename to doip-library/src/test/kotlin/DoipUdpMessageParserTest.kt index 84d7989..8bc6cf7 100644 --- a/doip-library/src/test/kotlin/DoipUDPMessageParserTest.kt +++ b/doip-library/src/test/kotlin/DoipUdpMessageParserTest.kt @@ -3,7 +3,7 @@ import assertk.assertions.isEqualTo import org.junit.Test import kotlin.test.assertIs -class DoipUDPMessageParserTest { +class DoipUdpMessageParserTest { @Test fun `test udp messages`() { val eid = byteArrayOf(0x10, 0x20, 0x30, 0x40, 0x50, 0x60) @@ -11,30 +11,30 @@ class DoipUDPMessageParserTest { val vin = "01234567891234567".encodeToByteArray() val negAckMsg = doipMessage(0x0000, 0x10) - val negAck = DoipUDPMessageParser.parseUDP(negAckMsg) + val negAck = DoipUdpMessageParser.parseUDP(negAckMsg) assertIs(negAck) assertThat(negAck.code).isEqualTo(0x10) assertThat(negAck.message).isEqualTo(negAckMsg) val virMsg = doipMessage(0x0001) - val vir = DoipUDPMessageParser.parseUDP(virMsg) + val vir = DoipUdpMessageParser.parseUDP(virMsg) assertIs(vir) assertThat(vir.message).isEqualTo(virMsg) val virEIDMsg = doipMessage(0x0002, *eid) - val virEid = DoipUDPMessageParser.parseUDP(virEIDMsg) + val virEid = DoipUdpMessageParser.parseUDP(virEIDMsg) assertIs(virEid) assertThat(virEid.eid).isEqualTo(eid) assertThat(virEid.message).isEqualTo(virEIDMsg) val virVINMsg = doipMessage(0x0003, *vin) - val virVIN = DoipUDPMessageParser.parseUDP(virVINMsg) + val virVIN = DoipUdpMessageParser.parseUDP(virVINMsg) assertIs(virVIN) assertThat(virVIN.vin).isEqualTo(vin) assertThat(virVIN.message).isEqualTo(virVINMsg) val vamMsg = doipMessage(0x0004, *vin, 0x10, 0x01, *eid, *gid, 0x02, 0x03) - val vam = DoipUDPMessageParser.parseUDP(vamMsg) + val vam = DoipUdpMessageParser.parseUDP(vamMsg) assertIs(vam) assertThat(vam.vin).isEqualTo(vin) assertThat(vam.logicalAddress).isEqualTo(0x1001) @@ -45,12 +45,12 @@ class DoipUDPMessageParserTest { assertThat(vam.message).isEqualTo(vamMsg) val esReqMsg = doipMessage(0x4001) - val esReq = DoipUDPMessageParser.parseUDP(esReqMsg) + val esReq = DoipUdpMessageParser.parseUDP(esReqMsg) assertIs(esReq) assertThat(esReq.message).isEqualTo(esReqMsg) val esResMsg = doipMessage(0x4002, 0x01, 0x02, 0x03, 0xff.toByte(), 0x00, 0xff.toByte(), 0x00) - val esRes = DoipUDPMessageParser.parseUDP(esResMsg) + val esRes = DoipUdpMessageParser.parseUDP(esResMsg) assertIs(esRes) assertThat(esRes.nodeType).isEqualTo(0x01) assertThat(esRes.numberOfSockets).isEqualTo(0x02) @@ -59,12 +59,12 @@ class DoipUDPMessageParserTest { assertThat(esRes.message).isEqualTo(esResMsg) val pmReqMsg = doipMessage(0x4003) - val pmReq = DoipUDPMessageParser.parseUDP(pmReqMsg) + val pmReq = DoipUdpMessageParser.parseUDP(pmReqMsg) assertIs(pmReq) assertThat(pmReq.message).isEqualTo(pmReqMsg) val pmResMsg = doipMessage(0x4004, 0x01) - val pmRes = DoipUDPMessageParser.parseUDP(pmResMsg) + val pmRes = DoipUdpMessageParser.parseUDP(pmResMsg) assertIs(pmRes) assertThat(pmRes.diagPowerMode).isEqualTo(0x01) assertThat(pmRes.message).isEqualTo(pmResMsg) diff --git a/doip-sim-ecu-dsl/build.gradle.kts b/doip-sim-ecu-dsl/build.gradle.kts index 2ee17f7..f7cffb8 100644 --- a/doip-sim-ecu-dsl/build.gradle.kts +++ b/doip-sim-ecu-dsl/build.gradle.kts @@ -17,10 +17,6 @@ dependencies { implementation(project(":doip-library")) implementation(kotlin("stdlib")) - api("com.github.doip-sim-ecu:doip-simulation:1.4.8") - api("com.github.doip-sim-ecu:doip-library:1.1.11") - api("com.github.doip-sim-ecu:doip-logging:1.1.9") - testImplementation(kotlin("test")) testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0") testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") diff --git a/doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt b/doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt index 39779d0..38fe92f 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt +++ b/doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt @@ -1,4 +1,3 @@ -import doip.library.message.UdsMessage import helper.decodeHex import helper.toHexString import java.util.* @@ -55,10 +54,11 @@ object NrcError { open class RequestMessage(udsMessage: UdsMessage, val isBusy: Boolean) : UdsMessage( - udsMessage.sourceAdrress, + udsMessage.sourceAddress, udsMessage.targetAddress, udsMessage.targetAddressType, - udsMessage.message) + udsMessage.message, + udsMessage.output) /** * Define the response to be sent after the function returns @@ -337,8 +337,8 @@ open class RequestsData( */ open class EcuData( name: String, - var physicalAddress: Int = 0, - var functionalAddress: Int = 0, + var physicalAddress: Short = 0, + var functionalAddress: Short = 0, nrcOnNoMatch: Boolean = true, requests: List = emptyList(), resetHandler: List = emptyList(), @@ -370,7 +370,7 @@ fun reset() { } fun stop() { - gatewayInstances.forEach { it.stop() } +// gatewayInstances.forEach { it.stop() } gatewayInstances.clear() } diff --git a/doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt b/doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt index 3e89b80..5ac13bb 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt +++ b/doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt @@ -1,22 +1,9 @@ -import doip.library.message.UdsMessage -import doip.logging.Logger -import doip.simulation.nodes.EcuConfig -import doip.simulation.standard.StandardEcu import helper.* +import kotlinx.coroutines.runBlocking import java.util.* import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration -/** - * Extension function to provide the address by the targetAddressType provided in the UdsMessage - */ -fun EcuConfig.addressByType(message: UdsMessage): Int = - when (message.targetAddressType) { - UdsMessage.PHYSICAL -> this.physicalAddress - UdsMessage.FUNCTIONAL -> this.functionalAddress - else -> throw IllegalStateException("Unknown targetAddressType ${message.targetAddressType}") - } - class InterceptorData( val name: String, val interceptor: InterceptorResponseHandler, @@ -24,18 +11,16 @@ class InterceptorData( val isExpired: () -> Boolean ) : DataStorage() -fun EcuData.toEcuConfig(): EcuConfig { - val config = EcuConfig() - config.name = this.name - config.physicalAddress = this.physicalAddress - config.functionalAddress = this.functionalAddress - return config -} +fun EcuData.toEcuConfig(): EcuConfig = + EcuConfig( + name = name, + physicalAddress = physicalAddress, + functionalAddress = functionalAddress + ) -@Open -class SimEcu(private val data: EcuData) : StandardEcu(data.toEcuConfig()) { - val logger: Logger = doip.logging.LogManager.getLogger(SimEcu::class.java) +@Open +class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { private val internalDataStorage: MutableMap = ConcurrentHashMap() val name @@ -52,19 +37,8 @@ class SimEcu(private val data: EcuData) : StandardEcu(data.toEcuConfig()) { private val mainTimer: Timer by lazy { Timer("$name-Timer", true) } private val timers = ConcurrentHashMap() - fun sendResponse(request: UdsMessage, response: ByteArray) { - val sourceAddress = config.addressByType(request) - - val udsResponse = UdsMessage( - sourceAddress, - request.sourceAdrress, - request.targetAddressType, - response - ) - - clearCurrentRequest() - onSendUdsMessage(udsResponse) + request.respond(response) } private fun handleInterceptors(request: UdsMessage, busy: Boolean): Boolean { @@ -90,7 +64,9 @@ class SimEcu(private val data: EcuData) : StandardEcu(data.toEcuConfig()) { if (responseData.continueMatching) { return false } else if (responseData.response.isNotEmpty()) { - sendResponse(request, responseData.response) + runBlocking { + sendResponse(request, responseData.response) + } } return true } @@ -101,10 +77,9 @@ class SimEcu(private val data: EcuData) : StandardEcu(data.toEcuConfig()) { return false } - override fun handleRequestIfBusy(request: UdsMessage) { + override fun handleRequestIfBusy(request: UdsMessage){ if (handleInterceptors(request, true)) { logger.debugIf { "Incoming busy request ${request.message.toHexString(limit = 10)} was handled by interceptors" } - return } super.handleRequestIfBusy(request) } @@ -152,7 +127,6 @@ class SimEcu(private val data: EcuData) : StandardEcu(data.toEcuConfig()) { sendResponse(request, responseData.response) } else { logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> No response" } - clearCurrentRequest() } return } @@ -162,7 +136,6 @@ class SimEcu(private val data: EcuData) : StandardEcu(data.toEcuConfig()) { sendResponse(request, byteArrayOf(0x7F, request.message[0], NrcError.RequestOutOfRange)) } else { logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Ignore (nrcOnNoMatch = false)" } - clearCurrentRequest() } } diff --git a/doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt b/doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt index e85e052..ac6ac31 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt +++ b/doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt @@ -1,8 +1,3 @@ -import doip.simulation.nodes.Ecu -import doip.simulation.nodes.EcuConfig -import doip.simulation.nodes.GatewayConfig -import doip.simulation.standard.StandardGateway -import doip.simulation.standard.StandardTcpConnectionGateway import java.net.InetAddress import kotlin.properties.Delegates @@ -35,12 +30,12 @@ open class GatewayData(name: String) : RequestsData(name) { /** * The logical address under which the gateway shall be reachable */ - var logicalAddress by Delegates.notNull() + var logicalAddress by Delegates.notNull() /** * The functional address under which the gateway (and other ecus) shall be reachable */ - var functionalAddress by Delegates.notNull() + var functionalAddress by Delegates.notNull() /** * Vehicle identifier, 17 chars, will be filled with '0`, or if left null, set to 0xFF @@ -73,51 +68,41 @@ open class GatewayData(name: String) : RequestsData(name) { } } -private fun GatewayData.toGatewayConfig(): GatewayConfig { - val config = GatewayConfig() - config.name = this.name - config.gid = this.gid - config.eid = this.eid - config.localAddress = this.localAddress - config.localPort = this.localPort - config.multicastAddress = this.multicastAddress - config.broadcastEnable = this.broadcastEnable - config.broadcastAddress = this.broadcastAddress - - config.logicalAddress = this.logicalAddress - - // Fill up too short vin's with 'Z' - if no vin is given, use 0xFF, as defined in ISO 13400 for when no vin is set (yet) - config.vin = this.vin?.padEnd(17, 'Z')?.toByteArray() ?: ByteArray(17).let { it.fill(0xFF.toByte()); it } +private fun GatewayData.toGatewayConfig(): DoipEntityConfig { + val config = DoipEntityConfig( + name = this.name, + gid = this.gid, + eid = this.eid, + localAddress = this.localAddress, + localPort = this.localPort, + logicalAddress = this.logicalAddress, + broadcastEnabled = this.broadcastEnable, + broadcastAddress = this.broadcastAddress, + // Fill up too short vin's with 'Z' - if no vin is given, use 0xFF, as defined in ISO 13400 for when no vin is set (yet) + vin = this.vin?.padEnd(17, 'Z')?.toByteArray() ?: ByteArray(17).let { it.fill(0xFF.toByte()); it }, + ) // Add the gateway itself as an ecu, so it too can receive requests - val gateway = EcuConfig() - gateway.name = this.name - gateway.physicalAddress = this.logicalAddress - gateway.functionalAddress = this.functionalAddress - config.ecuConfigList.add(gateway) + val gatewayEcuConfig = EcuConfig( + name = this.name, + physicalAddress = this.logicalAddress, + functionalAddress = this.functionalAddress + ) + config.ecuConfigList.add(gatewayEcuConfig) // Add all the ecus defined for the gateway to the ecuConfigList, so they can later be found and instantiated as SimDslEcu config.ecuConfigList.addAll(this.ecus.map { it.toEcuConfig() }) return config } -class SimGateway(private val data: GatewayData) : StandardGateway(data.toGatewayConfig()) { - private val logger = doip.logging.LogManager.getLogger(SimGateway::class.java) - +class SimGateway(private val data: GatewayData) : DoipEntity(data.toGatewayConfig()) { val name: String get() = data.name val requests: List get() = data.requests - override fun createConnection(): StandardTcpConnectionGateway { - // Hacky way to increase stream buffer size -- there should be a setter in the connection - val con = super.createConnection() - con.streamBuffer.maxPayloadLength = 70000 - return con - } - - override fun createEcu(config: EcuConfig): Ecu { + override fun createEcu(config: EcuConfig): SimulatedEcu { // To be able to handle requests for the gateway itself, insert a dummy ecu with the gateways logicalAddress if (config.name == data.name) { val ecu = EcuData( @@ -143,7 +128,7 @@ class SimGateway(private val data: GatewayData) : StandardGateway(data.toGateway logger.info("Resetting Gateway $name") this.requests.forEach { it.reset() } if (recursiveEcus) { - this.ecuList.forEach { (it as SimEcu).reset() } + this.targetEcusByPhysical.forEach { (it.value as SimEcu).reset() } } } } diff --git a/doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt b/doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt index a132d2f..6525d7f 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt +++ b/doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt @@ -1,12 +1,14 @@ package helper -fun doip.logging.Logger.traceIf(supplier: () -> String) { +import org.slf4j.Logger + +fun Logger.traceIf(supplier: () -> String) { if (this.isTraceEnabled) { this.trace(supplier.invoke()) } } -fun doip.logging.Logger.debugIf(supplier: () -> String) { +fun Logger.debugIf(supplier: () -> String) { if (this.isDebugEnabled) { this.debug(supplier.invoke()) } diff --git a/doip-sim-ecu-dsl/src/main/kotlin/helper/Timer.kt b/doip-sim-ecu-dsl/src/main/kotlin/helper/Timer.kt index 42e3030..7772f5a 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/helper/Timer.kt +++ b/doip-sim-ecu-dsl/src/main/kotlin/helper/Timer.kt @@ -1,10 +1,9 @@ package helper -import doip.library.util.Helper -import doip.logging.LogManager +import org.slf4j.LoggerFactory import java.util.* -private val logger = LogManager.getLogger(EcuTimerTask::class.java) +private val logger = LoggerFactory.getLogger(EcuTimerTask::class.java) class EcuTimerTask(private val action: TimerTask.() -> Unit) : TimerTask() { private var _canBeRemoved = false @@ -13,7 +12,7 @@ class EcuTimerTask(private val action: TimerTask.() -> Unit) : TimerTask() { try { action() } catch (e: Exception) { - logger.error("Error while executing timer: " + Helper.getExceptionAsString(e)) + logger.error("Error while executing timer: " + e.stackTraceToString()) } finally { _canBeRemoved = true } diff --git a/doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt b/doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt index 4c9a34d..02edad6 100644 --- a/doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt +++ b/doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt @@ -2,10 +2,11 @@ import assertk.assertThat import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull -import doip.library.message.UdsMessage +import io.ktor.utils.io.* import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.mockito.Mockito import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -62,7 +63,13 @@ class SimDslTest { assertThat(gateways.size).isEqualTo(1) val ecuData = gateways[0].ecus[0] - val msg = UdsMessage(0x1, 0x2, byteArrayOf(0x22, 0x10, 0x20)) + val msg = UdsMessage( + 0x1, + 0x2, + UdsMessage.PHYSICAL, + byteArrayOf(0x22, 0x10, 0x20), + Mockito.mock(ByteWriteChannel::class.java) + ) val simEcu = SimEcu(ecuData) val responseData = RequestResponseData(ecuData.requests[0], msg, simEcu) ecuData.requests[0].responseHandler.invoke(responseData) diff --git a/doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt b/doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt index f9fc807..5a11233 100644 --- a/doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt +++ b/doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt @@ -3,9 +3,12 @@ import assertk.assertions.containsExactly import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue -import doip.library.message.UdsMessage +import io.ktor.utils.io.* import org.junit.jupiter.api.Test +import org.mockito.Mockito import org.mockito.kotlin.* +import java.lang.Thread.sleep +import kotlin.concurrent.thread import kotlin.time.Duration.Companion.milliseconds class SimEcuTest { @@ -53,7 +56,7 @@ class SimEcuTest { ecu.handleRequest(req(byteArrayOf(0x00, 0x10))) assertThat(first).isTrue() assertThat(second).isTrue() - verify(ecu, times(0)).onSendUdsMessage(any()) + verify(ecu, times(0)).sendResponse(any(), any()) } @Test @@ -74,7 +77,7 @@ class SimEcuTest { ecu.handleRequest(req(byteArrayOf(0x00, 0x10))) assertThat(first).isTrue() assertThat(second).isFalse() - verify(ecu, times(1)).onSendUdsMessage(any()) + verify(ecu, times(1)).sendResponse(any(), any()) } @Test @@ -150,15 +153,15 @@ class SimEcuTest { ecuWithNrc.handleRequest(req(byteArrayOf(0x10, 0x21))) ecuNoNrc.handleRequest(req(byteArrayOf(0x10, 0x21))) - verify(ecuWithNrc, times(1)).onSendUdsMessage(any()) - verify(ecuNoNrc, times(0)).onSendUdsMessage(any()) + verify(ecuWithNrc, times(1)).sendResponse(any(), any()) + verify(ecuNoNrc, times(0)).sendResponse(any(), any()) } @Test fun `test interceptor`() { val ecu = spy(SimEcu(ecuData(name = "TEST"))) verify(ecu, times(0)).sendResponse(any(), any()) - ecu.handleRequest(UdsMessage(0x0000, 0x0001, byteArrayOf(0x11, 0x03))) + ecu.handleRequest(req(byteArrayOf(0x11, 0x03))) // sendResponse got called, because there's no interceptor and NRC was sent verify(ecu, times(1)).sendResponse(any(), any()) @@ -172,20 +175,20 @@ class SimEcuTest { ecu.addOrReplaceEcuInterceptor("TEST", 200.milliseconds) { intercepted = true; true } ecu.addOrReplaceEcuInterceptor("TESTAFTER", 500.milliseconds) { afterInterceptor = true; false } - ecu.handleRequest(UdsMessage(0x0000, 0x0001, byteArrayOf(0x11, 0x03))) + ecu.handleRequest(req(byteArrayOf(0x11, 0x03))) // sendResponse didn't get called again, because there's one true interceptor, therefore no response was sent verify(ecu, times(1)).sendResponse(any(), any()) assertThat(removeInterceptor).isTrue() assertThat(beforeInterceptor).isTrue() assertThat(intercepted).isTrue() assertThat(afterInterceptor).isFalse() - Thread.sleep(200) + sleep(200) removeInterceptor = false beforeInterceptor = false intercepted = false ecu.removeInterceptor("TESTREMOVE") // sendResponse did get called again, because there's no true-interceptor anymore - ecu.handleRequest(UdsMessage(0x0000, 0x0001, byteArrayOf(0x11, 0x03))) + ecu.handleRequest(req(byteArrayOf(0x11, 0x03))) verify(ecu, times(2)).sendResponse(any(), any()) assertThat(removeInterceptor).isFalse() assertThat(beforeInterceptor).isTrue() @@ -198,24 +201,20 @@ class SimEcuTest { val ecu = SimEcu(ecuData(name = "TEST")) var noBusyCalled = false var busyCalled = false - ecu.requests.add(RequestMatcher("TEST", byteArrayOf(0x10, 0x03), null) { println("WAITING"); Thread.sleep(400); println("DONE") }) - ecu.addOrReplaceEcuInterceptor("NOBUSY", 1500.milliseconds) { println("NOTBUSY"); noBusyCalled = true; false; } - ecu.addOrReplaceEcuInterceptor("BUSY", 1500.milliseconds, true) { println("BUSY ${it.isBusy}"); if (it.isBusy) busyCalled = true; false; } - - ecu.start() - try { - ecu.putRequest(req(byteArrayOf(0x10, 0x03))) - Thread.sleep(100) - assertThat(noBusyCalled).isTrue() - assertThat(busyCalled).isFalse() - noBusyCalled = false - ecu.putRequest(req(byteArrayOf(0x10, 0x03))) - Thread.sleep(100) - assertThat(noBusyCalled).isFalse() - assertThat(busyCalled).isTrue() - } finally { - ecu.stop() + ecu.requests.add(RequestMatcher("TEST", byteArrayOf(0x10, 0x03), null) { println("WAITING"); sleep(1500); println("DONE") }) + ecu.addOrReplaceEcuInterceptor("NOBUSY", 3500.milliseconds) { println("NOTBUSY"); noBusyCalled = true; false; } + ecu.addOrReplaceEcuInterceptor("BUSY", 3500.milliseconds, true) { println("BUSY ${it.isBusy}"); if (it.isBusy) busyCalled = true; false; } + + thread { + ecu.onIncomingUdsMessage(req(byteArrayOf(0x10, 0x03))) } + sleep(600) + assertThat(noBusyCalled).isTrue() + assertThat(busyCalled).isFalse() + noBusyCalled = false + ecu.onIncomingUdsMessage(req(byteArrayOf(0x10, 0x03))) + assertThat(noBusyCalled).isFalse() + assertThat(busyCalled).isTrue() } @Test @@ -255,15 +254,15 @@ class SimEcuTest { ecu.handleRequest(req(byteArrayOf(0x3E, 0x00))) assertThat(timerCalled).isFalse() - Thread.sleep(20) + sleep(20) assertThat(timerCalled).isFalse() ecu.handleRequest(req(byteArrayOf(0x3E, 0x00))) assertThat(timerCalled).isFalse() - Thread.sleep(150) + sleep(150) assertThat(timerCalled).isFalse() ecu.handleRequest(req(byteArrayOf(0x3E, 0x00))) assertThat(timerCalled).isFalse() - Thread.sleep(220) + sleep(220) assertThat(timerCalled).isTrue() } @@ -383,8 +382,8 @@ class SimEcuTest { private fun ecuData( name: String, - physicalAddress: Int = 0x0001, - functionalAddress: Int = 0x0002, + physicalAddress: Short = 0x0001, + functionalAddress: Short = 0x0002, nrcOnNoMatch: Boolean = true, requests: List = emptyList() ): EcuData = @@ -396,6 +395,17 @@ class SimEcuTest { requests = requests ) - private fun req(data: ByteArray, sourceAddress: Int = 0x0000, targetAddress: Int = 0x0001): UdsMessage = - UdsMessage(sourceAddress, targetAddress, data) + private fun req( + data: ByteArray, + sourceAddress: Short = 0x0000, + targetAddress: Short = 0x0001, + targetAddressType: Int = UdsMessage.PHYSICAL + ): UdsMessage = + UdsMessage( + sourceAddress = sourceAddress, + targetAddress = targetAddress, + targetAddressType = targetAddressType, + message = data, + output = Mockito.mock(ByteWriteChannel::class.java) + ) } From b273f4254463fe4894848ce5f2ae0d3d71964b81 Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sun, 6 Feb 2022 12:44:46 +0100 Subject: [PATCH 04/10] more work on replacing library & restructured project --- build.gradle.kts | 46 ++++-- doip-library/build.gradle.kts | 23 --- doip-sim-ecu-dsl/build.gradle.kts | 48 ------- .../src/test/resources/log4j2.xml | 42 ------ settings.gradle.kts | 4 +- .../src => src}/main/kotlin/DataStorage.kt | 0 .../main/kotlin/ResponseExtensions.kt | 0 .../src => src}/main/kotlin/SimDsl.kt | 5 +- .../src => src}/main/kotlin/SimEcu.kt | 1 + .../src => src}/main/kotlin/SimGateway.kt | 4 + .../main/kotlin/helper/Annotations.kt | 0 .../src => src}/main/kotlin/helper/Timer.kt | 0 .../DefaultDoipTcpConnectionMessageHandler.kt | 132 +++--------------- .../library}/DefaultDoipUdpMessageHandler.kt | 2 + .../main/kotlin/library}/DoipEntity.kt | 83 ++++++----- .../main/kotlin/library}/DoipMessages.kt | 2 + src/main/kotlin/library/DoipTcpMessages.kt | 102 ++++++++++++++ .../kotlin/library}/DoipUdpMessageHandler.kt | 2 + .../kotlin/library}/DoipUdpMessageParser.kt | 5 +- .../main/kotlin/library}/DoipUdpMessages.kt | 3 + .../main/kotlin/library}/EcuConfig.kt | 2 + .../main/kotlin/library}/HexExtensions.kt | 2 +- .../main/kotlin/library}/LoggerExtensions.kt | 2 +- .../main/kotlin/library}/SimulatedEcu.kt | 3 + .../main/kotlin/library}/UdsMessage.kt | 2 + .../main/kotlin/library}/Utils.kt | 13 +- src/main/resources/logback.xml | 13 ++ .../src => src}/test/kotlin/SimDslTest.kt | 1 + .../src => src}/test/kotlin/SimEcuTest.kt | 1 + .../library}/DoipUdpMessageParserTest.kt | 4 +- .../kotlin/library}/HexExtensionsKtTest.kt | 5 +- .../test/kotlin/library}/UtilsKtTest.kt | 4 +- 32 files changed, 270 insertions(+), 286 deletions(-) delete mode 100644 doip-library/build.gradle.kts delete mode 100644 doip-sim-ecu-dsl/build.gradle.kts delete mode 100644 doip-sim-ecu-dsl/src/test/resources/log4j2.xml rename {doip-sim-ecu-dsl/src => src}/main/kotlin/DataStorage.kt (100%) rename {doip-sim-ecu-dsl/src => src}/main/kotlin/ResponseExtensions.kt (100%) rename {doip-sim-ecu-dsl/src => src}/main/kotlin/SimDsl.kt (99%) rename {doip-sim-ecu-dsl/src => src}/main/kotlin/SimEcu.kt (99%) rename {doip-sim-ecu-dsl/src => src}/main/kotlin/SimGateway.kt (97%) rename {doip-sim-ecu-dsl/src => src}/main/kotlin/helper/Annotations.kt (100%) rename {doip-sim-ecu-dsl/src => src}/main/kotlin/helper/Timer.kt (100%) rename doip-library/src/main/kotlin/DoipTcpMessages.kt => src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt (67%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/DefaultDoipUdpMessageHandler.kt (99%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/DoipEntity.kt (77%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/DoipMessages.kt (97%) create mode 100644 src/main/kotlin/library/DoipTcpMessages.kt rename {doip-library/src/main/kotlin => src/main/kotlin/library}/DoipUdpMessageHandler.kt (99%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/DoipUdpMessageParser.kt (97%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/DoipUdpMessages.kt (98%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/EcuConfig.kt (86%) rename {doip-sim-ecu-dsl/src/main/kotlin/helper => src/main/kotlin/library}/HexExtensions.kt (99%) rename {doip-sim-ecu-dsl/src/main/kotlin/helper => src/main/kotlin/library}/LoggerExtensions.kt (94%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/SimulatedEcu.kt (89%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/UdsMessage.kt (98%) rename {doip-library/src/main/kotlin => src/main/kotlin/library}/Utils.kt (72%) create mode 100644 src/main/resources/logback.xml rename {doip-sim-ecu-dsl/src => src}/test/kotlin/SimDslTest.kt (99%) rename {doip-sim-ecu-dsl/src => src}/test/kotlin/SimEcuTest.kt (99%) rename {doip-library/src/test/kotlin => src/test/kotlin/library}/DoipUdpMessageParserTest.kt (98%) rename {doip-sim-ecu-dsl/src/test/kotlin/helper => src/test/kotlin/library}/HexExtensionsKtTest.kt (92%) rename {doip-library/src/test/kotlin => src/test/kotlin/library}/UtilsKtTest.kt (97%) diff --git a/build.gradle.kts b/build.gradle.kts index 96b5d88..7503e72 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,25 +1,49 @@ -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -group = "com.github.doip-sim-ecu" -version = "0.2.5" - plugins { kotlin("jvm") + kotlin("plugin.allopen") + `maven-publish` + `java-library` } +group = "com.github.doip-sim-ecu" +version = "0.5.0" + repositories { mavenCentral() } dependencies { implementation(kotlin("stdlib-jdk8")) + api("io.ktor:ktor-network:1.6.7") + api("ch.qos.logback:logback-classic:1.2.10") + + testImplementation(kotlin("test")) + testImplementation("org.junit.jupiter:junit-jupiter:5.8.2") + testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0") + testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") +} + +tasks.test { + useJUnitPlatform() +} + +java { + withSourcesJar() } -val compileKotlin: KotlinCompile by tasks -compileKotlin.kotlinOptions { - jvmTarget = "1.8" +publishing { + publications { + create("mavenJava") { + from(components["java"]) + } + } } -val compileTestKotlin: KotlinCompile by tasks -compileTestKotlin.kotlinOptions { - jvmTarget = "1.8" + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" } + +allOpen { + annotation("helper.Open") +} + diff --git a/doip-library/build.gradle.kts b/doip-library/build.gradle.kts deleted file mode 100644 index 04b156c..0000000 --- a/doip-library/build.gradle.kts +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - kotlin("jvm") // Standalone: Add version - `java-library` -} - -repositories { - mavenCentral() -} - -val ktorVersion = "1.6.7" - -dependencies { - implementation(kotlin("stdlib")) - -// implementation("io.ktor:ktor-server-core:$ktorVersion") -// implementation("io.ktor:ktor-server-netty:$ktorVersion") - api("io.ktor:ktor-network:$ktorVersion") -// implementation("ch.qos.logback:logback-classic:1.2.5") - - testImplementation(kotlin("test")) - testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0") - testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") -} diff --git a/doip-sim-ecu-dsl/build.gradle.kts b/doip-sim-ecu-dsl/build.gradle.kts deleted file mode 100644 index f7cffb8..0000000 --- a/doip-sim-ecu-dsl/build.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -plugins { - kotlin("jvm") - kotlin("plugin.allopen") - `maven-publish` - `java-library` -} - -group = rootProject.group -version = rootProject.version - -repositories { - mavenCentral() - maven("https://jitpack.io") -} - -dependencies { - implementation(project(":doip-library")) - implementation(kotlin("stdlib")) - - testImplementation(kotlin("test")) - testImplementation("org.mockito.kotlin:mockito-kotlin:4.0.0") - testImplementation("com.willowtreeapps.assertk:assertk-jvm:0.25") -} - -tasks.test { - useJUnitPlatform() -} - -java { - withSourcesJar() -} - -publishing { - publications { - create("mavenJava") { - from(components["java"]) - } - } -} - -tasks.withType { - kotlinOptions.jvmTarget = "1.8" -} - -allOpen { - annotation("helper.Open") -} - diff --git a/doip-sim-ecu-dsl/src/test/resources/log4j2.xml b/doip-sim-ecu-dsl/src/test/resources/log4j2.xml deleted file mode 100644 index 71e9a33..0000000 --- a/doip-sim-ecu-dsl/src/test/resources/log4j2.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/settings.gradle.kts b/settings.gradle.kts index 2f122a6..211f3c8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,12 +2,10 @@ pluginManagement { val kotlinVersion: String by settings plugins { kotlin("jvm").version(kotlinVersion).apply(false) - id("com.github.johnrengelman.shadow") version "7.1.0" apply false kotlin("plugin.allopen").version(kotlinVersion).apply(false) } } rootProject.name = "doip-sim-ecu-dsl" -include("doip-library") -include("doip-sim-ecu-dsl") +//include("doip-sim-ecu-dsl-test") diff --git a/doip-sim-ecu-dsl/src/main/kotlin/DataStorage.kt b/src/main/kotlin/DataStorage.kt similarity index 100% rename from doip-sim-ecu-dsl/src/main/kotlin/DataStorage.kt rename to src/main/kotlin/DataStorage.kt diff --git a/doip-sim-ecu-dsl/src/main/kotlin/ResponseExtensions.kt b/src/main/kotlin/ResponseExtensions.kt similarity index 100% rename from doip-sim-ecu-dsl/src/main/kotlin/ResponseExtensions.kt rename to src/main/kotlin/ResponseExtensions.kt diff --git a/doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt b/src/main/kotlin/SimDsl.kt similarity index 99% rename from doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt rename to src/main/kotlin/SimDsl.kt index 38fe92f..b06adaf 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/SimDsl.kt +++ b/src/main/kotlin/SimDsl.kt @@ -1,5 +1,6 @@ -import helper.decodeHex -import helper.toHexString +import library.UdsMessage +import library.decodeHex +import library.toHexString import java.util.* import kotlin.IllegalArgumentException import kotlin.time.Duration diff --git a/doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt b/src/main/kotlin/SimEcu.kt similarity index 99% rename from doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt rename to src/main/kotlin/SimEcu.kt index 5ac13bb..35deec9 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/SimEcu.kt +++ b/src/main/kotlin/SimEcu.kt @@ -1,5 +1,6 @@ import helper.* import kotlinx.coroutines.runBlocking +import library.* import java.util.* import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration diff --git a/doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt b/src/main/kotlin/SimGateway.kt similarity index 97% rename from doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt rename to src/main/kotlin/SimGateway.kt index ac6ac31..97434fe 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/SimGateway.kt +++ b/src/main/kotlin/SimGateway.kt @@ -1,3 +1,7 @@ +import library.DoipEntity +import library.DoipEntityConfig +import library.EcuConfig +import library.SimulatedEcu import java.net.InetAddress import kotlin.properties.Delegates diff --git a/doip-sim-ecu-dsl/src/main/kotlin/helper/Annotations.kt b/src/main/kotlin/helper/Annotations.kt similarity index 100% rename from doip-sim-ecu-dsl/src/main/kotlin/helper/Annotations.kt rename to src/main/kotlin/helper/Annotations.kt diff --git a/doip-sim-ecu-dsl/src/main/kotlin/helper/Timer.kt b/src/main/kotlin/helper/Timer.kt similarity index 100% rename from doip-sim-ecu-dsl/src/main/kotlin/helper/Timer.kt rename to src/main/kotlin/helper/Timer.kt diff --git a/doip-library/src/main/kotlin/DoipTcpMessages.kt b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt similarity index 67% rename from doip-library/src/main/kotlin/DoipTcpMessages.kt rename to src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt index d98810c..abf04ef 100644 --- a/doip-library/src/main/kotlin/DoipTcpMessages.kt +++ b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt @@ -1,130 +1,23 @@ +package library + import io.ktor.network.sockets.* import io.ktor.utils.io.* import kotlin.experimental.xor -open class DoipTcpMessage - -interface DoipTcpConnectionMessageHandler { - suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage - suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) - suspend fun isAutoFlushEnabled(): Boolean -} - -class DoipTcpHeaderNegAck( - val code: Byte -) : DoipTcpMessage() { - val message by lazy { doipMessage(TYPE_HEADER_NACK, code) } -} - -class DoipTcpRoutingActivationRequest( - val sourceAddress: Short, - val activationType: Byte, - val oemData: Int = -1 -) : DoipTcpMessage() { - val message by lazy { - if (oemData != -1) - doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType, *oemData.toByteArray()) - else - doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType) - } -} - -class DoipTcpRoutingActivationResponse( - val testerAddress: Short, - val entityAddress: Short, - val responseCode: Byte -) : DoipTcpMessage() { - @Suppress("unused") - companion object { - const val RC_ERROR_UNKNOWN_SOURCE_ADDRESS: Byte = 0x00 - const val RC_ERROR_TCP_DATA_SOCKETS_EXHAUSED: Byte = 0x01 - const val RC_ERROR_DIFFERENT_SOURCE_ADDRESS: Byte = 0x02 - const val RC_ERROR_SOURCE_ADDRESS_ALREADY_ACTIVE: Byte = 0x03 - const val RC_ERROR_AUTHENTICATION_MISSING: Byte = 0x04 - const val RC_ERROR_CONFIRMATION_REJECTED: Byte = 0x05 - const val RC_ERROR_UNSUPPORTED_ACTIVATION_TYPE: Byte = 0x06 - const val RC_OK: Byte = 0x10 - const val RC_OK_REQUIRES_CONFIRMATION: Byte = 0x11 - } - val message by lazy { - doipMessage( - TYPE_TCP_ROUTING_RES, *testerAddress.toByteArray(), *entityAddress.toByteArray(), responseCode - ) - } -} - -class DoipTcpAliveCheckRequest : DoipTcpMessage() { - val message by lazy { doipMessage(TYPE_TCP_ALIVE_REQ) } -} - -class DoipTcpAliveCheckResponse(val sourceAddress: Short) : DoipTcpMessage() { - val message by lazy { doipMessage(TYPE_TCP_ALIVE_RES) } -} - -class DoipTcpDiagMessage( - val sourceAddress: Short, - val targetAddress: Short, - val payload: ByteArray -) : DoipTcpMessage() { - val message by lazy { - doipMessage( - TYPE_TCP_DIAG_MESSAGE, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), *payload - ) - } -} - -class DoipTcpDiagMessagePosAck( - val sourceAddress: Short, - val targetAddress: Short, - val ackCode: Byte, - val payload: ByteArray = ByteArray(0) -) : DoipTcpMessage() { - val message by lazy { doipMessage(TYPE_TCP_DIAG_MESSAGE_POS_ACK, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), ackCode, *payload) } -} - -class DoipTcpDiagMessageNegAck( - val sourceAddress: Short, - val targetAddress: Short, - val nackCode: Byte, - val payload: ByteArray = ByteArray(0) -) : DoipTcpMessage() { - @Suppress("unused") - companion object { - const val NACK_CODE_INVALID_SOURCE_ADDRESS: Byte = 0x02 - const val NACK_CODE_UNKNOWN_TARGET_ADDRESS: Byte = 0x03 - const val NACK_CODE_DIAGNOSTIC_MESSAGE_TOO_LARGE: Byte = 0x04 - const val NACK_CODE_OUT_OF_MEMORY: Byte = 0x05 - const val NACK_CODE_TARGET_UNREACHABLE: Byte = 0x06 - const val NACK_CODE_UNKNOWN_NETWORK: Byte = 0x07 - const val NACK_CODE_TRANSPORT_PROTOCOL_ERROR: Byte = 0x08 - } - val message by lazy { doipMessage(TYPE_TCP_DIAG_MESSAGE_NEG_ACK, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), nackCode, *payload) } -} - -private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int) { - if (condition) { - this.discardExact(n.toLong()) - } -} - -interface DiagnosticMessageHandler { - fun existsTargetAddress(targetAddress: Short): Boolean - suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) -} - open class DefaultDoipTcpConnectionMessageHandler( val socket: Socket, val logicalAddress: Short, val maxPayloadLength: Int, - val diagMessageHandler: DiagnosticMessageHandler) : + val diagMessageHandler: DiagnosticMessageHandler +) : DoipTcpConnectionMessageHandler { override suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage { val protocolVersion = brc.readByte() val inverseProtocolVersion = brc.readByte() - if (protocolVersion != 0x02.toByte() || protocolVersion != (inverseProtocolVersion xor 0xFF.toByte())) { - throw IncorrectPatternFormat("Invalid header") - } + if (protocolVersion != (inverseProtocolVersion xor 0xFF.toByte())) { + throw IncorrectPatternFormat("Invalid header $protocolVersion != $inverseProtocolVersion xor 255") + } val payloadType = brc.readShort() val payloadLength = brc.readInt() if (payloadLength > maxPayloadLength) { @@ -260,6 +153,7 @@ open class DefaultDoipTcpConnectionMessageHandler( ) output.writeFully(ack.message) + // Handle the UDS message diagMessageHandler.onIncomingDiagMessage(message, output) } else { // Reject message with unknown target address @@ -279,5 +173,15 @@ open class DefaultDoipTcpConnectionMessageHandler( protected open suspend fun handleTcpDiagMessageNegAck(message: DoipTcpDiagMessageNegAck, output: ByteWriteChannel) { // No implementation } +} +private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int) { + if (condition) { + this.discardExact(n.toLong()) + } +} + +interface DiagnosticMessageHandler { + fun existsTargetAddress(targetAddress: Short): Boolean + suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) } diff --git a/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt b/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt similarity index 99% rename from doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt rename to src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt index 0cafe21..23861b4 100644 --- a/doip-library/src/main/kotlin/DefaultDoipUdpMessageHandler.kt +++ b/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt @@ -1,3 +1,5 @@ +package library + import io.ktor.network.sockets.* import io.ktor.util.network.* import io.ktor.utils.io.core.* diff --git a/doip-library/src/main/kotlin/DoipEntity.kt b/src/main/kotlin/library/DoipEntity.kt similarity index 77% rename from doip-library/src/main/kotlin/DoipEntity.kt rename to src/main/kotlin/library/DoipEntity.kt index a01504a..1445ed4 100644 --- a/doip-library/src/main/kotlin/DoipEntity.kt +++ b/src/main/kotlin/library/DoipEntity.kt @@ -1,9 +1,11 @@ +package library + import io.ktor.network.selector.* import io.ktor.network.sockets.* import io.ktor.utils.io.* import io.ktor.utils.io.core.* import kotlinx.coroutines.* -import kotlinx.coroutines.channels.SendChannel +import kotlinx.coroutines.channels.ClosedReceiveChannelException import org.slf4j.LoggerFactory import org.slf4j.MDC import java.net.InetAddress @@ -50,24 +52,11 @@ open class DoipEntity( ) : DiagnosticMessageHandler { protected val logger = LoggerFactory.getLogger(DoipEntity::class.java) - protected var targetEcusByPhysical: Map - protected var targetEcusByFunctional: MutableMap> + protected var targetEcusByPhysical: Map = emptyMap() + protected var targetEcusByFunctional: MutableMap> = mutableMapOf() protected var vamSentCounter = 0 - init { - targetEcusByPhysical = this.config.ecuConfigList.associate { Pair(it.physicalAddress, createEcu(it)) } - targetEcusByFunctional = mutableMapOf() - targetEcusByPhysical.forEach { - val list = targetEcusByFunctional[it.key] - if (list == null) { - targetEcusByFunctional[it.key] = mutableListOf(it.value) - } else { - list.add(it.value) - } - } - } - protected open fun createEcu(config: EcuConfig): SimulatedEcu = SimulatedEcu(config) @@ -84,7 +73,7 @@ open class DoipEntity( diagMessageHandler = this ) - protected suspend fun startVamTimer(sendChannel: SendChannel) { + protected suspend fun startVamTimer(socket: BoundDatagramSocket) { if (config.broadcastEnabled) { fixedRateTimer("${config.name}-VAM", daemon = true, initialDelay = 500, period = 500) { if (vamSentCounter >= 3) { @@ -93,7 +82,7 @@ open class DoipEntity( } val vam = DefaultDoipUdpMessageHandler.generateVamByEntityConfig(config) runBlocking(Dispatchers.IO) { - sendChannel.send( + socket.send( Datagram( packet = ByteReadPacket(vam.message), address = InetSocketAddress(config.broadcastAddress, 13400) @@ -123,31 +112,48 @@ open class DoipEntity( override suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) { val ecu = targetEcusByPhysical[diagMessage.targetAddress] - ecu?.run { onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.PHYSICAL, output)) } + ecu?.run { + MDC.put("ecu", this.config.name) + onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.PHYSICAL, output)) + } val ecus = targetEcusByFunctional[diagMessage.targetAddress] ecus?.forEach { + MDC.put("ecu", this.config.name) it.onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.FUNCTIONAL, output)) } } fun start() { + targetEcusByPhysical = this.config.ecuConfigList.associate { Pair(it.physicalAddress, createEcu(it)) } + targetEcusByFunctional = mutableMapOf() + targetEcusByPhysical.forEach { + val list = targetEcusByFunctional[it.key] + if (list == null) { + targetEcusByFunctional[it.key] = mutableListOf(it.value) + } else { + list.add(it.value) + } + } + thread(name = "UDP-RECV") { runBlocking { val socket = aSocket(ActorSelectorManager(Dispatchers.IO)) - .udp() - .bind(InetSocketAddress(config.localAddress, config.localPort)) { + .udp() // InetSocketAddress(config.localAddress, config.localPort) + .bind(localAddress=InetSocketAddress(config.localAddress, 13400)) { this.broadcast = true // socket.joinGroup(multicastAddress) } - startVamTimer(socket.outgoing) + startVamTimer(socket) val udpMessageHandler = createDoipUdpMessageHandler() while (!socket.isClosed) { val datagram = socket.receive() try { - MDC.put("ecuName", config.name) + logger.traceIf { "Incoming UDP message" } + MDC.put("ecu", config.name) val message = udpMessageHandler.parseMessage(datagram) + logger.debugIf { "Message of type $message" } udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) } catch (e: HeaderNegAckException) { val code = when (e) { @@ -186,21 +192,32 @@ open class DoipEntity( val output = socket.openWriteChannel(autoFlush = tcpMessageReceiver.isAutoFlushEnabled()) try { while (!socket.isClosed) { + MDC.put("ecu", config.name) try { val message = tcpMessageReceiver.receiveTcpData(input) tcpMessageReceiver.handleTcpMessage(message, output) + } catch (e: ClosedReceiveChannelException) { + // ignore - socket was closed + logger.debug("Socket was closed unexpectedly") + withContext(Dispatchers.IO) { + socket.close() + } } catch (e: HeaderNegAckException) { - e.printStackTrace() - val response = - DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message - output.writeFully(response, 0, response.size) - output.flush() + if (!socket.isClosed) { + e.printStackTrace() + val response = + DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message + output.writeFully(response, 0, response.size) + output.flush() + } } catch (e: Exception) { - e.printStackTrace() - val response = - DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message - output.writeFully(response, 0, response.size) - output.flush() + if (!socket.isClosed) { + e.printStackTrace() + val response = + DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message + output.writeFully(response, 0, response.size) + output.flush() + } } } } catch (e: Throwable) { diff --git a/doip-library/src/main/kotlin/DoipMessages.kt b/src/main/kotlin/library/DoipMessages.kt similarity index 97% rename from doip-library/src/main/kotlin/DoipMessages.kt rename to src/main/kotlin/library/DoipMessages.kt index b0ee407..55dbe32 100644 --- a/doip-library/src/main/kotlin/DoipMessages.kt +++ b/src/main/kotlin/library/DoipMessages.kt @@ -1,3 +1,5 @@ +package library + const val TYPE_HEADER_NACK: Short = 0x0000 const val TYPE_UDP_VIR: Short = 0x0001 const val TYPE_UDP_VIR_EID: Short = 0x0002 diff --git a/src/main/kotlin/library/DoipTcpMessages.kt b/src/main/kotlin/library/DoipTcpMessages.kt new file mode 100644 index 0000000..3e158ae --- /dev/null +++ b/src/main/kotlin/library/DoipTcpMessages.kt @@ -0,0 +1,102 @@ +package library + +import io.ktor.utils.io.* + +open class DoipTcpMessage + +interface DoipTcpConnectionMessageHandler { + suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage + suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) + suspend fun isAutoFlushEnabled(): Boolean +} + +class DoipTcpHeaderNegAck( + val code: Byte +) : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_HEADER_NACK, code) } +} + +class DoipTcpRoutingActivationRequest( + val sourceAddress: Short, + val activationType: Byte, + val oemData: Int = -1 +) : DoipTcpMessage() { + val message by lazy { + if (oemData != -1) + doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType, *oemData.toByteArray()) + else + doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType) + } +} + +class DoipTcpRoutingActivationResponse( + val testerAddress: Short, + val entityAddress: Short, + val responseCode: Byte +) : DoipTcpMessage() { + @Suppress("unused") + companion object { + const val RC_ERROR_UNKNOWN_SOURCE_ADDRESS: Byte = 0x00 + const val RC_ERROR_TCP_DATA_SOCKETS_EXHAUSED: Byte = 0x01 + const val RC_ERROR_DIFFERENT_SOURCE_ADDRESS: Byte = 0x02 + const val RC_ERROR_SOURCE_ADDRESS_ALREADY_ACTIVE: Byte = 0x03 + const val RC_ERROR_AUTHENTICATION_MISSING: Byte = 0x04 + const val RC_ERROR_CONFIRMATION_REJECTED: Byte = 0x05 + const val RC_ERROR_UNSUPPORTED_ACTIVATION_TYPE: Byte = 0x06 + const val RC_OK: Byte = 0x10 + const val RC_OK_REQUIRES_CONFIRMATION: Byte = 0x11 + } + val message by lazy { + doipMessage( + TYPE_TCP_ROUTING_RES, *testerAddress.toByteArray(), *entityAddress.toByteArray(), responseCode + ) + } +} + +class DoipTcpAliveCheckRequest : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_TCP_ALIVE_REQ) } +} + +class DoipTcpAliveCheckResponse(val sourceAddress: Short) : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_TCP_ALIVE_RES) } +} + +class DoipTcpDiagMessage( + val sourceAddress: Short, + val targetAddress: Short, + val payload: ByteArray +) : DoipTcpMessage() { + val message by lazy { + doipMessage( + TYPE_TCP_DIAG_MESSAGE, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), *payload + ) + } +} + +class DoipTcpDiagMessagePosAck( + val sourceAddress: Short, + val targetAddress: Short, + val ackCode: Byte, + val payload: ByteArray = ByteArray(0) +) : DoipTcpMessage() { + val message by lazy { doipMessage(TYPE_TCP_DIAG_MESSAGE_POS_ACK, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), ackCode, *payload) } +} + +class DoipTcpDiagMessageNegAck( + val sourceAddress: Short, + val targetAddress: Short, + val nackCode: Byte, + val payload: ByteArray = ByteArray(0) +) : DoipTcpMessage() { + @Suppress("unused") + companion object { + const val NACK_CODE_INVALID_SOURCE_ADDRESS: Byte = 0x02 + const val NACK_CODE_UNKNOWN_TARGET_ADDRESS: Byte = 0x03 + const val NACK_CODE_DIAGNOSTIC_MESSAGE_TOO_LARGE: Byte = 0x04 + const val NACK_CODE_OUT_OF_MEMORY: Byte = 0x05 + const val NACK_CODE_TARGET_UNREACHABLE: Byte = 0x06 + const val NACK_CODE_UNKNOWN_NETWORK: Byte = 0x07 + const val NACK_CODE_TRANSPORT_PROTOCOL_ERROR: Byte = 0x08 + } + val message by lazy { doipMessage(TYPE_TCP_DIAG_MESSAGE_NEG_ACK, *sourceAddress.toByteArray(), *targetAddress.toByteArray(), nackCode, *payload) } +} diff --git a/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt b/src/main/kotlin/library/DoipUdpMessageHandler.kt similarity index 99% rename from doip-library/src/main/kotlin/DoipUdpMessageHandler.kt rename to src/main/kotlin/library/DoipUdpMessageHandler.kt index 3b9b089..acdb734 100644 --- a/doip-library/src/main/kotlin/DoipUdpMessageHandler.kt +++ b/src/main/kotlin/library/DoipUdpMessageHandler.kt @@ -1,3 +1,5 @@ +package library + import io.ktor.network.sockets.* import io.ktor.util.network.* import io.ktor.utils.io.core.* diff --git a/doip-library/src/main/kotlin/DoipUdpMessageParser.kt b/src/main/kotlin/library/DoipUdpMessageParser.kt similarity index 97% rename from doip-library/src/main/kotlin/DoipUdpMessageParser.kt rename to src/main/kotlin/library/DoipUdpMessageParser.kt index eae88f0..98b72df 100644 --- a/doip-library/src/main/kotlin/DoipUdpMessageParser.kt +++ b/src/main/kotlin/library/DoipUdpMessageParser.kt @@ -1,3 +1,5 @@ +package library + import io.ktor.utils.io.core.* import kotlin.experimental.xor @@ -10,8 +12,7 @@ class UnknownPayloadType(message: String) : HeaderNegAckException(message) object DoipUdpMessageParser { private fun checkSyncPattern(protocolVersion: Byte, inverseProtocolVersion: Byte): Boolean { - if (protocolVersion != 0x02.toByte() || - inverseProtocolVersion != 0x02.toByte() xor 0xFF.toByte() + if (protocolVersion != inverseProtocolVersion xor 0xFF.toByte() ) { return false } diff --git a/doip-library/src/main/kotlin/DoipUdpMessages.kt b/src/main/kotlin/library/DoipUdpMessages.kt similarity index 98% rename from doip-library/src/main/kotlin/DoipUdpMessages.kt rename to src/main/kotlin/library/DoipUdpMessages.kt index 2753885..de73374 100644 --- a/doip-library/src/main/kotlin/DoipUdpMessages.kt +++ b/src/main/kotlin/library/DoipUdpMessages.kt @@ -1,6 +1,9 @@ +package library + open class DoipUdpMessage : DoipMessage() class DoipUdpHeaderNegAck(val code: Byte) : DoipUdpMessage() { + @Suppress("unused") companion object { const val NACK_INCORRECT_PATTERN_FORMAT: Byte = 0 const val NACK_UNKNOWN_PAYLOAD_TYPE: Byte = 1 diff --git a/doip-library/src/main/kotlin/EcuConfig.kt b/src/main/kotlin/library/EcuConfig.kt similarity index 86% rename from doip-library/src/main/kotlin/EcuConfig.kt rename to src/main/kotlin/library/EcuConfig.kt index be51c0d..587c0ed 100644 --- a/doip-library/src/main/kotlin/EcuConfig.kt +++ b/src/main/kotlin/library/EcuConfig.kt @@ -1,3 +1,5 @@ +package library + open class EcuConfig( val name: String, val physicalAddress: Short, diff --git a/doip-sim-ecu-dsl/src/main/kotlin/helper/HexExtensions.kt b/src/main/kotlin/library/HexExtensions.kt similarity index 99% rename from doip-sim-ecu-dsl/src/main/kotlin/helper/HexExtensions.kt rename to src/main/kotlin/library/HexExtensions.kt index 5e45808..efc13e2 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/helper/HexExtensions.kt +++ b/src/main/kotlin/library/HexExtensions.kt @@ -1,4 +1,4 @@ -package helper +package library import java.nio.ByteBuffer import kotlin.math.min diff --git a/doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt b/src/main/kotlin/library/LoggerExtensions.kt similarity index 94% rename from doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt rename to src/main/kotlin/library/LoggerExtensions.kt index 6525d7f..be88f4a 100644 --- a/doip-sim-ecu-dsl/src/main/kotlin/helper/LoggerExtensions.kt +++ b/src/main/kotlin/library/LoggerExtensions.kt @@ -1,4 +1,4 @@ -package helper +package library import org.slf4j.Logger diff --git a/doip-library/src/main/kotlin/SimulatedEcu.kt b/src/main/kotlin/library/SimulatedEcu.kt similarity index 89% rename from doip-library/src/main/kotlin/SimulatedEcu.kt rename to src/main/kotlin/library/SimulatedEcu.kt index da7bc0a..39c585d 100644 --- a/doip-library/src/main/kotlin/SimulatedEcu.kt +++ b/src/main/kotlin/library/SimulatedEcu.kt @@ -1,3 +1,5 @@ +package library + import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.atomic.AtomicBoolean @@ -16,6 +18,7 @@ open class SimulatedEcu(val config: EcuConfig) { } open fun onIncomingUdsMessage(request: UdsMessage) { + logger.debugIf { "Incoming UDS-Message: ${request.message.toHexString()}" } return if (isBusy.compareAndSet(false, true)) { try { handleRequest(request) diff --git a/doip-library/src/main/kotlin/UdsMessage.kt b/src/main/kotlin/library/UdsMessage.kt similarity index 98% rename from doip-library/src/main/kotlin/UdsMessage.kt rename to src/main/kotlin/library/UdsMessage.kt index 27d031c..bc67ecf 100644 --- a/doip-library/src/main/kotlin/UdsMessage.kt +++ b/src/main/kotlin/library/UdsMessage.kt @@ -1,3 +1,5 @@ +package library + import io.ktor.utils.io.* import kotlinx.coroutines.* diff --git a/doip-library/src/main/kotlin/Utils.kt b/src/main/kotlin/library/Utils.kt similarity index 72% rename from doip-library/src/main/kotlin/Utils.kt rename to src/main/kotlin/library/Utils.kt index b02066c..ec4a0ca 100644 --- a/doip-library/src/main/kotlin/Utils.kt +++ b/src/main/kotlin/library/Utils.kt @@ -1,3 +1,5 @@ +package library + import java.nio.ByteBuffer /** @@ -26,8 +28,15 @@ fun ByteBuffer.sliceArray(index: Int, length: Int): ByteArray { */ fun doipMessage(payloadType: Short, vararg data: Byte): ByteArray { val bb = ByteBuffer.allocate(8 + data.size) - bb.put(0x02) - bb.put(0xFD.toByte()) + if (payloadType == TYPE_UDP_VIR || payloadType == TYPE_UDP_VIR_EID || payloadType == TYPE_UDP_VIR_VIN) { + /// Protocol version for vehicle identification request messages + bb.put(0xFF.toByte()) + bb.put(0x00) + } else { + // protocol version ISO 13400-2:2012 + bb.put(0x02) + bb.put(0xFD.toByte()) + } bb.putShort(payloadType) bb.putInt(data.size) bb.put(data) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..fc6fa17 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{ecu} %msg%n + + + + + + + diff --git a/doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt b/src/test/kotlin/SimDslTest.kt similarity index 99% rename from doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt rename to src/test/kotlin/SimDslTest.kt index 02edad6..811ad87 100644 --- a/doip-sim-ecu-dsl/src/test/kotlin/SimDslTest.kt +++ b/src/test/kotlin/SimDslTest.kt @@ -3,6 +3,7 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import assertk.assertions.isNull import io.ktor.utils.io.* +import library.UdsMessage import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows diff --git a/doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt b/src/test/kotlin/SimEcuTest.kt similarity index 99% rename from doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt rename to src/test/kotlin/SimEcuTest.kt index 5a11233..0902351 100644 --- a/doip-sim-ecu-dsl/src/test/kotlin/SimEcuTest.kt +++ b/src/test/kotlin/SimEcuTest.kt @@ -4,6 +4,7 @@ import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import io.ktor.utils.io.* +import library.UdsMessage import org.junit.jupiter.api.Test import org.mockito.Mockito import org.mockito.kotlin.* diff --git a/doip-library/src/test/kotlin/DoipUdpMessageParserTest.kt b/src/test/kotlin/library/DoipUdpMessageParserTest.kt similarity index 98% rename from doip-library/src/test/kotlin/DoipUdpMessageParserTest.kt rename to src/test/kotlin/library/DoipUdpMessageParserTest.kt index 8bc6cf7..618559c 100644 --- a/doip-library/src/test/kotlin/DoipUdpMessageParserTest.kt +++ b/src/test/kotlin/library/DoipUdpMessageParserTest.kt @@ -1,6 +1,8 @@ +package library + import assertk.assertThat import assertk.assertions.isEqualTo -import org.junit.Test +import org.junit.jupiter.api.Test import kotlin.test.assertIs class DoipUdpMessageParserTest { diff --git a/doip-sim-ecu-dsl/src/test/kotlin/helper/HexExtensionsKtTest.kt b/src/test/kotlin/library/HexExtensionsKtTest.kt similarity index 92% rename from doip-sim-ecu-dsl/src/test/kotlin/helper/HexExtensionsKtTest.kt rename to src/test/kotlin/library/HexExtensionsKtTest.kt index 16d851e..3601574 100644 --- a/doip-sim-ecu-dsl/src/test/kotlin/helper/HexExtensionsKtTest.kt +++ b/src/test/kotlin/library/HexExtensionsKtTest.kt @@ -1,7 +1,8 @@ -package helper +package library import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows class HexExtensionsKtTest { @Test @@ -42,6 +43,6 @@ class HexExtensionsKtTest { 0xfe.toByte() ), "AB CD EF 01 2E 1e 1f 00 fE".decodeHex() ) - assertThrows(IllegalArgumentException::class.java) { "54 45 53 5".decodeHex() } + assertThrows { "54 45 53 5".decodeHex() } } } diff --git a/doip-library/src/test/kotlin/UtilsKtTest.kt b/src/test/kotlin/library/UtilsKtTest.kt similarity index 97% rename from doip-library/src/test/kotlin/UtilsKtTest.kt rename to src/test/kotlin/library/UtilsKtTest.kt index 9df468a..fb100f6 100644 --- a/doip-library/src/test/kotlin/UtilsKtTest.kt +++ b/src/test/kotlin/library/UtilsKtTest.kt @@ -1,6 +1,8 @@ +package library + import assertk.assertThat import assertk.assertions.isEqualTo -import org.junit.Test +import org.junit.jupiter.api.Test import java.nio.ByteBuffer class UtilsKtTest { From 19b3d00c644fadec1e0b3187974a2899d3d83a25 Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sun, 6 Feb 2022 13:08:55 +0100 Subject: [PATCH 05/10] Remove unused function that doesn't work with jdk11, since ByteBuffers absolute bulk gets were only introduced in jdk13 --- src/main/kotlin/library/Utils.kt | 9 --------- src/test/kotlin/library/UtilsKtTest.kt | 18 ------------------ 2 files changed, 27 deletions(-) diff --git a/src/main/kotlin/library/Utils.kt b/src/main/kotlin/library/Utils.kt index ec4a0ca..d4817ea 100644 --- a/src/main/kotlin/library/Utils.kt +++ b/src/main/kotlin/library/Utils.kt @@ -14,15 +14,6 @@ fun Int.toByteArray(): ByteArray = fun Short.toByteArray(): ByteArray = byteArrayOf((this.toInt() and 0xFF00 shr 8).toByte(), this.toByte()) -/** - * Returns an array out of a bytebuffer, with absolute index position and length - */ -fun ByteBuffer.sliceArray(index: Int, length: Int): ByteArray { - val ba = ByteArray(length) - this.get(index, ba, 0, length) - return ba -} - /** * Convenience function to create a doip message */ diff --git a/src/test/kotlin/library/UtilsKtTest.kt b/src/test/kotlin/library/UtilsKtTest.kt index fb100f6..1320efe 100644 --- a/src/test/kotlin/library/UtilsKtTest.kt +++ b/src/test/kotlin/library/UtilsKtTest.kt @@ -26,22 +26,4 @@ class UtilsKtTest { assertThat((-1).toByteArray()).isEqualTo(byteArrayOf(0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())) assertThat(0xFFFFFFFF.toInt().toByteArray()).isEqualTo(byteArrayOf(0xff.toByte(), 0xff.toByte(), 0xff.toByte(), 0xff.toByte())) } - - @Test - fun testByteBufferSlicedArray() { - fun genByteBuffer(length: Int): ByteBuffer { - val bb = ByteBuffer.allocate(length) - for (i in 0 until length) { - bb.put((i % 256).toByte()) - } - return bb - } - assertThat(ByteBuffer.wrap(byteArrayOf()).sliceArray(0, 0)).isEqualTo(byteArrayOf()) - val bb = genByteBuffer(512) - assertThat(bb.sliceArray(3, 3)).isEqualTo(byteArrayOf(3, 4, 5)) - assertThat(bb.sliceArray(0, 0)).isEqualTo(byteArrayOf()) - assertThat(bb.sliceArray(0, 1)).isEqualTo(byteArrayOf(0)) - assertThat(bb.sliceArray(2, 3)).isEqualTo(byteArrayOf(2, 3, 4)) - assertThat(bb.sliceArray(255, 3)).isEqualTo(byteArrayOf(0xff.toByte(), 0x00, 0x01)) - } } From f0929345ef54b3f533a478142017462c833a24d9 Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sun, 6 Feb 2022 17:40:42 +0100 Subject: [PATCH 06/10] bugfixes for routing activation messages parsing, fixed & improved logging --- src/main/kotlin/SimEcu.kt | 28 +++++++------- src/main/kotlin/SimGateway.kt | 7 +--- .../DefaultDoipTcpConnectionMessageHandler.kt | 38 +++++++++++++------ .../library/DefaultDoipUdpMessageHandler.kt | 2 +- src/main/kotlin/library/DoipEntity.kt | 34 ++++++++--------- src/main/kotlin/library/DoipTcpMessages.kt | 17 +++++---- .../kotlin/library/DoipUdpMessageHandler.kt | 27 +++++++++---- src/main/kotlin/library/LoggerExtensions.kt | 14 +++++-- src/main/kotlin/library/SimulatedEcu.kt | 3 +- src/main/kotlin/library/UdsMessage.kt | 2 +- src/main/resources/logback.xml | 2 +- 11 files changed, 104 insertions(+), 70 deletions(-) diff --git a/src/main/kotlin/SimEcu.kt b/src/main/kotlin/SimEcu.kt index 35deec9..21e08d2 100644 --- a/src/main/kotlin/SimEcu.kt +++ b/src/main/kotlin/SimEcu.kt @@ -80,7 +80,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { override fun handleRequestIfBusy(request: UdsMessage){ if (handleInterceptors(request, true)) { - logger.debugIf { "Incoming busy request ${request.message.toHexString(limit = 10)} was handled by interceptors" } + logger.debugIf { "${config.name}: Incoming busy request ${request.message.toHexString(limit = 10)} was handled by interceptors" } } super.handleRequestIfBusy(request) } @@ -90,13 +90,13 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { */ override fun handleRequest(request: UdsMessage) { if (handleInterceptors(request, false)) { - logger.debugIf { "Incoming request ${request.message.toHexString(limit = 10)} was handled by interceptors" } + logger.debugIf { "${config.name}: Incoming request ${request.message.toHexString(limit = 10)} was handled by interceptors" } return } val normalizedRequest by lazy { request.message.toHexString("", limit = data.requestRegexMatchBytes, limitExceededSuffix = "") } - logger.traceIf { "Incoming request: ${request.message.toHexString()}" } + logger.traceIf { "${config.name}: Incoming request (${request.targetAddress}): ${request.message.toHexString()}" } // Note: We could build a lookup map to directly find the correct RequestMatcher for a binary input @@ -108,11 +108,11 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { requestIter.requestRegex!!.matches(normalizedRequest) } } catch (e: Exception) { - logger.error("Error while matching requests: ${e.message}") + logger.error("${config.name}: Error while matching requests: ${e.message}") throw e } - logger.traceIf { "Request: '${request.message.toHexString(limit = 10)}' try match '$requestIter' -> $matches" } + logger.traceIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' try match '$requestIter' -> $matches" } if (!matches) { continue @@ -121,22 +121,22 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { val responseData = ResponseData(caller = requestIter, request = request, ecu = this) requestIter.responseHandler.invoke(responseData) if (responseData.continueMatching) { - logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Continue matching" } + logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Continue matching" } continue } else if (responseData.response.isNotEmpty()) { - logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Send response '${responseData.response.toHexString(limit = 10)}'" } + logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Send response '${responseData.response.toHexString(limit = 10)}'" } sendResponse(request, responseData.response) } else { - logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> No response" } + logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> No response" } } return } if (this.data.nrcOnNoMatch) { - logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Sending NRC" } + logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Sending NRC" } sendResponse(request, byteArrayOf(0x7F, request.message[0], NrcError.RequestOutOfRange)) } else { - logger.debugIf { "Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Ignore (nrcOnNoMatch = false)" } + logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Ignore (nrcOnNoMatch = false)" } } } @@ -151,7 +151,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { alsoCallWhenEcuIsBusy: Boolean = false, interceptor: InterceptorResponseHandler ): String { - logger.traceIf { "Adding interceptor '$name' for $duration (busy: $alsoCallWhenEcuIsBusy)"} + logger.traceIf { "${config.name}: Adding interceptor '$name' for $duration (busy: $alsoCallWhenEcuIsBusy)"} // expires at expirationTime val expirationTime = if (duration == Duration.INFINITE) Long.MAX_VALUE else System.nanoTime() + duration.inWholeNanoseconds @@ -178,7 +178,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { * Please note that the internal resolution for delay is milliseconds */ fun addOrReplaceTimer(name: String, delay: Duration, handler: TimerTask.() -> Unit) { - logger.traceIf { "Adding or replacing timer '$name' to be executed after $delay"} + logger.traceIf { "${config.name}: Adding or replacing timer '$name' to be executed after $delay"} synchronized(mainTimer) { timers[name]?.cancel() @@ -199,7 +199,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { * Explicitly cancel a running timer */ fun cancelTimer(name: String) { - logger.traceIf { "Cancelling timer '$name'" } + logger.traceIf { "${config.name}: Cancelling timer '$name'" } synchronized(mainTimer) { timers[name]?.cancel() timers.remove(name) @@ -232,7 +232,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { this.data.requests.forEach { it.reset() } this.data.resetHandler.forEach { if (it.name != null) { - logger.trace("Calling onReset-Handler ${it.name}") + logger.trace("${config.name}: Calling onReset-Handler ${it.name}") } it.handler(this) } diff --git a/src/main/kotlin/SimGateway.kt b/src/main/kotlin/SimGateway.kt index 97434fe..ddc22a2 100644 --- a/src/main/kotlin/SimGateway.kt +++ b/src/main/kotlin/SimGateway.kt @@ -1,7 +1,4 @@ -import library.DoipEntity -import library.DoipEntityConfig -import library.EcuConfig -import library.SimulatedEcu +import library.* import java.net.InetAddress import kotlin.properties.Delegates @@ -129,7 +126,7 @@ class SimGateway(private val data: GatewayData) : DoipEntity(data.toGatewayConfi } fun reset(recursiveEcus: Boolean = true) { - logger.info("Resetting Gateway $name") + logger.infoIf { "Resetting Gateway $name" } this.requests.forEach { it.reset() } if (recursiveEcus) { this.targetEcusByPhysical.forEach { (it.value as SimEcu).reset() } diff --git a/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt index abf04ef..c773f25 100644 --- a/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt +++ b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt @@ -2,6 +2,9 @@ package library import io.ktor.network.sockets.* import io.ktor.utils.io.* +import library.DoipUdpMessageHandler.Companion.logger +import org.slf4j.Logger +import org.slf4j.LoggerFactory import kotlin.experimental.xor open class DefaultDoipTcpConnectionMessageHandler( @@ -9,10 +12,11 @@ open class DefaultDoipTcpConnectionMessageHandler( val logicalAddress: Short, val maxPayloadLength: Int, val diagMessageHandler: DiagnosticMessageHandler -) : - DoipTcpConnectionMessageHandler { +) : DoipTcpConnectionMessageHandler { + private val logger: Logger = LoggerFactory.getLogger(DefaultDoipTcpConnectionMessageHandler::class.java) override suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage { + logger.traceIf { "# receiveTcpData" } val protocolVersion = brc.readByte() val inverseProtocolVersion = brc.readByte() if (protocolVersion != (inverseProtocolVersion xor 0xFF.toByte())) { @@ -26,23 +30,28 @@ open class DefaultDoipTcpConnectionMessageHandler( when (payloadType) { TYPE_HEADER_NACK -> { val code = brc.readByte() - brc.discardIf(payloadLength > 1, payloadLength - 1) + brc.discardIf(payloadLength > 1, payloadLength - 1, payloadType) return DoipTcpHeaderNegAck(code) } TYPE_TCP_ROUTING_REQ -> { val sourceAddress = brc.readShort() val activationType = brc.readByte() - val oemData = if (payloadLength == 11) brc.readInt() else -1 - brc.discardIf(payloadLength > 11, payloadLength - 11) + brc.readInt() // Reserved for future standardization use + val oemData = if (payloadLength > 7) brc.readInt() else null + brc.discardIf(payloadLength > 11, payloadLength - 11, payloadType) return DoipTcpRoutingActivationRequest(sourceAddress, activationType, oemData) } TYPE_TCP_ROUTING_RES -> { val testerAddress = brc.readShort() val entityAddress = brc.readShort() val responseCode = brc.readByte() - brc.discardIf(payloadLength > 5, payloadLength - 5) + brc.readInt() // Reserved for future standardization use + val oemData = if (payloadLength > 9) brc.readInt() else null return DoipTcpRoutingActivationResponse( - testerAddress = testerAddress, entityAddress = entityAddress, responseCode = responseCode + testerAddress = testerAddress, + entityAddress = entityAddress, + responseCode = responseCode, + oemData = oemData ) } TYPE_TCP_ALIVE_REQ -> { @@ -92,6 +101,7 @@ open class DefaultDoipTcpConnectionMessageHandler( } override suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) { + logger.traceIf { "# handleTcpMessage $message" } when (message) { is DoipTcpHeaderNegAck -> handleTcpHeaderNegAck(message, output) is DoipTcpRoutingActivationRequest -> handleTcpRoutingActivationRequest(message, output) @@ -108,10 +118,11 @@ open class DefaultDoipTcpConnectionMessageHandler( override suspend fun isAutoFlushEnabled(): Boolean = false protected open suspend fun handleTcpHeaderNegAck(message: DoipTcpHeaderNegAck, output: ByteWriteChannel) { - // No implementation + logger.traceIf { "# handleTcpHeaderNegAck $message" } } protected open suspend fun handleTcpRoutingActivationRequest(message: DoipTcpRoutingActivationRequest, output: ByteWriteChannel) { + logger.traceIf { "# handleTcpRoutingActivationRequest $message" } if (message.activationType != 0x00.toByte() && message.activationType != 0x01.toByte()) { output.writeFully( DoipTcpRoutingActivationResponse( @@ -132,19 +143,22 @@ open class DefaultDoipTcpConnectionMessageHandler( } protected open suspend fun handleTcpRoutingActivationResponse(message: DoipTcpRoutingActivationResponse, output: ByteWriteChannel) { - // No implementation + logger.traceIf { "# handleTcpRoutingActivationResponse $message" } } protected open suspend fun handleTcpAliveCheckRequest(message: DoipTcpAliveCheckRequest, output: ByteWriteChannel) { + logger.traceIf { "# handleTcpAliveCheckRequest $message" } output.writeFully(DoipTcpAliveCheckResponse(logicalAddress).message) } protected open suspend fun handleTcpAliveCheckResponse(message: DoipTcpAliveCheckResponse, output: ByteWriteChannel) { - // No implementation + logger.traceIf { "# handleTcpAliveCheckResponse $message" } } protected open suspend fun handleTcpDiagMessage(message: DoipTcpDiagMessage, output: ByteWriteChannel) { + logger.traceIf { "# handleTcpDiagMessage $message for ${message.targetAddress}" } if (diagMessageHandler.existsTargetAddress(message.targetAddress)) { + logger.traceIf { "# targetAddress ${message.targetAddress} exists, sending positive ack" } // Acknowledge message val ack = DoipTcpDiagMessagePosAck( message.targetAddress, @@ -156,6 +170,7 @@ open class DefaultDoipTcpConnectionMessageHandler( // Handle the UDS message diagMessageHandler.onIncomingDiagMessage(message, output) } else { + logger.traceIf { "# targetAddress ${message.targetAddress} exists, sending negative ack" } // Reject message with unknown target address val reject = DoipTcpDiagMessageNegAck( message.targetAddress, @@ -175,8 +190,9 @@ open class DefaultDoipTcpConnectionMessageHandler( } } -private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int) { +private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int, payloadType: Short) { if (condition) { + logger.error("Discarding $n bytes for payload-type $payloadType") this.discardExact(n.toLong()) } } diff --git a/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt b/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt index 23861b4..bfc30b7 100644 --- a/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt +++ b/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt @@ -6,7 +6,7 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.channels.SendChannel open class DefaultDoipUdpMessageHandler( - protected val config: DoipEntityConfig + val config: DoipEntityConfig ) : DoipUdpMessageHandler { companion object { diff --git a/src/main/kotlin/library/DoipEntity.kt b/src/main/kotlin/library/DoipEntity.kt index 1445ed4..63c1e02 100644 --- a/src/main/kotlin/library/DoipEntity.kt +++ b/src/main/kotlin/library/DoipEntity.kt @@ -7,7 +7,6 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.ClosedReceiveChannelException import org.slf4j.LoggerFactory -import org.slf4j.MDC import java.net.InetAddress import java.net.InetSocketAddress import kotlin.concurrent.fixedRateTimer @@ -113,30 +112,30 @@ open class DoipEntity( override suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) { val ecu = targetEcusByPhysical[diagMessage.targetAddress] ecu?.run { - MDC.put("ecu", this.config.name) onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.PHYSICAL, output)) + return } val ecus = targetEcusByFunctional[diagMessage.targetAddress] ecus?.forEach { - MDC.put("ecu", this.config.name) it.onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.FUNCTIONAL, output)) } } fun start() { targetEcusByPhysical = this.config.ecuConfigList.associate { Pair(it.physicalAddress, createEcu(it)) } + targetEcusByFunctional = mutableMapOf() targetEcusByPhysical.forEach { - val list = targetEcusByFunctional[it.key] + val list = targetEcusByFunctional[it.value.config.functionalAddress] if (list == null) { - targetEcusByFunctional[it.key] = mutableListOf(it.value) + targetEcusByFunctional[it.value.config.functionalAddress] = mutableListOf(it.value) } else { list.add(it.value) } } - thread(name = "UDP-RECV") { + thread(name = "UDP") { runBlocking { val socket = aSocket(ActorSelectorManager(Dispatchers.IO)) @@ -151,9 +150,8 @@ open class DoipEntity( val datagram = socket.receive() try { logger.traceIf { "Incoming UDP message" } - MDC.put("ecu", config.name) val message = udpMessageHandler.parseMessage(datagram) - logger.debugIf { "Message of type $message" } + logger.traceIf { "Message is of type $message" } udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) } catch (e: HeaderNegAckException) { val code = when (e) { @@ -162,23 +160,23 @@ open class DoipEntity( is InvalidPayloadLength -> DoipUdpHeaderNegAck.NACK_INVALID_PAYLOAD_LENGTH is UnknownPayloadType -> DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE else -> { - e.printStackTrace(); DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE - } // TODO log message + DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE + } } + logger.debug("Error in Message-Header, sending negative acknowledgement", e) udpMessageHandler.respondHeaderNegAck( socket.outgoing, datagram.address, code ) } catch (e: Exception) { - // TODO log - e.printStackTrace() + logger.error("Unknown error while processing message", e) } } } } - thread(name = "TCP-RECV") { + thread(name = "TCP") { runBlocking { val serverSocket = aSocket(ActorSelectorManager(Dispatchers.IO)) @@ -187,24 +185,24 @@ open class DoipEntity( while (!serverSocket.isClosed) { val socket = serverSocket.accept() launch { + logger.debugIf { "New incoming TCP connection from ${socket.remoteAddress}" } val tcpMessageReceiver = createDoipTcpMessageHandler(socket) val input = socket.openReadChannel() val output = socket.openWriteChannel(autoFlush = tcpMessageReceiver.isAutoFlushEnabled()) try { while (!socket.isClosed) { - MDC.put("ecu", config.name) try { val message = tcpMessageReceiver.receiveTcpData(input) tcpMessageReceiver.handleTcpMessage(message, output) } catch (e: ClosedReceiveChannelException) { // ignore - socket was closed - logger.debug("Socket was closed unexpectedly") + logger.debug("Socket was closed by remote ${socket.remoteAddress}") withContext(Dispatchers.IO) { socket.close() } } catch (e: HeaderNegAckException) { if (!socket.isClosed) { - e.printStackTrace() + logger.debug("Error in Header while parsing message, sending negative acknowledgment", e) val response = DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message output.writeFully(response, 0, response.size) @@ -212,7 +210,7 @@ open class DoipEntity( } } catch (e: Exception) { if (!socket.isClosed) { - e.printStackTrace() + logger.error("Unknown error parsing/handling message, sending negative acknowledgment", e) val response = DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message output.writeFully(response, 0, response.size) @@ -221,7 +219,7 @@ open class DoipEntity( } } } catch (e: Throwable) { - e.printStackTrace() + logger.error("Unknown inside socket processing loop, closing socket", e) } finally { withContext(Dispatchers.IO) { socket.close() diff --git a/src/main/kotlin/library/DoipTcpMessages.kt b/src/main/kotlin/library/DoipTcpMessages.kt index 3e158ae..760f569 100644 --- a/src/main/kotlin/library/DoipTcpMessages.kt +++ b/src/main/kotlin/library/DoipTcpMessages.kt @@ -19,20 +19,18 @@ class DoipTcpHeaderNegAck( class DoipTcpRoutingActivationRequest( val sourceAddress: Short, val activationType: Byte, - val oemData: Int = -1 + val oemData: Int? = null ) : DoipTcpMessage() { val message by lazy { - if (oemData != -1) - doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType, *oemData.toByteArray()) - else - doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType) + doipMessage(TYPE_TCP_ROUTING_REQ, *sourceAddress.toByteArray(), activationType, *(oemData?.toByteArray() ?: ByteArray(0))) } } class DoipTcpRoutingActivationResponse( val testerAddress: Short, val entityAddress: Short, - val responseCode: Byte + val responseCode: Byte, + val oemData: Int? = null ) : DoipTcpMessage() { @Suppress("unused") companion object { @@ -48,7 +46,12 @@ class DoipTcpRoutingActivationResponse( } val message by lazy { doipMessage( - TYPE_TCP_ROUTING_RES, *testerAddress.toByteArray(), *entityAddress.toByteArray(), responseCode + TYPE_TCP_ROUTING_RES, + *testerAddress.toByteArray(), + *entityAddress.toByteArray(), + responseCode, + 0, 0, 0, 0, // Reserved for standardization use. + *(oemData?.toByteArray() ?: ByteArray(0)) ) } } diff --git a/src/main/kotlin/library/DoipUdpMessageHandler.kt b/src/main/kotlin/library/DoipUdpMessageHandler.kt index acdb734..68cd09d 100644 --- a/src/main/kotlin/library/DoipUdpMessageHandler.kt +++ b/src/main/kotlin/library/DoipUdpMessageHandler.kt @@ -4,13 +4,20 @@ import io.ktor.network.sockets.* import io.ktor.util.network.* import io.ktor.utils.io.core.* import kotlinx.coroutines.channels.SendChannel +import org.slf4j.Logger +import org.slf4j.LoggerFactory interface DoipUdpMessageHandler { + companion object { + val logger: Logger = LoggerFactory.getLogger(DoipUdpMessageHandler::class.java) + } + suspend fun handleUdpMessage( sendChannel: SendChannel, sourceAddress: NetworkAddress, message: DoipUdpMessage ) { + logger.traceIf { "> handleUdpMessage $message" } when (message) { is DoipUdpHeaderNegAck -> { handleUdpHeaderNegAck(sendChannel, sourceAddress, message) @@ -50,6 +57,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpHeaderNegAck ) { + logger.traceIf { "> handleUdpHeaderNegAck $message" } } suspend fun handleUdpVehicleInformationRequest( @@ -57,6 +65,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpVehicleInformationRequest ) { + logger.traceIf { "> handleUdpVehicleInformationRequest $message" } } suspend fun handleUdpVehicleInformationRequestWithEid( @@ -64,6 +73,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpVehicleInformationRequestWithEid ) { + logger.traceIf { "> handleUdpVehicleInformationRequestWithEid $message" } } suspend fun handleUdpVehicleInformationRequestWithVIN( @@ -71,6 +81,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpVehicleInformationRequestWithVIN ) { + logger.traceIf { "> handleUdpVehicleInformationRequestWithVIN $message" } } suspend fun handleUdpVehicleAnnouncementMessage( @@ -78,6 +89,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpVehicleAnnouncementMessage ) { + logger.traceIf { "> handleUdpVehicleAnnouncementMessage $message" } } suspend fun handleUdpEntityStatusRequest( @@ -85,7 +97,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpEntityStatusRequest ) { - + logger.traceIf { "> handleUdpEntityStatusRequest $message" } } suspend fun handleUdpEntityStatusResponse( @@ -93,7 +105,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpEntityStatusResponse ) { - + logger.traceIf { "> handleUdpEntityStatusResponse $message" } } suspend fun handleUdpDiagnosticPowerModeRequest( @@ -101,7 +113,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpDiagnosticPowerModeRequest ) { - + logger.traceIf { "> handleUdpDiagnosticPowerModeRequest $message" } } suspend fun handleUdpDiagnosticPowerModeResponse( @@ -109,7 +121,7 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpDiagnosticPowerModeResponse ) { - + logger.traceIf { "> handleUdpDiagnosticPowerModeResponse $message" } } suspend fun handleUnknownDoipUdpMessage( @@ -117,18 +129,19 @@ interface DoipUdpMessageHandler { sourceAddress: NetworkAddress, message: DoipUdpMessage ) { - + logger.traceIf { "> handleUnknownDoipUdpMessage $message" } } suspend fun respondHeaderNegAck( sendChannel: SendChannel, - sourceAddress: NetworkAddress, + address: NetworkAddress, code: Byte ) { + logger.traceIf { "> respondHeaderNegAck $code" } sendChannel.send( Datagram( packet = ByteReadPacket(DoipUdpHeaderNegAck(code).message), - address = sourceAddress + address = address ) ) } diff --git a/src/main/kotlin/library/LoggerExtensions.kt b/src/main/kotlin/library/LoggerExtensions.kt index be88f4a..d9ffbe6 100644 --- a/src/main/kotlin/library/LoggerExtensions.kt +++ b/src/main/kotlin/library/LoggerExtensions.kt @@ -2,14 +2,20 @@ package library import org.slf4j.Logger -fun Logger.traceIf(supplier: () -> String) { +fun Logger.traceIf(t: Throwable? = null, supplier: () -> String) { if (this.isTraceEnabled) { - this.trace(supplier.invoke()) + this.trace(supplier.invoke(), t) } } -fun Logger.debugIf(supplier: () -> String) { +fun Logger.debugIf(t: Throwable? = null, supplier: () -> String) { if (this.isDebugEnabled) { - this.debug(supplier.invoke()) + this.debug(supplier.invoke(), t) + } +} + +fun Logger.infoIf(t: Throwable? = null, supplier: () -> String) { + if (this.isInfoEnabled) { + this.info(supplier.invoke(), t) } } diff --git a/src/main/kotlin/library/SimulatedEcu.kt b/src/main/kotlin/library/SimulatedEcu.kt index 39c585d..b5c2935 100644 --- a/src/main/kotlin/library/SimulatedEcu.kt +++ b/src/main/kotlin/library/SimulatedEcu.kt @@ -10,15 +10,16 @@ open class SimulatedEcu(val config: EcuConfig) { private val isBusy: AtomicBoolean = AtomicBoolean(false) open fun handleRequest(request: UdsMessage) { + logger.debugIf { "${config.name}: Handle Request message: ${request.message.toHexString(limit = 20)}" } } open fun handleRequestIfBusy(request: UdsMessage) { // Busy NRC + logger.debugIf { "${config.name}: ECU is busy, sending busy-NRC" } request.respond(byteArrayOf(0x7f, request.message[0], 0x21)) } open fun onIncomingUdsMessage(request: UdsMessage) { - logger.debugIf { "Incoming UDS-Message: ${request.message.toHexString()}" } return if (isBusy.compareAndSet(false, true)) { try { handleRequest(request) diff --git a/src/main/kotlin/library/UdsMessage.kt b/src/main/kotlin/library/UdsMessage.kt index bc67ecf..336f162 100644 --- a/src/main/kotlin/library/UdsMessage.kt +++ b/src/main/kotlin/library/UdsMessage.kt @@ -30,5 +30,5 @@ fun DoipTcpDiagMessage.toUdsMessage(addressType: Int, output: ByteWriteChannel): targetAddress = this.targetAddress, targetAddressType = addressType, message = this.payload, - output = output, + output = output ) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index fc6fa17..9f31eed 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -3,7 +3,7 @@ - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %X{ecu} %msg%n + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n From 2838dd62a8646ec5b2d92f30cecbc192be43234f Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Sun, 6 Feb 2022 20:05:27 +0100 Subject: [PATCH 07/10] test logging with colors and MDC --- src/main/kotlin/SimEcu.kt | 31 +++++++++---------- src/main/kotlin/SimGateway.kt | 3 -- .../DefaultDoipTcpConnectionMessageHandler.kt | 3 ++ src/main/kotlin/library/DoipEntity.kt | 15 +++++++-- src/main/kotlin/library/SimulatedEcu.kt | 7 +++-- src/main/resources/logback.xml | 2 +- 6 files changed, 35 insertions(+), 26 deletions(-) diff --git a/src/main/kotlin/SimEcu.kt b/src/main/kotlin/SimEcu.kt index 21e08d2..272f8b1 100644 --- a/src/main/kotlin/SimEcu.kt +++ b/src/main/kotlin/SimEcu.kt @@ -24,9 +24,6 @@ fun EcuData.toEcuConfig(): EcuConfig = class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { private val internalDataStorage: MutableMap = ConcurrentHashMap() - val name - get() = data.name - val requests get() = data.requests @@ -80,7 +77,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { override fun handleRequestIfBusy(request: UdsMessage){ if (handleInterceptors(request, true)) { - logger.debugIf { "${config.name}: Incoming busy request ${request.message.toHexString(limit = 10)} was handled by interceptors" } + logger.debugIf { "[${name}] Incoming busy request ${request.message.toHexString(limit = 10)} was handled by interceptors" } } super.handleRequestIfBusy(request) } @@ -90,13 +87,13 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { */ override fun handleRequest(request: UdsMessage) { if (handleInterceptors(request, false)) { - logger.debugIf { "${config.name}: Incoming request ${request.message.toHexString(limit = 10)} was handled by interceptors" } + logger.debugIf { "[${name}] Incoming request ${request.message.toHexString(limit = 10)} was handled by interceptors" } return } val normalizedRequest by lazy { request.message.toHexString("", limit = data.requestRegexMatchBytes, limitExceededSuffix = "") } - logger.traceIf { "${config.name}: Incoming request (${request.targetAddress}): ${request.message.toHexString()}" } + logger.traceIf { "[${name}] Incoming request (${request.targetAddress}): ${request.message.toHexString()}" } // Note: We could build a lookup map to directly find the correct RequestMatcher for a binary input @@ -108,11 +105,11 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { requestIter.requestRegex!!.matches(normalizedRequest) } } catch (e: Exception) { - logger.error("${config.name}: Error while matching requests: ${e.message}") + logger.error("[${name}] Error while matching requests: ${e.message}") throw e } - logger.traceIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' try match '$requestIter' -> $matches" } + logger.traceIf { "[${name}] Request: '${request.message.toHexString(limit = 10)}' try match '$requestIter' -> $matches" } if (!matches) { continue @@ -121,22 +118,22 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { val responseData = ResponseData(caller = requestIter, request = request, ecu = this) requestIter.responseHandler.invoke(responseData) if (responseData.continueMatching) { - logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Continue matching" } + logger.debugIf { "[${name}] Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Continue matching" } continue } else if (responseData.response.isNotEmpty()) { - logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Send response '${responseData.response.toHexString(limit = 10)}'" } + logger.debugIf { "[${name}] Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> Send response '${responseData.response.toHexString(limit = 10)}'" } sendResponse(request, responseData.response) } else { - logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> No response" } + logger.debugIf { "[${name}] Request: '${request.message.toHexString(limit = 10)}' matched '$requestIter' -> No response" } } return } if (this.data.nrcOnNoMatch) { - logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Sending NRC" } + logger.debugIf { "[${name}] Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Sending NRC" } sendResponse(request, byteArrayOf(0x7F, request.message[0], NrcError.RequestOutOfRange)) } else { - logger.debugIf { "${config.name}: Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Ignore (nrcOnNoMatch = false)" } + logger.debugIf { "[${name}] Request: '${request.message.toHexString(limit = 10)}' no matching request found -> Ignore (nrcOnNoMatch = false)" } } } @@ -151,7 +148,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { alsoCallWhenEcuIsBusy: Boolean = false, interceptor: InterceptorResponseHandler ): String { - logger.traceIf { "${config.name}: Adding interceptor '$name' for $duration (busy: $alsoCallWhenEcuIsBusy)"} + logger.traceIf { "[${this.name}] Adding interceptor '$name' for $duration (busy: $alsoCallWhenEcuIsBusy)"} // expires at expirationTime val expirationTime = if (duration == Duration.INFINITE) Long.MAX_VALUE else System.nanoTime() + duration.inWholeNanoseconds @@ -178,7 +175,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { * Please note that the internal resolution for delay is milliseconds */ fun addOrReplaceTimer(name: String, delay: Duration, handler: TimerTask.() -> Unit) { - logger.traceIf { "${config.name}: Adding or replacing timer '$name' to be executed after $delay"} + logger.traceIf { "[${this.name}] Adding or replacing timer '$name' to be executed after $delay"} synchronized(mainTimer) { timers[name]?.cancel() @@ -199,7 +196,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { * Explicitly cancel a running timer */ fun cancelTimer(name: String) { - logger.traceIf { "${config.name}: Cancelling timer '$name'" } + logger.traceIf { "[${this.name}] Cancelling timer '$name'" } synchronized(mainTimer) { timers[name]?.cancel() timers.remove(name) @@ -232,7 +229,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { this.data.requests.forEach { it.reset() } this.data.resetHandler.forEach { if (it.name != null) { - logger.trace("${config.name}: Calling onReset-Handler ${it.name}") + logger.traceIf { "[${this.name}] Calling onReset-Handler ${it.name}" } } it.handler(this) } diff --git a/src/main/kotlin/SimGateway.kt b/src/main/kotlin/SimGateway.kt index ddc22a2..4fccd6b 100644 --- a/src/main/kotlin/SimGateway.kt +++ b/src/main/kotlin/SimGateway.kt @@ -97,9 +97,6 @@ private fun GatewayData.toGatewayConfig(): DoipEntityConfig { } class SimGateway(private val data: GatewayData) : DoipEntity(data.toGatewayConfig()) { - val name: String - get() = data.name - val requests: List get() = data.requests diff --git a/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt index c773f25..024ef16 100644 --- a/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt +++ b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt @@ -5,9 +5,11 @@ import io.ktor.utils.io.* import library.DoipUdpMessageHandler.Companion.logger import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.slf4j.MDC import kotlin.experimental.xor open class DefaultDoipTcpConnectionMessageHandler( + val doipEntity: DoipEntity?, val socket: Socket, val logicalAddress: Short, val maxPayloadLength: Int, @@ -101,6 +103,7 @@ open class DefaultDoipTcpConnectionMessageHandler( } override suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) { + MDC.put("ecu", doipEntity?.name) logger.traceIf { "# handleTcpMessage $message" } when (message) { is DoipTcpHeaderNegAck -> handleTcpHeaderNegAck(message, output) diff --git a/src/main/kotlin/library/DoipEntity.kt b/src/main/kotlin/library/DoipEntity.kt index 63c1e02..cba8a4f 100644 --- a/src/main/kotlin/library/DoipEntity.kt +++ b/src/main/kotlin/library/DoipEntity.kt @@ -7,6 +7,7 @@ import io.ktor.utils.io.core.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.ClosedReceiveChannelException import org.slf4j.LoggerFactory +import org.slf4j.MDC import java.net.InetAddress import java.net.InetSocketAddress import kotlin.concurrent.fixedRateTimer @@ -49,6 +50,9 @@ open class DoipEntityConfig( open class DoipEntity( val config: DoipEntityConfig, ) : DiagnosticMessageHandler { + val name: String = + config.name + protected val logger = LoggerFactory.getLogger(DoipEntity::class.java) protected var targetEcusByPhysical: Map = emptyMap() @@ -66,6 +70,7 @@ open class DoipEntity( protected open fun createDoipTcpMessageHandler(socket: Socket): DoipTcpConnectionMessageHandler = DefaultDoipTcpConnectionMessageHandler( + doipEntity = this, socket = socket, logicalAddress = config.logicalAddress, maxPayloadLength = config.maxDataSize - 8, @@ -74,11 +79,13 @@ open class DoipEntity( protected suspend fun startVamTimer(socket: BoundDatagramSocket) { if (config.broadcastEnabled) { - fixedRateTimer("${config.name}-VAM", daemon = true, initialDelay = 500, period = 500) { + fixedRateTimer("VAM-TIMER-${name}", daemon = true, initialDelay = 500, period = 500) { + MDC.put("ecu", name) if (vamSentCounter >= 3) { this.cancel() return@fixedRateTimer } + logger.info("[${name}] Sending VAM") val vam = DefaultDoipUdpMessageHandler.generateVamByEntityConfig(config) runBlocking(Dispatchers.IO) { socket.send( @@ -112,12 +119,14 @@ open class DoipEntity( override suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) { val ecu = targetEcusByPhysical[diagMessage.targetAddress] ecu?.run { + MDC.put("ecu", ecu.name) onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.PHYSICAL, output)) return } val ecus = targetEcusByFunctional[diagMessage.targetAddress] ecus?.forEach { + MDC.put("ecu", it.name) it.onIncomingUdsMessage(diagMessage.toUdsMessage(UdsMessage.FUNCTIONAL, output)) } } @@ -149,9 +158,9 @@ open class DoipEntity( while (!socket.isClosed) { val datagram = socket.receive() try { - logger.traceIf { "Incoming UDP message" } + logger.traceIf { "[${name}] Incoming UDP message" } val message = udpMessageHandler.parseMessage(datagram) - logger.traceIf { "Message is of type $message" } + logger.traceIf { "[${name}] Message is of type $message" } udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) } catch (e: HeaderNegAckException) { val code = when (e) { diff --git a/src/main/kotlin/library/SimulatedEcu.kt b/src/main/kotlin/library/SimulatedEcu.kt index b5c2935..1d4bf9a 100644 --- a/src/main/kotlin/library/SimulatedEcu.kt +++ b/src/main/kotlin/library/SimulatedEcu.kt @@ -5,17 +5,20 @@ import org.slf4j.LoggerFactory import java.util.concurrent.atomic.AtomicBoolean open class SimulatedEcu(val config: EcuConfig) { + val name: String = + config.name + protected val logger: Logger = LoggerFactory.getLogger(SimulatedEcu::class.java) private val isBusy: AtomicBoolean = AtomicBoolean(false) open fun handleRequest(request: UdsMessage) { - logger.debugIf { "${config.name}: Handle Request message: ${request.message.toHexString(limit = 20)}" } + logger.debugIf { "[${name}] Handle Request message: ${request.message.toHexString(limit = 20)}" } } open fun handleRequestIfBusy(request: UdsMessage) { // Busy NRC - logger.debugIf { "${config.name}: ECU is busy, sending busy-NRC" } + logger.debugIf { "[${name}] ECU is busy, sending busy-NRC" } request.respond(byteArrayOf(0x7f, request.message[0], 0x21)) } diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index 9f31eed..d732a32 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -3,7 +3,7 @@ - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + %cyan(%d{HH:mm:ss.SSS}) %highlight(%-5level) %green([%thread]) %magenta(%logger{12}) %boldWhite(%mdc{ecu:-NONE}) %msg%n From 72f824506a70d5b396f24d299c5bdec6824aa6ad Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Mon, 7 Feb 2022 08:44:05 +0100 Subject: [PATCH 08/10] some improvements & backward compatibility --- src/main/kotlin/SimEcu.kt | 10 ++++++++-- src/main/kotlin/SimGateway.kt | 10 ++++++++-- src/main/kotlin/library/DoipEntity.kt | 20 +++++++++++++++----- src/main/kotlin/library/SimulatedEcu.kt | 2 +- src/main/resources/logback.xml | 5 ++++- 5 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/SimEcu.kt b/src/main/kotlin/SimEcu.kt index 272f8b1..cef7d55 100644 --- a/src/main/kotlin/SimEcu.kt +++ b/src/main/kotlin/SimEcu.kt @@ -1,6 +1,9 @@ import helper.* import kotlinx.coroutines.runBlocking import library.* +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.MDC import java.util.* import java.util.concurrent.ConcurrentHashMap import kotlin.time.Duration @@ -24,6 +27,8 @@ fun EcuData.toEcuConfig(): EcuConfig = class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { private val internalDataStorage: MutableMap = ConcurrentHashMap() + val logger: Logger = LoggerFactory.getLogger(SimEcu::class.java) + val requests get() = data.requests @@ -216,7 +221,8 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { * Resets all the ECUs stored properties, timers, interceptors and requests */ fun reset() { - logger.debug("Resetting interceptors, timers and stored data for ECU $name") + MDC.put("ecu", name) + logger.debug("Resetting interceptors, timers and stored data") this.interceptors.clear() @@ -229,7 +235,7 @@ class SimEcu(private val data: EcuData) : SimulatedEcu(data.toEcuConfig()) { this.data.requests.forEach { it.reset() } this.data.resetHandler.forEach { if (it.name != null) { - logger.traceIf { "[${this.name}] Calling onReset-Handler ${it.name}" } + logger.traceIf { "Calling onReset-Handler" } } it.handler(this) } diff --git a/src/main/kotlin/SimGateway.kt b/src/main/kotlin/SimGateway.kt index 4fccd6b..c6f2b47 100644 --- a/src/main/kotlin/SimGateway.kt +++ b/src/main/kotlin/SimGateway.kt @@ -1,4 +1,5 @@ import library.* +import org.slf4j.MDC import java.net.InetAddress import kotlin.properties.Delegates @@ -122,11 +123,16 @@ class SimGateway(private val data: GatewayData) : DoipEntity(data.toGatewayConfi return SimEcu(ecuData) } + override fun findEcuByName(name: String): SimEcu? { + return super.findEcuByName(name) as SimEcu? + } + fun reset(recursiveEcus: Boolean = true) { - logger.infoIf { "Resetting Gateway $name" } + MDC.put("ecu", name) + logger.infoIf { "Resetting gateway" } this.requests.forEach { it.reset() } if (recursiveEcus) { - this.targetEcusByPhysical.forEach { (it.value as SimEcu).reset() } + this.ecus.forEach { (it as SimEcu).reset() } } } } diff --git a/src/main/kotlin/library/DoipEntity.kt b/src/main/kotlin/library/DoipEntity.kt index cba8a4f..37888a4 100644 --- a/src/main/kotlin/library/DoipEntity.kt +++ b/src/main/kotlin/library/DoipEntity.kt @@ -60,6 +60,11 @@ open class DoipEntity( protected var vamSentCounter = 0 + private val _ecus: MutableList = mutableListOf() + + val ecus: List + get() = _ecus + protected open fun createEcu(config: EcuConfig): SimulatedEcu = SimulatedEcu(config) @@ -131,16 +136,21 @@ open class DoipEntity( } } + open fun findEcuByName(name: String): SimulatedEcu? = + this.ecus.firstOrNull { it.name == name } + fun start() { - targetEcusByPhysical = this.config.ecuConfigList.associate { Pair(it.physicalAddress, createEcu(it)) } + this._ecus.addAll(this.config.ecuConfigList.map { createEcu(it) }) + + targetEcusByPhysical = this.ecus.associateBy { it.config.physicalAddress } targetEcusByFunctional = mutableMapOf() - targetEcusByPhysical.forEach { - val list = targetEcusByFunctional[it.value.config.functionalAddress] + _ecus.forEach { + val list = targetEcusByFunctional[it.config.functionalAddress] if (list == null) { - targetEcusByFunctional[it.value.config.functionalAddress] = mutableListOf(it.value) + targetEcusByFunctional[it.config.functionalAddress] = mutableListOf(it) } else { - list.add(it.value) + list.add(it) } } diff --git a/src/main/kotlin/library/SimulatedEcu.kt b/src/main/kotlin/library/SimulatedEcu.kt index 1d4bf9a..1ddf1a4 100644 --- a/src/main/kotlin/library/SimulatedEcu.kt +++ b/src/main/kotlin/library/SimulatedEcu.kt @@ -8,7 +8,7 @@ open class SimulatedEcu(val config: EcuConfig) { val name: String = config.name - protected val logger: Logger = LoggerFactory.getLogger(SimulatedEcu::class.java) + private val logger: Logger = LoggerFactory.getLogger(SimulatedEcu::class.java) private val isBusy: AtomicBoolean = AtomicBoolean(false) diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index d732a32..26a6d8e 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -7,7 +7,10 @@ - + + + + From 69439c46ee5728ff03c94eea21904b208aa9cf47 Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Mon, 7 Feb 2022 22:06:56 +0100 Subject: [PATCH 09/10] handle routing activation and status query properly --- build.gradle.kts | 7 +- .../DefaultDoipTcpConnectionMessageHandler.kt | 72 +++++-- .../library/DefaultDoipUdpMessageHandler.kt | 9 +- src/main/kotlin/library/DoipEntity.kt | 183 +++++++++++------- src/main/kotlin/library/DoipTcpMessages.kt | 2 + 5 files changed, 183 insertions(+), 90 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 7503e72..d7249fe 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,9 +12,14 @@ repositories { mavenCentral() } +val ktorVersion = "1.6.7" + dependencies { implementation(kotlin("stdlib-jdk8")) - api("io.ktor:ktor-network:1.6.7") + api("io.ktor:ktor-network:$ktorVersion") +// implementation("io.ktor:ktor-network-sockets:$ktorVersion") +// implementation("io.ktor:ktor-network-tls:$ktorVersion") +// implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion") api("ch.qos.logback:logback-classic:1.2.10") testImplementation(kotlin("test")) diff --git a/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt index 024ef16..6bc5eba 100644 --- a/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt +++ b/src/main/kotlin/library/DefaultDoipTcpConnectionMessageHandler.kt @@ -2,14 +2,13 @@ package library import io.ktor.network.sockets.* import io.ktor.utils.io.* -import library.DoipUdpMessageHandler.Companion.logger import org.slf4j.Logger import org.slf4j.LoggerFactory import org.slf4j.MDC import kotlin.experimental.xor open class DefaultDoipTcpConnectionMessageHandler( - val doipEntity: DoipEntity?, + val doipEntity: DoipEntity, val socket: Socket, val logicalAddress: Short, val maxPayloadLength: Int, @@ -17,6 +16,11 @@ open class DefaultDoipTcpConnectionMessageHandler( ) : DoipTcpConnectionMessageHandler { private val logger: Logger = LoggerFactory.getLogger(DefaultDoipTcpConnectionMessageHandler::class.java) + private var _registeredSourceAddress: Short? = null + + override fun getRegisteredSourceAddress(): Short? = + _registeredSourceAddress + override suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage { logger.traceIf { "# receiveTcpData" } val protocolVersion = brc.readByte() @@ -32,7 +36,6 @@ open class DefaultDoipTcpConnectionMessageHandler( when (payloadType) { TYPE_HEADER_NACK -> { val code = brc.readByte() - brc.discardIf(payloadLength > 1, payloadLength - 1, payloadType) return DoipTcpHeaderNegAck(code) } TYPE_TCP_ROUTING_REQ -> { @@ -40,7 +43,6 @@ open class DefaultDoipTcpConnectionMessageHandler( val activationType = brc.readByte() brc.readInt() // Reserved for future standardization use val oemData = if (payloadLength > 7) brc.readInt() else null - brc.discardIf(payloadLength > 11, payloadLength - 11, payloadType) return DoipTcpRoutingActivationRequest(sourceAddress, activationType, oemData) } TYPE_TCP_ROUTING_RES -> { @@ -103,7 +105,7 @@ open class DefaultDoipTcpConnectionMessageHandler( } override suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) { - MDC.put("ecu", doipEntity?.name) + MDC.put("ecu", doipEntity.name) logger.traceIf { "# handleTcpMessage $message" } when (message) { is DoipTcpHeaderNegAck -> handleTcpHeaderNegAck(message, output) @@ -135,13 +137,35 @@ open class DefaultDoipTcpConnectionMessageHandler( ).message ) } else { - output.writeFully( - DoipTcpRoutingActivationResponse( - message.sourceAddress, - logicalAddress, - DoipTcpRoutingActivationResponse.RC_OK - ).message - ) + if (_registeredSourceAddress == null) { + _registeredSourceAddress = message.sourceAddress + } + + if (_registeredSourceAddress != message.sourceAddress) { + output.writeFully( + DoipTcpRoutingActivationResponse( + message.sourceAddress, + logicalAddress, + DoipTcpRoutingActivationResponse.RC_ERROR_DIFFERENT_SOURCE_ADDRESS + ).message + ) + } else if (doipEntity.hasAlreadyActiveConnection(message.sourceAddress, this)) { + output.writeFully( + DoipTcpRoutingActivationResponse( + message.sourceAddress, + logicalAddress, + DoipTcpRoutingActivationResponse.RC_ERROR_SOURCE_ADDRESS_ALREADY_ACTIVE + ).message + ) + } else { + output.writeFully( + DoipTcpRoutingActivationResponse( + message.sourceAddress, + logicalAddress, + DoipTcpRoutingActivationResponse.RC_OK + ).message + ) + } } } @@ -159,6 +183,15 @@ open class DefaultDoipTcpConnectionMessageHandler( } protected open suspend fun handleTcpDiagMessage(message: DoipTcpDiagMessage, output: ByteWriteChannel) { + if (_registeredSourceAddress != message.sourceAddress) { + val reject = DoipTcpDiagMessageNegAck( + message.targetAddress, + message.sourceAddress, + DoipTcpDiagMessageNegAck.NACK_CODE_INVALID_SOURCE_ADDRESS + ) + output.writeFully(reject.message) + return + } logger.traceIf { "# handleTcpDiagMessage $message for ${message.targetAddress}" } if (diagMessageHandler.existsTargetAddress(message.targetAddress)) { logger.traceIf { "# targetAddress ${message.targetAddress} exists, sending positive ack" } @@ -193,14 +226,17 @@ open class DefaultDoipTcpConnectionMessageHandler( } } -private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int, payloadType: Short) { - if (condition) { - logger.error("Discarding $n bytes for payload-type $payloadType") - this.discardExact(n.toLong()) - } -} +//private suspend fun ByteReadChannel.discardIf(condition: Boolean, n: Int, payloadType: Short) { +// if (condition) { +// logger.error("Discarding $n bytes for payload-type $payloadType") +// this.discardExact(n.toLong()) +// } +//} interface DiagnosticMessageHandler { fun existsTargetAddress(targetAddress: Short): Boolean suspend fun onIncomingDiagMessage(diagMessage: DoipTcpDiagMessage, output: ByteWriteChannel) } + +fun DoipEntity.hasAlreadyActiveConnection(sourceAddress: Short, exclude: DoipTcpConnectionMessageHandler?) = + this.connectionHandlers.any { it.getRegisteredSourceAddress() == sourceAddress && it != exclude } diff --git a/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt b/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt index bfc30b7..896a94d 100644 --- a/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt +++ b/src/main/kotlin/library/DefaultDoipUdpMessageHandler.kt @@ -4,8 +4,10 @@ import io.ktor.network.sockets.* import io.ktor.util.network.* import io.ktor.utils.io.core.* import kotlinx.coroutines.channels.SendChannel +import kotlin.math.max open class DefaultDoipUdpMessageHandler( + val doipEntity: DoipEntity, val config: DoipEntityConfig ) : DoipUdpMessageHandler { @@ -62,7 +64,12 @@ open class DefaultDoipUdpMessageHandler( sendChannel.send( Datagram( packet = ByteReadPacket( - DoipUdpEntityStatusResponse(0, 255.toByte(), 0, 0xFFFF) + DoipUdpEntityStatusResponse( + config.nodeType.value, + 255.toByte(), + max(doipEntity.connectionHandlers.size, 255).toByte(), + config.maxDataSize + ) .message ), address = sourceAddress diff --git a/src/main/kotlin/library/DoipEntity.kt b/src/main/kotlin/library/DoipEntity.kt index 37888a4..88d0bb8 100644 --- a/src/main/kotlin/library/DoipEntity.kt +++ b/src/main/kotlin/library/DoipEntity.kt @@ -17,6 +17,12 @@ typealias GID = ByteArray typealias EID = ByteArray typealias VIN = ByteArray + +enum class DoipNodeType(val value: Byte) { + GATEWAY(0), + NODE(1) +} + open class DoipEntityConfig( val name: String, val logicalAddress: Short, @@ -29,7 +35,8 @@ open class DoipEntityConfig( val broadcastEnabled: Boolean = true, val broadcastAddress: InetAddress = InetAddress.getByName("255.255.255.255"), // TODO tlsEnabled, tlsPort, certificate chain? - val ecuConfigList: MutableList = mutableListOf() + val ecuConfigList: MutableList = mutableListOf(), + val nodeType: DoipNodeType = DoipNodeType.GATEWAY, ) { init { if (name.isEmpty()) { @@ -60,6 +67,8 @@ open class DoipEntity( protected var vamSentCounter = 0 + val connectionHandlers: MutableList = mutableListOf() + private val _ecus: MutableList = mutableListOf() val ecus: List @@ -70,7 +79,8 @@ open class DoipEntity( protected open fun createDoipUdpMessageHandler(): DoipUdpMessageHandler = DefaultDoipUdpMessageHandler( - config + doipEntity = this, + config = config ) protected open fun createDoipTcpMessageHandler(socket: Socket): DoipTcpConnectionMessageHandler = @@ -82,7 +92,7 @@ open class DoipEntity( diagMessageHandler = this ) - protected suspend fun startVamTimer(socket: BoundDatagramSocket) { + protected open suspend fun startVamTimer(socket: BoundDatagramSocket) { if (config.broadcastEnabled) { fixedRateTimer("VAM-TIMER-${name}", daemon = true, initialDelay = 500, period = 500) { MDC.put("ecu", name) @@ -139,6 +149,90 @@ open class DoipEntity( open fun findEcuByName(name: String): SimulatedEcu? = this.ecus.firstOrNull { it.name == name } + protected open fun CoroutineScope.handleTcpSocket(socket: Socket) { + launch { + logger.debugIf { "New incoming TCP connection from ${socket.remoteAddress}" } + val tcpMessageReceiver = createDoipTcpMessageHandler(socket) + val input = socket.openReadChannel() + val output = socket.openWriteChannel(autoFlush = tcpMessageReceiver.isAutoFlushEnabled()) + try { + connectionHandlers.add(tcpMessageReceiver) + while (!socket.isClosed) { + try { + val message = tcpMessageReceiver.receiveTcpData(input) + tcpMessageReceiver.handleTcpMessage(message, output) + } catch (e: ClosedReceiveChannelException) { + // ignore - socket was closed + logger.debug("Socket was closed by remote ${socket.remoteAddress}") + withContext(Dispatchers.IO) { + socket.close() + } + } catch (e: HeaderNegAckException) { + if (!socket.isClosed) { + logger.debug("Error in Header while parsing message, sending negative acknowledgment", e) + val response = + DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message + output.writeFully(response, 0, response.size) + output.flush() + } + } catch (e: Exception) { + if (!socket.isClosed) { + logger.error("Unknown error parsing/handling message, sending negative acknowledgment", e) + val response = + DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message + output.writeFully(response, 0, response.size) + output.flush() + } + } + } + } catch (e: Throwable) { + logger.error("Unknown inside socket processing loop, closing socket", e) + } finally { + try { + withContext(Dispatchers.IO) { + socket.close() + } + } finally { + connectionHandlers.remove(tcpMessageReceiver) + } + } + } + } + + protected open fun CoroutineScope.handleUdpMessage( + udpMessageHandler: DoipUdpMessageHandler, + datagram: Datagram, + socket: BoundDatagramSocket + ) { + launch { + MDC.put("ecu", name) + try { + logger.traceIf { "[${name}] Incoming UDP message" } + val message = udpMessageHandler.parseMessage(datagram) + logger.traceIf { "[${name}] Message is of type $message" } + udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) + } catch (e: HeaderNegAckException) { + val code = when (e) { + is IncorrectPatternFormat -> DoipUdpHeaderNegAck.NACK_INCORRECT_PATTERN_FORMAT + is HeaderTooShort -> DoipUdpHeaderNegAck.NACK_INCORRECT_PATTERN_FORMAT + is InvalidPayloadLength -> DoipUdpHeaderNegAck.NACK_INVALID_PAYLOAD_LENGTH + is UnknownPayloadType -> DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE + else -> { + DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE + } + } + logger.debug("Error in Message-Header, sending negative acknowledgement", e) + udpMessageHandler.respondHeaderNegAck( + socket.outgoing, + datagram.address, + code + ) + } catch (e: Exception) { + logger.error("Unknown error while processing message", e) + } + } + } + fun start() { this._ecus.addAll(this.config.ecuConfigList.map { createEcu(it) }) @@ -167,30 +261,7 @@ open class DoipEntity( val udpMessageHandler = createDoipUdpMessageHandler() while (!socket.isClosed) { val datagram = socket.receive() - try { - logger.traceIf { "[${name}] Incoming UDP message" } - val message = udpMessageHandler.parseMessage(datagram) - logger.traceIf { "[${name}] Message is of type $message" } - udpMessageHandler.handleUdpMessage(socket.outgoing, datagram.address, message) - } catch (e: HeaderNegAckException) { - val code = when (e) { - is IncorrectPatternFormat -> DoipUdpHeaderNegAck.NACK_INCORRECT_PATTERN_FORMAT - is HeaderTooShort -> DoipUdpHeaderNegAck.NACK_INCORRECT_PATTERN_FORMAT - is InvalidPayloadLength -> DoipUdpHeaderNegAck.NACK_INVALID_PAYLOAD_LENGTH - is UnknownPayloadType -> DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE - else -> { - DoipUdpHeaderNegAck.NACK_UNKNOWN_PAYLOAD_TYPE - } - } - logger.debug("Error in Message-Header, sending negative acknowledgement", e) - udpMessageHandler.respondHeaderNegAck( - socket.outgoing, - datagram.address, - code - ) - } catch (e: Exception) { - logger.error("Unknown error while processing message", e) - } + handleUdpMessage(udpMessageHandler, datagram, socket) } } } @@ -203,51 +274,23 @@ open class DoipEntity( .bind(InetSocketAddress(config.localAddress, config.localPort)) while (!serverSocket.isClosed) { val socket = serverSocket.accept() - launch { - logger.debugIf { "New incoming TCP connection from ${socket.remoteAddress}" } - val tcpMessageReceiver = createDoipTcpMessageHandler(socket) - val input = socket.openReadChannel() - val output = socket.openWriteChannel(autoFlush = tcpMessageReceiver.isAutoFlushEnabled()) - try { - while (!socket.isClosed) { - try { - val message = tcpMessageReceiver.receiveTcpData(input) - tcpMessageReceiver.handleTcpMessage(message, output) - } catch (e: ClosedReceiveChannelException) { - // ignore - socket was closed - logger.debug("Socket was closed by remote ${socket.remoteAddress}") - withContext(Dispatchers.IO) { - socket.close() - } - } catch (e: HeaderNegAckException) { - if (!socket.isClosed) { - logger.debug("Error in Header while parsing message, sending negative acknowledgment", e) - val response = - DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message - output.writeFully(response, 0, response.size) - output.flush() - } - } catch (e: Exception) { - if (!socket.isClosed) { - logger.error("Unknown error parsing/handling message, sending negative acknowledgment", e) - val response = - DoipTcpHeaderNegAck(DoipTcpDiagMessageNegAck.NACK_CODE_TRANSPORT_PROTOCOL_ERROR).message - output.writeFully(response, 0, response.size) - output.flush() - } - } - } - } catch (e: Throwable) { - logger.error("Unknown inside socket processing loop, closing socket", e) - } finally { - withContext(Dispatchers.IO) { - socket.close() - } - } - } + handleTcpSocket(socket) } - } } + +// thread(name = "TLS") { +// runBlocking { +// val serverSocket = +// aSocket(ActorSelectorManager(Dispatchers.IO)) +// .tcp() +// .bind(InetSocketAddress(config.localAddress, config.localPort)) +// +// while (!serverSocket.isClosed) { +// val socket = serverSocket.accept() +// handleTcpSocket(socket) +// } +// } +// } } } diff --git a/src/main/kotlin/library/DoipTcpMessages.kt b/src/main/kotlin/library/DoipTcpMessages.kt index 760f569..3188403 100644 --- a/src/main/kotlin/library/DoipTcpMessages.kt +++ b/src/main/kotlin/library/DoipTcpMessages.kt @@ -8,6 +8,8 @@ interface DoipTcpConnectionMessageHandler { suspend fun receiveTcpData(brc: ByteReadChannel): DoipTcpMessage suspend fun handleTcpMessage(message: DoipTcpMessage, output: ByteWriteChannel) suspend fun isAutoFlushEnabled(): Boolean + + fun getRegisteredSourceAddress(): Short? } class DoipTcpHeaderNegAck( From 0494d98cee7246782ad196883a688ea9828c013d Mon Sep 17 00:00:00 2001 From: Florian Roks Date: Mon, 7 Feb 2022 22:17:53 +0100 Subject: [PATCH 10/10] rename variable --- src/main/kotlin/library/DoipEntity.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/library/DoipEntity.kt b/src/main/kotlin/library/DoipEntity.kt index 88d0bb8..3a4a0dd 100644 --- a/src/main/kotlin/library/DoipEntity.kt +++ b/src/main/kotlin/library/DoipEntity.kt @@ -152,15 +152,15 @@ open class DoipEntity( protected open fun CoroutineScope.handleTcpSocket(socket: Socket) { launch { logger.debugIf { "New incoming TCP connection from ${socket.remoteAddress}" } - val tcpMessageReceiver = createDoipTcpMessageHandler(socket) + val tcpMessageHandler = createDoipTcpMessageHandler(socket) val input = socket.openReadChannel() - val output = socket.openWriteChannel(autoFlush = tcpMessageReceiver.isAutoFlushEnabled()) + val output = socket.openWriteChannel(autoFlush = tcpMessageHandler.isAutoFlushEnabled()) try { - connectionHandlers.add(tcpMessageReceiver) + connectionHandlers.add(tcpMessageHandler) while (!socket.isClosed) { try { - val message = tcpMessageReceiver.receiveTcpData(input) - tcpMessageReceiver.handleTcpMessage(message, output) + val message = tcpMessageHandler.receiveTcpData(input) + tcpMessageHandler.handleTcpMessage(message, output) } catch (e: ClosedReceiveChannelException) { // ignore - socket was closed logger.debug("Socket was closed by remote ${socket.remoteAddress}") @@ -193,7 +193,7 @@ open class DoipEntity( socket.close() } } finally { - connectionHandlers.remove(tcpMessageReceiver) + connectionHandlers.remove(tcpMessageHandler) } } }