From a639b37db0b81d1ec99a0c227b4ea0a7c1268dc8 Mon Sep 17 00:00:00 2001 From: Davide Pianca Date: Fri, 14 May 2021 22:36:05 +0200 Subject: [PATCH] Update interfaces to allow more customizability * pass clientId in Authentication * pass username, password (if set in broker constructor), payload in Authorization * pass clientId and username in PacketInterceptor --- Readme.md | 37 +++++++++++++++++-- src/commonMain/kotlin/mqtt/broker/Broker.kt | 1 + .../kotlin/mqtt/broker/ClientConnection.kt | 31 +++++++++++----- .../mqtt/broker/interfaces/Authentication.kt | 8 +++- .../mqtt/broker/interfaces/Authorization.kt | 19 +++++++++- .../mqtt/broker/interfaces/BytesMetrics.kt | 10 +++++ .../interfaces/EnhancedAuthentication.kt | 2 +- .../broker/interfaces/PacketInterceptor.kt | 8 +++- src/jvmTest/kotlin/RunLocal.kt | 9 ++++- .../kotlin/integration/AuthenticationTest.kt | 4 +- 10 files changed, 110 insertions(+), 19 deletions(-) diff --git a/Readme.md b/Readme.md index 59057d1..81eaac7 100644 --- a/Readme.md +++ b/Readme.md @@ -41,7 +41,7 @@ repositories { maven { url "https://jitpack.io" } } dependencies { - implementation 'com.github.davidepianca98.KMQTT:KMQTT-jvm:0.2.7' + implementation 'com.github.davidepianca98.KMQTT:kmqtt-jvm:0.2.7' } ``` @@ -92,7 +92,7 @@ fun main() { ```kotlin fun main() { val broker = Broker(authentication = object : Authentication { - override fun authenticate(username: String?, password: UByteArray?): Boolean { + override fun authenticate(clientId: String, username: String?, password: UByteArray?): Boolean { // TODO Implement your authentication method return username == "user" && password?.toByteArray()?.decodeToString() == "pass" } @@ -101,11 +101,33 @@ fun main() { } ``` +#### Authorization code example + +```kotlin +fun main() { + val broker = Broker(authorization = object : Authorization { + override fun authorize( + clientId: String, + username: String?, + password: UByteArray?, // != null only if savePassword set to true in the broker constructor + topicName: String, + isSubscription: Boolean, + payload: UByteArray? + ): Boolean { + // TODO Implement your authorization method + return topicName == "$clientId/topic" + } + }) + broker.listen() +} +``` + #### Message interceptor code example + ```kotlin fun main() { val broker = Broker(packetInterceptor = object : PacketInterceptor { - override fun packetReceived(packet: MQTTPacket) { + override fun packetReceived(clientId: String, username: String?, packet: MQTTPacket) { when(packet) { is MQTTConnect -> println(packet.protocolName) is MQTTPublish -> println(packet.topicName) @@ -130,3 +152,12 @@ fun main() { broker.listen() } ``` + +#### Other advanced functionality + +MQTT5 Enhanced Authentication: set the `enhancedAuthenticationProviders` Broker constructor parameter, implementing the +provider interface `EnhancedAuthenticationProvider`. + +Session persistence: set the `persistence` Broker constructor parameter, implementing `Persistence` interface. + +Bytes metrics: set the `bytesMetrics` Broker constructor parameter, implementing `BytesMetrics` interface. diff --git a/src/commonMain/kotlin/mqtt/broker/Broker.kt b/src/commonMain/kotlin/mqtt/broker/Broker.kt index 55c138c..e74f45b 100644 --- a/src/commonMain/kotlin/mqtt/broker/Broker.kt +++ b/src/commonMain/kotlin/mqtt/broker/Broker.kt @@ -24,6 +24,7 @@ class Broker( val authentication: Authentication? = null, val enhancedAuthenticationProviders: Map = mapOf(), val authorization: Authorization? = null, + val savePassword: Boolean = false, val maximumSessionExpiryInterval: UInt = 0xFFFFFFFFu, val receiveMaximum: UShort? = 1024u, val maximumQos: Qos? = null, diff --git a/src/commonMain/kotlin/mqtt/broker/ClientConnection.kt b/src/commonMain/kotlin/mqtt/broker/ClientConnection.kt index d02238c..ed0e2cb 100644 --- a/src/commonMain/kotlin/mqtt/broker/ClientConnection.kt +++ b/src/commonMain/kotlin/mqtt/broker/ClientConnection.kt @@ -26,6 +26,8 @@ class ClientConnection( } private var clientId: String? = null + private var username: String? = null + private var password: UByteArray? = null private var session: Session? = null // Client connection state @@ -169,7 +171,7 @@ class ClientConnection( else -> throw MQTTException(ReasonCode.PROTOCOL_ERROR) } } - broker.packetInterceptor?.packetReceived(packet) + broker.packetInterceptor?.packetReceived(clientId!!, username, packet) } /** @@ -218,7 +220,7 @@ class ClientConnection( private fun handleAuthentication(packet: MQTTConnect) { if (broker.authentication != null) { if (packet.userName != null || packet.password != null) { - if (!broker.authentication.authenticate(packet.userName, packet.password)) { + if (!broker.authentication.authenticate(clientId!!, packet.userName, packet.password)) { throw MQTTException(ReasonCode.NOT_AUTHORIZED) } } else { @@ -272,16 +274,20 @@ class ClientConnection( private fun handleConnect(packet: MQTTConnect) { connectPacket = packet - handleAuthentication(packet) - val clientId = if (packet.clientID.isEmpty()) { + val clientId = packet.clientID.ifEmpty { if (packet is MQTT4Connect && !packet.connectFlags.cleanStart) { writePacket(MQTT4Connack(ConnectAcknowledgeFlags(false), ConnectReturnCode.IDENTIFIER_REJECTED)) return } generateClientId() - } else packet.clientID + } this.clientId = clientId + this.username = packet.userName + if (broker.savePassword) { + this.password = packet.password + } + handleAuthentication(packet) if (packet is MQTT5Connect && packet.properties.authenticationMethod != null) { packet.properties.authenticationMethod?.let { authenticationMethod -> @@ -463,8 +469,15 @@ class ClientConnection( packetsReceivedBeforeConnack.clear() } - private fun checkAuthorization(topicName: String, isSubscription: Boolean): Boolean { - return broker.authorization?.authorize(clientId!!, topicName, isSubscription) != false + private fun checkAuthorization(topicName: String, isSubscription: Boolean, payload: UByteArray?): Boolean { + return broker.authorization?.authorize( + clientId!!, + username, + password, + topicName, + isSubscription, + payload + ) != false } private fun handlePublish(packet: MQTTPublish) { @@ -476,7 +489,7 @@ class ClientConnection( // Handle topic alias val topic = getTopicOrAlias(packet) - if (!checkAuthorization(topic, false)) + if (!checkAuthorization(topic, false, packet.payload)) throw MQTTException(ReasonCode.NOT_AUTHORIZED) if (packet.qos > broker.maximumQos ?: Qos.EXACTLY_ONCE) { @@ -687,7 +700,7 @@ class ClientConnection( val retainedMessagesList = mutableListOf() val reasonCodes = packet.subscriptions.map { subscription -> - if (!checkAuthorization(subscription.topicFilter, true)) + if (!checkAuthorization(subscription.topicFilter, true, null)) return@map ReasonCode.NOT_AUTHORIZED if (!subscription.matchTopicFilter.isValidTopic()) diff --git a/src/commonMain/kotlin/mqtt/broker/interfaces/Authentication.kt b/src/commonMain/kotlin/mqtt/broker/interfaces/Authentication.kt index e227802..f1179f4 100644 --- a/src/commonMain/kotlin/mqtt/broker/interfaces/Authentication.kt +++ b/src/commonMain/kotlin/mqtt/broker/interfaces/Authentication.kt @@ -2,5 +2,11 @@ package mqtt.broker.interfaces interface Authentication { - fun authenticate(username: String?, password: UByteArray?): Boolean + /** + * Checks if the client is allowed to connect to the broker + * @param clientId the MQTT clientId assigned to the client + * @param username the MQTT username provided in the CONNECT packet, if present, null otherwise + * @param password the MQTT password provided in the CONNECT packet, if present, null otherwise + */ + fun authenticate(clientId: String, username: String?, password: UByteArray?): Boolean } diff --git a/src/commonMain/kotlin/mqtt/broker/interfaces/Authorization.kt b/src/commonMain/kotlin/mqtt/broker/interfaces/Authorization.kt index 962b485..44797b6 100644 --- a/src/commonMain/kotlin/mqtt/broker/interfaces/Authorization.kt +++ b/src/commonMain/kotlin/mqtt/broker/interfaces/Authorization.kt @@ -2,5 +2,22 @@ package mqtt.broker.interfaces interface Authorization { - fun authorize(clientId: String, topicName: String, isSubscription: Boolean): Boolean + /** + * Checks if the client is allowed to do the specified operation + * @param clientId the MQTT clientId assigned to the client + * @param username the MQTT username provided in the CONNECT packet, if present, null otherwise + * @param password the MQTT password provided in the CONNECT packet, if present and broker parameter savePassword=true, null otherwise + * @param topicName the topic of the PUBLISH or SUBSCRIBE packet + * @param isSubscription true if the packet received is a SUBSCRIBE, false if it's a PUBLISH + * @param payload the content of the PUBLISH message, present only if isSubscription is false + * @return true if the client is allowed to publish or subscribe, false otherwise + */ + fun authorize( + clientId: String, + username: String?, + password: UByteArray?, + topicName: String, + isSubscription: Boolean, + payload: UByteArray? + ): Boolean } diff --git a/src/commonMain/kotlin/mqtt/broker/interfaces/BytesMetrics.kt b/src/commonMain/kotlin/mqtt/broker/interfaces/BytesMetrics.kt index 683ab19..d0f1129 100644 --- a/src/commonMain/kotlin/mqtt/broker/interfaces/BytesMetrics.kt +++ b/src/commonMain/kotlin/mqtt/broker/interfaces/BytesMetrics.kt @@ -2,7 +2,17 @@ package mqtt.broker.interfaces interface BytesMetrics { + /** + * Called when a client receives a packet + * @param clientId the clientId of the client that received the packet + * @param bytesCount the size of the received packet + */ fun received(clientId: String, bytesCount: Long) + /** + * Called when a client sends a packet + * @param clientId the clientId of the client that sent the packet + * @param bytesCount the size of the sent packet + */ fun sent(clientId: String, bytesCount: Long) } diff --git a/src/commonMain/kotlin/mqtt/broker/interfaces/EnhancedAuthentication.kt b/src/commonMain/kotlin/mqtt/broker/interfaces/EnhancedAuthentication.kt index 7d6b4d5..d663325 100644 --- a/src/commonMain/kotlin/mqtt/broker/interfaces/EnhancedAuthentication.kt +++ b/src/commonMain/kotlin/mqtt/broker/interfaces/EnhancedAuthentication.kt @@ -12,7 +12,7 @@ interface EnhancedAuthenticationProvider { * Gets called upon reception of a CONNECT packet with the Authentication Method property set or upon reception * of an AUTH packet * @param clientId the requested or assigned Client ID if none is given by the client - * @param authenticationData the Authentication Data received if present in the received packet + * @param authenticationData the Authentication Data received, if present, in the received packet * @param result function to call to continue the authentication or to set it as complete */ fun authReceived( diff --git a/src/commonMain/kotlin/mqtt/broker/interfaces/PacketInterceptor.kt b/src/commonMain/kotlin/mqtt/broker/interfaces/PacketInterceptor.kt index 1ee861b..035aa8e 100644 --- a/src/commonMain/kotlin/mqtt/broker/interfaces/PacketInterceptor.kt +++ b/src/commonMain/kotlin/mqtt/broker/interfaces/PacketInterceptor.kt @@ -4,5 +4,11 @@ import mqtt.packets.MQTTPacket interface PacketInterceptor { - fun packetReceived(packet: MQTTPacket) + /** + * Called when a packet is received from a client + * @param clientId the clientId assigned to the MQTT client that sent the packet + * @param username the MQTT username provided in the CONNECT packet, if present, null otherwise + * @param packet the MQTT packet sent by the client + */ + fun packetReceived(clientId: String, username: String?, packet: MQTTPacket) } diff --git a/src/jvmTest/kotlin/RunLocal.kt b/src/jvmTest/kotlin/RunLocal.kt index cdaa038..dbe5ada 100644 --- a/src/jvmTest/kotlin/RunLocal.kt +++ b/src/jvmTest/kotlin/RunLocal.kt @@ -3,7 +3,14 @@ import mqtt.broker.interfaces.Authorization fun main() { Broker(serverKeepAlive = 60, authorization = object : Authorization { - override fun authorize(clientId: String, topicName: String, isSubscription: Boolean): Boolean { + override fun authorize( + clientId: String, + username: String?, + password: UByteArray?, + topicName: String, + isSubscription: Boolean, + payload: UByteArray? + ): Boolean { return topicName != "test/nosubscribe" } }).listen() diff --git a/src/jvmTest/kotlin/integration/AuthenticationTest.kt b/src/jvmTest/kotlin/integration/AuthenticationTest.kt index ac1b7e7..dc7b255 100644 --- a/src/jvmTest/kotlin/integration/AuthenticationTest.kt +++ b/src/jvmTest/kotlin/integration/AuthenticationTest.kt @@ -38,7 +38,7 @@ class AuthenticationTest { @Test fun testSimpleAuthentication() { val broker = Broker(authentication = object : Authentication { - override fun authenticate(username: String?, password: UByteArray?): Boolean { + override fun authenticate(clientId: String, username: String?, password: UByteArray?): Boolean { return username == "user" && password?.toByteArray()?.decodeToString() == "pass" } }) @@ -58,7 +58,7 @@ class AuthenticationTest { @Test(expected = Mqtt5ConnAckException::class) fun testSimpleAuthenticationFailure() { val broker = Broker(authentication = object : Authentication { - override fun authenticate(username: String?, password: UByteArray?): Boolean { + override fun authenticate(clientId: String, username: String?, password: UByteArray?): Boolean { return username == "user" && password?.toByteArray()?.decodeToString() == "pass" } })