├── project ├── build.properties └── plugins.sbt ├── .gitignore ├── core └── src │ ├── test │ └── scala │ │ └── net │ │ └── sigusr │ │ └── mqtt │ │ ├── package.scala │ │ ├── SpecsTestKit.scala │ │ ├── api │ │ ├── ManagerSpec.scala │ │ └── MessagesSpec.scala │ │ ├── SpecUtils.scala │ │ ├── integration │ │ └── ActorSpec.scala │ │ └── impl │ │ ├── frames │ │ └── CodecSpec.scala │ │ └── protocol │ │ ├── HandlersSpec.scala │ │ └── EngineSpec.scala │ └── main │ └── scala │ └── net │ └── sigusr │ └── mqtt │ ├── api │ ├── ErrorKind.scala │ ├── QualityOfService.scala │ ├── Manager.scala │ ├── MessageId.scala │ ├── APIResponse.scala │ ├── ConnectionFailureReason.scala │ ├── package.scala │ └── APICommand.scala │ └── impl │ ├── frames │ ├── Header.scala │ ├── package.scala │ ├── ConnectVariableHeader.scala │ ├── RemainingLengthCodec.scala │ └── Frame.scala │ └── protocol │ ├── Action.scala │ ├── Registers.scala │ ├── Engine.scala │ └── Handlers.scala ├── examples └── src │ └── main │ └── scala │ └── net │ └── sigusr │ └── mqtt │ └── examples │ ├── package.scala │ ├── LocalPublisher.scala │ └── LocalSubscriber.scala ├── .travis.yml ├── README.md └── LICENSE /project/build.properties: -------------------------------------------------------------------------------- 1 | #Activator-generated Properties 2 | #Mon May 12 09:07:10 CEST 2014 3 | template.uuid=7259c720-f2cc-437f-b85b-61610508bb5f 4 | sbt.version=1.3.6 5 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") 2 | 3 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.7") 4 | 5 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.3") 6 | 7 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.0") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea*/ 2 | # Created by .gitignore support plugin (hsz.mobi) 3 | ### Scala template 4 | *.class 5 | *.log 6 | 7 | # sbt specific 8 | .cache/ 9 | .history/ 10 | .lib/ 11 | dist/* 12 | target/ 13 | lib_managed/ 14 | src_managed/ 15 | project/boot/ 16 | project/plugins/project/ 17 | 18 | # Scala-IDE specific 19 | .scala_dependencies 20 | .worksheet 21 | 22 | .bloop/ 23 | .metals/ 24 | project/metals.sbt 25 | 26 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/package.scala: -------------------------------------------------------------------------------- 1 | package net.sigusr 2 | 3 | package object mqtt { 4 | val configDebug = 5 | """akka { 6 | loglevel = DEBUG 7 | actor { 8 | debug { 9 | receive = on 10 | autoreceive = off 11 | lifecycle = off 12 | } 13 | } 14 | } 15 | """ 16 | 17 | val config = 18 | """akka { 19 | loglevel = INFO 20 | actor { 21 | debug { 22 | receive = off 23 | autoreceive = off 24 | lifecycle = off 25 | } 26 | } 27 | } 28 | """ 29 | } 30 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/ErrorKind.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | sealed trait ErrorKind 20 | 21 | case object AlreadyConnected extends ErrorKind 22 | case object NotConnected extends ErrorKind 23 | -------------------------------------------------------------------------------- /examples/src/main/scala/net/sigusr/mqtt/examples/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt 18 | 19 | package object examples { 20 | val localSubscriber: String = "Local-Subscriber" 21 | val localPublisher: String = "Local-Publisher" 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | scala: 3 | - 2.11.8 4 | - 2.12.1 5 | - 2.13.1 6 | jdk: 7 | - openjdk11 8 | sudo: false 9 | script: sbt clean coverage test core/coverageReport && sbt coverageAggregate 10 | after_success: sbt coveralls $(if [[ "${TRAVIS_PULL_REQUEST}" == "false" && "${TRAVIS_BRANCH}" == "master" && "${TRAVIS_TAG}" == "false" ]]; then echo "core/publish"; fi) 11 | env: 12 | global: 13 | - secure: fFRvPhrTWMYzVr6UkPXaVvnFn5p3XGh3IL8HrI1qYRdRkqaV+eTRXWtwmqN+miqVd/V/YsD9vYNjNbBoiC8tSH9gtK4DWtK01eJEmmBLtm3DdvtL/iH3Jh3HcPe5lTK4rZiicoO51zPGDQi+OBHAPix+8WwECeAUGlV2knGlcT4= 14 | - secure: cFwqmVV5q6TzxWakLLc43z9L9MTlVQarlzcKB3I01CoSXf5oq39pdR5EOHPIQYIEzgihFOGandu6DeKJb6nlvKCZQ9KXpbB/Y5pBwuZ5F5hfwNU/i+pw3+wM7OIIUfS/GD+YcGkD3SUmlTp2vX81ZNc3CEFPfOVL3h18ER4Shvg= 15 | - secure: T0+31O4+vPQcwz11asrgTDc0OTQFAWv3KROavFK2te03rz1msOh5x1bllTlCMxWlLI3FntMxa9QpQgH6Q2Uo9q+ReSBfX0xbUrtB1HU9WROxPl17FcpWXoUUQf/cB1zdGW7mxEzF2qNWImbYJvpC8YFS+D28AyPY7CLzWd4xRgg= 16 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/SpecsTestKit.scala: -------------------------------------------------------------------------------- 1 | package net.sigusr.mqtt 2 | 3 | import akka.actor._ 4 | import akka.testkit.{ ImplicitSender, TestKit } 5 | import com.typesafe.config.ConfigFactory 6 | import org.specs2.specification.{ AfterEach, Scope } 7 | 8 | import scala.concurrent.Future 9 | 10 | class SpecsTestKit extends TestKit(ActorSystem("MQTTClient-system", ConfigFactory.parseString(config))) with ImplicitSender with Scope with AfterEach { 11 | 12 | class TestActorProxy(val actorBuilder: ActorContext => ActorRef) extends Actor { 13 | val child: ActorRef = actorBuilder(context) 14 | 15 | def receive: Receive = { 16 | case x if sender == child => testActor forward x 17 | case x => child forward x 18 | } 19 | } 20 | 21 | def after: Future[Terminated] = system.terminate() 22 | def clientActor: ActorRef = testActor 23 | def testActorProxy(actorBuilder: ActorContext => ActorRef): ActorRef = system.actorOf(Props(new TestActorProxy(actorBuilder))) 24 | } 25 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/frames/Header.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.frames 18 | 19 | import net.sigusr.mqtt.api.QualityOfService.AtMostOnce 20 | import scodec.Codec 21 | import scodec.codecs._ 22 | 23 | case class Header(dup: Boolean = false, qos: Int = AtMostOnce.value, retain: Boolean = false) 24 | 25 | object Header { 26 | implicit val headerCodec: Codec[Header] = (bool :: qosCodec :: bool).as[Header] 27 | } -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/frames/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl 18 | 19 | import scodec.bits._ 20 | import scodec.codecs._ 21 | 22 | package object frames { 23 | 24 | val qosCodec = uint2 25 | val returnCodeCodec = uint8 26 | val messageIdCodec = uint16 27 | val keepAliveCodec = uint16 28 | 29 | val remainingLengthCodec = new RemainingLengthCodec 30 | val stringCodec = variableSizeBytes(uint16, utf8) 31 | val bytePaddingCodec = constant(bin"00000000") 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/QualityOfService.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | import enumeratum.values._ 20 | 21 | sealed abstract class QualityOfService(val value: Int) extends IntEnumEntry 22 | 23 | object QualityOfService extends IntEnum[QualityOfService] { 24 | object AtMostOnce extends QualityOfService(0) 25 | object AtLeastOnce extends QualityOfService(1) 26 | object ExactlyOnce extends QualityOfService(2) 27 | 28 | val values: IndexedSeq[QualityOfService] = findValues 29 | } -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/Manager.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor.Props 22 | import akka.io.{ IO, Tcp } 23 | import net.sigusr.mqtt.impl.protocol.Engine 24 | 25 | class Manager(remote: InetSocketAddress) extends Engine(remote) { 26 | import context.system 27 | override def tcpManagerActor = IO(Tcp) 28 | } 29 | 30 | object Manager { 31 | def props(remote: InetSocketAddress) = Props(classOf[Manager], remote) 32 | } 33 | 34 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/MessageId.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | class MessageId(val identifier: Int) extends AnyVal 20 | 21 | object MessageId { 22 | 23 | def checkValue(value: Int): Boolean = value >= 0 && value < 65536 24 | 25 | def apply(value: Int): MessageId = { 26 | if (!checkValue(value)) 27 | throw new IllegalArgumentException("The value of a message identifier must be in the range [0..65535]") 28 | new MessageId(value) 29 | } 30 | 31 | def unapply(identifier: MessageId): Option[Int] = Some(identifier.identifier) 32 | } -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/api/ManagerSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor._ 22 | import net.sigusr.mqtt.SpecsTestKit 23 | import org.specs2.mutable._ 24 | 25 | object ManagerSpec extends Specification { 26 | 27 | "An MQTT protocol manager" should { 28 | "Provide its status" in new SpecsTestKit { 29 | 30 | val manager: ActorRef = testActorProxy { context => context.actorOf(Manager.props(new InetSocketAddress(1883))) } 31 | 32 | manager ! Status 33 | expectMsg(Disconnected) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/APIResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | sealed trait APIResponse 20 | 21 | case object Connected extends APIResponse 22 | case class ConnectionFailure(reason: ConnectionFailureReason) extends APIResponse 23 | 24 | case object Disconnected extends APIResponse 25 | 26 | case class Published(messageId: MessageId) extends APIResponse 27 | case class Message(topic: String, payload: Vector[Byte]) extends APIResponse 28 | 29 | case class Subscribed(topicResults: Vector[QualityOfService], messageId: MessageId) extends APIResponse 30 | 31 | case class Unsubscribed(messageId: MessageId) extends APIResponse 32 | 33 | case class Error(kind: ErrorKind) extends APIResponse -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/ConnectionFailureReason.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | import enumeratum.values._ 19 | 20 | sealed abstract class ConnectionFailureReason(val value: Int) extends IntEnumEntry 21 | 22 | object ConnectionFailureReason extends IntEnum[ConnectionFailureReason] { 23 | case object ServerNotResponding extends ConnectionFailureReason(0) 24 | case object BadProtocolVersion extends ConnectionFailureReason(1) 25 | case object IdentifierRejected extends ConnectionFailureReason(2) 26 | case object ServerUnavailable extends ConnectionFailureReason(3) 27 | case object BadUserNameOrPassword extends ConnectionFailureReason(4) 28 | case object NotAuthorized extends ConnectionFailureReason(5) 29 | 30 | val values: IndexedSeq[ConnectionFailureReason] = findValues 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt 18 | 19 | package object api { 20 | 21 | val DEFAULT_KEEP_ALIVE: Int = 30 22 | 23 | implicit def asMessageIdentifier(int: Int): MessageId = MessageId(int) 24 | 25 | implicit class MessageIdentifierLiteral(val sc: StringContext) extends AnyVal { 26 | def mi(args: Any*): MessageId = { 27 | val strings = sc.parts.iterator 28 | val expressions = args.iterator 29 | val buf = new StringBuffer(strings.next()) 30 | while (strings.hasNext) { 31 | buf append expressions.next 32 | buf append strings.next 33 | } 34 | MessageId(buf.toString.toInt) 35 | } 36 | } 37 | 38 | @inline final def assert(requirement: Boolean, message: => Any): Unit = { 39 | if (!requirement) 40 | throw new IllegalArgumentException(message.toString) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/frames/ConnectVariableHeader.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.frames 18 | 19 | import scodec.bits.{ BitVector, _ } 20 | import scodec.codecs._ 21 | 22 | case class ConnectVariableHeader( 23 | userNameFlag: Boolean, 24 | passwordFlag: Boolean, 25 | willRetain: Boolean, 26 | willQoS: Int, 27 | willFlag: Boolean, 28 | cleanSession: Boolean, 29 | keepAliveTimer: Int) { 30 | } 31 | 32 | object ConnectVariableHeader { 33 | 34 | /** 35 | * This is the, once for all encoded, protocol name [MQIsdp] and protocol version [3] 36 | */ 37 | val connectVariableHeaderFixedBytes: BitVector = BitVector(hex"00064D514973647003") 38 | 39 | implicit val connectVariableHeaderCodec = ( 40 | constant(connectVariableHeaderFixedBytes) :~>: 41 | bool :: 42 | bool :: 43 | bool :: 44 | qosCodec :: 45 | bool :: 46 | bool :: 47 | ignore(1) :~>: 48 | keepAliveCodec).as[ConnectVariableHeader] 49 | } -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/protocol/Action.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.protocol 18 | 19 | import net.sigusr.mqtt.api.APIResponse 20 | import net.sigusr.mqtt.impl.frames.Frame 21 | 22 | private[protocol] sealed trait Action 23 | 24 | private[protocol] case class Sequence(actions: Seq[Action] = Nil) extends Action 25 | private[protocol] case class SendToClient(message: APIResponse) extends Action 26 | private[protocol] case class SendToNetwork(frame: Frame) extends Action 27 | private[protocol] case object ForciblyCloseTransport extends Action 28 | private[protocol] case class SetKeepAlive(keepAlive: Long) extends Action 29 | private[protocol] case class StartPingRespTimer(timeout: Long) extends Action 30 | private[protocol] case class SetPendingPingResponse(isPending: Boolean) extends Action 31 | private[protocol] case class StoreSentInFlightFrame(id: Int, frame: Frame) extends Action 32 | private[protocol] case class RemoveSentInFlightFrame(id: Int) extends Action 33 | private[protocol] case class StoreRecvInFlightFrameId(id: Int) extends Action 34 | private[protocol] case class RemoveRecvInFlightFrameId(id: Int) extends Action 35 | 36 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/SpecUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt 18 | 19 | import org.specs2.matcher.{ Expectable, MatchResult, Matcher } 20 | import scodec.{ Attempt, Err } 21 | 22 | import scala.util.Random 23 | 24 | object SpecUtils { 25 | 26 | class SuccessfulAttemptMatcher[T](v: T) extends Matcher[Attempt[T]] { 27 | def apply[S <: Attempt[T]](e: Expectable[S]): MatchResult[S] = { 28 | result( 29 | e.value.fold(_ => false, _ == v), 30 | s"${e.description} equals to $v", 31 | s"The result is ${e.description}, instead of the expected value '$v'", 32 | e) 33 | } 34 | } 35 | 36 | class FailedAttemptMatcher[T](m: Err) extends Matcher[Attempt[T]] { 37 | def apply[S <: Attempt[T]](e: Expectable[S]): MatchResult[S] = { 38 | result( 39 | e.value.fold(_ => true, _ != m), 40 | s"${e.description} equals to $m", 41 | s"The result is ${e.description} instead of the expected error message '$m'", 42 | e) 43 | } 44 | } 45 | 46 | def succeedWith[T](t: T) = new SuccessfulAttemptMatcher[T](t) 47 | 48 | def failWith[T](t: Err) = new FailedAttemptMatcher[T](t) 49 | 50 | def makeRandomByteVector(size: Int): Vector[Byte] = { 51 | val bytes = new Array[Byte](size) 52 | Random.nextBytes(bytes) 53 | bytes.toVector 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/api/APICommand.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | import net.sigusr.mqtt.api.QualityOfService.AtMostOnce 20 | 21 | sealed trait APICommand 22 | 23 | case class Will(retain: Boolean, qos: QualityOfService, topic: String, message: String) 24 | 25 | case class Connect( 26 | clientId: String, 27 | keepAlive: Int = DEFAULT_KEEP_ALIVE, 28 | cleanSession: Boolean = true, 29 | will: Option[Will] = None, 30 | user: Option[String] = None, 31 | password: Option[String] = None) extends APICommand { 32 | assert(keepAlive >= 0 && keepAlive < 65636, "Keep alive value should be in the range [0..65535]") 33 | assert(user.isDefined || password.isEmpty, "A password cannot be provided without user") 34 | } 35 | 36 | case object Status extends APICommand 37 | 38 | case object Disconnect extends APICommand 39 | 40 | case class Publish( 41 | topic: String, 42 | payload: Vector[Byte], 43 | qos: QualityOfService = AtMostOnce, 44 | messageId: Option[MessageId] = None, 45 | retain: Boolean = false) extends APICommand { 46 | assert(qos == AtMostOnce || messageId.isDefined, "A message identifier must be provided when QoS is greater than 0") 47 | } 48 | 49 | case class Subscribe(topics: Vector[(String, QualityOfService)], messageId: MessageId) extends APICommand 50 | 51 | case class Unsubscribe(topics: Vector[String], messageId: MessageId) extends APICommand -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/frames/RemainingLengthCodec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.frames 18 | 19 | import scodec.Attempt._ 20 | import scodec._ 21 | import scodec.bits.{ BitVector, _ } 22 | import scodec.codecs._ 23 | 24 | final class RemainingLengthCodec extends Codec[Int] { 25 | 26 | val MinValue = 0 27 | val MaxValue = 268435455 28 | 29 | def sizeBound = SizeBound.bounded(8, 32) 30 | 31 | def decode(bits: BitVector): Attempt[DecodeResult[Int]] = { 32 | @annotation.tailrec 33 | def decodeAux(step: Attempt[DecodeResult[Int]], factor: Int, depth: Int, value: Int): Attempt[DecodeResult[Int]] = 34 | if (depth == 4) failure(Err("The remaining length must be 4 bytes long at most")) 35 | else step match { 36 | case f: Failure => f 37 | case Successful(d) => 38 | if ((d.value & 128) == 0) successful(DecodeResult(value + (d.value & 127) * factor, d.remainder)) 39 | else decodeAux(uint8.decode(d.remainder), factor * 128, depth + 1, value + (d.value & 127) * factor) 40 | } 41 | decodeAux(uint8.decode(bits), 1, 0, 0) 42 | } 43 | 44 | def encode(value: Int): Attempt[BitVector] = { 45 | @annotation.tailrec 46 | def encodeAux(value: Int, digit: Int, bytes: ByteVector): ByteVector = 47 | if (value == 0) bytes :+ digit.asInstanceOf[Byte] 48 | else encodeAux(value / 128, value % 128, bytes :+ (digit | 0x80).asInstanceOf[Byte]) 49 | if (value < MinValue || value > MaxValue) failure(Err(s"The remaining length must be in the range [$MinValue..$MaxValue], $value is not valid")) 50 | else successful(BitVector(encodeAux(value / 128, value % 128, ByteVector.empty))) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /examples/src/main/scala/net/sigusr/mqtt/examples/LocalPublisher.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.examples 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor.{ Actor, ActorRef, ActorSystem, Props } 22 | import com.typesafe.config.ConfigFactory 23 | import net.sigusr.mqtt.api._ 24 | 25 | import scala.concurrent.duration.{ FiniteDuration, _ } 26 | import scala.util.Random 27 | 28 | class LocalPublisher(toPublish: Vector[String]) extends Actor { 29 | 30 | import context.dispatcher 31 | 32 | context.actorOf(Manager.props(new InetSocketAddress(1883))) ! Connect(localSubscriber) 33 | 34 | val length: Int = toPublish.length 35 | 36 | def scheduleRandomMessage(): Unit = { 37 | val message = toPublish(Random.nextInt(length)) 38 | context.system.scheduler.scheduleOnce(FiniteDuration(Random.nextInt(2000).toLong + 1000, MILLISECONDS), self, message) 39 | () 40 | } 41 | 42 | def receive: Receive = { 43 | case Connected => 44 | println("Successfully connected to localhost:1883") 45 | println(s"Ready to publish to topic [ $localPublisher ]") 46 | scheduleRandomMessage() 47 | context become ready(sender()) 48 | case ConnectionFailure(reason) => 49 | println(s"Connection to localhost:1883 failed [$reason]") 50 | } 51 | 52 | def ready(mqttManager: ActorRef): Receive = { 53 | case m: String => 54 | println(s"Publishing [ $m ]") 55 | mqttManager ! Publish(localPublisher, m.getBytes("UTF-8").toVector) 56 | scheduleRandomMessage() 57 | } 58 | } 59 | 60 | object LocalPublisher { 61 | 62 | val config = 63 | """akka { 64 | loglevel = INFO 65 | actor { 66 | debug { 67 | receive = off 68 | autoreceive = off 69 | lifecycle = off 70 | } 71 | } 72 | } 73 | """ 74 | 75 | val system: ActorSystem = ActorSystem(localPublisher, ConfigFactory.parseString(config)) 76 | 77 | def shutdown(): Unit = { 78 | system.terminate() 79 | println(s"<$localPublisher> stopped") 80 | } 81 | 82 | def main(args: Array[String]): Unit = { 83 | system.actorOf(Props(classOf[LocalPublisher], args.toVector)) 84 | sys.addShutdownHook { shutdown() } 85 | println(s"<$localPublisher> started") 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /examples/src/main/scala/net/sigusr/mqtt/examples/LocalSubscriber.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.examples 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor.{ Actor, ActorRef, ActorSystem, Props } 22 | import com.typesafe.config.ConfigFactory 23 | import net.sigusr.mqtt.api.QualityOfService.AtMostOnce 24 | import net.sigusr.mqtt.api._ 25 | 26 | class LocalSubscriber(topics: Vector[String]) extends Actor { 27 | 28 | val stopTopic: String = s"$localSubscriber/stop" 29 | 30 | context.actorOf(Manager.props(new InetSocketAddress(1883))) ! Connect(localSubscriber) 31 | 32 | def receive: Receive = { 33 | case Connected => 34 | println("Successfully connected to localhost:1883") 35 | sender() ! Subscribe((stopTopic +: topics) zip Vector.fill(topics.length + 1) { AtMostOnce }, 1) 36 | context become ready(sender()) 37 | case ConnectionFailure(reason) => 38 | println(s"Connection to localhost:1883 failed [$reason]") 39 | } 40 | 41 | def ready(mqttManager: ActorRef): Receive = { 42 | case Subscribed(_, MessageId(1)) => 43 | println("Successfully subscribed to topics:") 44 | println(topics.mkString(" ", ",\n ", "")) 45 | case Message(`stopTopic`, _) => 46 | mqttManager ! Disconnect 47 | context become disconnecting 48 | case Message(topic, payload) => 49 | val message = new String(payload.toArray, "UTF-8") 50 | println(s"[$topic] $message") 51 | } 52 | 53 | def disconnecting(): Receive = { 54 | case Disconnected => 55 | println("Disconnected from localhost:1883") 56 | LocalSubscriber.shutdown() 57 | } 58 | } 59 | 60 | object LocalSubscriber { 61 | 62 | val config = 63 | """akka { 64 | loglevel = INFO 65 | actor { 66 | debug { 67 | receive = off 68 | autoreceive = off 69 | lifecycle = off 70 | } 71 | } 72 | } 73 | """ 74 | val system: ActorSystem = ActorSystem(localSubscriber, ConfigFactory.parseString(config)) 75 | 76 | def shutdown(): Unit = { 77 | system.terminate() 78 | println(s"<$localSubscriber> stopped") 79 | } 80 | 81 | def main(args: Array[String]): Unit = { 82 | system.actorOf(Props(classOf[LocalSubscriber], args.toVector)) 83 | sys.addShutdownHook { shutdown() } 84 | println(s"<$localSubscriber> started") 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/protocol/Registers.scala: -------------------------------------------------------------------------------- 1 | package net.sigusr.mqtt.impl.protocol 2 | 3 | import akka.actor.{ Actor, ActorContext, ActorRef, Cancellable } 4 | import akka.util.ByteString 5 | import net.sigusr.mqtt.api._ 6 | import net.sigusr.mqtt.impl.frames.Frame 7 | import scodec.bits.BitVector 8 | 9 | import scala.collection.immutable.{ TreeMap, TreeSet } 10 | 11 | case class Registers( 12 | lastSentMessageTimestamp: Long = 0, 13 | isPingResponsePending: Boolean = false, 14 | keepAlive: Long = DEFAULT_KEEP_ALIVE.toLong, 15 | timerTask: Option[Cancellable] = None, 16 | client: ActorRef = null, 17 | inFlightSentFrame: TreeMap[Int, Frame] = TreeMap.empty[Int, Frame], 18 | inFlightRecvFrame: TreeSet[Int] = TreeSet.empty[Int], 19 | remainingBytes: BitVector = BitVector.empty, 20 | tcpManager: ActorRef = null) { 21 | } 22 | 23 | object Registers { 24 | 25 | import akka.io.Tcp.Command 26 | import scalaz.State 27 | import scalaz.State._ 28 | 29 | type RegistersState[A] = State[Registers, A] 30 | 31 | def setLastSentMessageTimestamp(lastSentMessageTimeStamp: Long) = 32 | modify[Registers](_.copy(lastSentMessageTimestamp = lastSentMessageTimeStamp)) 33 | 34 | def setTimeOut(keepAlive: Long) = modify[Registers](_.copy(keepAlive = keepAlive)) 35 | 36 | def setPingResponsePending(isPingResponsePending: Boolean) = 37 | modify[Registers](_.copy(isPingResponsePending = isPingResponsePending)) 38 | 39 | def setTimerTask(timerTask: Cancellable) = modify[Registers](_.copy(timerTask = Some(timerTask))) 40 | 41 | def resetTimerTask = modify[Registers] { r => 42 | r.timerTask foreach { _.cancel() } 43 | r.copy(timerTask = None) 44 | } 45 | 46 | def storeInFlightSentFrame(id: Int, frame: Frame) = modify[Registers] { r => 47 | r.copy(inFlightSentFrame = r.inFlightSentFrame.updated(id, frame)) 48 | } 49 | 50 | def removeInFlightSentFrame(id: Int) = modify[Registers] { r => 51 | r.copy(inFlightSentFrame = r.inFlightSentFrame.drop(id)) 52 | } 53 | 54 | def storeInFlightRecvFrame(id: Int) = modify[Registers] { r => 55 | r.copy(inFlightRecvFrame = r.inFlightRecvFrame.incl(id)) 56 | } 57 | 58 | def removeInFlightRecvFrame(id: Int) = modify[Registers] { r => 59 | r.copy(inFlightRecvFrame = r.inFlightRecvFrame.drop(id)) 60 | } 61 | 62 | def getRemainingBits(bytes: ByteString) = gets[Registers, BitVector](_.remainingBytes ++ BitVector.view(bytes.toArray)) 63 | 64 | def setRemainingBits(bits: BitVector) = modify[Registers](_.copy(remainingBytes = bits)) 65 | 66 | def setTCPManager(tcpManager: ActorRef) = modify[Registers](_.copy(tcpManager = tcpManager)) 67 | 68 | def sendToTcpManager(command: Command)(implicit sender: ActorRef = Actor.noSender) = gets[Registers, Unit](_.tcpManager ! command) 69 | 70 | def sendToClient(response: APIResponse)(implicit sender: ActorRef = Actor.noSender) = gets[Registers, Unit](_.client ! response) 71 | 72 | def watchTcpManager(implicit context: ActorContext) = gets[Registers, ActorRef](context watch _.tcpManager) 73 | 74 | def unwatchTcpManager(implicit context: ActorContext) = gets[Registers, ActorRef](context unwatch _.tcpManager) 75 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A Scala MQTT client library [![Build Status](https://travis-ci.org/fcabestre/Scala-MQTT-client.svg?branch=master)](https://travis-ci.org/fcabestre/Scala-MQTT-client) [![Coverage Status](https://coveralls.io/repos/fcabestre/Scala-MQTT-client/badge.png?branch=master)](https://coveralls.io/r/fcabestre/Scala-MQTT-client?branch=master) 2 | 3 | ## In the beginning... 4 | 5 | I wanted to build a presentation of [Akka](http://akka.io). But not just a deck of slides, I wanted something to 6 | live-code with, something backed by real hardware. Maybe a Raspberry Pi. I imagined the small devices sending 7 | messages to an MQTT broker, probably [Mosquitto](http://mosquitto.org). 8 | 9 | To this purpose, I looked for an MQTT library which I could use with Scala, but those I found were only Java based. 10 | I thought: "Could be fun to implement the MQTT protocol directly with [Akka IO](http://doc.akka.io/docs/akka/snapshot/scala/io.html). 11 | Its [specification](http://public.dhe.ibm.com/software/dw/webservices/ws-mqtt/mqtt-v3r1.html) is rather short 12 | (around 42 printed pages)". 13 | 14 | And quickly, when I came to look at how to encode/decode MQTT protocol frames, I stumbled upon 15 | [Scodec](http://typelevel.org/projects/scodec). This seemed to be the encoding/decoding framework I was waiting for 16 | a long time. So I decided to give it a try... 17 | 18 | ## And now 19 | 20 | I have a basic and far from complete implementation of the thing. Frame encoding and decoding works pretty well, and 21 | it's possible to write some code to talk to [Mosquitto](http://mosquitto.org). For examples you can have a look to the [local subscriber](https://github.com/fcabestre/Scala-MQTT-client/blob/master/examples/src/main/scala/net/sigusr/mqtt/examples/LocalSubscriber.scala) or the 22 | [local publisher](https://github.com/fcabestre/Scala-MQTT-client/blob/master/examples/src/main/scala/net/sigusr/mqtt/examples/LocalPublisher.scala). 23 | I'm starting to take it a bit more seriously. I mean, thinking of doing something that could be useful to others. But 24 | there is still a lot of work to be done: 25 | 26 | * Learning how other MQTT APIs are organised to polish this one 27 | * Managing communication back pressure with [Akka IO](http://doc.akka.io/docs/akka/snapshot/scala/io.html) 28 | * Suppoting both MQTT v3.1 and v3.1.1 29 | * If I dare, passing [Paho](http://www.eclipse.org/paho/clients/testing/) conformance tests 30 | * And many, many, many more I can't foresee... 31 | 32 | ## Releases 33 | 34 | [ci]: https://travis-ci.org/fcabestre/Scala-MQTT-client/ 35 | [sonatype]: https://oss.sonatype.org/index.html#nexus-search;quick~scala-mqtt-client 36 | 37 | Artifacts are available at [Sonatype OSS Repository Hosting service][sonatype], even the ```SNAPSHOTS``` automatically 38 | built by [Travis CI][ci]. To include the Sonatype repositories in your SBT build you should add, 39 | 40 | ```scala 41 | resolvers ++= Seq( 42 | Resolver.sonatypeRepo("releases"), 43 | Resolver.sonatypeRepo("snapshots") 44 | ) 45 | ``` 46 | 47 | In case you want to easily give a try to this library, without the burden of adding resolvers, there is a release synced 48 | to Maven Central. In this case just add, 49 | 50 | ```scala 51 | scalaVersion := "2.11.6" 52 | 53 | libraryDependencies ++= Seq( 54 | "net.sigusr" %% "scala-mqtt-client" % "0.6.0" 55 | ) 56 | ``` 57 | 58 | Moreover, since version `0.7.0-SNAPSHOT`, there is a cross built set up for Scala 2.11 and 2.12. 59 | 60 | ## Dependencies 61 | 62 | Roughly speaking this library depends on `Scala`, `Akka` (core and soon I hope streams), `Scodec` core and `Scalaz`. 63 | 64 | ## License 65 | 66 | This work is licenced under an [Apache Version 2.0 license](http://github.com/fcabestre/Scala-MQTT-client/blob/master/LICENSE) 67 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/api/MessagesSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.api 18 | 19 | import net.sigusr.mqtt.api.ConnectionFailureReason.{ BadProtocolVersion, BadUserNameOrPassword, IdentifierRejected, NotAuthorized, ServerNotResponding, ServerUnavailable } 20 | import net.sigusr.mqtt.api.QualityOfService.{ AtLeastOnce, AtMostOnce, ExactlyOnce } 21 | import org.specs2.mutable._ 22 | 23 | object MessagesSpec extends Specification { 24 | 25 | "Quality of service" should { 26 | "Provide their «enum» value" in { 27 | AtMostOnce.value should be_==(0) 28 | AtLeastOnce.value should be_==(1) 29 | ExactlyOnce.value should be_==(2) 30 | } 31 | 32 | "Be constructable from their corresponding «enum» value" in { 33 | import net.sigusr.mqtt.api.QualityOfService._ 34 | 35 | withValue(-1) should throwA[IllegalArgumentException] 36 | withValue(0) should_=== AtMostOnce 37 | withValue(1) should_=== AtLeastOnce 38 | withValue(2) should_=== ExactlyOnce 39 | withValue(3) should throwA[IllegalArgumentException] 40 | } 41 | } 42 | 43 | "Connect failure reason" should { 44 | "Provide their «enum» value" in { 45 | ServerNotResponding.value should be_==(0) 46 | BadProtocolVersion.value should be_==(1) 47 | IdentifierRejected.value should be_==(2) 48 | ServerUnavailable.value should be_==(3) 49 | BadUserNameOrPassword.value should be_==(4) 50 | NotAuthorized.value should be_==(5) 51 | } 52 | 53 | "Be constructable from their corresponding «enum» value" in { 54 | import net.sigusr.mqtt.api.ConnectionFailureReason._ 55 | 56 | withValue(-1) should throwA[IllegalArgumentException] 57 | withValue(0) should_=== ServerNotResponding 58 | withValue(1) should_=== BadProtocolVersion 59 | withValue(2) should_=== IdentifierRejected 60 | withValue(3) should_=== ServerUnavailable 61 | withValue(4) should_=== BadUserNameOrPassword 62 | withValue(5) should_=== NotAuthorized 63 | withValue(6) should throwA[IllegalArgumentException] 64 | } 65 | } 66 | 67 | "MessageIdentifier" should { 68 | "Check the range of the integer provided to its constructor" in { 69 | MessageId(-1) should throwA[IllegalArgumentException] 70 | MessageId(0) should not(throwA[IllegalArgumentException]) 71 | MessageId(65535) should not(throwA[IllegalArgumentException]) 72 | MessageId(65536) should throwA[IllegalArgumentException] 73 | } 74 | 75 | "Allow pattern matching" in { 76 | MessageId(42) match { 77 | case MessageId(i) => i should_=== 42 78 | } 79 | } 80 | 81 | "Have a literal syntax" in { 82 | val four = 4 83 | val two = 2 84 | mi"$four$two" should_=== MessageId(42) 85 | mi"42" should_=== MessageId(42) 86 | mi"-1" should throwA[IllegalArgumentException] 87 | mi"65536" should throwA[IllegalArgumentException] 88 | mi"fortytwo" should throwA[NumberFormatException] 89 | } 90 | 91 | "Have a implicit conversion from Int" in { 92 | def id(messageIdentifier: MessageId): MessageId = messageIdentifier 93 | id(42) should_=== MessageId(42) 94 | id(-1) should throwA[IllegalArgumentException] 95 | id(65536) should throwA[IllegalArgumentException] 96 | } 97 | } 98 | 99 | "Connect" should { 100 | "Check the range of the keep alive value provided to its constructor" in { 101 | Connect("Client", keepAlive = -1) should throwA[IllegalArgumentException] 102 | Connect("Client", keepAlive = 0) should not(throwA[IllegalArgumentException]) 103 | Connect("Client", keepAlive = 65635) should not(throwA[IllegalArgumentException]) 104 | Connect("Client", keepAlive = 65636) should throwA[IllegalArgumentException] 105 | } 106 | 107 | "Check a user is provided when a password is set" in { 108 | Connect("Client", password = Some("pass")) should throwA[IllegalArgumentException] 109 | Connect("Client", user = Some("user"), password = Some("pass")) should not(throwA[IllegalArgumentException]) 110 | 111 | } 112 | } 113 | 114 | "Publish" should { 115 | "Have a valid combination of QoS and message identifier" in { 116 | Publish("topic", Vector(0x00), AtMostOnce) should not(throwA[IllegalArgumentException]) 117 | Publish("topic", Vector(0x00), AtLeastOnce) should throwA[IllegalArgumentException] 118 | Publish("topic", Vector(0x00), AtLeastOnce, Some(1)) should not(throwA[IllegalArgumentException]) 119 | Publish("topic", Vector(0x00), ExactlyOnce) should throwA[IllegalArgumentException] 120 | Publish("topic", Vector(0x00), ExactlyOnce, Some(1)) should not(throwA[IllegalArgumentException]) 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/protocol/Engine.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.protocol 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor.{ Actor, ActorLogging, ActorRef, Terminated } 22 | import akka.event.LoggingReceive 23 | import akka.io.Tcp 24 | import akka.util.ByteString 25 | import net.sigusr.mqtt.api._ 26 | import net.sigusr.mqtt.impl.frames.Frame 27 | import net.sigusr.mqtt.impl.protocol.Registers._ 28 | import scalaz.State 29 | import scodec.Err.InsufficientBits 30 | import scodec.bits.BitVector 31 | import scodec.{ Codec, DecodeResult, Err } 32 | 33 | import scala.concurrent.duration.{ FiniteDuration, _ } 34 | 35 | private[protocol] case object TimerSignal 36 | 37 | abstract class Engine(mqttBrokerAddress: InetSocketAddress) extends Actor with Handlers with ActorLogging { 38 | 39 | import context.dispatcher 40 | 41 | type RegistersManagerBody = Any => RegistersState[Unit] 42 | 43 | var registers: Registers = Registers(client = context.parent, tcpManager = tcpManagerActor) 44 | 45 | def tcpManagerActor: ActorRef 46 | 47 | def receive: Receive = notConnected 48 | 49 | private def registersManager(body: => RegistersManagerBody): Receive = { 50 | case m => registers = body(m).exec(registers) 51 | } 52 | 53 | private def notConnected: Receive = LoggingReceive { 54 | registersManager { 55 | case Status => 56 | sendToClient(Disconnected) 57 | case c: Connect => 58 | for { 59 | _ <- sendToTcpManager(Tcp.Connect(mqttBrokerAddress)) 60 | actions <- handleApiConnect(c) 61 | } yield context become connecting(actions) 62 | case _: APICommand => 63 | sendToClient(Error(NotConnected)) 64 | } 65 | } 66 | 67 | private def connecting(pendingActions: Action): Receive = LoggingReceive { 68 | registersManager { 69 | case Status => 70 | sendToClient(Disconnected) 71 | case _: APICommand => 72 | sendToClient(Error(NotConnected)) 73 | case Tcp.CommandFailed(_: Tcp.Connect) => 74 | for { 75 | _ <- setTCPManager(sender()) 76 | _ <- processAction(transportNotReady()) 77 | } yield context become notConnected 78 | case Tcp.Connected(_, _) => 79 | for { 80 | _ <- setTCPManager(sender()) 81 | _ <- sendToTcpManager(Tcp.Register(self)) 82 | _ <- processAction(pendingActions) 83 | _ <- watchTcpManager 84 | } yield context become connected 85 | } 86 | } 87 | 88 | private def connected: Receive = LoggingReceive { 89 | registersManager { 90 | case message: APICommand => 91 | for { 92 | actions <- handleApiCommand(message) 93 | _ <- processAction(actions) 94 | } yield () 95 | case TimerSignal => 96 | for { 97 | actions <- timerSignal(System.currentTimeMillis()) 98 | _ <- processAction(actions) 99 | } yield () 100 | case Tcp.Received(encodedResponse) => 101 | for { 102 | bits <- getRemainingBits(encodedResponse) 103 | _ <- decode(bits) 104 | } yield () 105 | case Terminated(_) | _: Tcp.ConnectionClosed => 106 | disconnect() 107 | } 108 | } 109 | 110 | private def decode(bits: BitVector): RegistersState[Unit] = { 111 | def onSuccess(d: DecodeResult[Frame]): RegistersState[Unit] = { 112 | for { 113 | actions <- handleNetworkFrames(d.value) 114 | _ <- processAction(actions) 115 | _ <- decode(d.remainder) 116 | } yield () 117 | } 118 | def onError(bits: BitVector): Err => RegistersState[Unit] = { 119 | case _: InsufficientBits => setRemainingBits(bits) 120 | case _ => disconnect() 121 | } 122 | Codec[Frame].decode(bits).fold(onError(bits), onSuccess) 123 | } 124 | 125 | private def disconnect(): RegistersState[Unit] = { 126 | for { 127 | _ <- unwatchTcpManager 128 | _ <- setTCPManager(tcpManagerActor) 129 | _ <- resetTimerTask 130 | _ <- processAction(connectionClosed()) 131 | } yield context become notConnected 132 | } 133 | 134 | private def processActionSeq(actions: Seq[Action]): RegistersState[Unit] = 135 | if (actions.isEmpty) State { x => (x, ()) } 136 | else for { 137 | _ <- processAction(actions.head) 138 | _ <- processActionSeq(actions.tail) 139 | } yield () 140 | 141 | private def processAction(action: Action): RegistersState[Unit] = action match { 142 | case Sequence(actions) => 143 | processActionSeq(actions) 144 | case SetKeepAlive(keepAlive) => 145 | setTimeOut(keepAlive) 146 | case StartPingRespTimer(timeout) => 147 | setTimerTask(context.system.scheduler.scheduleOnce(FiniteDuration(timeout, MILLISECONDS), self, TimerSignal)) 148 | case SetPendingPingResponse(isPending) => 149 | setPingResponsePending(isPending) 150 | case SendToClient(message) => 151 | sendToClient(message) 152 | case SendToNetwork(frame) => 153 | for { 154 | _ <- sendToTcpManager(Tcp.Write(ByteString(Codec[Frame].encode(frame).require.toByteArray))) 155 | _ <- setLastSentMessageTimestamp(System.currentTimeMillis()) 156 | } yield () 157 | case ForciblyCloseTransport => 158 | sendToTcpManager(Tcp.Abort) 159 | case StoreSentInFlightFrame(id, frame) => 160 | storeInFlightSentFrame(id, frame) 161 | case RemoveSentInFlightFrame(id) => 162 | removeInFlightSentFrame(id) 163 | case StoreRecvInFlightFrameId(id) => 164 | storeInFlightRecvFrame(id) 165 | case RemoveRecvInFlightFrameId(id) => 166 | removeInFlightRecvFrame(id) 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/frames/Frame.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.frames 18 | 19 | import net.sigusr.mqtt.impl.frames.ConnectVariableHeader._ 20 | import net.sigusr.mqtt.impl.frames.Header._ 21 | import scodec.Codec 22 | import scodec.bits.ByteVector 23 | import scodec.codecs._ 24 | import shapeless._ 25 | 26 | sealed trait Frame { 27 | def header: Header 28 | } 29 | 30 | case class ConnectFrame( 31 | header: Header, 32 | variableHeader: ConnectVariableHeader, 33 | clientId: String, 34 | topic: Option[String], 35 | message: Option[String], 36 | user: Option[String], 37 | password: Option[String]) extends Frame 38 | 39 | case class ConnackFrame(header: Header, returnCode: Int) extends Frame 40 | case class PublishFrame(header: Header, topic: String, messageIdentifier: Int, payload: ByteVector) extends Frame 41 | case class PubackFrame(header: Header, messageIdentifier: Int) extends Frame 42 | case class PubrecFrame(header: Header, messageIdentifier: Int) extends Frame 43 | case class PubrelFrame(header: Header, messageIdentifier: Int) extends Frame 44 | case class PubcompFrame(header: Header, messageIdentifier: Int) extends Frame 45 | case class SubscribeFrame(header: Header, messageIdentifier: Int, topics: Vector[(String, Int)]) extends Frame 46 | case class SubackFrame(header: Header, messageIdentifier: Int, topics: Vector[Int]) extends Frame 47 | case class UnsubscribeFrame(header: Header, messageIdentifier: Int, topics: Vector[String]) extends Frame 48 | case class UnsubackFrame(header: Header, messageIdentifier: Int) extends Frame 49 | case class PingReqFrame(header: Header) extends Frame 50 | case class PingRespFrame(header: Header) extends Frame 51 | case class DisconnectFrame(header: Header) extends Frame 52 | 53 | object Frame { 54 | implicit val discriminated: Discriminated[Frame, Int] = Discriminated(uint4) 55 | implicit val frameCodec = Codec.coproduct[Frame].auto 56 | } 57 | 58 | object ConnectFrame { 59 | implicit val discriminator: Discriminator[Frame, ConnectFrame, Int] = Discriminator(1) 60 | implicit val codec: Codec[ConnectFrame] = (headerCodec :: variableSizeBytes( 61 | remainingLengthCodec, 62 | connectVariableHeaderCodec >>:~ { (hdr: ConnectVariableHeader) => 63 | stringCodec :: 64 | conditional(hdr.willFlag, stringCodec) :: 65 | conditional(hdr.willFlag, stringCodec) :: 66 | conditional(hdr.userNameFlag, stringCodec) :: 67 | conditional(hdr.passwordFlag, stringCodec) 68 | })).as[ConnectFrame] 69 | } 70 | 71 | object ConnackFrame { 72 | implicit val discriminator: Discriminator[Frame, ConnackFrame, Int] = Discriminator(2) 73 | implicit val codec: Codec[ConnackFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, bytePaddingCodec :~>: returnCodeCodec)).as[ConnackFrame] 74 | } 75 | 76 | object PublishFrame { 77 | val dupLens = lens[PublishFrame].header.dup 78 | implicit val discriminator: Discriminator[Frame, PublishFrame, Int] = Discriminator(3) 79 | implicit val codec: Codec[PublishFrame] = (headerCodec >>:~ { 80 | (hdr: Header) => variableSizeBytes(remainingLengthCodec, stringCodec :: (if (hdr.qos != 0) messageIdCodec else provide(0)) :: bytes) 81 | }).as[PublishFrame] 82 | } 83 | 84 | object PubackFrame { 85 | implicit val discriminator: Discriminator[Frame, PubackFrame, Int] = Discriminator(4) 86 | implicit val codec: Codec[PubackFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec)).as[PubackFrame] 87 | } 88 | 89 | object PubrecFrame { 90 | implicit val discriminator: Discriminator[Frame, PubrecFrame, Int] = Discriminator(5) 91 | implicit val codec: Codec[PubrecFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec)).as[PubrecFrame] 92 | } 93 | 94 | object PubrelFrame { 95 | val dupLens = lens[PubrelFrame].header.dup 96 | implicit val discriminator: Discriminator[Frame, PubrelFrame, Int] = Discriminator(6) 97 | implicit val codec: Codec[PubrelFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec)).as[PubrelFrame] 98 | } 99 | 100 | object PubcompFrame { 101 | implicit val discriminator: Discriminator[Frame, PubcompFrame, Int] = Discriminator(7) 102 | implicit val codec: Codec[PubcompFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec)).as[PubcompFrame] 103 | } 104 | 105 | object SubscribeFrame { 106 | implicit val discriminator: Discriminator[Frame, SubscribeFrame, Int] = Discriminator(8) 107 | val topicCodec: Codec[(String, Int)] = (stringCodec :: ignore(6) :: uint2).dropUnits.as[(String, Int)] 108 | implicit val topicsCodec: Codec[Vector[(String, Int)]] = vector(topicCodec) 109 | implicit val codec: Codec[SubscribeFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec :: topicsCodec)).as[SubscribeFrame] 110 | } 111 | 112 | object SubackFrame { 113 | implicit val discriminator: Discriminator[Frame, SubackFrame, Int] = Discriminator(9) 114 | implicit val qosCodec: Codec[Vector[Int]] = vector(ignore(6).dropLeft(uint2)) 115 | implicit val codec: Codec[SubackFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec :: qosCodec)).as[SubackFrame] 116 | } 117 | 118 | object UnsubscribeFrame { 119 | implicit val discriminator: Discriminator[Frame, UnsubscribeFrame, Int] = Discriminator(10) 120 | implicit val codec: Codec[UnsubscribeFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec :: vector(stringCodec))).as[UnsubscribeFrame] 121 | } 122 | 123 | object UnsubackFrame { 124 | implicit val discriminator: Discriminator[Frame, UnsubackFrame, Int] = Discriminator(11) 125 | implicit val codec: Codec[UnsubackFrame] = (headerCodec :: variableSizeBytes(remainingLengthCodec, messageIdCodec)).as[UnsubackFrame] 126 | } 127 | 128 | object PingReqFrame { 129 | implicit val discriminator: Discriminator[Frame, PingReqFrame, Int] = Discriminator(12) 130 | implicit val codec: Codec[PingReqFrame] = (headerCodec :: bytePaddingCodec).dropUnits.as[PingReqFrame] 131 | } 132 | 133 | object PingRespFrame { 134 | implicit val discriminator: Discriminator[Frame, PingRespFrame, Int] = Discriminator(13) 135 | implicit val codec: Codec[PingRespFrame] = (headerCodec :: bytePaddingCodec).dropUnits.as[PingRespFrame] 136 | } 137 | 138 | object DisconnectFrame { 139 | implicit val discriminator: Discriminator[Frame, DisconnectFrame, Int] = Discriminator(14) 140 | implicit val codec: Codec[DisconnectFrame] = (headerCodec :: bytePaddingCodec).dropUnits.as[DisconnectFrame] 141 | } 142 | -------------------------------------------------------------------------------- /core/src/main/scala/net/sigusr/mqtt/impl/protocol/Handlers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.protocol 18 | 19 | import net.sigusr.mqtt.api.ConnectionFailureReason.ServerNotResponding 20 | import net.sigusr.mqtt.api.QualityOfService.{ AtLeastOnce, AtMostOnce, ExactlyOnce } 21 | import net.sigusr.mqtt.api._ 22 | import net.sigusr.mqtt.impl.frames._ 23 | import net.sigusr.mqtt.impl.protocol.Registers.RegistersState 24 | import scalaz.State._ 25 | import scodec.bits.ByteVector 26 | 27 | trait Handlers { 28 | 29 | private val zeroId = MessageId(0) 30 | 31 | private[protocol] def handleApiConnect(connect: Connect): RegistersState[Action] = gets { registers => 32 | val header = Header(dup = false, AtMostOnce.value) 33 | val retain = connect.will.fold(false)(_.retain) 34 | val qos = connect.will.fold(AtMostOnce.value)(_.qos.value) 35 | val topic = connect.will.map(_.topic) 36 | val message = connect.will.map(_.message) 37 | val variableHeader = ConnectVariableHeader( 38 | connect.user.isDefined, 39 | connect.password.isDefined, 40 | willRetain = retain, 41 | qos, 42 | willFlag = connect.will.isDefined, 43 | connect.cleanSession, 44 | connect.keepAlive) 45 | val actions = Seq( 46 | SetKeepAlive(connect.keepAlive.toLong * 1000), 47 | SendToNetwork(ConnectFrame(header, variableHeader, connect.clientId, topic, message, connect.user, connect.password))) 48 | Sequence(if (!connect.cleanSession) actions ++ registers.inFlightSentFrame.toSeq.map(p => SendToNetwork(p._2)) else actions) 49 | } 50 | 51 | private[protocol] def handleApiCommand(apiCommand: APICommand): RegistersState[Action] = gets { registers => 52 | apiCommand match { 53 | case Connect(clientId, keepAlive, cleanSession, will, user, password) => 54 | SendToClient(Error(AlreadyConnected)) 55 | case Disconnect => 56 | val header = Header(dup = false, AtMostOnce.value) 57 | SendToNetwork(DisconnectFrame(header)) 58 | case Publish(topic, payload, qos, messageId, retain) if qos == AtMostOnce => 59 | val header = Header(dup = false, qos.value, retain = retain) 60 | SendToNetwork(PublishFrame(header, topic, messageId.getOrElse(zeroId).identifier, ByteVector(payload))) 61 | case Publish(topic, payload, qos, Some(messageId), retain) => 62 | val header = Header(dup = false, qos.value, retain = retain) 63 | val frame = PublishFrame(header, topic, messageId.identifier, ByteVector(payload)) 64 | Sequence(Seq( 65 | StoreSentInFlightFrame(messageId.identifier, PublishFrame.dupLens.set(frame)(true)), 66 | SendToNetwork(frame))) 67 | case Subscribe(topics, messageId) => 68 | val header = Header(dup = false, AtLeastOnce.value) 69 | SendToNetwork(SubscribeFrame(header, messageId.identifier, topics.map((v: (String, QualityOfService)) => (v._1, v._2.value)))) 70 | case Unsubscribe(topics, messageId) => 71 | val header = Header(dup = false, AtLeastOnce.value) 72 | SendToNetwork(UnsubscribeFrame(header, messageId.identifier, topics)) 73 | case Status => 74 | SendToClient(Connected) 75 | } 76 | } 77 | 78 | private[protocol] def handleNetworkFrames(frame: Frame): RegistersState[Action] = gets { registers => 79 | frame match { 80 | case ConnackFrame(_, 0) => 81 | if (registers.keepAlive == 0) SendToClient(Connected) 82 | else Sequence(Seq( 83 | StartPingRespTimer(registers.keepAlive), 84 | SendToClient(Connected))) 85 | case ConnackFrame(_, returnCode) => 86 | SendToClient(ConnectionFailure(ConnectionFailureReason.withValue(returnCode))) 87 | case PingRespFrame(_) => 88 | SetPendingPingResponse(isPending = false) 89 | case PublishFrame(header, topic, messageIdentifier, payload) => 90 | val toClient = SendToClient(Message(topic, payload.toArray.toVector)) 91 | header.qos match { 92 | case AtMostOnce.value => 93 | toClient 94 | case AtLeastOnce.value => 95 | Sequence(Seq( 96 | toClient, 97 | SendToNetwork(PubackFrame(Header(), messageIdentifier)))) 98 | case ExactlyOnce.value => 99 | if (registers.inFlightRecvFrame(messageIdentifier)) 100 | Sequence(Seq( 101 | SendToNetwork(PubrecFrame(Header(), messageIdentifier)))) 102 | else 103 | Sequence(Seq( 104 | toClient, 105 | StoreRecvInFlightFrameId(messageIdentifier), 106 | SendToNetwork(PubrecFrame(Header(), messageIdentifier)))) 107 | } 108 | case PubackFrame(_, messageId) => 109 | Sequence(Seq( 110 | RemoveSentInFlightFrame(messageId), 111 | SendToClient(Published(messageId)))) 112 | case PubrecFrame(header, messageIdentifier) => 113 | val pubrelFrame = PubrelFrame(header.copy(qos = 1), messageIdentifier) 114 | Sequence(Seq( 115 | RemoveSentInFlightFrame(messageIdentifier), 116 | StoreSentInFlightFrame(messageIdentifier.identifier, PubrelFrame.dupLens.set(pubrelFrame)(true)), 117 | SendToNetwork(pubrelFrame))) 118 | case PubrelFrame(header, messageIdentifier) => 119 | Sequence(Seq( 120 | RemoveRecvInFlightFrameId(messageIdentifier), 121 | SendToNetwork(PubcompFrame(header.copy(qos = 0), messageIdentifier)))) 122 | case PubcompFrame(_, messageId) => 123 | Sequence(Seq( 124 | RemoveSentInFlightFrame(messageId), 125 | SendToClient(Published(messageId)))) 126 | case SubackFrame(_, messageIdentifier, topicResults) => 127 | SendToClient(Subscribed(topicResults.map(QualityOfService.withValue), messageIdentifier.identifier)) 128 | case UnsubackFrame(_, messageId) => 129 | SendToClient(Unsubscribed(messageId)) 130 | case _ => ForciblyCloseTransport 131 | } 132 | } 133 | 134 | private[protocol] def timerSignal(currentTime: Long): RegistersState[Action] = gets { registers => 135 | if (registers.isPingResponsePending) 136 | ForciblyCloseTransport 137 | else { 138 | val timeout = registers.keepAlive - currentTime + registers.lastSentMessageTimestamp 139 | if (timeout < 1000) 140 | Sequence(Seq( 141 | SetPendingPingResponse(isPending = true), 142 | StartPingRespTimer(registers.keepAlive), 143 | SendToNetwork(PingReqFrame(Header(dup = false, AtMostOnce.value))))) 144 | else 145 | StartPingRespTimer(timeout) 146 | } 147 | } 148 | 149 | private[protocol] def connectionClosed(): Action = SendToClient(Disconnected) 150 | 151 | private[protocol] def transportNotReady(): Action = SendToClient(ConnectionFailure(ServerNotResponding)) 152 | } 153 | 154 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/integration/ActorSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.integration 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor.{ ActorRef, Props } 22 | import akka.io.{ IO, Tcp } 23 | import net.sigusr.mqtt.SpecUtils._ 24 | import net.sigusr.mqtt.SpecsTestKit 25 | import net.sigusr.mqtt.api.ConnectionFailureReason.IdentifierRejected 26 | import net.sigusr.mqtt.api.QualityOfService.{ AtLeastOnce, AtMostOnce, ExactlyOnce } 27 | import net.sigusr.mqtt.api._ 28 | import net.sigusr.mqtt.impl.protocol.Engine 29 | import org.specs2.mutable._ 30 | 31 | import scala.concurrent.duration._ 32 | 33 | object ActorSpec extends Specification { 34 | 35 | sequential 36 | 37 | val brokerHost = "localhost" 38 | 39 | class TestMQTTManager(remote: InetSocketAddress) extends Engine(remote) { 40 | import context.system 41 | override def tcpManagerActor: ActorRef = IO(Tcp) 42 | } 43 | 44 | "The MQTTClient API" should { 45 | 46 | "Allow to connect to a broker and then disconnect" in new SpecsTestKit { 47 | 48 | import net.sigusr.mqtt.api.{ Connect, Connected, Disconnect, Disconnected } 49 | 50 | val endpoint = new InetSocketAddress(brokerHost, 1883) 51 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 52 | 53 | mqttManager ! Connect("Test") 54 | 55 | receiveOne(1 seconds) should be_==(Connected) 56 | 57 | mqttManager ! Disconnect 58 | 59 | receiveOne(1 seconds) should be_==(Disconnected) 60 | } 61 | 62 | "Allow to connect to a broker with user and password and then disconnect" in new SpecsTestKit { 63 | 64 | import net.sigusr.mqtt.api.{ Connect, Connected, Disconnect, Disconnected } 65 | 66 | val endpoint = new InetSocketAddress(brokerHost, 1883) 67 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 68 | 69 | mqttManager ! Connect("Test", user = Some("user"), password = Some("pass")) 70 | 71 | receiveOne(1 seconds) should be_==(Connected) 72 | 73 | mqttManager ! Disconnect 74 | 75 | receiveOne(1 seconds) should be_==(Disconnected) 76 | } 77 | 78 | "Disallow to connect to a broker with a wrong user" in new SpecsTestKit { 79 | 80 | import net.sigusr.mqtt.api.Connect 81 | 82 | val endpoint = new InetSocketAddress(brokerHost, 1883) 83 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 84 | 85 | mqttManager ! Connect("Test", user = Some("wrong"), password = Some("pass")) 86 | 87 | receiveOne(1 seconds) should be_==(ConnectionFailure(IdentifierRejected)) 88 | } 89 | 90 | "Disallow to connect to a broker with a wrong password" in new SpecsTestKit { 91 | 92 | import net.sigusr.mqtt.api.Connect 93 | 94 | val endpoint = new InetSocketAddress(brokerHost, 1883) 95 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 96 | 97 | mqttManager ! Connect("Test", user = Some("user"), password = Some("wrong")) 98 | 99 | receiveOne(1 seconds) should be_==(ConnectionFailure(IdentifierRejected)) 100 | } 101 | 102 | "Allow to connect to a broker and keep connected even when idle" in new SpecsTestKit { 103 | 104 | import net.sigusr.mqtt.api.{ Connect, Connected } 105 | 106 | val endpoint = new InetSocketAddress(brokerHost, 1883) 107 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 108 | 109 | mqttManager ! Connect("Test", keepAlive = 2) 110 | 111 | receiveOne(1 seconds) should be_==(Connected) 112 | 113 | expectNoMessage(4 seconds) should not(throwA[IllegalArgumentException]) 114 | 115 | mqttManager ! Disconnect 116 | 117 | receiveOne(1 seconds) should be_==(Disconnected) 118 | } 119 | 120 | "Allow to subscribe to topics and receive a subscription acknowledgement" in new SpecsTestKit { 121 | 122 | import net.sigusr.mqtt.api.{ Connect, Connected } 123 | 124 | val endpoint = new InetSocketAddress(brokerHost, 1883) 125 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 126 | 127 | mqttManager ! Connect("Test") 128 | 129 | receiveOne(1 seconds) should be_==(Connected) 130 | 131 | mqttManager ! Subscribe(Vector(("topic0", AtMostOnce), ("topic1", AtLeastOnce), ("topic2", ExactlyOnce)), 42) 132 | 133 | receiveOne(1 seconds) should be_==(Subscribed(Vector(AtMostOnce, AtLeastOnce, ExactlyOnce), 42)) 134 | 135 | mqttManager ! Disconnect 136 | 137 | receiveOne(1 seconds) should be_==(Disconnected) 138 | } 139 | 140 | "Allow to publish a message with QOS 0" in new SpecsTestKit { 141 | import net.sigusr.mqtt.api.{ Connect, Connected } 142 | 143 | val endpoint = new InetSocketAddress(brokerHost, 1883) 144 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 145 | 146 | mqttManager ! Connect("Test") 147 | 148 | receiveOne(1 seconds) should be_==(Connected) 149 | 150 | mqttManager ! Publish("a/b", "Hello world".getBytes.toVector, AtMostOnce, Some(123)) 151 | 152 | mqttManager ! Disconnect 153 | 154 | receiveOne(1 seconds) should be_==(Disconnected) 155 | } 156 | 157 | "Allow to publish a 'large' message with QOS 0 and read it back" in new SpecsTestKit { 158 | import net.sigusr.mqtt.api.{ Connect, Connected } 159 | 160 | val endpoint = new InetSocketAddress(brokerHost, 1883) 161 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 162 | val payload: Vector[Byte] = makeRandomByteVector(131070) 163 | 164 | mqttManager ! Connect("Test") 165 | 166 | receiveOne(1 seconds) should be_==(Connected) 167 | 168 | mqttManager ! Subscribe(Vector(("a/b", AtMostOnce)), 1) 169 | 170 | receiveOne(1 seconds) should be_==(Subscribed(Vector(AtMostOnce), 1)) 171 | 172 | mqttManager ! Publish("a/b", payload, AtMostOnce) 173 | 174 | receiveOne(1 seconds) //should be_==(Message("a/b", payload)) 175 | 176 | mqttManager ! Unsubscribe(Vector("a/b"), 2) 177 | 178 | receiveOne(1 seconds) should be_==(Unsubscribed(2)) 179 | 180 | mqttManager ! Disconnect 181 | 182 | receiveOne(1 seconds) should be_==(Disconnected) 183 | } 184 | 185 | "Allow to publish a message with QOS 1 and receive a Puback response" in new SpecsTestKit { 186 | import net.sigusr.mqtt.api.{ Connect, Connected } 187 | 188 | val endpoint = new InetSocketAddress(brokerHost, 1883) 189 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 190 | 191 | mqttManager ! Connect("Test") 192 | 193 | receiveOne(1 seconds) should be_==(Connected) 194 | 195 | mqttManager ! Publish("a/b", "Hello world".getBytes.toVector, AtLeastOnce, Some(123)) 196 | 197 | receiveOne(1 seconds) should be_==(Published(123)) 198 | 199 | mqttManager ! Disconnect 200 | 201 | receiveOne(1 seconds) should be_==(Disconnected) 202 | } 203 | 204 | "Allow to publish a message with QOS 2 and complete the handshake" in new SpecsTestKit { 205 | import net.sigusr.mqtt.api.{ Connect, Connected } 206 | 207 | val endpoint = new InetSocketAddress(brokerHost, 1883) 208 | val mqttManager: ActorRef = testActorProxy { context => context.actorOf(Props(new TestMQTTManager(endpoint))) } 209 | 210 | mqttManager ! Connect("Test") 211 | 212 | receiveOne(1 seconds) should be_==(Connected) 213 | 214 | mqttManager ! Publish("a/b", "Hello world".getBytes.toVector, ExactlyOnce, Some(123)) 215 | 216 | receiveOne(2 seconds) should be_==(Published(123)) 217 | 218 | mqttManager ! Disconnect 219 | 220 | receiveOne(1 seconds) should be_==(Disconnected) 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/impl/frames/CodecSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.frames 18 | 19 | import net.sigusr.mqtt.SpecUtils._ 20 | import net.sigusr.mqtt.api.QualityOfService.{ AtLeastOnce, AtMostOnce, ExactlyOnce } 21 | import org.specs2.mutable._ 22 | import scodec.bits._ 23 | import scodec.{ Codec, DecodeResult, Err, SizeBound } 24 | 25 | import scala.util.Random 26 | 27 | object CodecSpec extends Specification { 28 | 29 | "A remaining length codec" should { 30 | 31 | "Provide its size bounds" in { 32 | remainingLengthCodec.sizeBound should_=== SizeBound.bounded(8, 32) 33 | } 34 | 35 | "Perform encoding of valid inputs" in { 36 | remainingLengthCodec.encode(0) should succeedWith(hex"00".bits) 37 | remainingLengthCodec.encode(127) should succeedWith(hex"7f".bits) 38 | remainingLengthCodec.encode(128) should succeedWith(hex"8001".bits) 39 | remainingLengthCodec.encode(16383) should succeedWith(hex"ff7f".bits) 40 | remainingLengthCodec.encode(16384) should succeedWith(hex"808001".bits) 41 | remainingLengthCodec.encode(2097151) should succeedWith(hex"ffff7f".bits) 42 | remainingLengthCodec.encode(2097152) should succeedWith(hex"80808001".bits) 43 | remainingLengthCodec.encode(268435455) should succeedWith(hex"ffffff7f".bits) 44 | } 45 | 46 | "Fail to encode certain input values" in { 47 | remainingLengthCodec.encode(-1) should failWith(Err(s"The remaining length must be in the range [0..268435455], -1 is not valid")) 48 | remainingLengthCodec.encode(268435455 + 1) should failWith(Err(s"The remaining length must be in the range [0..268435455], 268435456 is not valid")) 49 | } 50 | 51 | "Perform decoding of valid inputs" in { 52 | remainingLengthCodec.decode(hex"00".bits) should succeedWith(DecodeResult(0, BitVector.empty)) 53 | remainingLengthCodec.decode(hex"7f".bits) should succeedWith(DecodeResult(127, BitVector.empty)) 54 | remainingLengthCodec.decode(hex"8001".bits) should succeedWith(DecodeResult(128, BitVector.empty)) 55 | remainingLengthCodec.decode(hex"ff7f".bits) should succeedWith(DecodeResult(16383, BitVector.empty)) 56 | remainingLengthCodec.decode(hex"808001".bits) should succeedWith(DecodeResult(16384, BitVector.empty)) 57 | remainingLengthCodec.decode(hex"ffff7f".bits) should succeedWith(DecodeResult(2097151, BitVector.empty)) 58 | remainingLengthCodec.decode(hex"80808001".bits) should succeedWith(DecodeResult(2097152, BitVector.empty)) 59 | remainingLengthCodec.decode(hex"ffffff7f".bits) should succeedWith(DecodeResult(268435455, BitVector.empty)) 60 | } 61 | 62 | "Fail to decode certain input values" in { 63 | remainingLengthCodec.decode(hex"808080807f".bits) should failWith(Err("The remaining length must be 4 bytes long at most")) 64 | remainingLengthCodec.decode(hex"ffffff".bits) should failWith(Err.insufficientBits(8, 0)) 65 | } 66 | } 67 | 68 | "A header codec" should { 69 | "Perform encoding of valid input" in { 70 | val header = Header(dup = false, AtLeastOnce.value, retain = true) 71 | Codec.encode(header) should succeedWith(bin"0011") 72 | } 73 | 74 | "Perform decoding of valid inputs" in { 75 | val header = Header(dup = true, ExactlyOnce.value) 76 | Codec[Header].decode(bin"1100110011") should succeedWith(DecodeResult(header, bin"110011")) 77 | } 78 | } 79 | 80 | "A connect variable header codec" should { 81 | "Perform encoding of valid inputs" in { 82 | 83 | import net.sigusr.mqtt.impl.frames.ConnectVariableHeader._ 84 | 85 | val connectVariableHeader = ConnectVariableHeader(cleanSession = true, willFlag = true, willQoS = AtMostOnce.value, willRetain = false, passwordFlag = true, userNameFlag = true, keepAliveTimer = 1024) 86 | val res = connectVariableHeaderFixedBytes ++ bin"110001100000010000000000" 87 | Codec.encode(connectVariableHeader) should succeedWith(res) 88 | } 89 | 90 | "Perform decoding of valid inputs" in { 91 | 92 | import net.sigusr.mqtt.impl.frames.ConnectVariableHeader._ 93 | 94 | val res = ConnectVariableHeader(cleanSession = false, willFlag = false, willQoS = AtLeastOnce.value, willRetain = true, passwordFlag = false, userNameFlag = false, keepAliveTimer = 12683) 95 | Codec[ConnectVariableHeader].decode(connectVariableHeaderFixedBytes ++ bin"001010000011000110001011101010") should succeedWith(DecodeResult(res, bin"101010")) 96 | } 97 | } 98 | 99 | "A connect message codec should" should { 100 | "[0] Perform round trip encoding/decoding of a valid input" in { 101 | val header = Header(dup = false, AtMostOnce.value) 102 | val connectVariableHeader = ConnectVariableHeader(userNameFlag = true, passwordFlag = true, willRetain = true, AtLeastOnce.value, willFlag = true, cleanSession = true, 15) 103 | val connectMessage = ConnectFrame(header, connectVariableHeader, "clientId", Some("Topic"), Some("Message"), Some("User"), Some("Password")) 104 | 105 | val valid = Codec[Frame].encode(connectMessage).require 106 | Codec[Frame].decode(valid) should succeedWith(DecodeResult(connectMessage, bin"")) 107 | } 108 | 109 | "[1] Perform round trip encoding/decoding of a valid input" in { 110 | val header = Header(dup = false, AtMostOnce.value) 111 | val connectVariableHeader = ConnectVariableHeader(userNameFlag = true, passwordFlag = false, willRetain = true, AtLeastOnce.value, willFlag = false, cleanSession = true, 15) 112 | val connectMessage = ConnectFrame(header, connectVariableHeader, "clientId", None, None, Some("User"), None) 113 | 114 | Codec[Frame].decode(Codec[Frame].encode(connectMessage).require) should succeedWith(DecodeResult(connectMessage, bin"")) 115 | } 116 | 117 | "[2] Perform round trip encoding/decoding of a valid input" in { 118 | val header = Header(dup = false, AtMostOnce.value) 119 | val connectVariableHeader = ConnectVariableHeader(userNameFlag = false, passwordFlag = false, willRetain = true, ExactlyOnce.value, willFlag = false, cleanSession = false, 128) 120 | val connectMessage = ConnectFrame(header, connectVariableHeader, "clientId", None, None, None, None) 121 | 122 | Codec[Frame].decode(Codec[Frame].encode(connectMessage).require) should succeedWith(DecodeResult(connectMessage, bin"")) 123 | } 124 | 125 | "Perform encoding and match a captured value" in { 126 | val header = Header(dup = false, AtMostOnce.value) 127 | val connectVariableHeader = ConnectVariableHeader(userNameFlag = false, passwordFlag = false, willRetain = true, AtLeastOnce.value, willFlag = true, cleanSession = false, 60) 128 | val connectMessage = ConnectFrame(header, connectVariableHeader, "test", Some("test/topic"), Some("test death"), None, None) 129 | 130 | val capture = BitVector(0x10, 0x2a, 0x00, 0x06, 0x4d, 0x51, 0x49, 0x73, 0x64, 0x70, 0x03, 0x2c, 0x00, 0x3c, 0x00, 0x04, 0x74, 0x65, 0x73, 0x74, 0x00, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2f, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x00, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x20, 0x64, 0x65, 0x61, 0x74, 0x68) 131 | Codec[Frame].encode(connectMessage) should succeedWith(capture) 132 | } 133 | } 134 | 135 | "A connack message codec" should { 136 | "Perform round trip encoding/decoding of a valid input" in { 137 | val header = Header(dup = false, AtMostOnce.value) 138 | val connackFrame = ConnackFrame(header, 0) 139 | 140 | Codec[Frame].decode(Codec[Frame].encode(connackFrame).require) should succeedWith(DecodeResult(connackFrame, bin"")) 141 | } 142 | 143 | "Perform decoding of captured values" in { 144 | val header = Header(dup = false, AtMostOnce.value) 145 | val connackFrame = ConnackFrame(header, 0) 146 | 147 | Codec[Frame].decode(BitVector(0x20, 0x02, 0x00, 0x00)) should succeedWith(DecodeResult(connackFrame, bin"")) 148 | } 149 | } 150 | 151 | "A topics codec" should { 152 | "Perform round trip encoding/decoding of a valid input" in { 153 | import net.sigusr.mqtt.impl.frames.SubscribeFrame.topicsCodec 154 | val topics = Vector(("topic0", AtMostOnce.value), ("topic1", AtLeastOnce.value), ("topic2", ExactlyOnce.value)) 155 | Codec[Vector[(String, Int)]].decode(Codec[Vector[(String, Int)]].encode(topics).require) should succeedWith(DecodeResult(topics, bin"")) 156 | } 157 | } 158 | 159 | "A subscribe codec" should { 160 | "Perform round trip encoding/decoding of a valid input" in { 161 | val header = Header(dup = false, AtLeastOnce.value) 162 | val topics = Vector(("topic0", AtMostOnce.value), ("topic1", AtLeastOnce.value), ("topic2", ExactlyOnce.value)) 163 | val subscribeFrame = SubscribeFrame(header, 3, topics) 164 | Codec[Frame].decode(Codec[Frame].encode(subscribeFrame).require) should succeedWith(DecodeResult(subscribeFrame, bin"")) 165 | } 166 | 167 | "Perform encoding and match a captured value" in { 168 | val header = Header(dup = false, AtLeastOnce.value) 169 | val topics = Vector(("topic", AtLeastOnce.value)) 170 | val subscribeFrame = SubscribeFrame(header, 1, topics) 171 | val capture = BitVector(0x82, 0x0a, 0x00, 0x01, 0x00, 0x05, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x01) 172 | Codec[Frame].encode(subscribeFrame) should succeedWith(capture) 173 | } 174 | } 175 | 176 | "A suback codec" should { 177 | "Perform round trip encoding/decoding of a valid input" in { 178 | val header = Header(dup = false, AtLeastOnce.value) 179 | val qos = Vector(AtMostOnce.value, AtLeastOnce.value, ExactlyOnce.value) 180 | val subackFrame = SubackFrame(header, 3, qos) 181 | Codec[Frame].decode(Codec[Frame].encode(subackFrame).require) should succeedWith(DecodeResult(subackFrame, bin"")) 182 | } 183 | } 184 | 185 | "An unsubscribe codec" should { 186 | "Perform round trip encoding/decoding of a valid input" in { 187 | val header = Header(dup = false, AtLeastOnce.value) 188 | val topics = Vector("topic0", "topic1") 189 | val unsubscribeFrame = UnsubscribeFrame(header, Random.nextInt(65536), topics) 190 | Codec[Frame].decode(Codec[Frame].encode(unsubscribeFrame).require) should succeedWith(DecodeResult(unsubscribeFrame, bin"")) 191 | } 192 | } 193 | 194 | "An unsuback codec" should { 195 | "Perform round trip encoding/decoding of a valid input" in { 196 | val header = Header(dup = false, AtLeastOnce.value) 197 | val unsubackFrame = UnsubackFrame(header, Random.nextInt(65536)) 198 | Codec[Frame].decode(Codec[Frame].encode(unsubackFrame).require) should succeedWith(DecodeResult(unsubackFrame, bin"")) 199 | } 200 | } 201 | 202 | "A disconnect message codec" should { 203 | "Perform round trip encoding/decoding of a valid input" in { 204 | val header = Header(dup = false, AtMostOnce.value) 205 | val disconnectFrame = DisconnectFrame(header) 206 | 207 | Codec[Frame].decode(Codec[Frame].encode(disconnectFrame).require) should succeedWith(DecodeResult(disconnectFrame, bin"")) 208 | } 209 | } 210 | 211 | "A ping request message codec" should { 212 | "Perform round trip encoding/decoding of a valid input" in { 213 | val header = Header(dup = false, AtMostOnce.value) 214 | val pingReqFrame = PingReqFrame(header) 215 | 216 | Codec[Frame].decode(Codec[Frame].encode(pingReqFrame).require) should succeedWith(DecodeResult(pingReqFrame, bin"")) 217 | } 218 | 219 | "Perform encoding and match a captured value" in { 220 | val header = Header(dup = false, AtMostOnce.value) 221 | val connectMessage = PingReqFrame(header) 222 | 223 | val capture = BitVector(0xc0, 0x00) 224 | Codec[Frame].encode(connectMessage) should succeedWith(capture) 225 | } 226 | } 227 | 228 | "A ping response message codec" should { 229 | "Perform round trip encoding/decoding of a valid input" in { 230 | val header = Header(dup = false, AtMostOnce.value) 231 | val pingRespFrame = PingRespFrame(header) 232 | 233 | Codec[Frame].decode(Codec[Frame].encode(pingRespFrame).require) should succeedWith(DecodeResult(pingRespFrame, bin"")) 234 | } 235 | 236 | "Perform decoding of captured values" in { 237 | val header = Header(dup = false, AtMostOnce.value) 238 | val pingRespFrame = PingRespFrame(header) 239 | 240 | Codec[Frame].decode(BitVector(0xd0, 0x00)) should succeedWith(DecodeResult(pingRespFrame, bin"")) 241 | } 242 | } 243 | 244 | "A publish message codec" should { 245 | "Perform round trip encoding/decoding of a valid input with a QoS greater than 0" in { 246 | val header = Header(dup = false, AtLeastOnce.value) 247 | val topic = "a/b" 248 | val publishFrame = PublishFrame(header, topic, 10, ByteVector("Hello world".getBytes)) 249 | 250 | Codec[Frame].decode(Codec[Frame].encode(publishFrame).require) should succeedWith(DecodeResult(publishFrame, bin"")) 251 | } 252 | 253 | "Perform round trip encoding/decoding of a valid input with a QoS equals to 0" in { 254 | val header = Header(dup = false, AtMostOnce.value) 255 | val topic = "a/b" 256 | val publishFrame = PublishFrame(header, topic, 0, ByteVector("Hello world".getBytes)) 257 | 258 | Codec[Frame].decode(Codec[Frame].encode(publishFrame).require) should succeedWith(DecodeResult(publishFrame, bin"")) 259 | } 260 | } 261 | 262 | "A message codec" should { 263 | "Fail if there is not enough bytes to decodde" in { 264 | val header = Header(dup = false, AtLeastOnce.value) 265 | val topic = "a/b" 266 | val publishFrame = PublishFrame(header, topic, 10, ByteVector(makeRandomByteVector(256))) 267 | 268 | val bitVector = Codec[Frame].encode(publishFrame).require 269 | val head = bitVector.take(56 * 8) 270 | Codec[Frame].decode(head) should failWith(Err.insufficientBits(2104, 424)) 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/impl/protocol/HandlersSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.protocol 18 | 19 | import net.sigusr.mqtt.SpecUtils._ 20 | import net.sigusr.mqtt.api.ConnectionFailureReason.{ BadUserNameOrPassword, ServerNotResponding } 21 | import net.sigusr.mqtt.api.QualityOfService.{ AtLeastOnce, AtMostOnce, ExactlyOnce } 22 | import net.sigusr.mqtt.api._ 23 | import net.sigusr.mqtt.impl.frames._ 24 | import org.specs2.mutable.Specification 25 | import scodec.bits.ByteVector 26 | 27 | import scala.collection.immutable.{ TreeMap, TreeSet } 28 | import scala.util.Random 29 | 30 | object HandlersSpec extends Specification with Handlers { 31 | 32 | "The transportNotReady() function" should { 33 | "Define the action to perform when the transport is not ready" in { 34 | transportNotReady() shouldEqual SendToClient(ConnectionFailure(ServerNotResponding)) 35 | } 36 | } 37 | 38 | "The connectionClosed() function" should { 39 | "Define the action to perform when the connection is closed" in { 40 | connectionClosed() shouldEqual SendToClient(Disconnected) 41 | } 42 | } 43 | 44 | "The handleApiConnect() function" should { 45 | "Define the action to perform to handle a Connect API message with session cleaning" in { 46 | val clientId = "client id" 47 | val keepAlive = 60 48 | val cleanSession = true 49 | val topic = Some("topic") 50 | val message = Some("message") 51 | val will = Will(retain = false, AtLeastOnce, "topic", "message") 52 | val user = Some("user") 53 | val password = Some("password") 54 | val input = Connect(clientId, keepAlive, cleanSession, Some(will), user, password) 55 | val header = Header(dup = false, AtMostOnce.value) 56 | val variableHeader = ConnectVariableHeader(user.isDefined, password.isDefined, willRetain = false, AtLeastOnce.value, willFlag = true, cleanSession, keepAlive) 57 | val publishFrame = PublishFrame(Header(), "topic", 42, ByteVector(0x01)) 58 | val pubrecFrame = PubrecFrame(Header(), 19) 59 | val registers = Registers(inFlightSentFrame = TreeMap(42 -> publishFrame, 19 -> pubrecFrame)) 60 | val expected = Sequence(Seq( 61 | SetKeepAlive(keepAlive.toLong * 1000), 62 | SendToNetwork(ConnectFrame(header, variableHeader, clientId, topic, message, user, password)))) 63 | handleApiConnect(input).eval(registers) should_== expected 64 | } 65 | 66 | "Define the action to perform to handle a Connect API message when there is in flight frames and no session cleaning" in { 67 | val clientId = "client id" 68 | val keepAlive = 60 69 | val cleanSession = false 70 | val topic = Some("topic") 71 | val message = Some("message") 72 | val will = Will(retain = false, AtLeastOnce, "topic", "message") 73 | val user = Some("user") 74 | val password = Some("password") 75 | val input = Connect(clientId, keepAlive, cleanSession, Some(will), user, password) 76 | 77 | val header = Header(dup = false, AtMostOnce.value) 78 | val variableHeader = ConnectVariableHeader(user.isDefined, password.isDefined, willRetain = false, AtLeastOnce.value, willFlag = true, cleanSession, keepAlive) 79 | val connectFrame = ConnectFrame(header, variableHeader, clientId, topic, message, user, password) 80 | val publishFrame = PublishFrame(Header(dup = true), "topic", 42, ByteVector(0x01)) 81 | val pubrelFrame = PubrelFrame(Header(dup = true, qos = AtMostOnce.value), 19) 82 | val registers = Registers(inFlightSentFrame = TreeMap(42 -> publishFrame, 19 -> pubrelFrame)) 83 | 84 | val expected = Sequence(Seq( 85 | SetKeepAlive(keepAlive.toLong * 1000), 86 | SendToNetwork(connectFrame), 87 | SendToNetwork(pubrelFrame), 88 | SendToNetwork(publishFrame))) 89 | handleApiConnect(input).eval(registers) should_== expected 90 | } 91 | } 92 | 93 | "The handleApiCommand() function" should { 94 | 95 | "Define the action to perform to handle a Connect API message when already connected" in { 96 | val clientId = "client id" 97 | val keepAlive = 60 98 | val cleanSession = false 99 | val will = Will(retain = false, AtLeastOnce, "topic", "message") 100 | val user = Some("user") 101 | val password = Some("password") 102 | val input = Connect(clientId, keepAlive, cleanSession, Some(will), user, password) 103 | val expected = SendToClient(Error(AlreadyConnected)) 104 | handleApiCommand(input).eval(Registers()) should_== expected 105 | } 106 | 107 | "Define the action to perform to handle a Disconnect API message" in { 108 | val input = Disconnect 109 | val header = Header(dup = false, AtMostOnce.value) 110 | val expected = SendToNetwork(DisconnectFrame(header)) 111 | handleApiCommand(input).eval(Registers()) should_== expected 112 | } 113 | 114 | "Define the action to perform to handle a Status API message" in { 115 | val input = Status 116 | SendToClient(Connected) 117 | val expected = SendToClient(Connected) 118 | handleApiCommand(input).eval(Registers()) should_== expected 119 | } 120 | 121 | "Define the action to perform to handle a Subscribe API message" in { 122 | val topicsInput = Vector(("topic0", AtMostOnce), ("topic1", ExactlyOnce), ("topic2", AtLeastOnce)) 123 | val messageId = Random.nextInt(65536) 124 | val input = Subscribe(topicsInput, messageId) 125 | val header = Header(dup = false, AtLeastOnce.value) 126 | val topicsResult = Vector(("topic0", AtMostOnce.value), ("topic1", ExactlyOnce.value), ("topic2", AtLeastOnce.value)) 127 | val expected = SendToNetwork(SubscribeFrame(header, messageId, topicsResult)) 128 | handleApiCommand(input).eval(Registers()) should_== expected 129 | } 130 | 131 | "Define the action to perform to handle an Unsubscribe API message" in { 132 | val topicsInput = Vector("topic0") 133 | val messageId = Random.nextInt(65536) 134 | val input = Unsubscribe(topicsInput, messageId) 135 | val header = Header(dup = false, AtLeastOnce.value) 136 | val topicsResult = Vector("topic0") 137 | val expected = SendToNetwork(UnsubscribeFrame(header, messageId, topicsResult)) 138 | handleApiCommand(input).eval(Registers()) should_== expected 139 | } 140 | 141 | "Define the action to perform to handle a Publish API message with QoS of 'At most once'" in { 142 | val topic = "topic0" 143 | val qos = AtMostOnce 144 | val retain = true 145 | val payload = makeRandomByteVector(48) 146 | val messageId = Random.nextInt(65536) 147 | val input = Publish(topic, payload, qos, Some(messageId), retain) 148 | val header = Header(dup = false, qos.value, retain) 149 | val expected = SendToNetwork(PublishFrame(header, topic, messageId, ByteVector(payload))) 150 | handleApiCommand(input).eval(Registers()) should_== expected 151 | } 152 | 153 | "Define the action to perform to handle a Publish API message with QoS of 'at least once' or 'exactly once'" in { 154 | val topic = "topic0" 155 | val qos = AtLeastOnce 156 | val retain = true 157 | val payload = makeRandomByteVector(32) 158 | val messageId = Random.nextInt(65536) 159 | val input = Publish(topic, payload, qos, Some(messageId), retain) 160 | val inputHeader = Header(dup = false, qos.value, retain) 161 | val expectedHeader = Header(dup = true, qos.value, retain) 162 | val expected = 163 | Sequence( 164 | Seq( 165 | StoreSentInFlightFrame(messageId, PublishFrame(expectedHeader, topic, messageId, ByteVector(payload))), 166 | SendToNetwork(PublishFrame(inputHeader, topic, messageId, ByteVector(payload))))) 167 | handleApiCommand(input).eval(Registers()) should_== expected 168 | } 169 | } 170 | 171 | "The timerSignal() function" should { 172 | "Define the action to perform to handle a SendKeepAlive internal API message while not waiting for a ping response and messages were recently sent" in { 173 | val state = Registers(keepAlive = 30000, lastSentMessageTimestamp = 120000000) 174 | val expected = StartPingRespTimer(29500) 175 | timerSignal(120000500).eval(state) should_== expected 176 | } 177 | 178 | "Define the action to perform to handle a SendKeepAlive internal API message while not waiting for a ping response but no messages were recently sent" in { 179 | val state = Registers(keepAlive = 30000, lastSentMessageTimestamp = 120000000) 180 | val expected = Sequence(Seq( 181 | SetPendingPingResponse(isPending = true), 182 | StartPingRespTimer(30000), 183 | SendToNetwork(PingReqFrame(Header(dup = false, AtMostOnce.value))))) 184 | timerSignal(120029001).eval(state) should_== expected 185 | } 186 | 187 | "Define the action to perform to handle a SendKeepAlive internal API message while waiting for a ping response" in { 188 | val state = Registers(keepAlive = 30000, lastSentMessageTimestamp = 120000000, isPingResponsePending = true) 189 | val expected = ForciblyCloseTransport 190 | timerSignal(120029999).eval(state) should_== expected 191 | } 192 | } 193 | 194 | "The handleNetworkFrames() function" should { 195 | 196 | "Provide no actions when the frame should not be handled" in { 197 | val header = Header(dup = false, AtLeastOnce.value) 198 | val input = PingReqFrame(header) 199 | val state = Registers(keepAlive = 30000) 200 | val expected = ForciblyCloseTransport 201 | handleNetworkFrames(input).eval(state) should_== expected 202 | } 203 | 204 | "Define the actions to perform to handle a ConnackFrame on a successful connection with keep alive greater than 0" in { 205 | val header = Header(dup = false, AtLeastOnce.value) 206 | val input = ConnackFrame(header, 0) 207 | val state = Registers(keepAlive = 30000) 208 | val expected = Sequence(Seq(StartPingRespTimer(state.keepAlive), SendToClient(Connected))) 209 | handleNetworkFrames(input).eval(state) should_== expected 210 | } 211 | 212 | "Define the actions to perform to handle a ConnackFrame on a successful connection with keep alive equal to 0" in { 213 | val header = Header(dup = false, AtLeastOnce.value) 214 | val input = ConnackFrame(header, 0) 215 | val state = Registers(keepAlive = 0) 216 | val expected = SendToClient(Connected) 217 | handleNetworkFrames(input).eval(state) should_== expected 218 | } 219 | 220 | "Define the actions to perform to handle a ConnackFrame (failed connection)" in { 221 | val header = Header(dup = false, AtLeastOnce.value) 222 | val reason = BadUserNameOrPassword 223 | val input = ConnackFrame(header, reason.value) 224 | val state = Registers(keepAlive = 30000) 225 | val expected = SendToClient(ConnectionFailure(reason)) 226 | handleNetworkFrames(input).eval(state) should_== expected 227 | } 228 | 229 | "Define the actions to perform to handle a PingRespFrame" in { 230 | val header = Header(dup = false, AtLeastOnce.value) 231 | val input = PingRespFrame(header) 232 | val state = Registers(keepAlive = 30000) 233 | val expected = SetPendingPingResponse(isPending = false) 234 | handleNetworkFrames(input).eval(state) should_== expected 235 | } 236 | 237 | "Define the actions to perform to handle a PublishFrame with a QoS of at most once" in { 238 | val header = Header(dup = false, AtMostOnce.value) 239 | val topic = "topic" 240 | val payload = makeRandomByteVector(64) 241 | val input = PublishFrame(header, topic, Random.nextInt(65536), ByteVector(payload)) 242 | val state = Registers(keepAlive = 30000) 243 | val expected = SendToClient(Message(topic, payload)) 244 | handleNetworkFrames(input).eval(state) should_== expected 245 | } 246 | 247 | "Define the actions to perform to handle a PublishFrame with a QoS of at least once" in { 248 | val header = Header(dup = false, AtLeastOnce.value) 249 | val topic = "topic" 250 | val payload = makeRandomByteVector(64) 251 | val messageId = Random.nextInt(65536) 252 | val input = PublishFrame(header, topic, messageId, ByteVector(payload)) 253 | val state = Registers(keepAlive = 30000) 254 | val expected = Sequence(Seq( 255 | SendToClient(Message(topic, payload)), 256 | SendToNetwork(PubackFrame(header.copy(qos = 0), messageId)))) 257 | handleNetworkFrames(input).eval(state) should_== expected 258 | } 259 | 260 | "Define the actions to perform to handle an already received PublishFrame with a QoS of exactly once" in { 261 | val header = Header(dup = false, ExactlyOnce.value) 262 | val topic = "topic" 263 | val payload = makeRandomByteVector(64) 264 | val messageId = Random.nextInt(65536) 265 | val input = PublishFrame(header, topic, messageId, ByteVector(payload)) 266 | val state = Registers(keepAlive = 30000, inFlightRecvFrame = TreeSet(messageId)) 267 | val expected = Sequence(Seq( 268 | SendToNetwork(PubrecFrame(header.copy(qos = 0), messageId)))) 269 | handleNetworkFrames(input).eval(state) should_== expected 270 | } 271 | 272 | "Define the actions to perform to handle a new PublishFrame with a QoS of exactly once" in { 273 | val header = Header(dup = false, ExactlyOnce.value) 274 | val topic = "topic" 275 | val payload = makeRandomByteVector(64) 276 | val messageId = Random.nextInt(65536) 277 | val input = PublishFrame(header, topic, messageId, ByteVector(payload)) 278 | val state = Registers(keepAlive = 30000) 279 | val expected = Sequence(Seq( 280 | SendToClient(Message(topic, payload)), 281 | StoreRecvInFlightFrameId(messageId), 282 | SendToNetwork(PubrecFrame(header.copy(qos = 0), messageId)))) 283 | handleNetworkFrames(input).eval(state) should_== expected 284 | } 285 | 286 | "Define the actions to perform to handle a PubackFrame" in { 287 | val header = Header(dup = false, AtMostOnce.value) 288 | val messageId = Random.nextInt(65536) 289 | val input = PubackFrame(header, messageId) 290 | val state = Registers(keepAlive = 30000) 291 | val expected = Sequence(Seq(RemoveSentInFlightFrame(messageId), SendToClient(Published(messageId)))) 292 | handleNetworkFrames(input).eval(state) should_== expected 293 | } 294 | 295 | "Define the actions to perform to handle a PubrecFrame" in { 296 | val header = Header(dup = false, AtMostOnce.value) 297 | val messageId = Random.nextInt(65536) 298 | val input = PubrecFrame(header, messageId) 299 | val state = Registers(keepAlive = 30000) 300 | val frame = PubrelFrame(header.copy(qos = 1), messageId) 301 | val expected = 302 | Sequence( 303 | Seq( 304 | RemoveSentInFlightFrame(messageId), 305 | StoreSentInFlightFrame(messageId, PubrelFrame(header.copy(dup = true, qos = 1), messageId)), 306 | SendToNetwork(frame))) 307 | handleNetworkFrames(input).eval(state) should_== expected 308 | } 309 | 310 | "Define the actions to perform to handle a PubrelFrame" in { 311 | val header = Header(dup = false, AtMostOnce.value) 312 | val messageId = Random.nextInt(65536) 313 | val input = PubrelFrame(header.copy(qos = 1), messageId) 314 | val state = Registers(keepAlive = 30000) 315 | val result = Sequence( 316 | Seq( 317 | RemoveRecvInFlightFrameId(messageId), 318 | SendToNetwork(PubcompFrame(header, messageId)))) 319 | handleNetworkFrames(input).eval(state) should_== result 320 | } 321 | 322 | "Define the actions to perform to handle a PubcompFrame" in { 323 | val header = Header(dup = false, AtMostOnce.value) 324 | val messageId = Random.nextInt(65536) 325 | val input = PubcompFrame(header, messageId) 326 | val state = Registers(keepAlive = 30000) 327 | val expected = Sequence(Seq(RemoveSentInFlightFrame(messageId), SendToClient(Published(messageId)))) 328 | handleNetworkFrames(input).eval(state) should_== expected 329 | } 330 | 331 | "Define the actions to perform to handle a SubackFrame" in { 332 | val header = Header(dup = false, AtMostOnce.value) 333 | val messageId = Random.nextInt(65536) 334 | val qosInput = Vector(AtLeastOnce.value, ExactlyOnce.value) 335 | val qosResult = Vector(AtLeastOnce, ExactlyOnce) 336 | val input = SubackFrame(header, messageId, qosInput) 337 | val state = Registers(keepAlive = 30000) 338 | val expected = SendToClient(Subscribed(qosResult, messageId)) 339 | handleNetworkFrames(input).eval(state) should_== expected 340 | } 341 | 342 | "Define the actions to perform to handle an UnsubackFrame" in { 343 | val header = Header(dup = false, AtMostOnce.value) 344 | val messageId = Random.nextInt(65536) 345 | val input = UnsubackFrame(header, messageId) 346 | val state = Registers(keepAlive = 30000) 347 | val expected = SendToClient(Unsubscribed(messageId)) 348 | handleNetworkFrames(input).eval(state) should_== expected 349 | } 350 | } 351 | } 352 | -------------------------------------------------------------------------------- /core/src/test/scala/net/sigusr/mqtt/impl/protocol/EngineSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Frédéric Cabestre 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package net.sigusr.mqtt.impl.protocol 18 | 19 | import java.net.InetSocketAddress 20 | 21 | import akka.actor._ 22 | import akka.io.Tcp.{ Abort => TCPAbort, Aborted => TCPAborted, Closed => TCPClosed, CommandFailed => TCPCommandFailed, Connect => TCPConnect, Connected => TCPConnected, Received => TCPReceived, Register => TCPRegister, Write => TCPWrite } 23 | import akka.testkit.{ ImplicitSender, TestProbe } 24 | import akka.util.ByteString 25 | import net.sigusr.mqtt.SpecsTestKit 26 | import net.sigusr.mqtt.api.ConnectionFailureReason.ServerNotResponding 27 | import net.sigusr.mqtt.api.QualityOfService.{ AtLeastOnce, AtMostOnce, ExactlyOnce } 28 | import net.sigusr.mqtt.api.{ Status => MqttApiStatus, _ } 29 | import net.sigusr.mqtt.impl.frames.{ Frame, Header, PublishFrame } 30 | import org.specs2.matcher.MatchResult 31 | import org.specs2.mutable.Specification 32 | import scodec.Codec 33 | import scodec.bits.ByteVector 34 | 35 | object EngineSpec extends Specification { 36 | 37 | sequential 38 | isolated 39 | 40 | private val fakeBrokerAddress: InetSocketAddress = new InetSocketAddress(0) 41 | private val fakeLocalAddress: InetSocketAddress = new InetSocketAddress(0) 42 | 43 | class TestManager(_tcpManagerActor: ActorRef) extends Engine(fakeBrokerAddress) with Handlers { 44 | override def tcpManagerActor: ActorRef = _tcpManagerActor 45 | } 46 | 47 | class FakeTCPManagerActor(implicit system: ActorSystem) extends TestProbe(system) with ImplicitSender { 48 | 49 | import net.sigusr.mqtt.SpecUtils._ 50 | 51 | val garbageFrame: ByteString = ByteString(0xff) 52 | val connackFrame: ByteString = ByteString(0x20, 0x02, 0x00, 0x00) 53 | val pingRespFrame: ByteString = ByteString(0xd0, 0x00) 54 | val pubrecFrame: ByteString = ByteString(0x50, 0x02, 0x00, 0x2a) 55 | val pubrelFrame: ByteString = ByteString(0x62, 0x02, 0x00, 0x2a) 56 | val pubcompFrame: ByteString = ByteString(0x70, 0x02, 0x00, 0x2a) 57 | 58 | private val payload0 = "payload".getBytes.toVector 59 | val frame0: PublishFrame = PublishFrame(Header(qos = ExactlyOnce.value), "topic", 42, ByteVector(payload0)) 60 | val publishFrame0: ByteString = ByteString(Codec[Frame].encode(frame0).require.toByteArray) 61 | 62 | val payload1: Vector[Byte] = "payload of frame 1".getBytes.toVector 63 | val frame1: PublishFrame = PublishFrame(Header(qos = AtMostOnce.value), "topic", 42, ByteVector(payload1)) 64 | val publishFrame1: ByteString = ByteString(Codec[Frame].encode(frame1).require.toByteArray) 65 | 66 | val payload2: Vector[Byte] = "payload of frame 2".getBytes.toVector 67 | val frame2: PublishFrame = PublishFrame(Header(qos = AtMostOnce.value), "topic", 42, ByteVector(payload2)) 68 | val publishFrame2: ByteString = ByteString(Codec[Frame].encode(frame2).require.toByteArray) 69 | 70 | val bigPayload: Vector[Byte] = makeRandomByteVector(2500) 71 | val bigFrame: PublishFrame = PublishFrame(Header(qos = AtMostOnce.value), "topic", 42, ByteVector(bigPayload)) 72 | val encodedBigFrame: ByteString = ByteString(Codec[Frame].encode(bigFrame).require.toByteArray) 73 | val encodedBigFramePart1: ByteString = encodedBigFrame.take(1500) 74 | val encodedBigFramePart2: ByteString = encodedBigFrame.drop(1500) 75 | 76 | var pingReqCount = 0 77 | var mqttManager: Option[ActorRef] = None 78 | 79 | def expectConnect(): Unit = { 80 | expectMsgPF() { 81 | case TCPConnect(remote, _, _, _, _) => 82 | remote should be_==(fakeBrokerAddress) 83 | sender() ! TCPConnected(fakeBrokerAddress, fakeLocalAddress) 84 | } 85 | } 86 | 87 | def expectConnectThenFail(): Unit = { 88 | expectMsgPF() { 89 | case c @ TCPConnect(remote, _, _, _, _) => 90 | remote should_== fakeBrokerAddress 91 | sender() ! TCPCommandFailed(c) 92 | } 93 | } 94 | 95 | def expectRegister(): Unit = { 96 | expectMsgPF() { 97 | case TCPRegister(handler, _, _) => 98 | mqttManager = Some(handler) 99 | } 100 | } 101 | 102 | def expectWriteConnectFrame(): Unit = { 103 | expectMsgPF() { 104 | case TCPWrite(byteString, _) => 105 | (byteString.head & 0xff) should_== 0x10 106 | sender() ! TCPReceived(connackFrame) 107 | } 108 | } 109 | 110 | def expectWritePingReqFrame(): Unit = { 111 | expectMsgPF() { 112 | case TCPWrite(byteString, _) => 113 | (byteString.head & 0xff) should_== 0xc0 114 | if (pingReqCount == 0) { 115 | sender() ! TCPReceived(pingRespFrame) 116 | } 117 | pingReqCount += 1 118 | } 119 | } 120 | 121 | def expectWritePublishFrame(): Unit = { 122 | expectMsgPF() { 123 | case TCPWrite(byteString, _) => 124 | (byteString.head & 0xff) should_== 0x34 125 | sender() ! TCPReceived(pubrecFrame) 126 | } 127 | } 128 | 129 | def expectWritePubrelFrame(): Unit = { 130 | expectMsgPF() { 131 | case TCPWrite(byteString, _) => 132 | (byteString.head & 0xff) should_== 0x62 133 | sender() ! TCPReceived(pubcompFrame) 134 | } 135 | } 136 | 137 | def expectWritePubrecFrame(): Unit = { 138 | expectMsgPF() { 139 | case TCPWrite(byteString, _) => 140 | (byteString.head & 0xff) should_== 0x50 141 | sender() ! TCPReceived(pubrelFrame) 142 | } 143 | } 144 | 145 | def expectWritePubcompFrame(): MatchResult[Any] = { 146 | expectMsgPF() { 147 | case TCPWrite(byteString, _) => 148 | (byteString.head & 0xff) should_== 0x70 149 | } 150 | } 151 | 152 | def expectWriteDisconnectFrame(): Unit = { 153 | expectMsgPF() { 154 | case TCPWrite(byteString, _) => 155 | (byteString.head & 0xff) should_== 0xe0 156 | sender() ! TCPClosed 157 | } 158 | } 159 | 160 | def expectClose(): Unit = { 161 | expectMsgPF() { 162 | case TCPAbort => sender() ! TCPAborted 163 | } 164 | } 165 | 166 | def sendPublishFrame(publishFrame: ByteString): Unit = mqttManager.foreach(_ ! TCPReceived(publishFrame)) 167 | 168 | def sendGarbageFrame(): Unit = mqttManager.foreach(_ ! TCPReceived(garbageFrame)) 169 | } 170 | 171 | class FakeMQTTManagerParent(testMQTTManagerName: String, fakeTCPManagerActor: ActorRef)(implicit testActor: ActorRef) extends Actor { 172 | val child: ActorRef = context.actorOf(Props(new TestManager(fakeTCPManagerActor)), testMQTTManagerName) 173 | def receive: Receive = { 174 | case x if sender == child => testActor forward x 175 | case x => child forward x 176 | } 177 | } 178 | 179 | "The Transport" should { 180 | 181 | "Manage successful connection" in new SpecsTestKit { 182 | val fakeTCPManagerActor = new FakeTCPManagerActor 183 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 184 | 185 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 186 | 187 | fakeTCPManagerActor.expectConnect() 188 | fakeTCPManagerActor.expectRegister() 189 | fakeTCPManagerActor.expectWriteConnectFrame() 190 | 191 | expectMsg(Connected) 192 | } 193 | 194 | "Manage unsuccessful connection" in new SpecsTestKit { 195 | val fakeTCPManagerActor = new FakeTCPManagerActor 196 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 197 | 198 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 199 | 200 | fakeTCPManagerActor.expectConnectThenFail() 201 | 202 | expectMsg(ConnectionFailure(ServerNotResponding)) 203 | } 204 | 205 | "Provide the right connection status [0]" in new SpecsTestKit { 206 | val fakeTCPManagerActor = new FakeTCPManagerActor 207 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 208 | 209 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 210 | 211 | fakeTCPManagerActor.expectConnect() 212 | fakeTCPManagerActor.expectRegister() 213 | fakeTCPManagerActor.expectWriteConnectFrame() 214 | 215 | expectMsg(Connected) 216 | 217 | mqttManagerActor ! MqttApiStatus 218 | 219 | expectMsg(Connected) 220 | } 221 | 222 | "Provide the right connection status [1]" in new SpecsTestKit { 223 | val fakeTCPManagerActor = new FakeTCPManagerActor 224 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 225 | 226 | mqttManagerActor ! MqttApiStatus 227 | 228 | expectMsg(Disconnected) 229 | } 230 | 231 | "Provide the right connection status [2]" in new SpecsTestKit { 232 | val fakeTCPManagerActor = new FakeTCPManagerActor 233 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 234 | 235 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 236 | 237 | mqttManagerActor ! MqttApiStatus 238 | 239 | expectMsg(Disconnected) 240 | } 241 | 242 | "Send back an error when sending an API command while not connected" in new SpecsTestKit { 243 | val fakeTCPManagerActor = new FakeTCPManagerActor 244 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 245 | 246 | mqttManagerActor ! Disconnect 247 | 248 | expectMsg(Error(NotConnected)) 249 | } 250 | 251 | "Send back an error when sending an API command during connection" in new SpecsTestKit { 252 | val fakeTCPManagerActor = new FakeTCPManagerActor 253 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 254 | 255 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 256 | 257 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 258 | 259 | expectMsg(Error(NotConnected)) 260 | } 261 | 262 | "Allow graceful disconnection" in new SpecsTestKit { 263 | val fakeTCPManagerActor = new FakeTCPManagerActor 264 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 265 | 266 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 267 | 268 | fakeTCPManagerActor.expectConnect() 269 | fakeTCPManagerActor.expectRegister() 270 | fakeTCPManagerActor.expectWriteConnectFrame() 271 | 272 | expectMsg(Connected) 273 | 274 | mqttManagerActor ! Disconnect 275 | fakeTCPManagerActor.expectWriteDisconnectFrame() 276 | 277 | expectMsg(Disconnected) 278 | } 279 | 280 | "Send back an error when sending a Connect API message if already connected" in new SpecsTestKit { 281 | val fakeTCPManagerActor = new FakeTCPManagerActor 282 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 283 | 284 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 285 | 286 | fakeTCPManagerActor.expectConnect() 287 | fakeTCPManagerActor.expectRegister() 288 | fakeTCPManagerActor.expectWriteConnectFrame() 289 | 290 | expectMsg(Connected) 291 | 292 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 293 | 294 | expectMsg(Error(AlreadyConnected)) 295 | 296 | mqttManagerActor ! Disconnect 297 | fakeTCPManagerActor.expectWriteDisconnectFrame() 298 | 299 | expectMsg(Disconnected) 300 | } 301 | 302 | "Manage the connection actor's death" in new SpecsTestKit { 303 | val fakeTCPManagerActor = new FakeTCPManagerActor 304 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 305 | 306 | mqttManagerActor ! Connect("test", 30, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 307 | 308 | fakeTCPManagerActor.expectConnect() 309 | fakeTCPManagerActor.expectRegister() 310 | fakeTCPManagerActor.expectWriteConnectFrame() 311 | 312 | expectMsg(Connected) 313 | 314 | fakeTCPManagerActor.ref ! PoisonPill 315 | 316 | expectMsg(Disconnected) 317 | } 318 | 319 | "Keep an idle connection alive or disconnect" in new SpecsTestKit { 320 | val fakeTCPManagerActor = new FakeTCPManagerActor 321 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 322 | 323 | mqttManagerActor ! Connect("test", 1, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 324 | 325 | fakeTCPManagerActor.expectConnect() 326 | fakeTCPManagerActor.expectRegister() 327 | fakeTCPManagerActor.expectWriteConnectFrame() 328 | 329 | expectMsg(Connected) 330 | 331 | fakeTCPManagerActor.expectWritePingReqFrame() 332 | fakeTCPManagerActor.expectWritePingReqFrame() 333 | fakeTCPManagerActor.expectClose() 334 | 335 | expectMsg(Disconnected) 336 | } 337 | 338 | "Disconnect when a wrong frame is received" in new SpecsTestKit { 339 | val fakeTCPManagerActor = new FakeTCPManagerActor 340 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 341 | 342 | mqttManagerActor ! Connect("test", 1, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 343 | 344 | fakeTCPManagerActor.expectConnect() 345 | fakeTCPManagerActor.expectRegister() 346 | fakeTCPManagerActor.expectWriteConnectFrame() 347 | 348 | expectMsg(Connected) 349 | 350 | fakeTCPManagerActor.sendGarbageFrame() 351 | 352 | expectMsg(Disconnected) 353 | } 354 | 355 | "Manage publishing a message with a QOS of exactly once" in new SpecsTestKit { 356 | val fakeTCPManagerActor = new FakeTCPManagerActor 357 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 358 | 359 | mqttManagerActor ! Connect("test", 1, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 360 | 361 | fakeTCPManagerActor.expectConnect() 362 | fakeTCPManagerActor.expectRegister() 363 | fakeTCPManagerActor.expectWriteConnectFrame() 364 | 365 | expectMsg(Connected) 366 | 367 | mqttManagerActor ! Publish("topic", "payload".getBytes.toVector, ExactlyOnce, Some(42)) 368 | 369 | fakeTCPManagerActor.expectWritePublishFrame() 370 | fakeTCPManagerActor.expectWritePubrelFrame() 371 | 372 | expectMsg(Published(42)) 373 | 374 | mqttManagerActor ! Disconnect 375 | fakeTCPManagerActor.expectWriteDisconnectFrame() 376 | 377 | expectMsg(Disconnected) 378 | } 379 | 380 | "Manage receiving a message with a QOS of exactly once" in new SpecsTestKit { 381 | val fakeTCPManagerActor = new FakeTCPManagerActor 382 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 383 | 384 | mqttManagerActor ! Connect("test", 1, cleanSession = false, Some(Will(retain = false, AtMostOnce, "test/topic", "test death")), None, None) 385 | 386 | fakeTCPManagerActor.expectConnect() 387 | fakeTCPManagerActor.expectRegister() 388 | fakeTCPManagerActor.expectWriteConnectFrame() 389 | 390 | expectMsg(Connected) 391 | 392 | fakeTCPManagerActor.sendPublishFrame(fakeTCPManagerActor.publishFrame0) 393 | fakeTCPManagerActor.expectWritePubrecFrame() 394 | 395 | expectMsg(Message("topic", "payload".getBytes.toVector)) 396 | 397 | fakeTCPManagerActor.expectWritePubcompFrame() 398 | 399 | mqttManagerActor ! Disconnect 400 | fakeTCPManagerActor.expectWriteDisconnectFrame() 401 | 402 | expectMsg(Disconnected) 403 | } 404 | 405 | "Manage receiving a message in multiple packets" in new SpecsTestKit { 406 | val fakeTCPManagerActor = new FakeTCPManagerActor 407 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 408 | 409 | mqttManagerActor ! Connect("test", 1, cleanSession = true, Some(Will(retain = false, AtLeastOnce, "test/topic", "test death")), None, None) 410 | 411 | fakeTCPManagerActor.expectConnect() 412 | fakeTCPManagerActor.expectRegister() 413 | fakeTCPManagerActor.expectWriteConnectFrame() 414 | 415 | expectMsg(Connected) 416 | 417 | fakeTCPManagerActor.sendPublishFrame(fakeTCPManagerActor.encodedBigFramePart1) 418 | fakeTCPManagerActor.sendPublishFrame(fakeTCPManagerActor.encodedBigFramePart2) 419 | 420 | expectMsg(Message("topic", fakeTCPManagerActor.bigPayload)) 421 | 422 | mqttManagerActor ! Disconnect 423 | fakeTCPManagerActor.expectWriteDisconnectFrame() 424 | 425 | expectMsg(Disconnected) 426 | } 427 | 428 | "Manage receiving two messages in one packet" in new SpecsTestKit { 429 | val fakeTCPManagerActor = new FakeTCPManagerActor 430 | val mqttManagerActor: ActorRef = system.actorOf(Props(new FakeMQTTManagerParent("MQTTClient", fakeTCPManagerActor.ref))) 431 | 432 | mqttManagerActor ! Connect("test", 1, cleanSession = true, Some(Will(retain = false, AtLeastOnce, "test/topic", "test death")), None, None) 433 | 434 | fakeTCPManagerActor.expectConnect() 435 | fakeTCPManagerActor.expectRegister() 436 | fakeTCPManagerActor.expectWriteConnectFrame() 437 | 438 | expectMsg(Connected) 439 | 440 | fakeTCPManagerActor.sendPublishFrame(fakeTCPManagerActor.publishFrame1 ++ fakeTCPManagerActor.publishFrame2) 441 | 442 | expectMsg(Message("topic", fakeTCPManagerActor.payload1)) 443 | expectMsg(Message("topic", fakeTCPManagerActor.payload2)) 444 | 445 | mqttManagerActor ! Disconnect 446 | fakeTCPManagerActor.expectWriteDisconnectFrame() 447 | 448 | expectMsg(Disconnected) 449 | } 450 | } 451 | } --------------------------------------------------------------------------------