diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..571230d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,19 @@ +sudo: required + +language: scala +scala: + - 2.12.5 +jdk: + - oraclejdk8 + +# These directories are cached to S3 at the end of the build +cache: + directories: + - $HOME/.ivy2/cache + - $HOME/.sbt/boot + - $HOME/.sbt/launchers + +before_cache: + # Tricks to avoid unnecessary cache updates + - find $HOME/.sbt -name "*.lock" | xargs rm + - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm \ No newline at end of file diff --git a/README.md b/README.md index 16efae4..e4ee70a 100644 --- a/README.md +++ b/README.md @@ -1 +1,74 @@ -# scala-multiaddr \ No newline at end of file +# scala-multiaddr + +> [multiaddr](https://github.com/multiformats/multiaddr) implementation in scala + +Multiaddr is a standard way to represent addresses that: + +- Support any standard network protocols. +- Self-describe (include protocols). +- Have a binary packed format. +- Have a nice string representation. +- Encapsulate well. + +## Install + +```scala +// Bintray repo is used so far. Migration to Maven Central is planned +resolvers += Resolver.bintrayRepo("fluencelabs", "releases") + +libraryDependencies += "one.fluence" %%% "scala-multiaddr" % "0.0.1" +``` + +## Usage + +### Example + +#### Simple + +```scala +import fluence.multiaddr.Multiaddr + +val addr = "/ip4/127.0.0.1/tcp/1234" +// construct from a string (ErrorMessage return error message in ther parsing process) +val mEither: Either[ErrorMessage, Multiaddr] = Multiaddr(addr) +val m = mEither.right.get + +// true +m.address == "/ip4/127.0.0.1/tcp/1234" +``` + +#### Protocols + +```scala +// get the multiaddr protocol description objects +m.protoParameters + +//List( +// StringProtoParameter(IP4, "127.0.0.1"), +// IntProtoParameter(TCP, 1234), +// EmptyProtoParameter(HTTP) +//) +``` + +#### En/decapsulate + +```scala +m.encapsulate(Multiaddr.unsafe("/sctp/5678")) +// Multiaddr(/ip4/127.0.0.1/tcp/1234/sctp/5678,List(StringProtoParameter(IP4,127.0.0.1), IntProtoParameter(TCP,1234), IntProtoParameter(SCTP,5678))) +m.decapsulate("/tcp") // up to + inc last occurrence of subaddr +// Multiaddr(/ip4/127.0.0.1,List(StringProtoParameter(IP4,127.0.0.1))) +``` + +#### Tunneling + +Multiaddr allows expressing tunnels very nicely. + +```scala +val addr = Multiaddr.unsafe("/ip4/192.168.0.13/tcp/80") +val proxy = Multiaddr.unsafe("/ip4/10.20.30.40/tcp/443") +val addrOverProxy := proxy.encapsulate(m) +// /ip4/10.20.30.40/tcp/443/ip4/192.168.0.13/tcp/80 + +val proxyAgain = addrOverProxy.decapsulate(addr) +// /ip4/10.20.30.40/tcp/443 +``` \ No newline at end of file diff --git a/core/src/main/scala/fluence/multiaddr/Multiaddr.scala b/core/src/main/scala/fluence/multiaddr/Multiaddr.scala index e5bce06..6a83785 100644 --- a/core/src/main/scala/fluence/multiaddr/Multiaddr.scala +++ b/core/src/main/scala/fluence/multiaddr/Multiaddr.scala @@ -17,37 +17,77 @@ package fluence.multiaddr -case class Multiaddr private(stringAddress: String, protocolsWithParameters: List[(Protocol, Option[String])]) { - override def toString: String = stringAddress +import fluence.multiaddr.Multiaddr.ErrorMessage + +/** + * Multiaddress representation by string and list of protocols. + */ +case class Multiaddr private(address: String, protoParameters: List[ProtoParameter]) { /** - * Wraps a given Multiaddr, returning the resulting joined Multiaddr. + * Encapsulates a Multiaddr in another Multiaddr. * - * @return new joined Multiaddr + * Spec about encapsulate: + * https://github.com/multiformats/multiaddr#encapsulation-based-on-context + * + * val m = Multiaddr("/ip4/127.0.0.1/tcp/1234") + * println(m.encapsulate(Multiaddr.unsafe("/sctp/5678")).right.get.address) + * "/ip4/127.0.0.1/tcp/1234/sctp/5678" + * + * @param addr Multiaddr to add into this Multiaddr + * @return new Multiaddr */ - def encapsulate(addr: Multiaddr): Either[Throwable, Multiaddr] = { - Multiaddr(stringAddress + addr.toString) - } + def encapsulate(addr: Multiaddr): Either[ErrorMessage, Multiaddr] = Multiaddr(address + addr.address) /** - * Decapsulate unwraps Multiaddr up until the given Multiaddr is found. + * Decapsulates a Multiaddr from another Multiaddr. + * + * Spec about encapsulate and decapsulate: + * https://github.com/multiformats/multiaddr#encapsulation-based-on-context + * + * val m = Multiaddr("/ip4/127.0.0.1/tcp/1234/sctp/5678") + * println(m.decapsulate(Multiaddr.unsafe("/sctp/5678")).right.get.address) + * "/ip4/127.0.0.1/tcp/1234" + * + * @param addr Multiaddr to remove from this Multiaddr * * @return decapsulated Multiaddr */ - def decapsulate(addr: Multiaddr): Either[Throwable, Multiaddr] = { - val strAddr = addr.toString - val lastIndex = stringAddress.lastIndexOf(strAddr) + def decapsulate(addr: Multiaddr): Either[ErrorMessage, Multiaddr] = decapsulate(addr.address) + + /** + * Possibility to decapsulate by part of address. + * + * Spec about encapsulate and decapsulate: + * https://github.com/multiformats/multiaddr#encapsulation-based-on-context + * + * val m = Multiaddr("/ip4/127.0.0.1/tcp/1234/sctp/5678") + * println(m.decapsulate("/sctp").right.get.address) + * "/ip4/127.0.0.1/tcp/1234" + * + * @param addr Multiaddr to remove from this Multiaddr + * @return decapsulated Multiaddr + */ + def decapsulate(addr: String): Either[ErrorMessage, Multiaddr] = { + val lastIndex = address.lastIndexOf(addr) if (lastIndex < 0) - Right(this.copy()) + Right(this) else - Multiaddr(stringAddress.slice(0, lastIndex)) + Multiaddr(address.slice(0, lastIndex)) } } object Multiaddr { - def apply(addr: String): Either[Throwable, Multiaddr] = MultiaddrParser.parse(addr).map { - case (trimmed, protocols) => new Multiaddr(trimmed, protocols) + type ErrorMessage = String + + /** + * Parse and validate multiaddr string. + */ + def apply(addr: String): Either[ErrorMessage, Multiaddr] = MultiaddrParser.parse(addr).map { + case (trimmed, protoParameters) => new Multiaddr(trimmed, protoParameters) } + + def unsafe(addr: String): Multiaddr = apply(addr).right.get } diff --git a/core/src/main/scala/fluence/multiaddr/MultiaddrParser.scala b/core/src/main/scala/fluence/multiaddr/MultiaddrParser.scala index 05d663a..52e930a 100644 --- a/core/src/main/scala/fluence/multiaddr/MultiaddrParser.scala +++ b/core/src/main/scala/fluence/multiaddr/MultiaddrParser.scala @@ -17,18 +17,22 @@ package fluence.multiaddr +import fluence.multiaddr.Multiaddr.ErrorMessage +import fluence.multiaddr.Protocol._ + import scala.annotation.tailrec +import scala.util.Try private[multiaddr] object MultiaddrParser { - def parse(addr: String): Either[Throwable, (String, List[(Protocol, Option[String])])] = { + def parse(addr: String): Either[ErrorMessage, (String, List[ProtoParameter])] = { if (!addr.startsWith("/")) { - Left(new IllegalArgumentException("Address must be started with '/'.")) + Left("Address must be started with '/'.") } else { val parts = addr.stripPrefix("/").stripSuffix("/").split("/").toList if (parts.isEmpty) { - Left(new IllegalArgumentException("Address must be non-empty.")) + Left("Address must be non-empty.") } else { parsePrepared(parts).map(protocols ⇒ (addr.stripSuffix("/"), protocols)) @@ -36,31 +40,53 @@ private[multiaddr] object MultiaddrParser { } } - private def parsePrepared(list: List[String]): Either[Throwable, List[(Protocol, Option[String])]] = { + private def parseParameter(parameter: String, protocol: Protocol): Either[String, ProtoParameter] = { + protocol match { + case TCP | UDP | SCTP ⇒ + Try(parameter.toInt).toEither.right + .map(n ⇒ IntProtoParameter(protocol, n)) + .left + .map(_ ⇒ s"Parameter for protocol $protocol must be a number.") + case _ ⇒ + Right(StringProtoParameter(protocol, parameter)) + } + } + + private def parsePrepared(list: List[String]): Either[ErrorMessage, List[ProtoParameter]] = { @tailrec def parseRec( list: List[String], - res: Either[Throwable, List[(Protocol, Option[String])]] - ): Either[Throwable, List[(Protocol, Option[String])]] = { + accum: Either[ErrorMessage, List[ProtoParameter]] + ): Either[ErrorMessage, List[ProtoParameter]] = { list match { - case Nil ⇒ res + case Nil ⇒ accum case head :: tail ⇒ //todo per-protocol validation val protocolOp = Protocol.withNameOption(head) protocolOp match { case None ⇒ - Left(new IllegalArgumentException(s"There is no protocol with name '$head'.")) + Left(s"There is no protocol with name '$head'.") case Some(protocol) ⇒ protocol.size match { - case 0 ⇒ parseRec(tail, res.map(els ⇒ els :+ (protocol, None))) + case 0 ⇒ + parseRec(tail, accum.map(els ⇒ els :+ EmptyProtoParameter(protocol))) case _ ⇒ tail match { case Nil ⇒ - Left(new IllegalArgumentException(s"There is no parameter for protocol with name '$head'.")) - case innerHead :: innerTail ⇒ - parseRec(innerTail, res.map(els ⇒ els :+ (protocol, Some(innerHead)))) + Left(s"There is no parameter for protocol with name '$head'.") + case parameter :: innerTail ⇒ + val partialResult = + for { + elements ← accum + parameter ← parseParameter(parameter, protocol) + } yield elements :+ parameter + + parseRec( + innerTail, + partialResult + ) } } } diff --git a/core/src/main/scala/fluence/multiaddr/ProtoParameter.scala b/core/src/main/scala/fluence/multiaddr/ProtoParameter.scala new file mode 100644 index 0000000..525bbec --- /dev/null +++ b/core/src/main/scala/fluence/multiaddr/ProtoParameter.scala @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2017 Fluence Labs Limited + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package fluence.multiaddr + +sealed trait ProtoParameter + +//todo generalize parameters per protocol type +case class EmptyProtoParameter(protocol: Protocol) extends ProtoParameter +case class StringProtoParameter(protocol: Protocol, parameter: String) extends ProtoParameter +case class IntProtoParameter(protocol: Protocol, parameter: Int) extends ProtoParameter diff --git a/core/src/main/scala/fluence/multiaddr/Protocol.scala b/core/src/main/scala/fluence/multiaddr/Protocol.scala index 284fb62..ab422f6 100644 --- a/core/src/main/scala/fluence/multiaddr/Protocol.scala +++ b/core/src/main/scala/fluence/multiaddr/Protocol.scala @@ -61,4 +61,7 @@ object Protocol extends Enum[Protocol] { case object P2PWebrtcStar extends Protocol(275, 0, "p2p-webrtc-star") case object P2PWebrtcDirect extends Protocol(276, 0, "p2p-webrtc-direct") case object P2PCircuit extends Protocol(290, 0, "p2p-circuit") + + //custom + case object GRPC extends Protocol(1001, 0, "grpc") } diff --git a/core/src/test/scala/fluence/multiaddr/MultiaddrParseSpec.scala b/core/src/test/scala/fluence/multiaddr/MultiaddrParseSpec.scala index 05603ba..05ff139 100644 --- a/core/src/test/scala/fluence/multiaddr/MultiaddrParseSpec.scala +++ b/core/src/test/scala/fluence/multiaddr/MultiaddrParseSpec.scala @@ -17,6 +17,7 @@ package fluence.multiaddr +import fluence.multiaddr.Multiaddr.ErrorMessage import org.scalatest.{Matchers, WordSpec} class MultiaddrParseSpec extends WordSpec with Matchers { @@ -27,7 +28,7 @@ class MultiaddrParseSpec extends WordSpec with Matchers { "throw exception if there is no leading '/'" in { val m = Multiaddr("ip4/127.0.0.1/tcp/123") m.isLeft shouldBe true - m.left.get.getMessage shouldBe "Address must be started with '/'." + m.left.get shouldBe "Address must be started with '/'." } "parse correct multiaddresses right" in { @@ -35,59 +36,65 @@ class MultiaddrParseSpec extends WordSpec with Matchers { val m1Either = Multiaddr(addr1) m1Either.isRight shouldBe true val m1 = m1Either.right.get - m1.protocolsWithParameters shouldBe List((IP4, Some("127.0.0.1")), (TCP, Some("123"))) - m1.stringAddress shouldBe addr1 + + m1.protoParameters shouldBe List(StringProtoParameter(IP4, "127.0.0.1"), IntProtoParameter(TCP, 123)) + m1.address shouldBe addr1 val addr2 = "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/https" val m2Either = Multiaddr(addr2) m2Either.isRight shouldBe true val m2 = m2Either.right.get - m2.protocolsWithParameters shouldBe List((IP6, Some("2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095")), (UDP, Some("5000")), (HTTPS, None)) - m2.stringAddress shouldBe addr2 + + m2.protoParameters shouldBe List( + StringProtoParameter(IP6, "2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095"), + IntProtoParameter(UDP, 5000), + EmptyProtoParameter(HTTPS) + ) + m2.address shouldBe addr2 } "throw exception if there is no protocol" in { val m = Multiaddr("/ip4/127.0.0.1/tc/123") m.isLeft shouldBe true - m.left.get.getMessage shouldBe "There is no protocol with name 'tc'." + m.left.get shouldBe "There is no protocol with name 'tc'." } "throw exception if there is no parameter in protocol with parameter" in { val m = Multiaddr("/ip4/127.0.0.1/tcp/") m.isLeft shouldBe true - m.left.get.getMessage shouldBe "There is no parameter for protocol with name 'tcp'." + m.left.get shouldBe "There is no parameter for protocol with name 'tcp'." } "encapsulate and decapsulate correct multiaddr" in { val addr1 = "/ip4/127.0.0.1/tcp/123" - val m1Either = Multiaddr(addr1) - m1Either.isRight shouldBe true - val m1 = m1Either.right.get + val m1 = Multiaddr.unsafe(addr1) val addr2 = "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/https" - val m2Either = Multiaddr(addr2) - m2Either.isRight shouldBe true - val m2 = m2Either.right.get + val m2 = Multiaddr.unsafe(addr2) val m3Either = m1.encapsulate(m2) m3Either.isRight shouldBe true val m3 = m3Either.right.get - val result = List((IP4, Some("127.0.0.1")), (TCP, Some("123")), (IP6, Some("2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095")), (UDP, Some("5000")), (HTTPS, None)) - m3.protocolsWithParameters shouldBe result - m3.stringAddress shouldBe (addr1 + addr2) + val result = List( + StringProtoParameter(IP4, "127.0.0.1"), + IntProtoParameter(TCP, 123), + StringProtoParameter(IP6, "2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095"), + IntProtoParameter(UDP, 5000), + EmptyProtoParameter(HTTPS) + ) + m3.protoParameters shouldBe result + m3.address shouldBe (addr1 + addr2) - m3.decapsulate(m2).right.get.stringAddress shouldBe addr1 + m3.decapsulate(m2).right.get.address shouldBe addr1 } "decapsulate correct multiaddr" in { val addr1 = "/ip4/127.0.0.1/udp/1234/sctp/5678" - val m1Either = Multiaddr(addr1) - m1Either.isRight shouldBe true - val m1 = m1Either.right.get + val m1 = Multiaddr.unsafe(addr1) val decapsulated = m1.decapsulate(Multiaddr("/sctp/5678").right.get).right.get - decapsulated.stringAddress shouldBe "/ip4/127.0.0.1/udp/1234" + decapsulated.address shouldBe "/ip4/127.0.0.1/udp/1234" } } }