Merge pull request #2 from fluencelabs/docs-and-refactoring

Docs, readme and refactoring
This commit is contained in:
Dima 2018-06-22 18:09:01 +03:00 committed by GitHub
commit 974c3bb8d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 242 additions and 49 deletions

19
.travis.yml Normal file
View File

@ -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

View File

@ -1 +1,74 @@
# scala-multiaddr # 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
```

View File

@ -17,37 +17,77 @@
package fluence.multiaddr package fluence.multiaddr
case class Multiaddr private(stringAddress: String, protocolsWithParameters: List[(Protocol, Option[String])]) { import fluence.multiaddr.Multiaddr.ErrorMessage
override def toString: String = stringAddress
/**
* 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] = { def encapsulate(addr: Multiaddr): Either[ErrorMessage, Multiaddr] = Multiaddr(address + addr.address)
Multiaddr(stringAddress + addr.toString)
}
/** /**
* 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 * @return decapsulated Multiaddr
*/ */
def decapsulate(addr: Multiaddr): Either[Throwable, Multiaddr] = { def decapsulate(addr: Multiaddr): Either[ErrorMessage, Multiaddr] = decapsulate(addr.address)
val strAddr = addr.toString
val lastIndex = stringAddress.lastIndexOf(strAddr) /**
* 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) if (lastIndex < 0)
Right(this.copy()) Right(this)
else else
Multiaddr(stringAddress.slice(0, lastIndex)) Multiaddr(address.slice(0, lastIndex))
} }
} }
object Multiaddr { object Multiaddr {
def apply(addr: String): Either[Throwable, Multiaddr] = MultiaddrParser.parse(addr).map { type ErrorMessage = String
case (trimmed, protocols) => new Multiaddr(trimmed, protocols)
/**
* 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
} }

View File

@ -17,18 +17,22 @@
package fluence.multiaddr package fluence.multiaddr
import fluence.multiaddr.Multiaddr.ErrorMessage
import fluence.multiaddr.Protocol._
import scala.annotation.tailrec import scala.annotation.tailrec
import scala.util.Try
private[multiaddr] object MultiaddrParser { 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("/")) { if (!addr.startsWith("/")) {
Left(new IllegalArgumentException("Address must be started with '/'.")) Left("Address must be started with '/'.")
} else { } else {
val parts = addr.stripPrefix("/").stripSuffix("/").split("/").toList val parts = addr.stripPrefix("/").stripSuffix("/").split("/").toList
if (parts.isEmpty) { if (parts.isEmpty) {
Left(new IllegalArgumentException("Address must be non-empty.")) Left("Address must be non-empty.")
} else { } else {
parsePrepared(parts).map(protocols (addr.stripSuffix("/"), protocols)) 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 @tailrec
def parseRec( def parseRec(
list: List[String], list: List[String],
res: Either[Throwable, List[(Protocol, Option[String])]] accum: Either[ErrorMessage, List[ProtoParameter]]
): Either[Throwable, List[(Protocol, Option[String])]] = { ): Either[ErrorMessage, List[ProtoParameter]] = {
list match { list match {
case Nil res case Nil accum
case head :: tail case head :: tail
//todo per-protocol validation //todo per-protocol validation
val protocolOp = Protocol.withNameOption(head) val protocolOp = Protocol.withNameOption(head)
protocolOp match { protocolOp match {
case None 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) case Some(protocol)
protocol.size match { protocol.size match {
case 0 parseRec(tail, res.map(els els :+ (protocol, None))) case 0
parseRec(tail, accum.map(els els :+ EmptyProtoParameter(protocol)))
case _ case _
tail match { tail match {
case Nil case Nil
Left(new IllegalArgumentException(s"There is no parameter for protocol with name '$head'.")) Left(s"There is no parameter for protocol with name '$head'.")
case innerHead :: innerTail case parameter :: innerTail
parseRec(innerTail, res.map(els els :+ (protocol, Some(innerHead)))) val partialResult =
for {
elements accum
parameter parseParameter(parameter, protocol)
} yield elements :+ parameter
parseRec(
innerTail,
partialResult
)
} }
} }
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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

View File

@ -61,4 +61,7 @@ object Protocol extends Enum[Protocol] {
case object P2PWebrtcStar extends Protocol(275, 0, "p2p-webrtc-star") case object P2PWebrtcStar extends Protocol(275, 0, "p2p-webrtc-star")
case object P2PWebrtcDirect extends Protocol(276, 0, "p2p-webrtc-direct") case object P2PWebrtcDirect extends Protocol(276, 0, "p2p-webrtc-direct")
case object P2PCircuit extends Protocol(290, 0, "p2p-circuit") case object P2PCircuit extends Protocol(290, 0, "p2p-circuit")
//custom
case object GRPC extends Protocol(1001, 0, "grpc")
} }

View File

@ -17,6 +17,7 @@
package fluence.multiaddr package fluence.multiaddr
import fluence.multiaddr.Multiaddr.ErrorMessage
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
class MultiaddrParseSpec extends WordSpec with Matchers { class MultiaddrParseSpec extends WordSpec with Matchers {
@ -27,7 +28,7 @@ class MultiaddrParseSpec extends WordSpec with Matchers {
"throw exception if there is no leading '/'" in { "throw exception if there is no leading '/'" in {
val m = Multiaddr("ip4/127.0.0.1/tcp/123") val m = Multiaddr("ip4/127.0.0.1/tcp/123")
m.isLeft shouldBe true 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 { "parse correct multiaddresses right" in {
@ -35,59 +36,65 @@ class MultiaddrParseSpec extends WordSpec with Matchers {
val m1Either = Multiaddr(addr1) val m1Either = Multiaddr(addr1)
m1Either.isRight shouldBe true m1Either.isRight shouldBe true
val m1 = m1Either.right.get 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 addr2 = "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/https"
val m2Either = Multiaddr(addr2) val m2Either = Multiaddr(addr2)
m2Either.isRight shouldBe true m2Either.isRight shouldBe true
val m2 = m2Either.right.get 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 { "throw exception if there is no protocol" in {
val m = Multiaddr("/ip4/127.0.0.1/tc/123") val m = Multiaddr("/ip4/127.0.0.1/tc/123")
m.isLeft shouldBe true 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 { "throw exception if there is no parameter in protocol with parameter" in {
val m = Multiaddr("/ip4/127.0.0.1/tcp/") val m = Multiaddr("/ip4/127.0.0.1/tcp/")
m.isLeft shouldBe true 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 { "encapsulate and decapsulate correct multiaddr" in {
val addr1 = "/ip4/127.0.0.1/tcp/123" val addr1 = "/ip4/127.0.0.1/tcp/123"
val m1Either = Multiaddr(addr1) val m1 = Multiaddr.unsafe(addr1)
m1Either.isRight shouldBe true
val m1 = m1Either.right.get
val addr2 = "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/https" val addr2 = "/ip6/2001:8a0:7ac5:4201:3ac9:86ff:fe31:7095/udp/5000/https"
val m2Either = Multiaddr(addr2) val m2 = Multiaddr.unsafe(addr2)
m2Either.isRight shouldBe true
val m2 = m2Either.right.get
val m3Either = m1.encapsulate(m2) val m3Either = m1.encapsulate(m2)
m3Either.isRight shouldBe true m3Either.isRight shouldBe true
val m3 = m3Either.right.get 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)) val result = List(
m3.protocolsWithParameters shouldBe result StringProtoParameter(IP4, "127.0.0.1"),
m3.stringAddress shouldBe (addr1 + addr2) 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 { "decapsulate correct multiaddr" in {
val addr1 = "/ip4/127.0.0.1/udp/1234/sctp/5678" val addr1 = "/ip4/127.0.0.1/udp/1234/sctp/5678"
val m1Either = Multiaddr(addr1) val m1 = Multiaddr.unsafe(addr1)
m1Either.isRight shouldBe true
val m1 = m1Either.right.get
val decapsulated = m1.decapsulate(Multiaddr("/sctp/5678").right.get).right.get 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"
} }
} }
} }