├── project ├── build.properties ├── plugins.sbt ├── common.scala ├── MacroPlugin.scala └── CentralRequirementsPlugin.scala ├── core ├── src │ ├── test │ │ ├── resources │ │ │ └── ssl-testing │ │ │ │ ├── cert.srl │ │ │ │ ├── server_pubkey.pem │ │ │ │ ├── CA.pem │ │ │ │ ├── client_cert.pem │ │ │ │ ├── server_cert.pem │ │ │ │ ├── client.req │ │ │ │ ├── server.req │ │ │ │ ├── CA_key.pem │ │ │ │ ├── client_key.pem │ │ │ │ ├── server_key.pem │ │ │ │ ├── client_key.pk8 │ │ │ │ ├── server_key.pk8 │ │ │ │ └── generate_ssl_test_credentials.sh │ │ └── scala │ │ │ ├── TupleCodecSpec.scala │ │ │ ├── SignatureSpec.scala │ │ │ ├── ResponseSpec.scala │ │ │ ├── CountServer.scala │ │ │ ├── EndpointSpec.scala │ │ │ ├── CapabilitiesSpec.scala │ │ │ ├── ServerErrors.scala │ │ │ ├── CircuitBreakerSpec.scala │ │ │ ├── ProtocolSpec.scala │ │ │ ├── UberSpec.scala │ │ │ └── RemoteSpec.scala │ └── main │ │ ├── scala │ │ ├── transport │ │ │ └── netty │ │ │ │ ├── package.scala │ │ │ │ ├── Client.scala │ │ │ │ └── Transport.scala │ │ ├── codecs │ │ │ ├── DecodingFailure.scala │ │ │ └── EncodingFailure.scala │ │ ├── ServerException.scala │ │ ├── Utils.scala │ │ ├── Gen.scala │ │ ├── Codecs.scala │ │ ├── IORef.scala │ │ ├── CircuitBreaker.scala │ │ ├── GenClient.scala │ │ ├── tls │ │ │ └── tls.scala │ │ ├── Capabilities.scala │ │ ├── Environment.scala │ │ ├── Signatures.scala │ │ ├── Protocol.scala │ │ ├── package.scala │ │ ├── GenServer.scala │ │ ├── Values.scala │ │ ├── Server.scala │ │ ├── Monitoring.scala │ │ ├── Endpoint.scala │ │ ├── Remote.scala │ │ └── SSL.scala │ │ └── macros │ │ ├── scala-2.11 │ │ └── remotely │ │ │ └── MacrosCompatibility.scala │ │ └── scala-2.10 │ │ └── remotely │ │ └── MacrosCompatibility.scala └── build.sbt ├── version.sbt ├── test ├── build.sbt └── src │ └── main │ └── scala │ └── DescribeTest.scala ├── docs ├── src │ ├── site │ │ ├── img │ │ │ └── logo.png │ │ ├── _config.dev.yml │ │ ├── _config.yml │ │ ├── bin │ │ │ └── local-server │ │ ├── js │ │ │ └── scale.fix.js │ │ ├── css │ │ │ ├── custom.css │ │ │ └── styles.css │ │ └── _layouts │ │ │ └── default.html │ └── main │ │ └── tut │ │ └── internals.md └── build.sbt ├── examples ├── build.sbt └── src │ └── main │ └── scala │ ├── EchoServer.scala │ ├── Protocol.scala │ ├── Simple.scala │ └── Multiservice.scala ├── benchmark ├── server │ ├── build.sbt │ └── src │ │ ├── main │ │ └── scala │ │ │ ├── server.scala │ │ │ └── ServerImpl.scala │ │ └── test │ │ └── scala │ │ └── BenchmarkSpec.scala ├── protocol │ ├── build.sbt │ └── src │ │ └── main │ │ └── scala │ │ ├── model.scala │ │ ├── TestData.scala │ │ ├── transformations.scala │ │ └── Protocol.scala └── client │ ├── build.sbt │ └── src │ └── main │ └── scala │ └── Main.scala ├── .gitignore ├── README.md ├── project.sbt ├── test-server └── src │ ├── main │ └── scala │ │ └── server.scala │ └── test │ └── scala │ └── DescribeSpec.scala └── .travis.yml /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.12 2 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/cert.srl: -------------------------------------------------------------------------------- 1 | 02 2 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "1.4.6-SNAPSHOT" -------------------------------------------------------------------------------- /test/build.sbt: -------------------------------------------------------------------------------- 1 | 2 | enablePlugins(DisablePublishingPlugin) 3 | -------------------------------------------------------------------------------- /docs/src/site/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Verizon/remotely/HEAD/docs/src/site/img/logo.png -------------------------------------------------------------------------------- /docs/src/site/_config.dev.yml: -------------------------------------------------------------------------------- 1 | name: Remotely Documentation 2 | markdown: redcarpet 3 | highlighter: pygments 4 | baseurl: "/" 5 | -------------------------------------------------------------------------------- /docs/src/site/_config.yml: -------------------------------------------------------------------------------- 1 | name: Remotely Documentation 2 | markdown: redcarpet 3 | highlighter: pygments 4 | baseurl: "/remotely/" 5 | -------------------------------------------------------------------------------- /examples/build.sbt: -------------------------------------------------------------------------------- 1 | 2 | scalacOptions ++= Seq( 3 | "-language:existentials", 4 | "-language:postfixOps" 5 | ) 6 | 7 | enablePlugins(DisablePublishingPlugin) 8 | -------------------------------------------------------------------------------- /docs/src/site/bin/local-server: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 4 | 5 | jekyll serve --config "$DIR/../_config.dev.yml" --watch 6 | -------------------------------------------------------------------------------- /benchmark/server/build.sbt: -------------------------------------------------------------------------------- 1 | 2 | scalacOptions += "-language:reflectiveCalls" 3 | 4 | assemblySettings 5 | 6 | common.mergeSettings 7 | 8 | enablePlugins(DisablePublishingPlugin) 9 | -------------------------------------------------------------------------------- /benchmark/protocol/build.sbt: -------------------------------------------------------------------------------- 1 | 2 | scalacOptions in Compile := (scalacOptions in Compile).value.filterNot(f => f == "-Xlint" || f == "-Xfatal-warnings") 3 | 4 | enablePlugins(DisablePublishingPlugin) 5 | -------------------------------------------------------------------------------- /benchmark/client/build.sbt: -------------------------------------------------------------------------------- 1 | 2 | assemblySettings 3 | 4 | common.mergeSettings 5 | 6 | scalacOptions ++= Seq("-language:postfixOps", "-language:reflectiveCalls") 7 | 8 | enablePlugins(DisablePublishingPlugin) 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | *.log 3 | .DS_Store 4 | *.swp 5 | 6 | # sbt specific 7 | dist/* 8 | target/ 9 | lib_managed/ 10 | src_managed/ 11 | project/boot/ 12 | project/plugins/project/ 13 | 14 | # Scala-IDE specific 15 | .scala_dependencies 16 | .idea/* 17 | 18 | .ivy2 19 | catapult/ 20 | 21 | # vim specific 22 | *~ 23 | tags 24 | gpg.sbt 25 | -------------------------------------------------------------------------------- /docs/build.sbt: -------------------------------------------------------------------------------- 1 | import com.typesafe.sbt.SbtSite.SiteKeys._ 2 | import com.typesafe.sbt.SbtGhPages.GhPagesKeys._ 3 | 4 | site.settings 5 | 6 | tutSettings 7 | 8 | site.addMappingsToSiteDir(tut, "") 9 | 10 | ghpages.settings 11 | 12 | ghpagesNoJekyll := false 13 | 14 | includeFilter in makeSite := "*.yml" | "*.md" | "*.html" | "*.css" | "*.png" | "*.jpg" | "*.gif" | "*.js" | "*.swf" 15 | 16 | git.remoteRepo := "git@github.com:Verizon/remotely.git" 17 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/server_pubkey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvjQY24Fqsb1TfEYM5CZY 3 | VB59CH7H3sRRLeDTVhmyCKKvOUk4rG+k9qu+0NA1xLyoSdrZJR+XLHZTx18P5ygC 4 | oMSh/+5R2ILBnZUUEDZT6Yyazy1NSAK8LwZZ/vptLUXkeA/w5671+aDkCLZz+tga 5 | mMJewQ3dEQpy94PPkW4vxu2ThayctXhlgqSvyUx5P74kl7pznTcrkeGcXTwGoyAs 6 | BtwoC4wvF/YoUlMdnpCWyfPijeCVzS2HgIxJHuVvr5d5wi8GJvx7I978Y0OlAV1j 7 | /BXS7V4tsAxtVzGwaJNALUAS0Kxx/kIr/Tn537o4xfol9W8t1mO1t7F4zcgsk7DL 8 | 2QIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /docs/src/site/js/scale.fix.js: -------------------------------------------------------------------------------- 1 | var metas = document.getElementsByTagName('meta'); 2 | var i; 3 | if (navigator.userAgent.match(/iPhone/i)) { 4 | for (i=0; i li { 23 | margin-left: -20px; 24 | } 25 | header ol li { 26 | display: block; 27 | padding: 5px 0 5px 0; 28 | } 29 | header ol li, header ol { 30 | width: auto; 31 | white-space: nowrap !important; 32 | height: auto; 33 | } 34 | hr { 35 | clear: both; 36 | } 37 | section img { 38 | border-color: #ccc; 39 | border-width: 1px; 40 | border-style: solid; 41 | padding: 6px; 42 | } 43 | .red { 44 | color: red; 45 | } 46 | 47 | .msg { 48 | font-style: italic; 49 | border-radius: 5px; 50 | -moz-border-radius: 5px; 51 | } 52 | .warn { 53 | background-color: #F5EEA2; 54 | border: 1px solid #F7D386; 55 | } 56 | .msg p { 57 | padding: 10px; 58 | margin: 0; 59 | } 60 | -------------------------------------------------------------------------------- /project/common.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2015 Verizon. All Rights Reserved. 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 | import sbt._, Keys._ 18 | import sbtassembly.Plugin._ 19 | 20 | object common { 21 | import AssemblyKeys._ 22 | 23 | def mergeSettings = Seq( 24 | mergeStrategy in assembly <<= (mergeStrategy in assembly) { (old) => 25 | { 26 | case "META-INF/io.netty.versions.properties" => MergeStrategy.discard 27 | case x => old(x) 28 | } 29 | } 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /test-server/src/main/scala/server.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package test 20 | 21 | import DescribeTestNewerProtocol._ 22 | 23 | @GenServer(remotely.test.DescribeTestOlderProtocol.definition) abstract class DescribeTestOlderServer 24 | @GenServer(remotely.test.DescribeTestNewerProtocol.definition) abstract class DescribeTestNewerServer 25 | @GenClient(remotely.test.DescribeTestOlderProtocol.definition.signatures) object DescribeTestOlderClient 26 | @GenClient(remotely.test.DescribeTestNewerProtocol.definition.signatures) object DescribeTestNewerClient 27 | -------------------------------------------------------------------------------- /core/src/main/scala/Utils.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2015 Verizon. All Rights Reserved. 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 remotely 18 | 19 | import remotely.codecs.DecodingFailure 20 | import scodec.Attempt.{Successful, Failure} 21 | import scodec.{Attempt, Err} 22 | 23 | import scalaz.{\/-, -\/, \/} 24 | import scalaz.concurrent.Task 25 | 26 | package object utils { 27 | implicit class AugmentedEither[E,A](a: E \/ A) { 28 | def toTask(implicit conv: E => Throwable): Task[A] = a match { 29 | case -\/(e) => Task.fail(conv(e)) 30 | case \/-(a) => Task.now(a) 31 | } 32 | } 33 | implicit def errToE(err: Err) = new DecodingFailure(err) 34 | } 35 | -------------------------------------------------------------------------------- /core/src/test/scala/TupleCodecSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package test 20 | 21 | import org.scalacheck._ 22 | import Arbitrary._ 23 | import org.scalacheck.Prop.forAll 24 | import remotely.codecs._ 25 | import scodec.{DecodeResult, Attempt} 26 | import scodec.bits.BitVector 27 | 28 | object TupleCodecSpec extends Properties("TupleCodec") { 29 | property("tupele2Codec works") = forAll {ab: (String,List[Int]) ⇒ 30 | val abCodec = utf8 ~~ list(int32) 31 | val roundTripped = abCodec.encode(ab) flatMap abCodec.decode 32 | roundTripped == Attempt.successful(DecodeResult(ab, BitVector.empty)) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /benchmark/protocol/src/main/scala/model.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | 21 | import collection.immutable.IndexedSeq 22 | 23 | case class Large(one: Int, 24 | two: List[String], 25 | three: String, 26 | four: Map[String, String], 27 | five: List[Medium], 28 | six: IndexedSeq[Small]) 29 | 30 | case class Medium(ay: Int, 31 | bee: String, 32 | cee: List[Small], 33 | dee: Option[Int]) 34 | 35 | case class Small(alpha: Map[String,String], 36 | omega: List[String]) 37 | 38 | case class Big(one: Int) 39 | -------------------------------------------------------------------------------- /benchmark/protocol/src/main/scala/TestData.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | 21 | trait TestData { 22 | val sm: Small = Small((for(i <- 1 to 10) yield i.toString -> i.toString).toMap, (for(i <- 1 to 10) yield i.toString).toList) 23 | 24 | val medIn: Medium = Medium(1, (0 to 200).map(_ => "a").mkString, List.fill(10)(sm), Some(1)) 25 | 26 | val largeIn: Large = Large(1, List("asdf", "qwer", "qwer","ldsfdfsaj","aksldjfsdfkdfjasdfpoweurpaasdflsdkfjsllslosdfiuasdpoaisudpfidsaf"), (1 to 1000).map((x:Int) => "a").mkString, (for(i <- 1 to 20) yield i.toString -> i.toString).toMap,List.fill(10)(medIn), Vector.fill(10)(sm)) 27 | 28 | val bigIn: Big = Big(1) 29 | 30 | } 31 | -------------------------------------------------------------------------------- /core/src/test/scala/SignatureSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package test 20 | 21 | import org.scalatest.matchers.{Matcher,MatchResult} 22 | import org.scalatest.{FlatSpec,Matchers,BeforeAndAfterAll} 23 | 24 | class SignatureSpec extends FlatSpec 25 | with Matchers 26 | with BeforeAndAfterAll { 27 | 28 | behavior of "Signature" 29 | 30 | it should "be able to wrap a response type" in { 31 | Signature("foo",List(), "Baz").wrapResponse should be ("Response[Baz]") 32 | Signature("foo", List(Field("baz", "Baz")), "Qux").wrapResponse should be ("Baz => Response[Qux]") 33 | Signature("foo", List(Field("baz", "Baz"), Field("qux", "Qux")), "Zod").wrapResponse should be ("(Baz,Qux) => Response[Zod]") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/src/main/scala/Gen.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2015 Verizon. All Rights Reserved. 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 remotely 18 | 19 | private[remotely] object Gen extends MacrosCompatibility { 20 | /** 21 | * this just allows us to put a $signature into a quasi-quote. 22 | * implemented this way instead of by providing Liftable[Signature] 23 | * only because I gave up on trying to figure out the complex cake 24 | * of path-dependant types which is the current reflection api. 25 | */ 26 | def liftSignature(c: Context)(signature: Signature): c.universe.Tree = { 27 | import c.universe._ 28 | val s = signature 29 | val t: Tree = q"_root_.remotely.Signature(${s.name}, List(..${s.params.map(liftField(c)(_))}), ${s.outType})" 30 | t 31 | } 32 | 33 | def liftField(c: Context)(field: Field[Any]): c.universe.Tree = { 34 | import c.universe._ 35 | q"_root_.remotely.Field(${field.name}, ${field.typeString})" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: scala 3 | scala: 4 | - 2.10.6 5 | - 2.11.7 6 | 7 | jdk: 8 | - oraclejdk8 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | # push onto the new gce infra on travis 15 | sudo: required 16 | 17 | before_script: 18 | - "if [ $TRAVIS_PULL_REQUEST = 'false' ]; then git checkout -qf $TRAVIS_BRANCH; fi" 19 | 20 | script: 21 | - | 22 | if [ $TRAVIS_PULL_REQUEST = 'false' ]; then 23 | if [ $RELEASE_ON_PUSH = 'false' ]; then 24 | sbt ++$TRAVIS_SCALA_VERSION test coverageReport 25 | else 26 | sbt ++$TRAVIS_SCALA_VERSION 'release with-defaults' 27 | fi 28 | else 29 | sbt ++$TRAVIS_SCALA_VERSION test coverageReport 30 | fi 31 | - find $HOME/.sbt -name "*.lock" | xargs rm 32 | - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm 33 | 34 | cache: 35 | directories: 36 | - $HOME/.ivy2/cache 37 | - $HOME/.sbt/boot/scala-$TRAVIS_SCALA_VERSION 38 | 39 | after_success: 40 | - find $HOME/.sbt -name "*.lock" | xargs rm 41 | - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm 42 | # - "bash <(curl -s https://codecov.io/bash) -r $TRAVIS_REPO_SLUG -t $CODECOV_TOKEN" 43 | 44 | env: 45 | global: 46 | - secure: "mxN1J+HCnrVp2O4xOHNebxNSNhpcmFhs+I3TUMvOzCvMZey4n27YJ2KUTrJGrpJHlljtOgAmZEsZzKlrdIbRHK4MLSBiDpHedmWZS1VtR31g05jFgfarQRiLdTgsd4dFkdQIycpXagGPtVjoCGZAE1JKTKWF9W6RKNBJhuTXebg=" 47 | - secure: "m43l8ieQq84587jR1KpBjsvVnMzbzDoFDTU7HS+V1WAeYcopUH84gxTOTiqXaPgWkixIhX+MfQqzYzToocee2dSOlUfn6kH+VNXwx29RNoivAFXpm6tIaBRqC5fSLxLZpdA4BxNuKiZcc3nEMgYhpsuynGVo1OAZ/qx3u/80tZ4=" 48 | 49 | notifications: 50 | irc: 51 | channels: 52 | - "chat.freenode.net#oncue" 53 | template: 54 | - "%{repository} (%{commit}) : %{message} %{foo} " 55 | - "Build details: %{build_url}" 56 | -------------------------------------------------------------------------------- /core/src/main/scala/Codecs.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.reflect.runtime.universe.TypeTag 21 | import scodec.Codec 22 | import scodec.codecs.byteAligned 23 | 24 | case class Codecs(codecs: Map[String,Codec[Any]]) { 25 | 26 | def codec[C:TypeTag:Codec]: Codecs = { 27 | val name = Remote.toTag(implicitly[TypeTag[C]]) 28 | this.copy(codecs = codecs + (name -> byteAligned(Codec[C].asInstanceOf[Codec[Any]]))) 29 | } 30 | 31 | def ++(c: Codecs): Codecs = Codecs(codecs ++ c.codecs) 32 | 33 | def keySet = codecs.keySet 34 | 35 | def get(k: String): Option[Codec[Any]] = codecs.get(k) 36 | 37 | def pretty = "Codecs.empty\n " + codecs.keySet.toList.sorted.map(d => s".codec[$d]").mkString("\n ") 38 | 39 | override def toString = pretty 40 | } 41 | 42 | object Codecs { 43 | 44 | val empty: Codecs = Codecs(Map("List[remotely.Signature]" -> codecs.set(Signature.signatureCodec).asInstanceOf[Codec[Any]])) 45 | } 46 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/client_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA9ojDkUsD4VSyPc6XbBfpFopj87aM4ZWIAG6cmzl9bmFDuGdo 3 | 0zQLnxj64bdu1mdHt8A9VYSFz5uxg0sgk2Ho9xtH95o+B3cLuT0y70TrF9VHVRJm 4 | z1HdKUKAnNIkvGBRs8SMRriKQ7Nt/8WT4FQEho0My6u5ZcTImg8PamnxYxW19B/J 5 | z1yiUd6A7VKHFxv/fFU+m4yIDpY64JSYPKHYRdkFAcZjjIJu8EJ5nBRS9hhcLioE 6 | Dbz0ZtbJPTzb2G8h+vTQ1tROTKXqogzymJ3PqvLhXUrdOiCfOiIYE6kP/8F0g5UQ 7 | t9iR2UChODM92mhffWIP2gCAG90o8v80E54TywIDAQABAoIBAQCEt/GYeSrYlnDy 8 | JxKLZX/c25mdbQwAsTArE2EJi3SEBKahUkIzWRRcjOUauFZZ4/XO7RXXwOUMc8On 9 | a71Yul/hD3W5zPZRrHvKODEAcDijdvTUzng8adTGzn9QbSzLoIq7XTcrJ1N/yMuR 10 | 4vqNzI03Jh3aU1MWf06C6VKiNfB2Jv5svyX1gcoAkh3QJyghStQFFOrnqrYHuHsr 11 | r1vGc/XUez0sJbI3/315SNXEqyelPwI7RqCKZKCpJhjHVtB02yEbxBbiiHEyNS+B 12 | A/rZKrNYGMAYtpopbU2Rh9Xc3gY7myGBduKJOeOcr9Huo0Qe/i/lFo6FunPdXNQn 13 | vVZj7nYZAoGBAP8jFmCKxMrqL1JnRWbPF0tecU7lRdNEwRurq4JXYLIU+AtMu+od 14 | Sgyq3c6vmAdon/F+urr1llm27pnleP5m4glU+eFU0SJlD5401o+D8/tZQE93DD+8 15 | oHyA+48B7izTTC1p9J/P2qsECQ0GqFXe+cO81/I9PLUsAPxgNYr+n5G1AoGBAPde 16 | Oko6qVT/PXoZuelxufFDlCJ4kQKZVFmUiodjci7yHRgZkvyVCJXBFaRRL24CM11p 17 | dWV8ro9iROri406Paah4jHygOWBCalJjvWTKxXEudE31M5KKabM03sNfpWbM/OY+ 18 | BwMkuMC95nz+H/uqd5ArY3czU98UvqNolUVf/H9/AoGAK2BFv6gYCP+DiBjynaX7 19 | qd6mmOk109NOmWu4PvKoB960xQcIs6bDHiA2kZsc17Wb66zGBl8Wbq5BJnzy8x1+ 20 | tGN0EDEhVR2laBpHmCKw6jykSF5PAM8K4d/z5L5JRemue9IVus3VuVl4SHSnTeIY 21 | yIRkmlR667pNeJrKv5TGgDkCgYAf75Vc8ffclltMgTfEUL9ty4i7bo9iI4OFUS0V 22 | T5x1PSLOcrbMTz1dgCEYJrcV38jb3XqfkJpEvq4hGQu5n8BHVNDmYhUmPUzWuDPC 23 | aH9XHmU7j1WKLL56uW+tD5MeE4bu/sEPAdWsZJf2OSTN6/MltMRRplnEqjv2gtEK 24 | tNq3ZwKBgErPj1YRm0iU5YOABrmR1PSucRdTdw+KtlN+t0NOyUOmZ0CKiM6mIa5i 25 | B4LsVzRzPRm9+Hj/jEPG5pH3wmB1na6CjppOp4xJZTbupF9Ij/6UFPer007+p4PI 26 | 53JGbCtSDI9Vk7qTu5M1RBuETLkWsXeu7aitk0msdnb8wOvgr7jz 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /project/MacroPlugin.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2016 Verizon. All Rights Reserved. 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 verizon.build 18 | 19 | import sbt._, Keys._ 20 | import xerial.sbt.Sonatype.autoImport.sonatypeProfileName 21 | 22 | object MacroPlugin extends AutoPlugin { 23 | 24 | override def trigger = allRequirements 25 | 26 | override def requires = RigPlugin 27 | 28 | override lazy val projectSettings = Seq( 29 | addCompilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full), 30 | libraryDependencies ++= Seq( 31 | "org.scala-lang" % "scala-reflect" % scalaVersion.value % "provided" 32 | ) ++ ( 33 | CrossVersion.partialVersion(scalaVersion.value) match { 34 | case Some((2, scalaMajor)) if scalaMajor == 10 => Seq("org.scalamacros" %% "quasiquotes" % "2.1.0") 35 | case _ => Nil 36 | } 37 | ), 38 | unmanagedSourceDirectories in Compile += 39 | (sourceDirectory in Compile).value / "macros" / s"scala-${scalaBinaryVersion.value}" 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/server_key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAvjQY24Fqsb1TfEYM5CZYVB59CH7H3sRRLeDTVhmyCKKvOUk4 3 | rG+k9qu+0NA1xLyoSdrZJR+XLHZTx18P5ygCoMSh/+5R2ILBnZUUEDZT6Yyazy1N 4 | SAK8LwZZ/vptLUXkeA/w5671+aDkCLZz+tgamMJewQ3dEQpy94PPkW4vxu2Thayc 5 | tXhlgqSvyUx5P74kl7pznTcrkeGcXTwGoyAsBtwoC4wvF/YoUlMdnpCWyfPijeCV 6 | zS2HgIxJHuVvr5d5wi8GJvx7I978Y0OlAV1j/BXS7V4tsAxtVzGwaJNALUAS0Kxx 7 | /kIr/Tn537o4xfol9W8t1mO1t7F4zcgsk7DL2QIDAQABAoIBAE/2VvzxS4mtrotw 8 | Z7sC4995cddAdYaRwg8nlbF82/wFaOUMg2b7CL8rNO1Um0Y6igWCnAefYE6w8EfB 9 | D05+45vzDqgp6P68+ejl46ltLZniIrM2uGatv6+FXBB197Aw0fwnt3+vbnmjyjpf 10 | eiLa91OBY5SF34pqeirUn07ko/Fc9fmaWD8Kb7heG4cR1STs+naGW2hFnbluTxK/ 11 | TGwUmtA67OEXNi4FV5LyWum5/HDh6P5NocrP68FbNO2pK6QG4wUa3euyoegWitT4 12 | uw3hFOU45epAil4UEZOLinjQ7nG8MG2Yj70BzdDmd3o0JmKdvOUSQeqoRYYaUr5w 13 | UedsfIECgYEA6N3GCrXnOapzygkRMmNRIgVGGezFazrz+E+pV68H2AXe32o2YlIe 14 | /9Ld63YF/jEj3IJXkq7oraiTA0gi4zN1TDjykI+kr65BoOe06j22XF0ZD7QFjHoc 15 | p74UCEyvPxCbAY+XkpTSChpvOuaWEeFL4r5uC/M9PzxSFnWNVcQfmC8CgYEA0RlU 16 | HmYe3CJlCt5vWDElD+tZIHdZBG/Kxjxq2V2jHdhZx6BnuIF/GM+k7+P2K/iRWBsV 17 | WbYKEUiYuzOLTtCzNXPZH5oplRjrFOik4VsHVqbjFr59TpDvCCEFl2sF1RUc3uot 18 | k2SEjCfhj/0yHvOCQzBSiRbKcBq+AEsecOdeUncCgYEA5nQkPBFNZfpUYQucIzu6 19 | 3WpOt/VkYvyS2ZksxAQKRTV6HaxlaeRPnWm4CgBhafCDxwSPWjETQFlmNIc9T5Br 20 | cyWgmIQvvVU8ydrFZlPwl3PjC3FjVGS82xz3gkcx3721SVQr8vqo6yrCDY6J0eQf 21 | T6SaD+WHJX+y1Vs8pjZT9i0CgYAtZfdqhjeLCfiwLG02U609UJbQQrlUCkWXVsTI 22 | HtlsgG21tvDuEUecBnFEGFV5ZIWCAJdZG/oBmKZ4do93dD6Ei3uHrfv7QCbCKmSp 23 | /Q6R1LgXVxJzvWn4dNr27iidA/F5wJRWnQteNsZ1cb6xE6OjnNk+pHEdd+DqxWR9 24 | lO5YrwKBgQDNmLk6/1uUfTTwQMdEcTESMfhvM5NUk5Ma7P0ezxMdppAxmfReAtVi 25 | V6mUaNDhLK4Pt1/KbpMRg5UuPK+r9CSQspnVd3fhSlk7jKlg0jDLuyXJmg10+mn7 26 | J9OtxBT2O6LiMLtrBrFNcESE09ieqqO9xYEafj6T50HoKUYpWRjLlA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/client_key.pk8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQD2iMORSwPhVLI9 3 | zpdsF+kWimPztozhlYgAbpybOX1uYUO4Z2jTNAufGPrht27WZ0e3wD1VhIXPm7GD 4 | SyCTYej3G0f3mj4Hdwu5PTLvROsX1UdVEmbPUd0pQoCc0iS8YFGzxIxGuIpDs23/ 5 | xZPgVASGjQzLq7llxMiaDw9qafFjFbX0H8nPXKJR3oDtUocXG/98VT6bjIgOljrg 6 | lJg8odhF2QUBxmOMgm7wQnmcFFL2GFwuKgQNvPRm1sk9PNvYbyH69NDW1E5Mpeqi 7 | DPKYnc+q8uFdSt06IJ86IhgTqQ//wXSDlRC32JHZQKE4Mz3aaF99Yg/aAIAb3Sjy 8 | /zQTnhPLAgMBAAECggEBAIS38Zh5KtiWcPInEotlf9zbmZ1tDACxMCsTYQmLdIQE 9 | pqFSQjNZFFyM5Rq4Vlnj9c7tFdfA5Qxzw6drvVi6X+EPdbnM9lGse8o4MQBwOKN2 10 | 9NTOeDxp1MbOf1BtLMugirtdNysnU3/Iy5Hi+o3MjTcmHdpTUxZ/ToLpUqI18HYm 11 | /my/JfWBygCSHdAnKCFK1AUU6ueqtge4eyuvW8Zz9dR7PSwlsjf/fXlI1cSrJ6U/ 12 | AjtGoIpkoKkmGMdW0HTbIRvEFuKIcTI1L4ED+tkqs1gYwBi2miltTZGH1dzeBjub 13 | IYF24ok545yv0e6jRB7+L+UWjoW6c91c1Ce9VmPudhkCgYEA/yMWYIrEyuovUmdF 14 | Zs8XS15xTuVF00TBG6urgldgshT4C0y76h1KDKrdzq+YB2if8X66uvWWWbbumeV4 15 | /mbiCVT54VTRImUPnjTWj4Pz+1lAT3cMP7ygfID7jwHuLNNMLWn0n8/aqwQJDQao 16 | Vd75w7zX8j08tSwA/GA1iv6fkbUCgYEA9146SjqpVP89ehm56XG58UOUIniRAplU 17 | WZSKh2NyLvIdGBmS/JUIlcEVpFEvbgIzXWl1ZXyuj2JE6uLjTo9pqHiMfKA5YEJq 18 | UmO9ZMrFcS50TfUzkoppszTew1+lZsz85j4HAyS4wL3mfP4f+6p3kCtjdzNT3xS+ 19 | o2iVRV/8f38CgYArYEW/qBgI/4OIGPKdpfup3qaY6TXT006Za7g+8qgH3rTFBwiz 20 | psMeIDaRmxzXtZvrrMYGXxZurkEmfPLzHX60Y3QQMSFVHaVoGkeYIrDqPKRIXk8A 21 | zwrh3/PkvklF6a570hW6zdW5WXhIdKdN4hjIhGSaVHrruk14msq/lMaAOQKBgB/v 22 | lVzx99yWW0yBN8RQv23LiLtuj2Ijg4VRLRVPnHU9Is5ytsxPPV2AIRgmtxXfyNvd 23 | ep+QmkS+riEZC7mfwEdU0OZiFSY9TNa4M8Jof1ceZTuPVYosvnq5b60Pkx4Thu7+ 24 | wQ8B1axkl/Y5JM3r8yW0xFGmWcSqO/aC0Qq02rdnAoGASs+PVhGbSJTlg4AGuZHU 25 | 9K5xF1N3D4q2U363Q07JQ6ZnQIqIzqYhrmIHguxXNHM9Gb34eP+MQ8bmkffCYHWd 26 | roKOmk6njEllNu6kX0iP/pQU96vTTv6ng8jnckZsK1IMj1WTupO7kzVEG4RMuRax 27 | d67tqK2TSax2dvzA6+CvuPM= 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/server_key.pk8: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC+NBjbgWqxvVN8 3 | RgzkJlhUHn0IfsfexFEt4NNWGbIIoq85STisb6T2q77Q0DXEvKhJ2tklH5csdlPH 4 | Xw/nKAKgxKH/7lHYgsGdlRQQNlPpjJrPLU1IArwvBln++m0tReR4D/DnrvX5oOQI 5 | tnP62BqYwl7BDd0RCnL3g8+Rbi/G7ZOFrJy1eGWCpK/JTHk/viSXunOdNyuR4Zxd 6 | PAajICwG3CgLjC8X9ihSUx2ekJbJ8+KN4JXNLYeAjEke5W+vl3nCLwYm/Hsj3vxj 7 | Q6UBXWP8FdLtXi2wDG1XMbBok0AtQBLQrHH+Qiv9OfnfujjF+iX1by3WY7W3sXjN 8 | yCyTsMvZAgMBAAECggEAT/ZW/PFLia2ui3BnuwLj33lx10B1hpHCDyeVsXzb/AVo 9 | 5QyDZvsIvys07VSbRjqKBYKcB59gTrDwR8EPTn7jm/MOqCno/rz56OXjqW0tmeIi 10 | sza4Zq2/r4VcEHX3sDDR/Ce3f69ueaPKOl96Itr3U4FjlIXfimp6KtSfTuSj8Vz1 11 | +ZpYPwpvuF4bhxHVJOz6doZbaEWduW5PEr9MbBSa0Drs4Rc2LgVXkvJa6bn8cOHo 12 | /k2hys/rwVs07akrpAbjBRrd67Kh6BaK1Pi7DeEU5Tjl6kCKXhQRk4uKeNDucbww 13 | bZiPvQHN0OZ3ejQmYp285RJB6qhFhhpSvnBR52x8gQKBgQDo3cYKtec5qnPKCREy 14 | Y1EiBUYZ7MVrOvP4T6lXrwfYBd7fajZiUh7/0t3rdgX+MSPcgleSruitqJMDSCLj 15 | M3VMOPKQj6SvrkGg57TqPbZcXRkPtAWMehynvhQITK8/EJsBj5eSlNIKGm865pYR 16 | 4Uvivm4L8z0/PFIWdY1VxB+YLwKBgQDRGVQeZh7cImUK3m9YMSUP61kgd1kEb8rG 17 | PGrZXaMd2FnHoGe4gX8Yz6Tv4/Yr+JFYGxVZtgoRSJi7M4tO0LM1c9kfmimVGOsU 18 | 6KThWwdWpuMWvn1OkO8IIQWXawXVFRze6i2TZISMJ+GP/TIe84JDMFKJFspwGr4A 19 | Sx5w515SdwKBgQDmdCQ8EU1l+lRhC5wjO7rdak639WRi/JLZmSzEBApFNXodrGVp 20 | 5E+dabgKAGFp8IPHBI9aMRNAWWY0hz1PkGtzJaCYhC+9VTzJ2sVmU/CXc+MLcWNU 21 | ZLzbHPeCRzHfvbVJVCvy+qjrKsINjonR5B9PpJoP5Yclf7LVWzymNlP2LQKBgC1l 22 | 92qGN4sJ+LAsbTZTrT1QltBCuVQKRZdWxMge2WyAbbW28O4RR5wGcUQYVXlkhYIA 23 | l1kb+gGYpnh2j3d0PoSLe4et+/tAJsIqZKn9DpHUuBdXEnO9afh02vbuKJ0D8XnA 24 | lFadC142xnVxvrETo6Oc2T6kcR134OrFZH2U7livAoGBAM2YuTr/W5R9NPBAx0Rx 25 | MRIx+G8zk1STkxrs/R7PEx2mkDGZ9F4C1WJXqZRo0OEsrg+3X8pukxGDlS48r6v0 26 | JJCymdV3d+FKWTuMqWDSMMu7JcmaDXT6afsn063EFPY7ouIwu2sGsU1wRITT2J6q 27 | o73FgRp+PpPnQegpRilZGMuU 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /core/src/main/macros/scala-2.11/remotely/MacrosCompatibility.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2015 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | trait MacrosCompatibility { 21 | type Context = scala.reflect.macros.blackbox.Context 22 | 23 | def getDeclarations(c: Context)(tpe: c.universe.Type): c.universe.MemberScope = 24 | tpe.decls 25 | 26 | def getParameterLists(c: Context)(method: c.universe.MethodSymbol): List[List[c.universe.Symbol]] = 27 | method.paramLists 28 | 29 | def getDeclaration(c: Context)(tpe: c.universe.Type, name: c.universe.Name): c.universe.Symbol = 30 | tpe.decl(name) 31 | 32 | def createTermName(c: Context)(name: String): c.universe.TermName = 33 | c.universe.TermName(name) 34 | 35 | def createTypeName(c: Context)(name: String): c.universe.TypeName = 36 | c.universe.TypeName(name) 37 | 38 | def resetLocalAttrs(c: Context)(tree: c.Tree): c.Tree = 39 | c.untypecheck(tree) 40 | 41 | def getTermNames(c: Context): c.universe.TermNamesApi = 42 | c.universe.termNames 43 | 44 | def companionTpe(c: Context)(tpe: c.universe.Type): c.universe.Symbol = 45 | tpe.typeSymbol.companion 46 | } -------------------------------------------------------------------------------- /core/src/main/scala/IORef.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.util.concurrent.atomic.AtomicReference 21 | 22 | import scalaz.concurrent.Task 23 | 24 | /** An atomically updatable reference, guarded by the `Task` monad. */ 25 | sealed abstract class IORef[A] { 26 | def read: Task[A] 27 | def write(value: A): Task[Unit] 28 | def atomicModify[B](f: A => (A, B)): Task[B] 29 | def compareAndSet(oldVal: A, newVal: A): Task[Boolean] 30 | def modify(f: A => A): Task[Unit] = 31 | atomicModify(a => (f(a), ())) 32 | } 33 | 34 | object IORef { 35 | def apply[A](value: => A): IORef[A] = new IORef[A] { 36 | val ref = new AtomicReference(value) 37 | def read = Task.delay(ref.get) 38 | def write(value: A) = Task.delay(ref.set(value)) 39 | def compareAndSet(oldVal: A, newVal: A) = 40 | Task.delay(ref.compareAndSet(oldVal, newVal)) 41 | def atomicModify[B](f: A => (A, B)) = for { 42 | a <- read 43 | (a2, b) = f(a) 44 | p <- compareAndSet(a, a2) 45 | r <- if (p) Task.now(b) else atomicModify(f) 46 | } yield r 47 | } 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /core/src/main/macros/scala-2.10/remotely/MacrosCompatibility.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2015 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | trait MacrosCompatibility { 21 | type Context = scala.reflect.macros.Context 22 | 23 | def getDeclarations(c: Context)(tpe: c.universe.Type): c.universe.MemberScope = 24 | tpe.declarations 25 | 26 | def getParameterLists(c: Context)(method: c.universe.MethodSymbol): List[List[c.universe.Symbol]] = 27 | method.paramss 28 | 29 | def getDeclaration(c: Context)(tpe: c.universe.Type, name: c.universe.Name): c.universe.Symbol = 30 | tpe.declaration(name) 31 | 32 | def createTermName(c: Context)(name: String): c.universe.TermName = 33 | c.universe.newTermName(name) 34 | 35 | def createTypeName(c: Context)(name: String): c.universe.TypeName = 36 | c.universe.newTypeName(name) 37 | 38 | def resetLocalAttrs(c: Context)(tree: c.Tree): c.Tree = 39 | c.resetLocalAttrs(tree) 40 | 41 | def getTermNames(c: Context): c.universe.TermNamesApi = 42 | c.universe.`nme` 43 | 44 | def companionTpe(c: Context)(tpe: c.universe.Type): c.universe.Symbol = 45 | tpe.typeSymbol.companionSymbol 46 | } -------------------------------------------------------------------------------- /core/src/test/scala/ResponseSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import org.scalacheck._ 21 | import Prop._ 22 | import scala.concurrent.{ExecutionContext,Future} 23 | import scalaz.Monad 24 | 25 | object ResponseSpec extends Properties("Response") { 26 | 27 | property("stack safety") = { 28 | import ExecutionContext.Implicits.global 29 | val N = 100000 30 | val responses = (0 until N).map(Monad[Response].pure(_)) 31 | val responses2 = (0 until N).map(i => Response.async(Future(i))) 32 | 33 | def leftFold(responses: Seq[Response[Int]]): Response[Int] = 34 | responses.foldLeft(Monad[Response].pure(0))(Monad[Response].apply2(_,_)(_ + _)) 35 | def rightFold(responses: Seq[Response[Int]]): Response[Int] = 36 | responses.reverse.foldLeft(Monad[Response].pure(0))((tl,hd) => Monad[Response].apply2(hd,tl)(_ + _)) 37 | 38 | val ctx = Response.Context.empty 39 | val expected = (0 until N).sum 40 | 41 | leftFold(responses)(ctx).run == expected && 42 | rightFold(responses)(ctx).run == expected && 43 | leftFold(responses2)(ctx).run == expected && 44 | rightFold(responses2)(ctx).run == expected 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /core/src/test/scala/CountServer.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.util.concurrent.atomic.AtomicInteger 21 | import codecs._ 22 | 23 | trait CountServerBase { 24 | import Codecs._ 25 | 26 | def ping: Int => Response[Int] 27 | def describe: Response[List[Signature]] 28 | 29 | def environment: Environment = Environment( 30 | Codecs.empty.codec[Int], 31 | populateDeclarations(Values.empty) 32 | ) 33 | 34 | private def populateDeclarations(env: Values): Values = env 35 | .declare("ping", ping) 36 | .declare("describe", describe) 37 | } 38 | 39 | class CountServer extends CountServerBase { 40 | implicit val intcodec = int32 41 | val count: AtomicInteger = new AtomicInteger(0) 42 | def ping: Int => Response[Int] = { _ => 43 | val r = count.incrementAndGet() 44 | Response.delay(r) 45 | } 46 | 47 | def describe: Response[List[Signature]] = Response.now(List(Signature("describe", Nil, "scala.List[Signature]"), 48 | Signature("ping", List(Field("a", "Int")), "Int"))) 49 | } 50 | 51 | object CountClient { 52 | val ping = Remote.ref[Int => Int]("ping") 53 | } 54 | -------------------------------------------------------------------------------- /examples/src/main/scala/Protocol.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package examples 20 | import scalaz._ 21 | import remotely.codecs._ 22 | 23 | // NB: The GenServer macro needs to receive the FQN of all types, or import them 24 | // explicitly. The target of the macro needs to be an abstract class. 25 | @GenServer(remotely.Protocol.empty.codec[Int].specify1("fac", Field.strict[Int]("in"), Type[Int])) 26 | abstract class FacServer 27 | 28 | // The `GenClient` macro needs to receive the FQN of all types, or import them 29 | // explicitly. The target needs to be an object declaration. 30 | @GenClient(remotely.Protocol.empty.codec[Int].specify1[Int,Int]("fac", Field.strict[Int]("in"), Type[Int]).signatures) 31 | object FacClient 32 | 33 | // TODO(ahjohannessen): @stew <- Compiling Foo fails somehow? 34 | // @GenServer(remotely.Protocol.empty.codec[List[Int]]) abstract class Foo 35 | 36 | object TestProtocol { 37 | 38 | // We can make a server 39 | lazy val facServer = new FacServer { 40 | val fac = (n: Int) => Response.now { (1 to n).product } 41 | } 42 | 43 | // We can get the environment out of a generated server: 44 | lazy val env = facServer.environment 45 | } 46 | 47 | -------------------------------------------------------------------------------- /benchmark/server/src/main/scala/ServerImpl.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | package server 21 | 22 | import scalaz.concurrent._ 23 | import java.util.concurrent._ 24 | import java.util.concurrent.atomic.AtomicInteger 25 | 26 | class BenchmarkServerImpl extends BenchmarkServer with transformations { 27 | override def identityLarge = (large: LargeW) => Response[LargeW]((c: Response.Context) => Task.now{toLargeW(fromLargeW(large))}) 28 | override def identityMedium = (med: MediumW) => Response[MediumW]((c: Response.Context) => Task.now{toMediumW(fromMediumW(med))}) 29 | override def identityBig = (big: BigW) => Response[BigW]((c: Response.Context) => Task.now{toBigW(fromBigW(big))}) 30 | 31 | } 32 | 33 | object Main { 34 | def usage() { 35 | println("usage: BenchmarkServerImpl port numThreads") 36 | Runtime.getRuntime.exit(1) 37 | } 38 | 39 | def main(argv: Array[String]): Unit = { 40 | if(argv.length < 2) usage() 41 | 42 | val threadNo = new AtomicInteger(0) 43 | val port = Integer.parseInt(argv(0)) 44 | val addr = new java.net.InetSocketAddress("localhost", port) 45 | val server = new BenchmarkServerImpl 46 | val shutdown: Task[Unit] = server.environment.serve(addr).run 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/src/main/scala/DescribeTest.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package test 20 | 21 | import scodec.Codec 22 | 23 | case class Foo(a: Int) 24 | case class Bar(a: Int) 25 | 26 | object DescribeTestOlderProtocol { 27 | implicit lazy val fooCodec: Codec[Foo] = codecs.int32.as[Foo] 28 | implicit lazy val barCodec: Codec[Bar] = codecs.int32.as[Bar] 29 | implicit lazy val sigCodec: Codec[List[Signature]] = codecs.list(Signature.signatureCodec) 30 | 31 | val definition = Protocol.empty 32 | .codec[Foo] 33 | .codec[Bar] 34 | .specify0("foo", Type[Foo]) 35 | .specify1("fooId", Field.strict[Foo]("in"), Type[Foo]) 36 | .specify1("foobar", Field.strict[Foo]("in"), Type[Bar]) 37 | } 38 | 39 | 40 | /** 41 | * This is just like the older protocol, but it adds a new method 42 | */ 43 | object DescribeTestNewerProtocol { 44 | implicit lazy val fooCodec: Codec[Foo] = codecs.int32.as[Foo] 45 | implicit lazy val barCodec: Codec[Bar] = codecs.int32.as[Bar] 46 | implicit lazy val sigCodec: Codec[List[Signature]] = codecs.list(Signature.signatureCodec) 47 | 48 | val definition = Protocol.empty 49 | .codec[Foo] 50 | .codec[Bar] 51 | .specify0("foo", Type[Foo]) 52 | .specify1("fooId", Field.strict[Foo]("in"), Type[Foo]) 53 | .specify1("foobar", Field.strict[Foo]("in"), Type[Bar]) 54 | .specify0("bar", Type[Bar]) 55 | } 56 | -------------------------------------------------------------------------------- /core/src/test/scala/EndpointSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} 21 | import remotely.transport.netty.NettyTransport 22 | 23 | import scala.concurrent.duration.DurationInt 24 | import scalaz.stream.Process 25 | 26 | class EndpointSpec extends FlatSpec with Matchers with BeforeAndAfterAll { 27 | behavior of "failoverChain" 28 | it should "work" in { 29 | val goodAddress = new java.net.InetSocketAddress("localhost", 9007) 30 | val badAddress = new java.net.InetSocketAddress("localhost", 9009) 31 | 32 | val goodEndpoint = (NettyTransport.single(goodAddress) map Endpoint.single).run 33 | val badEndpoint = (NettyTransport.single(badAddress) map Endpoint.single).run 34 | 35 | def endpoints: Process[Nothing,Endpoint] = Process.emitAll(List(badEndpoint, goodEndpoint)) 36 | 37 | val server = new CountServer 38 | 39 | val shutdown = server.environment.serve(goodAddress).run 40 | 41 | val endpoint = Endpoint.failoverChain(10.seconds, endpoints) 42 | 43 | import Response.Context 44 | import Remote.implicits._ 45 | import codecs._ 46 | 47 | val call = evaluate(endpoint, Monitoring.empty)(CountClient.ping(1)) 48 | 49 | val i: Int = call.apply(Context.empty).run 50 | val j: Int = call.apply(Context.empty).run 51 | j should be (2) 52 | 53 | shutdown.run 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/test/scala/CapabilitiesSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.util.concurrent.Executors 21 | import org.scalatest.matchers.{Matcher,MatchResult} 22 | import org.scalatest.{FlatSpec,Matchers,BeforeAndAfterAll} 23 | import remotely.transport.netty._ 24 | import scala.concurrent.duration.DurationInt 25 | import scalaz.stream.Process 26 | import scalaz.concurrent.{Strategy,Task} 27 | import codecs._ 28 | 29 | class CapabilitiesSpec extends FlatSpec 30 | with Matchers 31 | with BeforeAndAfterAll { 32 | 33 | val addr1 = new java.net.InetSocketAddress("localhost", 9003) 34 | 35 | val server1 = new CountServer 36 | val shutdown1: Task[Unit] = server1.environment.serve(addr1, capabilities = Capabilities(Set())).run 37 | 38 | override def afterAll() { 39 | shutdown1.run 40 | } 41 | val endpoint1 = (NettyTransport.single(addr1) map Endpoint.single).run 42 | 43 | behavior of "Capabilities" 44 | 45 | it should "not call an incompatible server" in { 46 | import Response.Context 47 | import Remote.implicits._ 48 | import codecs._ 49 | 50 | an[IncompatibleServer] should be thrownBy ( 51 | try { 52 | val _ = evaluate(endpoint1, Monitoring.empty)(CountClient.ping(1)).apply(Context.empty).run 53 | } catch { 54 | case t: IncompatibleServer => 55 | throw t 56 | case t: Throwable => 57 | t.printStackTrace 58 | throw t 59 | } 60 | ) 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /core/src/test/resources/ssl-testing/generate_ssl_test_credentials.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | CA_passphrase=badpassword 4 | server_name=Paul-test-server 5 | client_name=Paul-test-client 6 | ca_name=Paul-test-CA 7 | 8 | 9 | # Generate a Certificate Authority: 10 | openssl req -out CA.pem -new -x509 -passout pass:$CA_passphrase \ 11 | -subj "/CN=$ca_name" -keyout CA_key.pem 12 | # CA has required book keeping (cert.srl: incremental serial number) 13 | echo "00" > cert.srl 14 | 15 | ## Generate a Server keypair and certificate 16 | openssl genrsa -f4 2048 > server_key.pem 17 | openssl rsa -in server_key.pem -pubout > server_pubkey.pem 18 | openssl req -key server_key.pem -new -out server.req -subj "/CN=$server_name" 19 | ** convert from SSLeay to pkcs8 20 | openssl pkcs8 -topk8 -inform pem -in server_key.pem -outform pem -nocrypt -out server_key.pk8 21 | 22 | # Sign the server certificate with the CA: 23 | openssl x509 -req -in server.req -CA CA.pem -passin pass:$CA_passphrase \ 24 | -CAkey CA_key.pem -CAserial cert.srl -out server_cert.pem -days 1000 25 | 26 | ## Generate a Client keypair and certificate: 27 | openssl genrsa -out client_key.pem 2048 28 | openssl req -key client_key.pem -new -out client.req -subj "/CN=$client_name" 29 | ** convert from SSLeay to pkcs8 30 | openssl pkcs8 -topk8 -inform pem -in client_key.pem -outform pem -nocrypt -out client_key.pk8 31 | 32 | # Sign the client certificate with the CA: 33 | openssl x509 -req -in client.req -CA CA.pem -passin pass:$CA_passphrase \ 34 | -CAkey CA_key.pem -CAserial cert.srl -out client_cert.pem -days 1000 35 | 36 | 37 | echo -e "\n\n\n" 38 | echo "TESTING PATTERNS:" 39 | echo "" 40 | echo "1. Start the Server with server auth only:" 41 | echo "openssl s_server -accept 9443 -key server_key.pem -cert server_cert.pem" 42 | echo "" 43 | echo "2. Start the Server with mutual auth:" 44 | echo "openssl s_server -accept 9443 -key server_key.pem -cert server_cert.pem -CAfile CA.pem -Verify 1" 45 | echo "" 46 | echo "3. Start a client SSL session, single auth:" 47 | echo "# establish an SSL connection and echo 'HELLO' to the server:" 48 | echo "openssl s_client -connect localhost:9443 -CAfile CA.pem" 49 | echo "" 50 | echo "4. Start a client SSL session, mutual auth:" 51 | echo "openssl s_client -connect localhost:9443 -CAfile CA.pem -key client_key.pem -cert client_cert.pem" 52 | echo "" 53 | 54 | -------------------------------------------------------------------------------- /benchmark/protocol/src/main/scala/transformations.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | 21 | trait transformations { 22 | def fromSmallW(s: SmallW): Small = Small(s.alpha, s.omega) 23 | def toSmallW(s: Small): SmallW = SmallW(s.alpha, s.omega) 24 | 25 | def fromMediumW(m: MediumW): Medium = Medium(m.ay, 26 | m.bee, 27 | m.cee.map(fromSmallW), 28 | m.dee) 29 | 30 | def toMediumW(m: Medium): MediumW = MediumW(m.ay, 31 | m.bee, 32 | m.cee.map(toSmallW), 33 | m.dee) 34 | 35 | def fromLargeW(l: LargeW): Large = Large(l.one, 36 | l.two, 37 | l.three, 38 | l.four, 39 | l.five.map(fromMediumW), 40 | l.six.map(fromSmallW)) 41 | 42 | def toLargeW(l: Large): LargeW = LargeW(l.one, 43 | l.two, 44 | l.three, 45 | l.four, 46 | l.five.map(toMediumW), 47 | l.six.map(toSmallW)) 48 | 49 | 50 | 51 | def fromBigW(b: BigW): Big = Big(b.one) 52 | def toBigW(b: Big): BigW = BigW(b.one) 53 | } 54 | 55 | -------------------------------------------------------------------------------- /benchmark/protocol/src/main/scala/Protocol.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | 21 | import collection.immutable.IndexedSeq 22 | import codecs._ 23 | import scodec.{Codec, codecs => C} 24 | 25 | case class LargeW(one: Int, 26 | two: List[String], 27 | three: String, 28 | four: Map[String, String], 29 | five: List[MediumW], 30 | six: IndexedSeq[SmallW]) 31 | 32 | case class MediumW(ay: Int, 33 | bee: String, 34 | cee: List[SmallW], 35 | dee: Option[Int]) 36 | 37 | case class SmallW(alpha: Map[String,String], 38 | omega: List[String]) 39 | 40 | case class BigW(one: Int) 41 | 42 | object protocol { 43 | 44 | implicit val smallWCodec: Codec[SmallW] = 45 | (map(utf8,utf8) ~~ list(utf8)).widenAs[SmallW](SmallW.apply, SmallW.unapply) 46 | 47 | implicit val mediumWCodec: Codec[MediumW] = 48 | (int32 ~~ utf8 ~~ list[SmallW] ~~ optional(int32)).widenAs(MediumW.apply, MediumW.unapply) 49 | 50 | implicit val largeWCodec: Codec[LargeW] = 51 | (int32 ~~ list(utf8) ~~ utf8 ~~ map(utf8,utf8) ~~ list[MediumW] ~~ indexedSeq[SmallW]).widenAs(LargeW.apply, LargeW.unapply) 52 | 53 | implicit val bigWCodec: Codec[BigW] = 54 | int32.widenOpt(BigW.apply, BigW.unapply) 55 | 56 | val definition = 57 | Protocol.empty 58 | .codec[LargeW] 59 | .codec[MediumW] 60 | .codec[SmallW] 61 | .codec[BigW] 62 | .specify1("identityLarge", Field.strict[LargeW]("in"), Type[LargeW]) 63 | .specify1[MediumW, MediumW]("identityMedium", Field.strict[MediumW]("in"), Type[MediumW]) 64 | .specify1[BigW, BigW]("identityBig", Field.strict[BigW]("in"), Type[BigW]) 65 | } 66 | -------------------------------------------------------------------------------- /project/CentralRequirementsPlugin.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2016 Verizon. All Rights Reserved. 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 verizon.build 18 | 19 | import sbt._, Keys._ 20 | import xerial.sbt.Sonatype.autoImport.sonatypeProfileName 21 | 22 | object CentralRequirementsPlugin extends AutoPlugin { 23 | 24 | override def trigger = allRequirements 25 | 26 | override def requires = RigPlugin 27 | 28 | override lazy val projectSettings = Seq( 29 | sonatypeProfileName := "io.verizon", 30 | pomExtra in Global := { 31 | 32 | 33 | timperrett 34 | Timothy Perrett 35 | http://github.com/timperrett 36 | 37 | 38 | runarorama 39 | Runar Bjarnason 40 | http://github.com/runarorama 41 | 42 | 43 | stew 44 | Stew O'Connor 45 | http://github.com/stew 46 | 47 | 48 | ahjohannessen 49 | Alex Henning Johannessen 50 | https://github.com/ahjohannessen 51 | 52 | 53 | pchiusano 54 | Paul Chiusano 55 | https://github.com/pchiusano 56 | 57 | 58 | jedesah 59 | Jean-Rémi Desjardins 60 | https://github.com/jedesah 61 | 62 | 63 | }, 64 | licenses := Seq("Apache-2.0" -> url("https://www.apache.org/licenses/LICENSE-2.0.html")), 65 | homepage := Some(url("http://verizon.github.io/remotely/")), 66 | scmInfo := Some(ScmInfo(url("https://github.com/verizon/remotely"), 67 | "git@github.com:verizon/remotely.git")) 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /core/src/test/scala/ServerErrors.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package test 20 | 21 | import org.scalatest.{Matchers, FlatSpec} 22 | import remotely.transport.netty.NettyTransport 23 | import codecs._ 24 | import Remote.implicits._ 25 | 26 | class ServerErrors extends FlatSpec with Matchers { 27 | behavior of "missing codec on the server" 28 | it should "throw the appropriate error if missing encoder for the response" in { 29 | val address = new java.net.InetSocketAddress("localhost", 9013) 30 | 31 | val endpoint = (NettyTransport.single(address) map Endpoint.single).run 32 | 33 | val server = new CountServer 34 | 35 | val shutdown = server.environment.serve(address).run 36 | 37 | val call = Remote.local(true).runWithoutContext(endpoint) 38 | 39 | val expectedMsg = (s"[decoding] server does not have response serializer for: ${Remote.toTag[Boolean]}") 40 | 41 | try(call.run) catch { 42 | case se: ServerException ⇒ se.getMessage should startWith(expectedMsg) 43 | case huh: Exception ⇒ huh.printStackTrace(); fail(huh) 44 | } 45 | 46 | shutdown.run 47 | } 48 | 49 | behavior of "incompatible reference on server" 50 | it should "throw the appropriate error if there is some kind of reference mismatch" in { 51 | val address = new java.net.InetSocketAddress("localhost", 9077) 52 | 53 | val endpoint = (NettyTransport.single(address) map Endpoint.single).run 54 | 55 | val server = new CountServer 56 | 57 | val shutdown = server.environment.serve(address).run 58 | 59 | val wrongRef = Remote.ref[(Int, Int) => Int]("ping") 60 | 61 | val call = wrongRef(1,2).runWithoutContext(endpoint) 62 | 63 | val expectedMsg = ("[validation] server values: Int, describe: List[remotely.Signature])> does not have referenced values:\n ping: (Int, Int) => Int") 64 | 65 | try(call.run) catch { 66 | case se: ServerException ⇒ se.getMessage should startWith(expectedMsg) 67 | case huh: Exception ⇒ huh.printStackTrace(); fail(huh) 68 | } 69 | 70 | shutdown.run 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /benchmark/server/src/test/scala/BenchmarkSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | package server 21 | package test 22 | 23 | import org.scalatest.matchers.{Matcher,MatchResult} 24 | import org.scalatest.{FlatSpec,Matchers,BeforeAndAfterAll} 25 | import remotely.ServerException 26 | import remotely.transport.netty._ 27 | import remotely.{Monitoring,Response,Endpoint,codecs}, codecs._, Response.Context 28 | import scala.collection.immutable.IndexedSeq 29 | import scalaz.{-\/,\/-} 30 | import scalaz.concurrent.{Strategy,Task} 31 | import remotely._ 32 | import java.util.concurrent._ 33 | import protocol._ 34 | 35 | class BenchmarkServerSpec extends FlatSpec 36 | with Matchers 37 | with BeforeAndAfterAll 38 | with transformations { 39 | 40 | val addr = new java.net.InetSocketAddress("localhost", 9001) 41 | val server = new BenchmarkServerImpl 42 | val shutdown: Task[Unit] = server.environment.serve(addr).run 43 | 44 | val endpoint = Endpoint.single(NettyTransport.single(addr).run) 45 | 46 | import remotely.Remote.implicits._ 47 | import remotely.codecs._ 48 | 49 | override def afterAll(){ 50 | Thread.sleep(500) 51 | shutdown.run 52 | } 53 | 54 | behavior of "identityBig" 55 | it should "work" in { 56 | val big = Big(1) 57 | val res = BenchmarkClient.identityBig(toBigW(big)).runWithoutContext(endpoint).run 58 | 59 | fromBigW(res) should equal (big) 60 | } 61 | behavior of "identityLarge" 62 | it should "serialize" in { 63 | val small = Small(Map.empty, List("asdf")) 64 | val big = Large(1, Nil, "string2", Map("key" -> "value"), List(Medium(2, "string", List(small), None)), Vector(small)) 65 | fromLargeW(toLargeW(big)) should equal (big) 66 | } 67 | 68 | it should "work" in { 69 | val small = Small(Map.empty, List("asdf")) 70 | val big = Large(1, Nil, "string2", Map("key" -> "value"), List(Medium(2, "string", List(small), None)), Vector(small)) 71 | val res = BenchmarkClient.identityLarge(toLargeW(big)).runWithoutContext(endpoint).run 72 | 73 | fromLargeW(res) should equal (big) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/src/main/tut/internals.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: "Internals" 4 | section: "internals" 5 | --- 6 | 7 | # Internals 8 | 9 | TODO 10 | 11 | 12 | 13 | ## Serialization 14 | 15 | ### Float 16 | 17 | Floats are encoded into 32 bits according to the [IEEE 754 floating-point single precision format](http://en.wikipedia.org/wiki/Single-precision_floating-point_format#IEEE_754_single-precision_binary_floating-point_format:_binary32) 18 | 19 | ### Double 20 | 21 | Doubles are encoded into 64 bits according to the [IEEE 754 floating-point double precision format)[http://en.wikipedia.org/wiki/Double-precision_floating-point_format#IEEE_754_double-precision_binary_floating-point_format:_binary64] 22 | 23 | ### Int 24 | 25 | Ints are encoded as either 32 or 64 bit [2s compliment](http://en.wikipedia.org/wiki/Two's_complement), [big-endian](http://en.wikipedia.org/wiki/Endianness) format 26 | 27 | ### Boolean 28 | 29 | Booleans are encoded as a single bit, 1 for true and 0 for false. 30 | 31 | ## Array[Byte] 32 | 33 | Byte arrays are stored as a 32-bit signed integer representing the number of bytes, followed by the actual bytes. 34 | 35 | ### String 36 | 37 | Strings are [UTF-8 encoded](http://en.wikipedia.org/wiki/UTF-8) into an Array of bytes, then serialized as Array[Byte] above 38 | 39 | ### Sequences: List[A], Seq[A], IndexedSeq[A], Set[A], SortedSet[A] 40 | 41 | Sequence like structures are all stored as a 32-bit signed integer representing the number of items in the collection followed by the bytes for each member of the collection 42 | 43 | ### Option[A] 44 | 45 | Optional values are represented by a single bit indicating if a byte encoding of the value follows. 46 | 47 | ### Tuple2..7 48 | 49 | Tuples are represented by a concatenation of the byte representation of each tuple member. 50 | 51 | ### Disjunctions: A \/ B, Either[A,B] 52 | 53 | Disjunctions are represented as a single bit discrimnator which indicates if the following bytes represent an A value or a B value. A zero bit indicates that A bytes follow, and a one bit indicates that B bytes follow 54 | 55 | ### Map[A,B] / SortedMap[A,B] 56 | 57 | Maps are encoded by converting them first to a Sequence of Tuple2 values (representing Key followed by Value), which is then encoded as an above List[(A,B)] would be. 58 | 59 | ### Execution Context 60 | - See the [Documentation about Excution Conext](/manual.html#execution-context]) for information about the Execution Context. 61 | 62 | An execution context is serialized as a Map[String,String] followed by a List[UUID] where uuid is encoded as 128 bits [as documented here](https://docs.oracle.com/javase/7/docs/api/java/util/UUID.html) 63 | 64 | 65 | 66 | ## Networking 67 | 68 | TODO 69 | 70 | ### Akka I/O 71 | 72 | Currently the network I/O in remotely is currently all plumbed through [Akka I/O](http://doc.akka.io/docs/akka/snapshot/scala/io.html) internally. 73 | 74 | TODO 75 | 76 | ### KeepAlive -------------------------------------------------------------------------------- /core/src/test/scala/CircuitBreakerSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import org.scalacheck._ 21 | import Prop._ 22 | import scalaz.concurrent.Task 23 | import scala.concurrent.duration._ 24 | import scalaz._ 25 | import scalaz.std.list._ 26 | import \/._ 27 | 28 | object CircuitBreakerSpec extends Properties("CircuitBreaker") { 29 | 30 | def failures(n: Int, cb: CircuitBreaker) = 31 | List.fill(n)(cb(Task.fail(new Error("oops"))).attempt).foldLeft(Task.now(right[Throwable, Int](0))) { 32 | (t1, t2) => t1.flatMap(_ => t2) 33 | } 34 | 35 | // The CB doesn't open until maxErrors has been reached. 36 | property("remains-closed") = forAll { (b: Byte) => 37 | val x = b.toInt 38 | val n = x.abs 39 | val p = failures(n + 1, CircuitBreaker(3.seconds, n)) 40 | p.run match { 41 | case -\/(e) => e.getMessage == "oops" 42 | case _ => false 43 | } 44 | } 45 | 46 | // The circuit-breaker opens when maxErrors has been reached. 47 | // Note that it opens AFTER the error has occurred, so maxErrors=0 will allow 48 | // one error to go through. 49 | property("opens") = forAll { (b: Byte) => 50 | // Scala! 51 | val x = b.toInt 52 | val n = x.abs 53 | val p = failures(n + 2, CircuitBreaker(3.seconds, n)) 54 | p.run match { 55 | case -\/(CircuitBreakerOpen) => true 56 | case _ => false 57 | } 58 | } 59 | 60 | // The CB closes again 61 | property("closes") = secure { 62 | val cb = CircuitBreaker(1.milliseconds, 0) 63 | val p = Monad[Task].sequence(List( 64 | cb(Task.fail(new Error("oops"))).attempt, 65 | // The breaker should have plenty of time to close 66 | Task(Thread.sleep(2)) 67 | )).map(_ => 0) 68 | p.attemptRun.fold(_ => false, _ == 0) 69 | } 70 | 71 | // The CB doesn't open as long as there are successes 72 | property("stays-closed") = secure { 73 | val cb = CircuitBreaker(3.hours, 1) 74 | val p = Monad[Task].sequence(List( 75 | cb(Task.fail(new Error("oops"))).attempt, 76 | cb(Task.now(0)), 77 | cb(Task.fail(new Error("oops"))).attempt 78 | )).map(_ => 1) 79 | p.attemptRun.fold(_ => false, _ == 1) 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /core/src/main/scala/CircuitBreaker.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scalaz._ 21 | import scalaz.concurrent.Task 22 | import scala.concurrent.duration._ 23 | 24 | case object CircuitBreakerOpen extends Exception 25 | 26 | case class BreakerState(halfOpen: Boolean = false, errors: Int = 0, openTime: Option[Long] = None) 27 | 28 | class CircuitBreaker(timeout: Duration, 29 | maxErrors: Int, 30 | breaker: IORef[BreakerState]) { self => 31 | 32 | def transform: Task ~> Task = new (Task ~> Task) { 33 | def apply[A](a: Task[A]) = self(a) 34 | } 35 | 36 | def apply[A](a: Task[A]): Task[A] = { 37 | def doAttempt: Task[A] = a.onFinish{ 38 | case Some(e) => addFailure 39 | case None => close 40 | } 41 | for { 42 | s <- breaker.read 43 | r <- s match { 44 | // Breaker is closed. Everything is fine. 45 | case BreakerState(_, _, None) => doAttempt 46 | // Breaker is open 47 | case bs@BreakerState(halfOpen, n, Some(t1)) => 48 | // Attempt to enter the half-open state 49 | val t2 = System.currentTimeMillis 50 | if (t2 - t1 >= timeout.toMillis && !halfOpen) 51 | breaker.compareAndSet(bs, BreakerState(true, n, Some(t1))).flatMap { b => 52 | if (b) doAttempt else apply(a) 53 | } 54 | else Task.fail(CircuitBreakerOpen) 55 | } 56 | } yield r 57 | } 58 | 59 | def addFailure: Task[Unit] = 60 | breaker.modify { 61 | // We haven't yet reached the breaking point 62 | case BreakerState(ho, n, None) if (n < maxErrors) => 63 | BreakerState(false, n + 1, None) 64 | // The breaker is closed, but should be opened 65 | case BreakerState(ho, _, None) => 66 | BreakerState(false, 0, Some(System.currentTimeMillis)) 67 | // The breaker is open. A non-failfast error just happened, so we reset the time. 68 | case BreakerState(ho, n, Some(t)) => 69 | BreakerState(false, n + 1, Some(System.currentTimeMillis)) 70 | } 71 | 72 | def close: Task[Unit] = breaker.write(BreakerState()) 73 | } 74 | 75 | object CircuitBreaker { 76 | def apply(timeout: Duration, maxErrors: Int): CircuitBreaker = 77 | new CircuitBreaker(timeout, maxErrors, IORef(BreakerState())) 78 | } 79 | -------------------------------------------------------------------------------- /examples/src/main/scala/Simple.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package examples 20 | 21 | import java.net.InetSocketAddress 22 | import remotely.transport.netty.NettyTransport 23 | import scalaz.concurrent.{Strategy,Task} 24 | import codecs._ 25 | 26 | object Simple { 27 | 28 | def foo(i: Int): String = "BONUS" 29 | 30 | // on server, populate environment with codecs and values 31 | val env = Environment.empty 32 | .codec[Int] 33 | .codec[String] 34 | .codec[Double] 35 | .codec[Float] 36 | .codec[List[Int]] 37 | .codec[List[String]].populate { _ 38 | .declare("sum", (d: List[Int]) => Response.now(d.sum) ) 39 | .declare("fac", (n: Int) => Response.delay { (1 to n).foldLeft(1)(_ * _)} ) // async functions also work 40 | .declare("foo", Response.now(foo _) ) // referencing existing functions works, too 41 | } 42 | 43 | val addr = new InetSocketAddress("localhost", 8083) 44 | 45 | // on client - create local, typed declarations for server 46 | // functions you wish to call. This can be code generated 47 | // from `env`, since `env` has name/types for all declarations! 48 | import Remote.implicits._ 49 | 50 | val fac = Remote.ref[Int => Int]("fac") 51 | val gcd = Remote.ref[(Int,Int) => Int]("gcd") 52 | val sum = Remote.ref[List[Int] => Int]("sum") 53 | 54 | // And actual client code uses normal looking function calls 55 | val ar = fac(9) 56 | val ar1 = gcd(1, 2) 57 | val ar3 = sum(List(0,1,2,3,4)) 58 | val ar2: Remote[Int] = ar 59 | val r: Remote[Int] = ar3 60 | } 61 | 62 | object SimpleMain extends App { 63 | import Simple.{env,addr,sum} 64 | import Remote.implicits._ 65 | 66 | println(env) 67 | 68 | // create a server for this environment 69 | val server = env.serve(addr, monitoring = Monitoring.consoleLogger("[server]")).run 70 | 71 | val transport = NettyTransport.single(addr).run 72 | val expr: Remote[Int] = sum(List(0,1,2,3,4)) 73 | val loc: Endpoint = Endpoint.single(transport) 74 | val result: Task[Int] = expr.runWithContext(loc, Response.Context.empty, Monitoring.consoleLogger("[client]")) 75 | 76 | // running a couple times just to see the latency improve for subsequent reqs 77 | try println { result.run; result.run; result.run } 78 | finally { 79 | transport.shutdown.run 80 | server.run 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /core/src/main/scala/GenClient.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.language.experimental.macros 21 | import scala.annotation.StaticAnnotation 22 | 23 | /** 24 | * Macro annotation that generates a client. Usage: 25 | * `@GenClient(remotely.Protocol.empty) object MyClient` 26 | */ 27 | class GenClient(sigs: Signatures) extends StaticAnnotation { 28 | def macroTransform(annottees: Any*): Any = macro GenClient.impl 29 | } 30 | 31 | object GenClient extends MacrosCompatibility { 32 | def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { 33 | import c.universe._ 34 | import Flag._ 35 | 36 | // Pull out the Signatures expression from the macro annotation 37 | // and evaluate it at compile-time. 38 | val s: Signatures = c.prefix.tree match { 39 | case q"new $name($sig)" => 40 | c.eval(c.Expr[Signatures](resetLocalAttrs(c)(q"{import remotely.codecs._; import remotely.Field; import remotely.Type; $sig}"))) 41 | case _ => c.abort(c.enclosingPosition, "GenClient must be used as an annotation.") 42 | } 43 | 44 | // Generate the val-defs that get inserted into the object declaration 45 | val signatures : Set[Tree] = s.signatures.map { sig => 46 | c.parse(s"""val ${sig.name} = Remote.ref[${sig.typeString}]("${sig.name}")""") 47 | } 48 | 49 | 50 | // Generate a Set[Signature] of the function signatures we are 51 | // expecting any server to support. This will be baked into the 52 | // generated client as an expectedSignatures val. 53 | val esSet = { 54 | val sigs = s.signatures.map { Gen.liftSignature(c)(_) } 55 | c.Expr[Set[Signature]](q"Set[Signature]( ..${sigs.toList} )") 56 | } 57 | 58 | // Generate the actual client object, with the signature val-defs generated above 59 | val result = annottees.map(_.tree).toList match { 60 | case q"object $name extends ..$parents { ..$body }" :: Nil => 61 | q""" 62 | object $name extends ..$parents { 63 | import remotely.Remote 64 | ..$signatures 65 | ..$body 66 | val expectedSignatures = ${esSet} 67 | } 68 | """ 69 | 70 | case _ => c.abort( 71 | c.enclosingPosition, 72 | "GenClient must annotate an object declaration." 73 | ) 74 | } 75 | c.Expr[Any](result) 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /core/src/test/scala/ProtocolSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import collection.immutable.SortedSet 21 | import org.scalatest.{FlatSpec,Matchers,BeforeAndAfterAll} 22 | import codecs._ 23 | import Response.Context 24 | import transport.netty._ 25 | 26 | class ProtocolSpec extends FlatSpec with Matchers with BeforeAndAfterAll { 27 | val addr = new java.net.InetSocketAddress("localhost", 9002) 28 | val server = new TestServer 29 | val shutdown = server.environment.serve(addr).run 30 | 31 | val endpoint = (NettyTransport.single(addr, monitoring = Monitoring.consoleLogger("ProtocolSpec")) map Endpoint.single).run 32 | 33 | it should "foo" in { 34 | import remotely.Remote.implicits._ 35 | val fact: Int = evaluate(endpoint, Monitoring.consoleLogger())(Client.factorial(10)).apply(Context.empty).run 36 | val lst: List[Int] = Client.foo(9).runWithContext(endpoint, Context.empty).run 37 | } 38 | 39 | override def afterAll(){ 40 | Thread.sleep(500) 41 | shutdown.run 42 | } 43 | } 44 | 45 | trait TestServerBase { 46 | 47 | import Codecs._ 48 | 49 | def environment: Environment = Environment( 50 | Codecs.empty 51 | .codec[Int] 52 | .codec[List[Int]] 53 | .codec[List[Signature]], 54 | populateDeclarations(Values.empty) 55 | ) 56 | 57 | def factorial: Int => Response[Int] 58 | def foo: Int => Response[scala.List[Int]] 59 | def describe: Response[scala.List[Signature]] 60 | 61 | private def populateDeclarations(env: Values): Values = env 62 | .declare("factorial", factorial) 63 | .declare("foo", foo) 64 | .declare("describe", describe) 65 | } 66 | 67 | class TestServer extends TestServerBase { 68 | implicit val intcodec = int32 69 | def factorial: Int => Response[Int] = i => Response.now(i * i) 70 | def foo: Int => Response[List[Int]] = i => { 71 | Response.now(collection.immutable.List.fill(10000)(i)) 72 | } 73 | def describe: Response[scala.List[Signature]] = Response.now(List(Signature("factorial", List(Field("a", "Int")), "Int"), 74 | Signature("foo", List(Field("a", "Int")), "List[Int]"), 75 | Signature("describe", Nil, "List[Signature]"))) 76 | } 77 | 78 | object Client { 79 | val factorial = Remote.ref[Int => Int]("factorial") 80 | val foo = Remote.ref[Int => scala.List[Int]]("foo") 81 | } 82 | -------------------------------------------------------------------------------- /core/src/main/scala/tls/tls.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import javax.net.ssl._ 21 | 22 | /** 23 | * Convenience functions for building up a `javax.net.ssl.SSLContext` needed to 24 | * create the `javax.net.ssl.SSLEngine` used for SSL connections. 25 | */ 26 | package object tls { 27 | 28 | def default = () => SSLContext.getDefault.createSSLEngine 29 | 30 | /** Create an `SSLEngine` provider from an `SSLContext`. */ 31 | def fromContext(ctx: SSLContext) = () => { 32 | ctx.createSSLEngine 33 | } 34 | 35 | /** Modify the given provider to set client mode on the `SSLEngine`. */ 36 | def client(ssl: () => SSLEngine): () => SSLEngine = () => { 37 | val engine = ssl() 38 | engine.setUseClientMode(true) 39 | engine 40 | } 41 | 42 | /** 43 | * Modify the given provider to set server mode on the `SSLEngine`, 44 | * and optionally require authentication of the client. 45 | */ 46 | def server(ssl: () => SSLEngine, authenticateClient: Boolean = false): () => SSLEngine = () => { 47 | val engine = ssl() 48 | engine.setUseClientMode(false) 49 | if (authenticateClient) engine.setNeedClientAuth(true) 50 | // Not positive this will be the same as `engine.setNeedClientAuth(authenticateClient)` 51 | engine 52 | } 53 | 54 | /** Modify the given provider to enable the given cipher suites. */ 55 | def enableCiphers(ciphers: Cipher*)(ssl: () => SSLEngine) = 56 | () => { 57 | val allowed = ciphers.map(_.name).toSet 58 | val engine = ssl() 59 | val supported = engine.getSupportedCipherSuites 60 | val enabled = supported.filter(allowed.contains) 61 | engine.setEnabledCipherSuites(enabled) 62 | engine 63 | } 64 | } 65 | 66 | package tls { 67 | case class Cipher(name: String) 68 | 69 | object ciphers { 70 | val TLS_DHE_RSA_WITH_AES_128_CBC_SHA = Cipher("TLS_DHE_RSA_WITH_AES_128_CBC_SHA") 71 | val SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA = Cipher("SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA") 72 | val TLS_RSA_WITH_AES_128_CBC_SHA = Cipher("TLS_RSA_WITH_AES_128_CBC_SHA") 73 | val SSL_RSA_WITH_3DES_EDE_CBC_SHA = Cipher("SSL_RSA_WITH_3DES_EDE_CBC_SHA") 74 | 75 | // http://stackoverflow.com/questions/2238135/good-list-of-weak-cipher-suites-for-java 76 | val rsa = List( 77 | TLS_DHE_RSA_WITH_AES_128_CBC_SHA, 78 | SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, 79 | TLS_RSA_WITH_AES_128_CBC_SHA, 80 | SSL_RSA_WITH_3DES_EDE_CBC_SHA 81 | ) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /core/src/main/scala/transport/netty/Client.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package transport.netty 20 | 21 | import java.net.InetSocketAddress 22 | import org.apache.commons.pool2.impl.GenericObjectPool 23 | import io.netty.channel.{Channel,ChannelFuture,ChannelHandlerContext,ChannelFutureListener} 24 | import scalaz.stream.Cause 25 | import scalaz.{-\/,\/,\/-} 26 | import scalaz.stream.{async,Process} 27 | import scalaz.concurrent.Task 28 | import scodec.bits.BitVector 29 | 30 | class NettyTransport(val pool: GenericObjectPool[Channel]) extends Handler { 31 | import NettyTransport._ 32 | 33 | def apply(toServer: Process[Task, BitVector]): Process[Task, BitVector] = { 34 | val fromServer = async.unboundedQueue[BitVector](scalaz.concurrent.Strategy.DefaultStrategy) 35 | val openConnection = Task.delay{ 36 | val c = pool.borrowObject() 37 | c.pipeline.addLast("clientDeframe", new ClientDeframedHandler(fromServer)) 38 | c 39 | } 40 | Process.await(openConnection) { c => 41 | val toFrame = toServer.map(Bits(_)) ++ Process.emit(EOS) 42 | val writeBytes: Task[Unit] = toFrame.evalMap(write(c)).run flatMap (_ => Task.delay { 43 | val _ = c.flush 44 | }) 45 | val result = Process.await(writeBytes)(_ => fromServer.dequeue).onHalt { 46 | case Cause.End => 47 | pool.returnObject(c) 48 | Process.Halt(Cause.End) 49 | case cause => 50 | pool.invalidateObject(c) 51 | Process.Halt(cause) 52 | } 53 | result 54 | } 55 | } 56 | 57 | def shutdown: Task[Unit] = Task.delay { 58 | pool.clear() 59 | pool.close() 60 | val _ = pool.getFactory().asInstanceOf[NettyConnectionPool].workerThreadPool.shutdownGracefully() 61 | } 62 | } 63 | 64 | 65 | object NettyTransport { 66 | def evalCF(cf: ChannelFuture): Task[Unit] = Task.async { cb => 67 | val _ = cf.addListener(new ChannelFutureListener { 68 | def operationComplete(cf: ChannelFuture): Unit = 69 | if(cf.isSuccess) cb(\/-(())) else cb(-\/(cf.cause)) 70 | }) 71 | } 72 | 73 | def write(c: Channel)(frame: Framed): Task[Unit] = evalCF(c.writeAndFlush(frame)) 74 | 75 | def single(host: InetSocketAddress, 76 | expectedSigs: Set[Signature] = Set.empty, 77 | workerThreads: Option[Int] = None, 78 | monitoring: Monitoring = Monitoring.empty, 79 | sslParams: Option[SslParameters] = None): Task[NettyTransport] = 80 | 81 | NettyConnectionPool.default(Process.constant(host), expectedSigs, workerThreads, monitoring, sslParams).map(new NettyTransport(_)) 82 | } 83 | -------------------------------------------------------------------------------- /core/src/test/scala/UberSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import org.scalatest.matchers.{Matcher,MatchResult} 21 | import org.scalatest.{FlatSpec,Matchers,BeforeAndAfterAll} 22 | import scalaz.concurrent.Task 23 | import scalaz.stream.Process 24 | import remotely.transport.netty.NettyTransport 25 | import scala.concurrent.duration.DurationInt 26 | import java.util.concurrent.Executors 27 | 28 | class UberSpec extends FlatSpec with Matchers with BeforeAndAfterAll { 29 | behavior of "permutations" 30 | it should "work" in { 31 | val input: Process[Task,Int] = Process.emitAll(Vector(1,2,3)) 32 | val permuted : IndexedSeq[Process[Task,Int]] = Endpoint.permutations(input).runLog.run 33 | permuted.size should be (6) 34 | val all = permuted.map(_.runLog.run).toSet 35 | all should be(Set(IndexedSeq(1,2,3), IndexedSeq(1,3,2), IndexedSeq(2,1,3), IndexedSeq(2,3,1), IndexedSeq(3,1,2), IndexedSeq(3,2,1))) 36 | } 37 | 38 | behavior of "isEmpty" 39 | it should "work" in { 40 | val empty: Process[Task,Int] = Process.halt 41 | Endpoint.isEmpty(empty).run should be (true) 42 | 43 | val notEmpty: Process[Task,Int] = Process.emit(1) 44 | Endpoint.isEmpty(notEmpty).run should be (false) 45 | 46 | val alsoNot: Process[Task,Int] = Process.eval(Task.now(1)) 47 | Endpoint.isEmpty(alsoNot).run should be (false) 48 | } 49 | 50 | behavior of "transpose" 51 | it should "work" in { 52 | val input = IndexedSeq(IndexedSeq("a", "b", "c"),IndexedSeq("q", "w", "e"), IndexedSeq("1", "2", "3")) 53 | val inputStream: Process[Task,Process[Task,String]] = Process.emitAll(input.map(Process.emitAll(_))) 54 | val transposed: IndexedSeq[IndexedSeq[String]] = Endpoint.transpose(inputStream).runLog.run.map(_.runLog.run) 55 | transposed should be (input.transpose) 56 | } 57 | 58 | val addr1 = new java.net.InetSocketAddress("localhost", 9000) 59 | val addr2 = new java.net.InetSocketAddress("localhost", 9009) 60 | 61 | val server1 = new CountServer 62 | val server2 = new CountServer 63 | val shutdown1 = server1.environment.serve(addr1).run 64 | val shutdown2 = server2.environment.serve(addr2).run 65 | 66 | override def afterAll() { 67 | shutdown1.run 68 | shutdown2.run 69 | } 70 | 71 | val endpoint1 = (NettyTransport.single(addr1) map Endpoint.single).run 72 | val endpoint2 = (NettyTransport.single(addr2) map Endpoint.single).run 73 | def endpoints: Process[Nothing,Endpoint] = Process.emitAll(List(endpoint1, endpoint2)) ++ endpoints 74 | val endpointUber = Endpoint.uber(1 second, 10 seconds, 10, endpoints) 75 | 76 | behavior of "uber" 77 | ignore should "work" in { // this seems to hang 78 | import Response.Context 79 | import Remote.implicits._ 80 | import codecs._ 81 | val call = evaluate(endpointUber, Monitoring.empty)(CountClient.ping(1)) 82 | 83 | val i: Int = call.apply(Context.empty).run 84 | val j: Int = call.apply(Context.empty).run 85 | j should be (2) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /core/src/main/scala/Capabilities.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scodec._ 21 | import scodec.bits.BitVector 22 | import scala.util.parsing.combinator._ 23 | 24 | /** 25 | * A Set of capabilities that a server possesses. 26 | * 27 | * this set of capabilities will be transmitted from Server to client 28 | * at the time when a connection is built. 29 | * 30 | * Currently there is only one known capability, which is the 31 | * "Remotely 1.0" capability. 32 | * 33 | * You can see this if you telnet to a running remotely server: 34 | * {{{ 35 | $ telnet localhost 9999 36 | Trying 127.0.0.1... 37 | Connected to localhost. 38 | Escape character is '^]'. 39 | OK: [Remotely 1.0] 40 | * }}} 41 | * 42 | * The OK line spit out by the server contains a comma separated list 43 | * of capabilities between the square brackets. 44 | * 45 | * In the future we might revision our wire format, perhaps because 46 | * we want to also allow for XML serialization of objects. Perhaps we 47 | * want to add STARTTLS support, perhaps we want to add a way to 48 | * fetch documentation for the functions exported by this server, 49 | * someday in the future, a remotely server might respond with: 50 | * 51 | * OK: [Remotely 1.0, STARTTLS, Remotely XML 1.0, Remotely 2.0] 52 | * 53 | * When the client receives this string as part of the connection 54 | * negotiation, it can perhaps adapt its behavior depending on the 55 | * server capabilities, or it could decide not to talk to this 56 | * server. 57 | */ 58 | case class Capabilities(capabilities: Set[String]) 59 | 60 | object Capabilities extends RegexParsers { 61 | 62 | /** 63 | * The Remotely 1.0 capability states that the server will have a 64 | * `describe` method, which can list all the available functions on 65 | * the server, and implies our current serialization format (which 66 | * is likely to change) 67 | */ 68 | val `remotely-1.0` = "Remotely 1.0" 69 | 70 | val required: Set[String] = Set(`remotely-1.0`) 71 | 72 | val default = Capabilities(required) 73 | 74 | val capability: Parser[String] = "[^,\\]]*".r 75 | 76 | val capabilitiesList: Parser[List[String]] = 77 | '[' ~ repsep(capability, ',') ~ ']' ^^ { case _ ~ c ~ _ => c } 78 | 79 | val helloString: Parser[Capabilities] = 80 | "OK: " ~ capabilitiesList ^^ { case _ ~ c => Capabilities(c.toSet) } 81 | 82 | def parseHelloString(str: String): Attempt[Capabilities] = { 83 | val pr = parseAll(helloString, str) 84 | if(pr.successful) Attempt.successful(pr.get) 85 | else Attempt.failure(Err("could not parse greeting from server: " + str)) 86 | } 87 | 88 | val capabilitiesCodec: Codec[Capabilities] = new Codec[Capabilities] { 89 | 90 | def sizeBound = SizeBound.unknown 91 | 92 | override def encode(c: Capabilities): Attempt[BitVector] = { 93 | val s = "OK: " + c.capabilities.mkString("[",",","]") + "\n" 94 | Attempt.successful(BitVector(s.getBytes)) 95 | } 96 | 97 | override def decode(bv: BitVector): Attempt[DecodeResult[Capabilities]] = { 98 | codecs.utf8.decode(bv).flatMap { 99 | case DecodeResult(s, b) ⇒ parseHelloString(s).map(DecodeResult(_, b)) 100 | } 101 | } 102 | 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /docs/src/site/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ site.name }} - {{ page.title }} 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 |
18 |
19 |

Remotely

20 |

Last updated on {{ site.time | date: '%B %d, %Y' }}

21 | 22 | 27 | 28 |
29 | 30 |
    31 | 32 |
  1. Home
  2. 33 | {% if page.section == 'home' %} 34 |
  3. 35 | 39 |
  4. 40 | {% endif %} 41 | 42 |
  5. Manual
  6. 43 | {% if page.section == 'manual' %} 44 |
  7. 45 | 54 |
  8. 55 | {% endif %} 56 | 57 | 58 |
  9. Internals
  10. 59 | {% if page.section == 'internals' %} 60 |
  11. 61 | 65 |
  12. 66 | {% endif %} 67 |
68 | 69 |
70 | 71 |
72 |
73 | {{ content }} 74 |
75 | 80 |
81 | 82 | 88 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /core/src/main/scala/Environment.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.net.InetSocketAddress 21 | import scala.reflect.runtime.universe.TypeTag 22 | import scodec.Codec 23 | import scodec.bits.BitVector 24 | import scalaz.stream.Process 25 | import scalaz.concurrent.{Strategy, Task} 26 | 27 | /** 28 | * A collection of codecs and values, which can be populated 29 | * and then served over RPC. 30 | * 31 | * Example: {{{ 32 | * val env: Environment = Environment.empty 33 | * .codec[Int] 34 | * .codec[List[Int]] 35 | * .populate { _ 36 | * .declareStrict[List[Int] => Int]("sum", _.sum) 37 | * .declare("fac", (n: Int) => Task { (1 to n).product }) 38 | * } 39 | * val stopper = env.serve(new InetSocketAddress("localhost",8080)) 40 | * /// 41 | * stopper() // shutdown the server 42 | * }}} 43 | */ 44 | case class Environment(codecs: Codecs, values: Values) { 45 | 46 | def codec[A](implicit T: TypeTag[A], C: Codec[A]): Environment = 47 | this.copy(codecs = codecs.codec[A]) 48 | 49 | /** 50 | * Modify the values inside this `Environment`, using the given function `f`. 51 | * Example: `Environment.empty.populate { _.declare("x")(Task.now(42)) }`. 52 | */ 53 | def populate(f: Values => Values): Environment = 54 | this.copy(values = f(values)) 55 | 56 | /** Alias for `this.populate(_ => v)`. */ 57 | def values(v: Values): Environment = 58 | this.populate(_ => v) 59 | 60 | private def serverHandler(monitoring: Monitoring): Handler = { bytes => 61 | // we assume the input is a framed stream, and encode the response(s) 62 | // as a framed stream as well 63 | bytes pipe Process.await1[BitVector] /*server.Handler.deframe*/ evalMap { bs => 64 | Server.handle(this)(bs)(monitoring) 65 | } 66 | } 67 | 68 | /** 69 | * start a netty server listening to the given address 70 | * 71 | * @param addr the address to bind to 72 | * @param strategy the strategy used for processing incoming requests 73 | * @param numBossThreads number of boss threads to create. These are 74 | * threads which accept incoming connection requests and assign 75 | * connections to a worker. If unspecified, the default of 2 will be used 76 | * @param numWorkerThreads number of worker threads to create. If 77 | * unspecified the default of 2 * number of cores will be used 78 | * @param capabilities, the capabilities which will be sent to the client upon connection 79 | */ 80 | def serve(addr: InetSocketAddress, 81 | strategy: Strategy = Strategy.DefaultStrategy, 82 | numBossThreads: Option[Int] = None, 83 | numWorkerThreads: Option[Int] = None, 84 | monitoring: Monitoring = Monitoring.empty, 85 | capabilities: Capabilities = Capabilities.default, 86 | sslParams: Option[SslParameters] = None): Task[Task[Unit]] = 87 | transport.netty.NettyServer.start(addr, 88 | serverHandler(monitoring), 89 | strategy, 90 | numBossThreads, 91 | numWorkerThreads, 92 | capabilities, 93 | monitoring, 94 | sslParams) 95 | } 96 | 97 | object Environment { 98 | val empty = Environment(Codecs.empty, Values.empty) 99 | } 100 | -------------------------------------------------------------------------------- /test-server/src/test/scala/DescribeSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package test 20 | 21 | import org.scalatest.matchers.{Matcher,MatchResult} 22 | import org.scalatest.{FlatSpec,Matchers,BeforeAndAfterAll} 23 | import scodec.Decoder 24 | import codecs.list 25 | import transport.netty._ 26 | import scalaz.-\/ 27 | 28 | trait ServerImpl { 29 | def foo = Response.delay(Foo(1)) 30 | def fooId = (foo: Foo) => Response.now(foo) 31 | def foobar = (foo: Foo) => Response.now(Bar(foo.a)) 32 | def bar = Response.delay(Bar(1)) 33 | } 34 | 35 | class DescribeTestOlderServerImpl extends DescribeTestOlderServer with ServerImpl 36 | class DescribeTestNewerServerImpl extends DescribeTestNewerServer with ServerImpl 37 | 38 | class DescribeSpec extends FlatSpec 39 | with Matchers 40 | with BeforeAndAfterAll { 41 | 42 | val addrN = new java.net.InetSocketAddress("localhost", 9006) 43 | val addrO = new java.net.InetSocketAddress("localhost", 9007) 44 | 45 | val serverN = new DescribeTestNewerServerImpl 46 | val serverO = new DescribeTestOlderServerImpl 47 | 48 | val shutdownN = serverN.environment.serve(addrN).run 49 | 50 | val shutdownO = serverO.environment.serve(addrO).run 51 | 52 | val endpointOldToOld = Endpoint.single(NettyTransport.single(addrO, DescribeTestOlderClient.expectedSignatures, monitoring = Monitoring.consoleLogger("OldToOld")).run) 53 | val endpointOldToNew = Endpoint.single(NettyTransport.single(addrN, DescribeTestOlderClient.expectedSignatures, monitoring = Monitoring.consoleLogger("OldToNew")).run) 54 | val endpointNewToOld = Endpoint.single(NettyTransport.single(addrO, DescribeTestNewerClient.expectedSignatures, monitoring = Monitoring.consoleLogger("NewToOld")).run) 55 | val endpointNewToNew = Endpoint.single(NettyTransport.single(addrN, DescribeTestNewerClient.expectedSignatures, monitoring = Monitoring.consoleLogger("NewToNew")).run) 56 | 57 | behavior of "Describe" 58 | 59 | it should "work" in { 60 | val desc = evaluate[List[Signature]](endpointNewToNew, Monitoring.consoleLogger())(DescribeTestNewerClient.describe).apply(Response.Context.empty).run 61 | desc should contain (Signature("foo", Nil, "remotely.test.Foo")) 62 | desc should contain (Signature("fooId", List(Field("in", "remotely.test.Foo")), "remotely.test.Foo")) 63 | desc should contain (Signature("foobar", List(Field("in", "remotely.test.Foo")), "remotely.test.Bar")) 64 | desc should contain (Signature("describe", Nil, "List[remotely.Signature]")) 65 | } 66 | 67 | behavior of "Client" 68 | 69 | it should "connect older to newer" in { 70 | val desc = evaluate(endpointOldToNew, Monitoring.consoleLogger())(DescribeTestOlderClient.describe).apply(Response.Context.empty).run 71 | desc should contain (Signature("foo", Nil, "remotely.test.Foo")) 72 | } 73 | 74 | it should "connect newer to newer" in { 75 | val desc = evaluate(endpointNewToNew, Monitoring.consoleLogger())(DescribeTestNewerClient.describe).apply(Response.Context.empty).run 76 | desc should contain (Signature("foo", Nil, "remotely.test.Foo")) 77 | } 78 | 79 | it should "not connect newer to older" in { 80 | val desc = evaluate(endpointNewToOld, Monitoring.consoleLogger())(DescribeTestNewerClient.describe).apply(Response.Context.empty).attemptRun 81 | desc match { 82 | case -\/(e) => e shouldBe a [IncompatibleServer] 83 | case e => withClue("newer client should have rejected older server")(fail()) 84 | } 85 | } 86 | 87 | override def afterAll() { 88 | shutdownN.run 89 | shutdownO.run 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/scala/Signatures.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.reflect.runtime.universe.TypeTag 21 | import codecs._ 22 | import scodec.Codec 23 | 24 | // TODO: ctor should really be private[remotely], but due 25 | // to macro code generation issue in `liftField` (`Gen.scala`) 26 | // we open it for the time being. See Github issue #89. 27 | case class Field[+A](name: String, typeString: String) 28 | object Field { 29 | def strict[A:TypeTag](name: String) = Field[A](name, Remote.toTag[A]) 30 | } 31 | case class Type[+A]private[remotely](name: String) 32 | object Type { 33 | def apply[A:TypeTag]: Type[A] = Type[A](Remote.toTag[A]) 34 | } 35 | case class Signature(name: String, params: List[Field[Any]], outType: String) { 36 | /** returns a string in the form "Type, Type => Response[Type]", 37 | * wrapping Response around the return type def 38 | */ 39 | def wrapResponse: String = 40 | s"${lhsWithArrow}Response[${outType}]" 41 | 42 | private def lhs = params match { 43 | case param :: Nil => param.typeString 44 | case _ => params.map(_.typeString).mkString("(", ",", ")") 45 | } 46 | 47 | private def lhsWithArrow = if (params.isEmpty) "" else s"$lhs => " 48 | 49 | def typeString = lhsWithArrow + outType 50 | 51 | def tag = { 52 | s"$name: $typeString" 53 | } 54 | 55 | } 56 | 57 | object Signature { 58 | // What is the difference between ~ and ~~ ? 59 | implicit val fieldCodec: Codec[Field[Any]] = (utf8 ~~ utf8).widenAs[Field[Any]](Field.apply, Field.unapply) 60 | implicit val signatureCodec: Codec[Signature] = (utf8 ~~ list(fieldCodec) ~~ utf8).widenAs[Signature](Signature.apply, Signature.unapply) 61 | } 62 | 63 | case class Signatures(signatures: Set[Signature]) { 64 | 65 | def specify(name: String, in: List[Field[Any]], outType: String) = 66 | Signatures(signatures + Signature(name, in, outType)) 67 | 68 | def specify0(name: String, out: String): Signatures = 69 | specify(name, Nil, out) 70 | 71 | def specify1(name: String, in: Field[Any], out: String): Signatures = 72 | specify(name, List(in), out) 73 | 74 | def specify2(name: String, in1: Field[Any], in2: Field[Any], out: String): Signatures = 75 | specify(name, List(in1, in2), out) 76 | 77 | def specify3(name: String, in1: Field[Any], in2: Field[Any], in3: Field[Any], out: String): Signatures = 78 | specify(name, List(in1, in2, in3), out) 79 | 80 | def specify4(name: String, in1: Field[Any], in2: Field[Any], in3: Field[Any], in4: Field[Any], out: String): Signatures = 81 | specify(name, List(in1, in2, in3, in4), out) 82 | 83 | def specify5(name: String, in1: Field[Any], in2: Field[Any], in3: Field[Any], in4: Field[Any], in5: Field[Any], out: String): Signatures = 84 | specify(name, List(in1, in2, in3, in4, in5), out) 85 | 86 | def pretty: String = "Signatures.empty\n" + 87 | signatures.map(s => 88 | s""" .specify[${s.typeString}]("${s.name}")""" 89 | ).mkString("\n") 90 | 91 | } 92 | 93 | object Signatures { 94 | val Arrow = "(.*)=>(.*)".r 95 | private[remotely] def wrapResponse(typename: String): String = typename match { 96 | case Arrow(l,r) => s"$l=> Response[${r.trim}]" 97 | case _ => s"Response[$typename]" 98 | } 99 | 100 | val empty = Signatures(Set(Signature("describe", List(), "List[remotely.Signature]"))) 101 | 102 | private[remotely] def indent(by: String)(s: String): String = 103 | by + s.replace("\n", "\n" + by) 104 | } 105 | -------------------------------------------------------------------------------- /core/src/main/scala/Protocol.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.reflect.runtime.universe.TypeTag 21 | import scodec.Codec 22 | 23 | case class Protocol(codecs: Codecs, signatures: Signatures) { 24 | 25 | def codec[A:TypeTag:Codec]: Protocol = 26 | this.copy(codecs = codecs.codec[A]) 27 | 28 | def specify0[O](name: String, out: Type[O]): Protocol = 29 | this.copy(signatures = signatures.specify0(name, out.name)) 30 | 31 | def specify1[A,O](name: String, in: Field[A], out: Type[O]): Protocol = 32 | this.copy(signatures = signatures.specify1(name, in, out.name)) 33 | 34 | def specify2[A,B,O](name: String, in1: Field[A], in2: Field[B], out: Type[O]): Protocol = 35 | this.copy(signatures = signatures.specify2(name, in1, in2, out.name)) 36 | 37 | def specify3[A,B,C,O](name: String, in1: Field[A], in2: Field[B], in3: Field[C], out: Type[O]): Protocol = 38 | this.copy(signatures = signatures.specify3(name, in1, in2, in3, out.name)) 39 | 40 | def specify4[A,B,C,D,O](name: String, in1: Field[A], in2: Field[B], in3: Field[C], in4: Field[D], out: Type[O]): Protocol = 41 | this.copy(signatures = signatures.specify4(name, in1, in2, in3, in4, out.name)) 42 | 43 | def specify5[A,B,C,D,E,O](name: String, in1: Field[A], in2: Field[B], in3: Field[C], in4: Field[D], in5: Field[E], out: Type[O]): Protocol = 44 | this.copy(signatures = signatures.specify5(name, in1, in2, in3, in4, in5, out.name)) 45 | 46 | def pretty: String = 47 | "Protocol(\n" + 48 | Signatures.indent(" ")(codecs.pretty) + ",\n" + 49 | Signatures.indent(" ")(signatures.pretty) + "\n)" 50 | 51 | override def toString = pretty 52 | } 53 | 54 | object Protocol { 55 | 56 | val empty = Protocol(Codecs.empty, Signatures.empty) 57 | 58 | /// Convenience Syntax 59 | 60 | implicit class ProtocolOps(inner: Protocol) { 61 | 62 | import Field._ 63 | 64 | def ++(other: Protocol): Protocol = Protocol( 65 | codecs = inner.codecs ++ other.codecs, 66 | signatures = Signatures(inner.signatures.signatures ++ other.signatures.signatures) 67 | ) 68 | 69 | def define0[O: TypeTag](name: String): Protocol = 70 | inner.specify0(name, Type[O]) 71 | 72 | def define1[A: TypeTag, O: TypeTag](name: String): Protocol = { 73 | 74 | val f1 = strict[A]("in1") 75 | 76 | inner.specify1(name, f1, Type[O]) 77 | } 78 | 79 | def define2[A: TypeTag, B: TypeTag, O: TypeTag](name: String): Protocol = { 80 | 81 | val f1 = strict[A]("in1") 82 | val f2 = strict[B]("in2") 83 | 84 | inner.specify2(name, f1, f2, Type[O]) 85 | } 86 | 87 | def define3[A: TypeTag, B: TypeTag, C: TypeTag, O: TypeTag](name: String): Protocol = { 88 | 89 | val f1 = strict[A]("in1") 90 | val f2 = strict[B]("in2") 91 | val f3 = strict[C]("in3") 92 | 93 | inner.specify3(name, f1, f2, f3, Type[O]) 94 | } 95 | 96 | def define4[A: TypeTag, B: TypeTag, C: TypeTag, D: TypeTag, O: TypeTag](name: String): Protocol = { 97 | 98 | val f1 = strict[A]("in1") 99 | val f2 = strict[B]("in2") 100 | val f3 = strict[C]("in3") 101 | val f4 = strict[D]("in4") 102 | 103 | inner.specify4(name, f1, f2, f3, f4, Type[O]) 104 | } 105 | 106 | def define5[A: TypeTag, B: TypeTag, C: TypeTag, D: TypeTag, E: TypeTag, O: TypeTag](name: String): Protocol = { 107 | 108 | val f1 = strict[A]("in1") 109 | val f2 = strict[B]("in2") 110 | val f3 = strict[C]("in3") 111 | val f4 = strict[D]("in4") 112 | val f5 = strict[E]("in5") 113 | 114 | inner.specify5(name, f1, f2, f3, f4, f5, Type[O]) 115 | } 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /core/src/main/scala/package.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | 19 | package object remotely { 20 | import java.util.concurrent.{ Executors, ExecutorService, ThreadFactory } 21 | import java.util.concurrent.atomic.AtomicInteger 22 | import scala.concurrent.duration._ 23 | import scala.reflect.runtime.universe.TypeTag 24 | import scalaz.stream.Process 25 | import scalaz.concurrent.Task 26 | import scalaz.\/.{left,right} 27 | import scalaz.Monoid 28 | import scodec.bits.{BitVector,ByteVector} 29 | import scodec.interop.scalaz._ 30 | // import scodec.Decoder 31 | import utils._ 32 | 33 | /** 34 | * Represents the logic of a connection handler, a function 35 | * from a stream of bytes to a stream of bytes, which will 36 | * be sent back to the client. The connection will be closed 37 | * when the returned stream terminates. 38 | * 39 | * NB: This type cannot represent certain kinds of 'coroutine' 40 | * client/server interactions, where the server awaits a response 41 | * to a particular packet sent before continuing. 42 | */ 43 | type Handler = Process[Task,BitVector] => Process[Task,BitVector] 44 | 45 | /** 46 | * Evaluate the given remote expression at the 47 | * specified endpoint, and get back the result. 48 | * This function is completely pure - no network 49 | * activity occurs until the returned `Response` is 50 | * run. 51 | * 52 | * The `Monitoring` instance is notified of each request. 53 | */ 54 | def evaluate[A:scodec.Codec:TypeTag](e: Endpoint, M: Monitoring = Monitoring.empty)(r: Remote[A]): Response[A] = 55 | Response.scope { Response { ctx => // push a fresh ID onto the call stack 56 | val refs = Remote.refs(r) 57 | 58 | def reportErrors[R](startNanos: Long)(t: Task[R]): Task[R] = 59 | t.onFinish { 60 | case Some(e) => Task.delay { 61 | M.handled(ctx, r, refs, left(e), Duration.fromNanos(System.nanoTime - startNanos)) 62 | } 63 | case None => Task.now(()) 64 | } 65 | 66 | Task.delay { System.nanoTime } flatMap { start => 67 | for { 68 | conn <- e.get 69 | reqBits <- codecs.encodeRequest(r, ctx).toTask 70 | respBytes <- reportErrors(start) { 71 | val reqBytestream = Process.emit(reqBits) 72 | val bytes = fullyRead(conn(reqBytestream)) 73 | bytes 74 | } 75 | resp <- { 76 | reportErrors(start) { codecs.responseDecoder[A].complete.decode(respBytes).map(_.value).toTask } 77 | } 78 | result <- resp.fold( 79 | { e => 80 | val ex = ServerException(e) 81 | val delta = System.nanoTime - start 82 | M.handled(ctx, r, Remote.refs(r), left(ex), Duration.fromNanos(delta)) 83 | Task.fail(ex) 84 | }, 85 | { a => 86 | val delta = System.nanoTime - start 87 | M.handled(ctx, r, refs, right(a), Duration.fromNanos(delta)) 88 | Task.now(a) 89 | } 90 | ) 91 | } yield result 92 | } 93 | }} 94 | 95 | private[remotely] def fullyRead(s: Process[Task,BitVector]): Task[BitVector] = s.runFoldMap(x => x) 96 | 97 | private[remotely] def fixedNamedThreadPool(name: String): ExecutorService = 98 | Executors.newFixedThreadPool(Runtime.getRuntime.availableProcessors, namedThreadFactory(name)) 99 | 100 | private[remotely] def namedThreadFactory(name: String): ThreadFactory = new ThreadFactory { 101 | val num = new AtomicInteger(1) 102 | def newThread(runnable: Runnable) = { 103 | val t = new Thread(runnable, s"$name - ${num.getAndIncrement}") 104 | t.setDaemon(true) 105 | t 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /core/src/main/scala/GenServer.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.language.experimental.macros 21 | import scala.annotation.StaticAnnotation 22 | 23 | /** 24 | * Macro annotation that generates a server interface. Usage: 25 | * `@GenServer(remotely.Protocol.empty) abstract class MyServer` 26 | */ 27 | class GenServer(p: Protocol) extends StaticAnnotation { 28 | def macroTransform(annottees: Any*): Any = macro GenServer.impl 29 | } 30 | 31 | object GenServer extends MacrosCompatibility { 32 | // Turns a `String` into a `TypeTree`. 33 | def parseType(c: Context)(s: String): c.universe.Tree = { 34 | import c.universe._ 35 | val q"type T = $t" = c.parse(s"type T = $s") 36 | t 37 | } 38 | 39 | def impl(c: Context)(annottees: c.Expr[Any]*): c.Expr[Any] = { 40 | import c.universe._ 41 | import Flag._ 42 | 43 | // Pull out the Protocol expression from the macro annotation 44 | // and evaluate it at compile-time. 45 | val p:Protocol = c.prefix.tree match { 46 | case q"new $name($protocol)" => 47 | c.eval(c.Expr[Protocol](resetLocalAttrs(c)(q"{import remotely.codecs._; import remotely.Field; import remotely.Type; $protocol}"))) 48 | case _ => c.abort(c.enclosingPosition, "GenServer must be used as an annotation.") 49 | } 50 | 51 | // Generates a method signature based on a method name and a `TypeTree`. 52 | def genSig(name: String, typ: c.Tree) = 53 | DefDef(Modifiers(DEFERRED), createTermName(c)(name), 54 | List(), List(), 55 | typ, EmptyTree) 56 | 57 | // Creates name/type pairs from the signatures in the protocol. 58 | val signatures = p.signatures.signatures.map { s => 59 | val typ = parseType(c)(Signatures.wrapResponse(s.typeString)) 60 | (s.name, typ) 61 | } 62 | 63 | // Generates the method defs for the generated class. 64 | val sigDefs = signatures.collect { case (n,t) if n != "describe" => 65 | genSig(n, t) 66 | } 67 | 68 | // Generates an expression that evaluates to the codecs specified by the protocol. 69 | val codecs = 70 | q"""${ p.codecs.keySet.toList.sorted.foldLeft(q"Codecs.empty":c.Tree)((dec, d) => 71 | q"$dec.codec[${parseType(c)(d)}]" 72 | )}""" 73 | 74 | // Generate the actual abstract class with the environment and method defs generated above. 75 | val result = { 76 | annottees.map(_.tree).toList match { 77 | case q"abstract class $name extends ..$parents { ..$body }" :: Nil => 78 | q""" 79 | abstract class $name extends ..$parents { 80 | import remotely.{Codecs,Environment,Response,Values} 81 | import remotely.codecs._ 82 | 83 | def environment: Environment = Environment( 84 | $codecs, 85 | populateDeclarations(Values.empty) 86 | ) 87 | 88 | ..$sigDefs 89 | 90 | def describe: Response[List[Signature]] = 91 | Response.delay(${p.signatures.signatures.foldLeft[Tree](q"List.empty[Signature]")((e,s) => q"$e.::(${Gen.liftSignature(c)(s)})")}) 92 | 93 | 94 | private def populateDeclarations(env: Values): Values = 95 | ${ signatures.foldLeft(q"env":c.Tree)((e,s) => 96 | q"$e.declare(${Literal(Constant(s._1))},${Ident(createTermName(c)(s._1))})" 97 | )} 98 | 99 | ..$body 100 | } 101 | """ 102 | 103 | case _ => c.abort( 104 | c.enclosingPosition, 105 | "GenServer must annotate an abstract class declaration." 106 | ) 107 | } 108 | } 109 | c.Expr[Any](result) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /docs/src/site/css/styles.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato:300italic,700italic,300,700); 2 | 3 | body { 4 | padding:50px; 5 | font:14px/1.5 Lato, "Helvetica Neue", Helvetica, Arial, sans-serif; 6 | color:#777; 7 | font-weight:300; 8 | } 9 | 10 | h1, h2, h3, h4, h5, h6 { 11 | color:#222; 12 | margin:0 0 20px; 13 | } 14 | 15 | p, ul, ol, table, pre, dl { 16 | margin:0 0 20px; 17 | } 18 | 19 | h1, h2, h3 { 20 | line-height:1.1; 21 | } 22 | 23 | h1 { 24 | font-size:28px; 25 | } 26 | 27 | h2 { 28 | color:#393939; 29 | } 30 | 31 | h3, h4, h5, h6 { 32 | color:#494949; 33 | } 34 | 35 | a { 36 | color:#39c; 37 | font-weight:400; 38 | text-decoration:none; 39 | } 40 | 41 | a small { 42 | font-size:11px; 43 | color:#777; 44 | margin-top:-0.6em; 45 | display:block; 46 | } 47 | 48 | .wrapper { 49 | width:860px; 50 | margin:0 auto; 51 | } 52 | 53 | blockquote { 54 | border-left:1px solid #e5e5e5; 55 | margin:0; 56 | padding:0 0 0 20px; 57 | font-style:italic; 58 | } 59 | 60 | code, pre { 61 | font-family:Monaco, Bitstream Vera Sans Mono, Lucida Console, Terminal; 62 | color:#333; 63 | font-size:12px; 64 | } 65 | 66 | pre { 67 | padding:8px 15px; 68 | background: #f8f8f8; 69 | border-radius:5px; 70 | border:1px solid #e5e5e5; 71 | overflow-x: auto; 72 | } 73 | 74 | table { 75 | width:100%; 76 | border-collapse:collapse; 77 | } 78 | 79 | th, td { 80 | text-align:left; 81 | padding:5px 10px; 82 | border-bottom:1px solid #e5e5e5; 83 | } 84 | 85 | dt { 86 | color:#444; 87 | font-weight:700; 88 | } 89 | 90 | th { 91 | color:#444; 92 | } 93 | 94 | img { 95 | max-width:100%; 96 | } 97 | 98 | header { 99 | width:270px; 100 | float:left; 101 | position:fixed; 102 | } 103 | 104 | header ul { 105 | list-style:none; 106 | height:40px; 107 | 108 | padding:0; 109 | 110 | background: #eee; 111 | background: -moz-linear-gradient(top, #f8f8f8 0%, #dddddd 100%); 112 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f8f8f8), color-stop(100%,#dddddd)); 113 | background: -webkit-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 114 | background: -o-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 115 | background: -ms-linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 116 | background: linear-gradient(top, #f8f8f8 0%,#dddddd 100%); 117 | 118 | border-radius:5px; 119 | border:1px solid #d2d2d2; 120 | box-shadow:inset #fff 0 1px 0, inset rgba(0,0,0,0.03) 0 -1px 0; 121 | width:270px; 122 | } 123 | 124 | header li { 125 | width:89px; 126 | float:left; 127 | border-right:1px solid #d2d2d2; 128 | height:40px; 129 | } 130 | 131 | header ul a { 132 | line-height:1; 133 | font-size:11px; 134 | color:#999; 135 | display:block; 136 | text-align:center; 137 | padding-top:6px; 138 | height:40px; 139 | } 140 | 141 | strong { 142 | color:#222; 143 | font-weight:700; 144 | } 145 | 146 | header ul li + li { 147 | width:88px; 148 | border-left:1px solid #fff; 149 | } 150 | 151 | header ul li + li + li { 152 | border-right:none; 153 | width:89px; 154 | } 155 | 156 | header ul a strong { 157 | font-size:14px; 158 | display:block; 159 | color:#222; 160 | } 161 | 162 | section { 163 | width:500px; 164 | float:right; 165 | padding-bottom:50px; 166 | } 167 | 168 | small { 169 | font-size:11px; 170 | } 171 | 172 | hr { 173 | border:0; 174 | background:#e5e5e5; 175 | height:1px; 176 | margin:0 0 20px; 177 | } 178 | 179 | footer { 180 | width:270px; 181 | float:left; 182 | position:fixed; 183 | bottom:50px; 184 | } 185 | 186 | @media print, screen and (max-width: 960px) { 187 | 188 | div.wrapper { 189 | width:auto; 190 | margin:0; 191 | } 192 | 193 | header, section, footer { 194 | float:none; 195 | position:static; 196 | width:auto; 197 | } 198 | 199 | header { 200 | padding-right:320px; 201 | } 202 | 203 | section { 204 | border:1px solid #e5e5e5; 205 | border-width:1px 0; 206 | padding:20px 0; 207 | margin:0 0 20px; 208 | } 209 | 210 | header a small { 211 | display:inline; 212 | } 213 | 214 | header ul { 215 | position:absolute; 216 | right:50px; 217 | top:52px; 218 | } 219 | } 220 | 221 | @media print, screen and (max-width: 720px) { 222 | body { 223 | word-wrap:break-word; 224 | } 225 | 226 | header { 227 | padding:0; 228 | } 229 | 230 | header ul, header p.view { 231 | position:static; 232 | } 233 | 234 | pre, code { 235 | word-wrap:normal; 236 | } 237 | } 238 | 239 | @media print, screen and (max-width: 480px) { 240 | body { 241 | padding:15px; 242 | } 243 | 244 | header ul { 245 | display:none; 246 | } 247 | } 248 | 249 | @media print { 250 | body { 251 | padding:0.4in; 252 | font-size:12pt; 253 | color:#444; 254 | } 255 | } -------------------------------------------------------------------------------- /core/src/main/scala/Values.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.reflect.runtime.universe.TypeTag 21 | import scalaz.Monad 22 | 23 | trait Value { 24 | def apply(args: Any*): Response[Any] 25 | } 26 | 27 | object Value { 28 | def fromValue(a: Any) = new Value { 29 | def apply(args: Any*) = args.length match { 30 | case 0 => a.asInstanceOf[Response[Any]] 31 | case 1 => a.asInstanceOf[Any => Response[Any]](args(0)) 32 | case 2 => a.asInstanceOf[(Any,Any) => Response[Any]](args(0), args(1)) 33 | case 3 => a.asInstanceOf[(Any,Any,Any) => Response[Any]](args(0), args(1), args(2)) 34 | case 4 => a.asInstanceOf[(Any, Any, Any, Any) => Response[Any]](args(0), args(1), args(2), args(3)) 35 | case 5 => a.asInstanceOf[(Any, Any, Any, Any, Any) => Response[Any]](args(0), args(1), args(2), args(3), args(4)) 36 | case 6 => a.asInstanceOf[(Any, Any, Any, Any, Any, Any) => Response[Any]](args(0), args(1), args(2), args(3), args(4), args(5)) 37 | case n => Response.fail(new Exception("functions of arity " + n + " not supported")) 38 | } 39 | } 40 | } 41 | 42 | case class Values(values: Map[String,Value]) { 43 | 44 | /** 45 | * Declare the value for the given name in this `Environment`, 46 | * or throw an error if the type-qualified name is already bound. 47 | */ 48 | // def declareStrict[A:TypeTag](name: String, a: A): Values = { 49 | // val tag = Remote.nameToTag[A](name) 50 | // if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 51 | // else this.copy(values = values + (tag -> Value.fromValue(Response.now(a)))) 52 | // } 53 | 54 | def declare[A:TypeTag](name: String, a: Response[A]): Values = { 55 | val tag = Remote.nameToTag[A](name) 56 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 57 | else this.copy(values = values + (tag -> Value.fromValue(a))) 58 | } 59 | 60 | def declare[A:TypeTag,B:TypeTag](name: String, f: A => Response[B]): Values = { 61 | val tag = Remote.nameToTag[A => B](name) 62 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 63 | else this.copy(values = values + (tag -> Value.fromValue(f))) 64 | } 65 | 66 | def declare[A:TypeTag,B:TypeTag,C:TypeTag](name: String, f: (A,B) => Response[C]): Values = { 67 | val tag = Remote.nameToTag[(A,B) => C](name) 68 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 69 | else this.copy(values = values + (tag -> Value.fromValue(f))) 70 | } 71 | 72 | def declare[A:TypeTag,B:TypeTag,C:TypeTag,D:TypeTag](name: String, f: (A,B,C) => Response[D]): Values = { 73 | val tag = Remote.nameToTag[(A,B,C) => D](name) 74 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 75 | else this.copy(values = values + (tag -> Value.fromValue(f))) 76 | } 77 | 78 | def declare[A:TypeTag,B:TypeTag,C:TypeTag,D:TypeTag,E:TypeTag](name: String, f: (A,B,C,D) => Response[E]): Values = { 79 | val tag = Remote.nameToTag[(A,B,C,D) => E](name) 80 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 81 | else this.copy(values = values + (tag -> Value.fromValue(f))) 82 | } 83 | 84 | def declare[A:TypeTag,B:TypeTag,C:TypeTag,D:TypeTag,E:TypeTag,F:TypeTag](name: String, f: (A,B,C,D,E) => Response[F]): Values = { 85 | val tag = Remote.nameToTag[(A,B,C,D,E) => F](name) 86 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 87 | else this.copy(values = values + (tag -> Value.fromValue(f))) 88 | } 89 | 90 | def declare[A:TypeTag,B:TypeTag,C:TypeTag,D:TypeTag,E:TypeTag,F:TypeTag,G:TypeTag](name: String, f: (A,B,C,D,E,F) => Response[G]): Values = { 91 | val tag = Remote.nameToTag[(A,B,C,D,E,F) => G](name) 92 | if (values.contains(tag)) sys.error("Environment already has declaration for: "+tag) 93 | else this.copy(values = values + (tag -> Value.fromValue(f))) 94 | } 95 | 96 | def keySet = values.keySet 97 | } 98 | 99 | object Values { 100 | 101 | val empty = Values(Map()) 102 | } 103 | -------------------------------------------------------------------------------- /core/src/main/scala/Server.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.net.InetSocketAddress 21 | import scala.concurrent.duration._ 22 | import scalaz.{\/,Monad} 23 | import scalaz.\/.{right,left} 24 | import scalaz.stream.{merge,nio,Process} 25 | import scalaz.concurrent.Task 26 | import remotely.Response.Context 27 | import scodec.bits.{BitVector,ByteVector} 28 | import scodec.{Attempt, DecodeResult, Encoder, Err} 29 | import scodec.Attempt.Successful 30 | import scodec.interop.scalaz._ 31 | 32 | object Server { 33 | 34 | /** 35 | * Handle a single RPC request, given a decoding 36 | * environment and a values environment. This `Task` is 37 | * guaranteed not to fail - failures will be encoded as 38 | * responses, to be sent back to the client, and one can 39 | * use the `monitoring` argument if you wish to observe 40 | * these failures. 41 | */ 42 | def handle(env: Environment)(request: BitVector)(monitoring: Monitoring): Task[BitVector] = { 43 | 44 | Task.delay(System.nanoTime).flatMap { startNanos => Task.suspend { 45 | // decode the request from the environment 46 | 47 | val DecodeResult((respEncoder,ctx,r), trailing) = 48 | codecs.requestDecoder(env).decode(request). 49 | fold(e => throw new Error(e.messageWithContext), identity) 50 | 51 | val expected = Remote.refs(r) 52 | val unknown = (expected -- env.values.keySet).toList 53 | if (unknown.nonEmpty) { // fail fast if the Environment doesn't know about some referenced values 54 | val missing = unknown.mkString("\n") 55 | fail(s"[validation] server values: <" + env.values.keySet + s"> does not have referenced values:\n $missing") 56 | } else if (trailing.nonEmpty) // also fail fast if the request has trailing bits (usually a codec error) 57 | fail(s"[validation] trailing bytes in request: ${trailing.toByteVector}") 58 | else // we are good to try executing the request 59 | eval(env.values)(r)(ctx).flatMap { 60 | a => 61 | val deltaNanos = System.nanoTime - startNanos 62 | val delta = Duration.fromNanos(deltaNanos) 63 | val result = right(a) 64 | monitoring.handled(ctx, r, expected, result, delta) 65 | toTask(codecs.responseEncoder(respEncoder).encode(Successful(a))) 66 | }.attempt.flatMap { 67 | // this is a little convoluted - we catch this exception just so 68 | // we can log the failure using `monitoring`, then reraise it 69 | _.fold( 70 | e => { 71 | val deltaNanos = System.nanoTime - startNanos 72 | val delta = Duration.fromNanos(deltaNanos) 73 | monitoring.handled(ctx, r, expected, left(e), delta) 74 | Task.fail(e) 75 | }, 76 | bits => Task.now(bits) 77 | ) 78 | } 79 | }}.attempt.flatMap { _.fold( 80 | e => toTask(codecs.responseEncoder(codecs.utf8).encode(Attempt.failure(Err(formatThrowable(e))))), 81 | bits => Task.now(bits) 82 | )} 83 | } 84 | 85 | val P = Process 86 | 87 | /** Evaluate a remote expression, using the given (untyped) environment. */ 88 | def eval[A](env: Values)(r: Remote[A]): Response[A] = { 89 | import Remote._ 90 | r match { 91 | case Local(a,_,_) => Response.now(a) 92 | case Ref(name) => env.values.lift(name) match { 93 | case None => Response.delay { sys.error("Unknown name on server: " + name) } 94 | case Some(a) => a().asInstanceOf[Response[A]] 95 | } 96 | // on the server, only concern ourselves w/ tree of fully saturated calls 97 | case Ap1(Ref(f),a) => eval(env)(a).flatMap(env.values(f)(_)) .asInstanceOf[Response[A]] 98 | case Ap2(Ref(f),a,b) => Monad[Response].tuple2(eval(env)(a), eval(env)(b)).flatMap{case (a,b) => env.values(f)(a,b)} .asInstanceOf[Response[A]] 99 | case Ap3(Ref(f),a,b,c) => Monad[Response].tuple3(eval(env)(a), eval(env)(b), eval(env)(c)).flatMap{case (a,b,c) => env.values(f)(a,b,c)} .asInstanceOf[Response[A]] 100 | case Ap4(Ref(f),a,b,c,d) => Monad[Response].tuple4(eval(env)(a), eval(env)(b), eval(env)(c), eval(env)(d)).flatMap{case (a,b,c,d) => env.values(f)(a,b,c,d)} .asInstanceOf[Response[A]] 101 | case Ap5(Ref(f),a,b,c,d,e) => Monad[Response].tuple5(eval(env)(a), eval(env)(b), eval(env)(c), eval(env)(d), eval(env)(e)).flatMap{case (a,b,c,d,e) => env.values(f)(a,b,c,d,e)} .asInstanceOf[Response[A]] 102 | case _ => Response.delay { sys.error("unable to interpret remote expression of form: " + r) } 103 | } 104 | } 105 | 106 | private def toTask[A](att: Attempt[A]): Task[A] = 107 | att.fold(e => Task.fail(new Error(e.messageWithContext)), 108 | a => Task.now(a)) 109 | 110 | def fail(msg: String): Task[Nothing] = Task.fail(new Error(msg)) 111 | 112 | class Error(msg: String) extends Exception(msg) 113 | 114 | def formatThrowable(err: Throwable): String = 115 | err.getMessage + "\n stack trace:\n" + err.getStackTrace.mkString("\n") 116 | } 117 | -------------------------------------------------------------------------------- /core/src/main/scala/Monitoring.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.net.InetSocketAddress 21 | import java.net.SocketAddress 22 | import scala.concurrent.duration._ 23 | import scalaz.\/ 24 | 25 | /** 26 | * A collection of callbacks that can be passed to `[[remotely.Environment#serve]]` 27 | * to gather statistics about a running RPC server. 28 | */ 29 | trait Monitoring { self => 30 | 31 | /** 32 | * Invoked with the request, the request context, 33 | * the set of names referenced by that request, 34 | * the result, and how long it took. 35 | */ 36 | def handled[A](ctx: Response.Context, 37 | req: Remote[A], 38 | references: Iterable[String], 39 | result: Throwable \/ A, 40 | took: Duration): Unit 41 | 42 | def negotiating(addr: Option[SocketAddress], 43 | what: String, 44 | error: Option[Throwable]): Unit 45 | 46 | protected def renderAddress(addr: Option[SocketAddress]): String = addr match { 47 | case Some(addr) if addr.isInstanceOf[InetSocketAddress] => addr.asInstanceOf[InetSocketAddress].toString 48 | case _ => "" 49 | } 50 | 51 | /** 52 | * Return a new `Monitoring` instance that send statistics 53 | * to both `this` and `other`. 54 | */ 55 | def ++(other: Monitoring): Monitoring = new Monitoring { 56 | override def handled[A](ctx: Response.Context, 57 | req: Remote[A], 58 | references: Iterable[String], 59 | result: Throwable \/ A, 60 | took: Duration): Unit = { 61 | self.handled(ctx, req, references, result, took) 62 | other.handled(ctx, req, references, result, took) 63 | } 64 | override def negotiating(addr: Option[SocketAddress], 65 | what: String, 66 | error: Option[Throwable]): Unit = { 67 | self.negotiating(addr, what, error) 68 | other.negotiating(addr, what, error) 69 | } 70 | } 71 | 72 | /** 73 | * Returns a `Monitoring` instance that records at most one 74 | * update for `every` elapsed duration. 75 | */ 76 | def sample(every: Duration): Monitoring = { 77 | val nextUpdate = new java.util.concurrent.atomic.AtomicLong(System.nanoTime + every.toNanos) 78 | new Monitoring { 79 | 80 | def maybe(t: => Unit) { 81 | val cur = nextUpdate.get 82 | if (System.nanoTime > cur) { 83 | t 84 | // only one thread gets to bump this 85 | nextUpdate.compareAndSet(cur, cur + every.toNanos) 86 | () 87 | } else () 88 | 89 | } 90 | 91 | override def handled[A](ctx: Response.Context, 92 | req: Remote[A], 93 | references: Iterable[String], 94 | result: Throwable \/ A, 95 | took: Duration): Unit = maybe(self.handled(ctx, req, references, result, took)) 96 | 97 | override def negotiating(addr: Option[SocketAddress], 98 | what: String, 99 | error: Option[Throwable]): Unit = maybe(self.negotiating(addr,what,error)) 100 | } 101 | } 102 | } 103 | 104 | object Monitoring { 105 | 106 | /** The `Monitoring` instance that just ignores all inputs. */ 107 | val empty: Monitoring = new Monitoring { 108 | override def handled[A](ctx: Response.Context, 109 | req: Remote[A], 110 | references: Iterable[String], 111 | result: Throwable \/ A, 112 | took: Duration): Unit = () 113 | 114 | override def negotiating(addr: Option[SocketAddress], what: String, error: Option[Throwable]): Unit = () 115 | } 116 | 117 | /** 118 | * A `Monitoring` instance that logs all requests to the console. 119 | * Useful for debugging. All lines output will begin with the given 120 | * prefix. 121 | */ 122 | def consoleLogger(prefix: String = ""): Monitoring = new Monitoring { 123 | def handled[A](ctx: Response.Context, 124 | req: Remote[A], 125 | references: Iterable[String], 126 | result: Throwable \/ A, 127 | took: Duration): Unit = { 128 | println(s"$prefix ----------------") 129 | println(s"$prefix header: " + ctx.header) 130 | println(s"$prefix trace: " + ctx.stack.mkString(" ")) 131 | println(s"$prefix request: " + req) 132 | println(s"$prefix result: " + result) 133 | println(s"$prefix duration: " + took) 134 | } 135 | 136 | override def negotiating(addr: Option[SocketAddress], 137 | what: String, 138 | error: Option[Throwable]): Unit = { 139 | error.fold(println(s"$prefix NEGOTIATION - $what with ${renderAddress(addr)}")){e => 140 | println(s"$prefix NEGOTIATION FAILURE - $what with ${renderAddress(addr)}: ${e.getMessage}") 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /core/src/main/scala/Endpoint.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.net.{InetSocketAddress,Socket,URL} 21 | import javax.net.ssl.SSLEngine 22 | import scalaz.std.anyVal._ 23 | import scalaz.concurrent.{Strategy,Task} 24 | import scalaz.syntax.functor._ 25 | import scalaz.stream.{async,Channel,Exchange,io,Process,nio,Process1, process1, Sink} 26 | import scodec.bits.{BitVector} 27 | import scala.concurrent.duration._ 28 | import scalaz._ 29 | import Process.{Await, Emit, Halt, emit, await, halt, eval, await1, iterate} 30 | import scalaz.stream.merge._ 31 | 32 | /** 33 | * A 'logical' endpoint for some service, represented 34 | * by a possibly rotating stream of `Transport`s. 35 | */ 36 | case class Endpoint(connections: Process[Task,Handler]) { 37 | def get: Task[Handler] = connections.once.runLast.flatMap { 38 | case None => Task.fail(new Exception("No available connections")) 39 | case Some(a) => Task.now(a) 40 | } 41 | 42 | 43 | /** 44 | * Adds a circuit-breaker to this endpoint that "opens" (fails fast) after 45 | * `maxErrors` consecutive failures, and attempts a connection again 46 | * when `timeout` has passed. 47 | */ 48 | def circuitBroken(timeout: Duration, maxErrors: Int): Endpoint = 49 | Endpoint(connections.map(c => (bs: Process[Task,BitVector]) => c(bs).translate(CircuitBreaker(timeout, maxErrors).transform))) 50 | } 51 | 52 | object Endpoint { 53 | 54 | def empty: Endpoint = Endpoint(Process.halt) 55 | def single(transport: Handler): Endpoint = Endpoint(Process.constant(transport)) 56 | 57 | /** 58 | * If a connection in an endpoint fails, then attempt the same call to the next 59 | * endpoint, but only if `timeout` has not passed AND we didn't fail in a 60 | * "committed state", i.e. we haven't received any bytes. 61 | */ 62 | def failoverChain(timeout: Duration, es: Process[Task, Endpoint]): Endpoint = 63 | Endpoint(transpose(es.map(_.connections)).flatMap { cs => 64 | cs.reduce((c1, c2) => bs => c1(bs) match { 65 | case w@Await(a, k, _) => 66 | await(time(a.attempt))((p: (Duration, Throwable \/ Any)) => p match { 67 | case (d, -\/(e)) => 68 | if (timeout - d > 0.milliseconds) c2(bs) 69 | else eval(Task.fail(new Exception(s"Failover chain timed out after $timeout"))) 70 | case (d, \/-(x)) => k(\/-(x)).run 71 | }) 72 | case x => x 73 | }) 74 | }) 75 | 76 | /** 77 | * An endpoint backed by a (static) pool of other endpoints. 78 | * Each endpoint has its own circuit-breaker, and fails over to all the others 79 | * on failure. 80 | */ 81 | def uber(maxWait: Duration, 82 | circuitOpenTime: Duration, 83 | maxErrors: Int, 84 | es: Process[Task, Endpoint]): Endpoint = { 85 | Endpoint(raceHandlerPool(permutations(es).map(ps => failoverChain(maxWait, ps.map(_.circuitBroken(circuitOpenTime, maxErrors))).connections))) 86 | } 87 | 88 | /** 89 | * Produce a stream of all the permutations of the given stream. 90 | */ 91 | private[remotely] def permutations[F[_] : Monad : Catchable,A](p: Process[F, A]): Process[F, Process[F, A]] = { 92 | val xs = iterate(0)(_ + 1) zip p 93 | for { 94 | b <- eval(isEmpty(xs)) 95 | r <- if (b) emit(xs) else for { 96 | x <- xs 97 | ps <- permutations(xs |> delete { case (i, v) => i == x._1 }) 98 | } yield emit(x) ++ ps 99 | } yield r.map(_._2) 100 | } 101 | 102 | /** Skips the first element that matches the predicate. */ 103 | private[remotely] def delete[I](f: I => Boolean): Process1[I,I] = { 104 | def go(s: Boolean): Process1[I,I] = 105 | await1[I] flatMap (i => if (s && f(i)) go(false) else emit(i) ++ go(s)) 106 | go(true) 107 | } 108 | 109 | /** 110 | * Transpose a process of processes to emit all their first elements, then all their second 111 | * elements, and so on. 112 | */ 113 | private[remotely] def transpose[F[_]:Monad: Catchable, A](as: Process[F, Process[F, A]]): Process[F, Process[F, A]] = 114 | emit(as.flatMap(_.take(1))) ++ eval(isEmpty(as.flatMap(_.drop(1)))).flatMap(b => 115 | if(b) halt else transpose(as.map(_.drop(1)))) 116 | 117 | /** 118 | * Returns true if the given process never emits. 119 | */ 120 | private[remotely] def isEmpty[F[_]: Monad: Catchable, O](p: Process[F, O]): F[Boolean] = ((p as false) |> Process.await1).runLast.map(_.getOrElse(true)) 121 | 122 | private def time[A](task: Task[A]): Task[(Duration, A)] = for { 123 | t1 <- Task.delay(System.currentTimeMillis) 124 | a <- task 125 | t2 <- Task.delay(System.currentTimeMillis) 126 | } yield ((t2 - t1).milliseconds, a) 127 | 128 | private object raceHandlerPool { 129 | implicit val s = Strategy.Executor(fixedNamedThreadPool("endpointHandlerPool")) 130 | def apply(pool: Process[Task, Process[Task, Handler]]): Process[Task, Handler] = mergeN(pool) 131 | } 132 | } 133 | 134 | 135 | -------------------------------------------------------------------------------- /examples/src/main/scala/Multiservice.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package examples 20 | 21 | import codecs._ 22 | import Remote.implicits._ 23 | import scalaz.concurrent.Task 24 | import transport.netty._ 25 | 26 | /** 27 | * This is a complete example of one service calling another service. 28 | * Service A exposes `sum`, `length`, and serivce B exposes `average` 29 | * by calling `sum` and `length` on Service A. A fresh id is generated 30 | * and pushed onto the trace stack for each nested remote request. 31 | */ 32 | object Multiservice extends App { 33 | 34 | // Define a service exposing `sum` and `length` functions for `List[Double]` 35 | val env1 = Environment.empty 36 | .codec[Double] 37 | .codec[List[Double]].populate { _ 38 | .declare("sum", (xs: List[Double]) => Response.now(xs.sum)) 39 | .declare("length", (xs: List[Double]) => Response.now(xs.length.toDouble)) 40 | .declare("divide", (x: Double, y: Double) => Response.now(x / y)) 41 | } 42 | 43 | // Manually generate the client API for this service 44 | val sum = Remote.ref[List[Double] => Double]("sum") 45 | val length = Remote.ref[List[Double] => Double]("length") 46 | val div = Remote.ref[(Double,Double) => Double]("divide") 47 | 48 | // if we really want, can give `div` infix syntax 49 | implicit class DivSyntax(r: Remote[Double]) { 50 | def /(r2: Remote[Double]): Remote[Double] = div(r,r2) 51 | } 52 | 53 | // Serve these functions 54 | val addr1 = new java.net.InetSocketAddress("localhost", 8081) 55 | val transport = NettyTransport.single(addr1).run 56 | val stopA = env1.serve(addr1, monitoring = Monitoring.consoleLogger("[service-a]")).run 57 | 58 | // And expose an `Endpoint` for making requests to this service 59 | val serviceA: Endpoint = Endpoint.single(transport) 60 | 61 | // Define a service exposing an `average` function, which calls `serviceA`. 62 | val env2 = Environment.empty 63 | .codec[Double] 64 | .codec[List[Double]].populate { _ 65 | // This version will make a single request to `serviceA` 66 | .declare("average2", (xs: List[Double]) => div(sum(xs), length(xs)).run(serviceA)) 67 | // this version will make two requests to `serviceA`, and run them sequentially 68 | .declare("average", (xs: List[Double]) => for { 69 | sumR <- sum(xs).run(serviceA) 70 | countR <- length(xs).run(serviceA) 71 | } yield sumR / countR ) 72 | // Using infix syntax 73 | .declare("average3", (xs: List[Double]) => (sum(xs) / length(xs)).run(serviceA)) 74 | // This version will make three requests to `serviceA`, but will make 75 | // the first two requests (for the sum and count) in parallel 76 | .declare("average4", (xs: List[Double]) => 77 | // The number of round trips is just the number of calls to run 78 | for{ 79 | summed <- sum(xs).run(serviceA) 80 | length <- length(xs).run(serviceA) 81 | } yield div(summed, length).run(serviceA) 82 | ) 83 | // This version checks the "flux-capacitor-status" key of the header 84 | .declare("average5", (xs: List[Double]) => for { 85 | ctx <- Response.ask 86 | avg <- if (ctx.header.contains("flux-capacitor")) { 87 | println("Flux capacitor is enabled, calling service A in a single request!!") 88 | (sum(xs) / length(xs)).run(serviceA) 89 | } 90 | else // okay, do the same thing anyway 91 | (sum(xs) / length(xs)).run(serviceA) 92 | } yield avg) 93 | } 94 | 95 | // Manually generate the client API for this service 96 | val average = Remote.ref[List[Double] => Double]("average") 97 | val average2 = Remote.ref[List[Double] => Double]("average2") 98 | val average3 = Remote.ref[List[Double] => Double]("average3") 99 | val average4 = Remote.ref[List[Double] => Double]("average4") 100 | val average5 = Remote.ref[List[Double] => Double]("average5") 101 | 102 | // Serve these functions 103 | val addr2 = new java.net.InetSocketAddress("localhost", 8082) 104 | val stopB = env2.serve(addr2, monitoring=Monitoring.consoleLogger("[service-b]")).run 105 | val transport2 = NettyTransport.single(addr2).run 106 | val serviceB: Endpoint = Endpoint.single(transport2) 107 | 108 | try { 109 | val ctx = Response.Context.empty ++ List("flux-capacitor" -> "great SCOTT!") 110 | val M = Monitoring.consoleLogger("[client]") 111 | val r1: Task[Double] = average(List(1.0, 2.0, 3.0)).runWithContext(at = serviceB, ctx, M) 112 | val r2: Task[Double] = average2(List(1.0, 2.0)).runWithContext(at = serviceB, ctx, M) 113 | val r3: Task[Double] = average3((0 to 10).map(_.toDouble).toList).runWithContext(at = serviceB, ctx, M) 114 | val r4: Task[Double] = average4((1 to 5).map(_.toDouble).toList).runWithContext(at = serviceB, ctx, M) 115 | val r5: Task[Double] = average5((1 to 5).map(_.toDouble).toList).runWithContext(at = serviceB, ctx, M) 116 | println { "RESULT 1: " + r1.run } 117 | println 118 | println { "RESULT 2: " + r2.run } 119 | println 120 | println { "RESULT 3: " + r3.run } 121 | println 122 | println { "RESULT 4: " + r4.run } 123 | println 124 | println { "RESULT 5: " + r5.run } 125 | } 126 | finally { 127 | stopA.run 128 | stopB.run 129 | transport.shutdown.run 130 | transport2.shutdown.run 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /benchmark/client/src/main/scala/Main.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package example.benchmark 20 | package client 21 | 22 | import example.benchmark.{SmallW, BigW, MediumW, LargeW} 23 | import scalaz.concurrent.Task 24 | import scalaz.Monoid 25 | import scalaz.syntax.validation._ 26 | import scalaz.syntax.monoid._ 27 | import scalaz.syntax.traverse._ 28 | import scalaz.std.anyVal._ 29 | import scalaz.std.map._ 30 | import remotely._ 31 | import remotely.Remote._ 32 | import remotely.Remote.implicits._ 33 | import remotely.{Monitoring,Response,Endpoint,codecs}, codecs._, Response.Context 34 | import remotely.transport.netty._ 35 | import scodec.Codec 36 | import remotely.example.benchmark.protocol._ 37 | 38 | case class Result(success: Int, 39 | failure: Int, 40 | successTime: Long, 41 | successTimeMin: Long, 42 | successTimeMax: Long, 43 | failureTime: Long, 44 | failures: Map[String,Int]) { 45 | 46 | def meanResponse = if(success == 0) 0 else successTime / success 47 | 48 | override def toString = 49 | s"OK: $success, KO: $failure, minResponse: ${successTimeMin}ns, meanResponse: ${meanResponse}ns maxResponse: ${successTimeMax}ns\nErrors: " + failures.map{ case (k,v) => s" $k -> $v" } 50 | } 51 | 52 | 53 | object Result { 54 | implicit val resultMonoid: Monoid[Result] = new Monoid[Result] { 55 | def zero: Result = Result(0,0,0,Long.MaxValue,0,0,Map.empty) 56 | def append(x: Result, y: => Result) = 57 | Result(x.success |+| y.success, 58 | x.failure |+| y.failure, 59 | x.successTime |+| y.successTime, 60 | x.successTimeMin min y.successTimeMin, 61 | x.successTimeMax max y.successTimeMax, 62 | x.failureTime |+| y.failureTime, 63 | x.failures |+| y.failures) 64 | 65 | } 66 | } 67 | 68 | class Test(results: Results, task: Task[_]) extends Runnable { 69 | 70 | var dead = false 71 | def die(): Unit = { 72 | dead = true 73 | } 74 | 75 | def error(start: Long)(e: Throwable): Unit = { 76 | results.failure(e.getMessage, System.nanoTime - start) 77 | } 78 | 79 | def success(start: Long)(x: Any): Unit = { 80 | results.success(System.nanoTime - start) 81 | } 82 | 83 | def run(): Unit = { 84 | while(!dead) { 85 | val start = System.nanoTime 86 | task.runAsync(_.fold(error(start), success(start))) 87 | } 88 | } 89 | } 90 | 91 | 92 | class Results { 93 | var results = Monoid[Result].zero 94 | 95 | def success(t: Long): Unit = { 96 | synchronized { 97 | results = Result(results.success + 1, 98 | results.failure, 99 | results.successTime + t, 100 | results.successTimeMin min t, 101 | results.successTimeMax max t, 102 | results.failureTime, 103 | results.failures) 104 | } 105 | } 106 | 107 | def updateFailures(old: Map[String,Int], f: String): Map[String,Int] = old.get(f).fold(old + (f -> 1))(n => old + (f -> (n+1))) 108 | 109 | def failure(reason: String, t: Long): Unit = { 110 | synchronized { 111 | results = Result(results.success, 112 | results.failure + 1, 113 | results.successTime, 114 | results.successTimeMin, 115 | results.successTimeMax, 116 | results.failureTime + t, 117 | updateFailures(results.failures, reason)) 118 | } 119 | } 120 | 121 | 122 | def print(): Unit = { 123 | synchronized { 124 | println(results.toString) 125 | } 126 | } 127 | } 128 | 129 | object BenchmarkClientMain extends TestData with transformations { 130 | 131 | def usage() { 132 | println("Usage: BenchmarkClientMain port threads seconds") 133 | Runtime.getRuntime.exit(1) 134 | } 135 | 136 | /** 137 | * run a client against the Benchmark server 138 | * 139 | * Usage: 140 | * main port numThreads duration 141 | * 142 | * port is the port number the server is running on 143 | * numThreads is the number of client threads to start 144 | * duration is the number of seconds to run the benchmark 145 | */ 146 | def main(argv: Array[String]): Unit = { 147 | if(argv.length < 3) usage() 148 | 149 | val port = Integer.parseInt(argv(0)) 150 | val addr = new java.net.InetSocketAddress("localhost", port) 151 | val nettyTrans = NettyTransport.single(addr, server.BenchmarkClient.expectedSignatures, monitoring = Monitoring.consoleLogger("benchmarkClient")).run 152 | val endpoint = Endpoint.single(nettyTrans) 153 | val num = Integer.parseInt(argv(1)) 154 | val duration = java.lang.Long.parseLong(argv(2)) 155 | val results = new Results 156 | val end = System.nanoTime + (duration * 1000000000l) 157 | 158 | val testers = (1 to num).toList.map{ _ => 159 | new Test(results, server.BenchmarkClient.identityBig(toBigW(bigIn)).runWithoutContext(endpoint)) 160 | // new Test(results, BenchmarkClient.identityMedium(toMediumW(medIn)).runWithoutContext(endpoint)) 161 | // new Test(results, BenchmarkClient.identityLarge(toLargeW(largeIn)).runWithoutContext(endpoint)) 162 | } 163 | val threads = testers.map(new Thread(_)) 164 | 165 | threads.foreach(_.start) 166 | 167 | while( System.nanoTime < end) { 168 | Thread.sleep(5000) 169 | results.print() 170 | } 171 | 172 | testers.foreach(_.die()) 173 | threads.foreach(_.join) 174 | 175 | results.print() 176 | nettyTrans.shutdown.run 177 | } 178 | } 179 | 180 | -------------------------------------------------------------------------------- /core/src/test/scala/RemoteSpec.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import java.net.InetSocketAddress 21 | import java.util.concurrent.Executors 22 | import org.scalacheck._ 23 | import Prop._ 24 | import scalaz.concurrent.Task 25 | import transport.netty._ 26 | 27 | object RemoteSpec extends Properties("Remote") { 28 | import codecs._ 29 | import Remote.implicits._ 30 | 31 | val env = Environment.empty 32 | .codec[Int] 33 | .codec[Double] 34 | .codec[List[Int]] 35 | .codec[List[Double]] 36 | .codec[List[Signature]] 37 | .populate { _ 38 | .declare("sum", (d: List[Int]) => Response.now(d.sum)) 39 | .declare("add3", (a: Int, b: Int, c: Int) => Response.now(a + b + c)) 40 | .declare("describe", Response.now(List(Signature("sum", List(Field("xs", "List[Double]")), "Double"), 41 | Signature("sum", List(Field("xs", "List[Int]")), "Int"), 42 | Signature("add1", List(Field("xs", "List[Int]")), "List[Int]"), 43 | Signature("describe", Nil, "List[Signature]")))) 44 | 45 | .declare("sum", (d: List[Double]) => Response.now(d.sum)) 46 | .declare("add1", (d: List[Int]) => Response.now(d.map(_ + 1):List[Int])) 47 | } 48 | 49 | val addr = new InetSocketAddress("localhost", 8082) 50 | val server = env.serve(addr).run 51 | val nettyTrans = NettyTransport.single(addr).run 52 | val loc: Endpoint = Endpoint.single(nettyTrans) 53 | 54 | val sum = Remote.ref[List[Int] => Int]("sum") 55 | val sumD = Remote.ref[List[Double] => Double]("sum") 56 | val mapI = Remote.ref[List[Int] => List[Int]]("add1") 57 | val add3 = Remote.ref[(Int, Int, Int) => Int]("add3") 58 | 59 | val ctx = Response.Context.empty 60 | 61 | property("roundtrip") = 62 | forAll { (l: List[Int], kvs: Map[String,String]) => 63 | l.sum == sum(l).runWithoutContext(loc).run//(loc, ctx ++ kvs).run 64 | } 65 | 66 | property("roundtrip[Double]") = 67 | forAll { (l: List[Double], kvs: Map[String,String]) => 68 | l.sum == sumD(l).runWithContext(loc, ctx ++ kvs).run 69 | } 70 | 71 | property("roundtrip[List[Int]]") = 72 | forAll { (l: List[Int], kvs: Map[String,String]) => 73 | l.map(_ + 1) == mapI(l).runWithContext(loc, ctx ++ kvs).run 74 | } 75 | 76 | property("check-serializers") = secure { 77 | // verify that server returns a meaningful error when it asks for 78 | // decoder(s) the server does not know about 79 | val wrongsum = Remote.ref[List[Float] => Float]("sum") 80 | val t: Task[Float] = wrongsum(List(1.0f, 2.0f, 3.0f)).runWithContext(loc, ctx) 81 | t.attemptRun.fold( 82 | e => { 83 | println("test resulted in error, as expected:") 84 | println(prettyError(e.toString)) 85 | true 86 | }, 87 | a => false 88 | ) 89 | } 90 | 91 | property("check-declarations") = secure { 92 | // verify that server returns a meaningful error when client asks 93 | // for a remote ref that is unknown 94 | val wrongsum = Remote.ref[List[Int] => Int]("product") 95 | val t: Task[Int] = wrongsum(List(1, 2, 3)).runWithContext(loc, ctx) 96 | t.attemptRun.fold( 97 | e => { 98 | println("test resulted in error, as expected:") 99 | println(prettyError(e.toString)) 100 | true 101 | }, 102 | a => false 103 | ) 104 | } 105 | 106 | property("add3") = 107 | forAll { (one: Int, two: Int, three: Int) => 108 | add3(one, two, three).runWithoutContext(loc).run == (one + two + three) 109 | } 110 | /* These take forever on travis, and so I'm disabling them, we should leave benchmarking of scodec to scodec and handle benchmarking of remotely in the benchmarking sub-projects 111 | 112 | property("encoding speed") = { 113 | val N = 2000 114 | val M = 1024 115 | val ints = List.range(0, M) 116 | val c = scodec.Codec[List[Int]] 117 | val t = time { 118 | (0 until N).foreach { _ => c.encode(ints); () } 119 | } 120 | println { "took " + t / 1000.0 +"s to encode " + (M*N*4 / 1e6) + " MB" } 121 | true 122 | } 123 | 124 | property("decoding speed") = { 125 | val N = 2000 126 | val M = 1024 127 | val ints = List.range(0, M) 128 | val c = scodec.Codec[List[Int]] 129 | val bits = c.encodeValid(ints) 130 | val t = time { 131 | (0 until N).foreach { _ => c.decode(bits); () } 132 | } 133 | println { "took " + t / 1000.0 +"s to decode " + (M*N*4 / 1e6) + " MB" } 134 | true 135 | } 136 | 137 | property("round trip speed") = { 138 | val l: List[Int] = List(1) 139 | val N = 5000 140 | val t = time { 141 | (0 until N).foreach { _ => sum(l).runWithContext(loc, ctx).run; () } 142 | } 143 | println { "round trip took average of: " + (t/N.toDouble) + " milliseconds" } 144 | true 145 | } 146 | */ 147 | 148 | // NB: this property should always appear last, so it runs after all properties have run 149 | property("cleanup") = lazily { 150 | server.run 151 | nettyTrans.pool.close() 152 | true 153 | } 154 | 155 | def time(a: => Unit): Long = { 156 | val start = System.currentTimeMillis 157 | a 158 | System.currentTimeMillis - start 159 | } 160 | 161 | def prettyError(msg: String): String = { 162 | msg.take(msg.indexOfSlice("stack trace:")) 163 | } 164 | 165 | def lazily(p: => Prop): Prop = { 166 | lazy val pe = secure { p } 167 | new Prop { def apply(p: org.scalacheck.Gen.Parameters) = pe(p) } 168 | } 169 | 170 | } 171 | -------------------------------------------------------------------------------- /core/src/main/scala/transport/netty/Transport.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | package transport.netty 20 | 21 | import java.net.InetSocketAddress 22 | import java.io.File 23 | import io.netty.buffer.{ByteBuf,Unpooled} 24 | import io.netty.channel._ 25 | import io.netty.channel.socket.SocketChannel 26 | import io.netty.channel.socket.nio.NioServerSocketChannel 27 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 28 | import org.apache.commons.pool2.ObjectPool 29 | import io.netty.handler.codec.ByteToMessageDecoder 30 | import javax.net.ssl.{TrustManagerFactory,CertPathTrustManagerParameters} 31 | import java.security.KeyStore 32 | import java.io.FileInputStream 33 | import scalaz.{-\/,\/,\/-,Monoid} 34 | import scalaz.concurrent.Task 35 | import scalaz.stream.{Process,async} 36 | import scodec.Err 37 | import scodec.bits.BitVector 38 | 39 | /////////////////////////////////////////////////////////////////////////// 40 | // A Netty based transport for remotely. 41 | // 42 | // Netty connections consist of two pipelines on each side of a 43 | // network connection, an outbound pipeline and an inbound pipeline 44 | // 45 | // Client Outbound Pipeline 46 | // 47 | // +---------+ 48 | // [ Network ] ← | Enframe | ← Client request 49 | // +---------+ 50 | // 51 | // Server Inbound Pipeline 52 | // 53 | // +---------+ +-----------------------+ 54 | // [ Network ] → | Deframe | → | ServerDeframedHandler | 55 | // +---------+ +-----------------------+ 56 | // 57 | // Deframe - decodes the framing in order to find message boundaries 58 | // 59 | // ServerDeframedHandler - accepts full messages from Deframe, for 60 | // each message, opens a queue/processs pair, calls the handler which 61 | // returns a result Process which is copied back out to the network 62 | // 63 | // Server Outbound Pipeline 64 | // 65 | // +---------+ +-----------------------+ 66 | // [ Network ] ← | Enframe | ← | ServerDeframedHandler | 67 | // +---------+ +-----------------------+ 68 | // 69 | // Enfrome - prepends each ByteVector emitted from the Process with a 70 | // int indicating how many bytes are in this ByteVector, when the 71 | // Process halts, a zero is written indicating the end of the stream 72 | // 73 | // +-----------------------+ 74 | // [ Network ] ← | ServerDeframedHandler | 75 | // +-----------------------+ 76 | // 77 | // 78 | // Client Intbound Pipeline 79 | // 80 | // +---------+ +-----------------------+ 81 | // [ Network ] → | Deframe | → | ClientDeframedHandler | 82 | // +---------+ +-----------------------+ 83 | // 84 | // Deframe - The same as in the Server pipeline 85 | // 86 | // ClientDeframedHandler - This is added to the pipeline when a 87 | // connection is borrowed from the connection pool. It holds onto a 88 | // queue which it feeds with frames passed up from Deframe. This queue 89 | // feeds the Process which represents the output of a remote call. 90 | // 91 | 92 | /** 93 | * set of messages passed in and out of the FrameEncoder/FrameDecoder 94 | * probably unnecessary, I'm probably just trying to sweep an 95 | * isInstanceOf test under the compiler 96 | */ 97 | sealed trait Framed 98 | case class Bits(bv: BitVector) extends Framed 99 | case object EOS extends Framed 100 | 101 | /** 102 | * handler which is at the lowest level of the stack, it decodes the 103 | * frames as described (where STU? where are they described?) it 104 | * emits Deframed things to the next level up which can then treat 105 | * the streams between each EOS we emit as a separate request 106 | */ 107 | class Deframe extends ByteToMessageDecoder { 108 | 109 | // stew loves mutable state 110 | // this will be None in between frames. 111 | // this will be Some(x) when we have seen all but x bytes of the 112 | // current frame. 113 | var remaining: Option[Int] = None 114 | 115 | override protected def decode(ctx: ChannelHandlerContext, // this is our network connection 116 | in: ByteBuf, // this is our input 117 | out: java.util.List[Object]): Unit = { 118 | remaining match { 119 | case None => 120 | // we are expecting a frame header of a single byte which is 121 | // the number of bytes in the upcoming frame 122 | if (in.readableBytes() >= 4) { 123 | val rem = in.readInt() 124 | if(rem == 0) { 125 | val _ = out.add(EOS) 126 | } else { 127 | remaining = Some(rem) 128 | } 129 | } 130 | case Some(rem) => 131 | // we are waiting for at least rem more bytes, as that is what 132 | // is outstanding in the current frame 133 | if(in.readableBytes() >= rem) { 134 | val bytes = new Array[Byte](rem) 135 | in.readBytes(bytes) 136 | remaining = None 137 | val bits = BitVector.view(bytes) 138 | val _ = out.add(Bits(bits)) 139 | } 140 | } 141 | } 142 | } 143 | 144 | class ClientDeframedHandler(queue: async.mutable.Queue[BitVector]) extends SimpleChannelInboundHandler[Framed] { 145 | // there has been an error 146 | private def fail(message: String, ctx: ChannelHandlerContext): Unit = { 147 | queue.fail(new Throwable(message)).runAsync(Function.const(())) 148 | val _ = ctx.channel.close() // should this be disconnect? is there a difference 149 | } 150 | 151 | // we've seen the end of the input, close the queue writing to the input stream 152 | private def close(): Unit = { 153 | queue.close.runAsync(Function.const(())) 154 | } 155 | 156 | override def exceptionCaught(ctx: ChannelHandlerContext, ee: Throwable): Unit = { 157 | ee.printStackTrace() 158 | fail(ee.getMessage, ctx) 159 | } 160 | 161 | override def channelRead0(ctx: ChannelHandlerContext, f: Framed): Unit = 162 | f match { 163 | case Bits(bv) => 164 | queue.enqueueOne(bv).runAsync(Function.const(())) 165 | case EOS => 166 | close() 167 | } 168 | } 169 | 170 | /** 171 | * output handler which gets a stream of BitVectors and enframes them 172 | */ 173 | 174 | @ChannelHandler.Sharable 175 | object Enframe extends ChannelOutboundHandlerAdapter { 176 | override def write(ctx: ChannelHandlerContext, obj: Object, cp: ChannelPromise): Unit = { 177 | obj match { 178 | case Bits(bv) => 179 | val byv = bv.toByteVector 180 | val _ = ctx.writeAndFlush(Unpooled.wrappedBuffer((codecs.int32.encode(byv.size.toInt).require ++ bv).toByteBuffer), cp) 181 | case EOS => 182 | val _ = ctx.writeAndFlush(Unpooled.wrappedBuffer(codecs.int32.encode(0).require.toByteBuffer), cp) 183 | case x => throw new IllegalArgumentException("was expecting Framed, got: " + x) 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /core/src/main/scala/Remote.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 | 18 | package remotely 19 | 20 | import scala.collection.immutable.SortedSet 21 | import scala.reflect.runtime.universe._ 22 | import scalaz.concurrent.Task 23 | import scalaz.{\/, Applicative, Monad, Nondeterminism} 24 | import scala.reflect.runtime.universe.TypeTag 25 | import scodec.{Codec,Decoder,Encoder} 26 | import scodec.bits.BitVector 27 | import shapeless._ 28 | 29 | /** 30 | * Represents a remote computation which yields a 31 | * value of type `A`. Remote expressions can be serialized 32 | * and sent to a server for evaluation. 33 | */ 34 | sealed trait Remote[+A] { 35 | 36 | override def toString = pretty 37 | 38 | def pretty: String = "Remote {\n " + 39 | Remote.refs(this).mkString("\n ") + "\n " + 40 | toString + "\n}" 41 | } 42 | 43 | object Remote { 44 | 45 | /** Reference a remote value on the server, assuming it has the given type. */ 46 | def ref[A:TypeTag](s: String): Remote[A] = { 47 | val tag = Remote.nameToTag[A](s) 48 | Remote.Ref[A](tag) 49 | } 50 | 51 | /** Promote a local value to a remote value. */ 52 | def local[A:Encoder:TypeTag](a: A): Remote[A] = 53 | Remote.Local(a, Some(Encoder[A]), Remote.toTag[A]) 54 | 55 | /** Provides the syntax `expr.run(endpoint)`, where `endpoint: Endpoint`. */ 56 | implicit class RunSyntax[A](self: Remote[A]) { 57 | /** 58 | * Run this `Remote[A]` at the given `Endpoint`. We require a `TypeTag[A]` and 59 | * `Codec[A]` in order to deserialize the response and check that it has the expected type. 60 | */ 61 | def run(at: Endpoint, M: Monitoring = Monitoring.empty)(implicit A: TypeTag[A], C: Codec[A]): Response[A] = 62 | evaluate(at, M)(self) 63 | 64 | /** Call `self.run(at, M).apply(ctx)` to get back a `Task[A]`. */ 65 | def runWithContext(at: Endpoint, ctx: Response.Context, M: Monitoring = Monitoring.empty)(implicit A: TypeTag[A], C: Codec[A]): Task[A] = 66 | run(at, M).apply(ctx) 67 | 68 | /** Run this with an empty context */ 69 | def runWithoutContext(at: Endpoint)(implicit A: TypeTag[A], C: Codec[A]): Task[A] = 70 | runWithContext(at, Response.Context.empty) 71 | } 72 | implicit class Ap1Syntax[A,B](self: Remote[A => B]) { 73 | def apply(a: Remote[A]): Remote[B] = 74 | Remote.Ap1(self, a) 75 | } 76 | implicit class Ap2Syntax[A,B,C](self: Remote[(A,B) => C]) { 77 | def apply(a: Remote[A], b: Remote[B]): Remote[C] = 78 | Remote.Ap2(self, a, b) 79 | } 80 | implicit class Ap3Syntax[A,B,C,D](self: Remote[(A,B,C) => D]) { 81 | def apply(a: Remote[A], b: Remote[B], c: Remote[C]): Remote[D] = 82 | Remote.Ap3(self, a, b, c) 83 | } 84 | implicit class Ap4Syntax[A,B,C,D,E](self: Remote[(A,B,C,D) => E]) { 85 | def apply(a: Remote[A], b: Remote[B], c: Remote[C], d: Remote[D]): Remote[E] = 86 | Remote.Ap4(self, a, b, c, d) 87 | } 88 | implicit class Ap5Syntax[A,B,C,D,E,F](self: Remote[(A,B,C,D,E) => F]) { 89 | def apply(a: Remote[A], b: Remote[B], c: Remote[C], d: Remote[D], e: Remote[E]): Remote[F] = 90 | Remote.Ap5(self, a, b, c, d, e) 91 | } 92 | 93 | /** Promote a local value to a remote value. */ 94 | private[remotely] case class Local[A]( 95 | a: A, // the value 96 | format: Option[Encoder[A]], // serializer for `A` 97 | tag: String // identifies the deserializer to be used by server 98 | ) extends Remote[A] { 99 | override def toString = a.toString 100 | } 101 | 102 | /** 103 | * Reference to a remote value on the server. 104 | */ 105 | private[remotely] case class Ref[A](name: String) extends Remote[A] { 106 | override def toString = name.takeWhile(_ != ':') 107 | } 108 | 109 | // we require a separate constructor for each function 110 | // arity, since remote invocations must be fully saturated 111 | private[remotely] case class Ap1[A,B]( 112 | f: Remote[A => B], 113 | a: Remote[A]) extends Remote[B] { 114 | override def toString = s"$f($a)" 115 | } 116 | 117 | private[remotely] case class Ap2[A,B,C]( 118 | f: Remote[(A,B) => C], 119 | a: Remote[A], 120 | b: Remote[B]) extends Remote[C] { 121 | override def toString = s"$f($a, $b)" 122 | } 123 | 124 | private[remotely] case class Ap3[A,B,C,D]( 125 | f: Remote[(A,B,C) => D], 126 | a: Remote[A], 127 | b: Remote[B], 128 | c: Remote[C]) extends Remote[D] { 129 | override def toString = s"$f($a, $b, $c)" 130 | } 131 | 132 | private[remotely] case class Ap4[A,B,C,D,E]( 133 | f: Remote[(A,B,C,D) => E], 134 | a: Remote[A], 135 | b: Remote[B], 136 | c: Remote[C], 137 | d: Remote[D]) extends Remote[E] { 138 | override def toString = s"$f($a, $b, $c, $d)" 139 | } 140 | 141 | private[remotely] case class Ap5[A,B,C,D,E,F]( 142 | f: Remote[(A,B,C,D,E) => F], 143 | a: Remote[A], 144 | b: Remote[B], 145 | c: Remote[C], 146 | d: Remote[D], 147 | e: Remote[E]) extends Remote[F] { 148 | override def toString = s"$f($a, $b, $c, $d, $e)" 149 | } 150 | 151 | /** Collect up all the `Ref` names referenced by `r`. */ 152 | def refs[A](r: Remote[A]): SortedSet[String] = r match { 153 | case Local(a,e,t) => SortedSet.empty 154 | case Ref(t) => SortedSet(t) 155 | case Ap1(f,a) => refs(f).union(refs(a)) 156 | case Ap2(f,a,b) => refs(f).union(refs(a)).union(refs(b)) 157 | case Ap3(f,a,b,c) => refs(f).union(refs(a)).union(refs(b)).union(refs(c)) 158 | case Ap4(f,a,b,c,d) => refs(f).union(refs(a)).union(refs(b)).union(refs(c)).union(refs(d)) 159 | case Ap5(f,a,b,c,d,e) => refs(f).union(refs(a)).union(refs(b)).union(refs(c)).union(refs(d)).union(refs(e)) 160 | } 161 | 162 | /** Collect up all the formats referenced by `r`. */ 163 | def formats[A](r: Remote[A]): SortedSet[String] = r match { 164 | case Local(a,e,t) => SortedSet(t) 165 | case Ref(t) => SortedSet.empty 166 | case Ap1(f,a) => formats(f).union(formats(a)) 167 | case Ap2(f,a,b) => formats(f).union(formats(a)).union(formats(b)) 168 | case Ap3(f,a,b,c) => formats(f).union(formats(a)).union(formats(b)).union(formats(c)) 169 | case Ap4(f,a,b,c,d) => formats(f).union(formats(a)).union(formats(b)).union(formats(c)).union(formats(d)) 170 | case Ap5(f,a,b,c,d,e) => formats(f).union(formats(a)).union(formats(b)).union(formats(c)).union(formats(d)).union(formats(e)) 171 | } 172 | 173 | def toTag[A:TypeTag]: String = { 174 | val tt = typeTag[A] 175 | val result = tt.tpe.toString 176 | if(result.startsWith("java.lang.")) result.drop(10) 177 | else if (result.startsWith("scala.")) result.drop(6) 178 | else result 179 | } 180 | 181 | def nameToTag[A:TypeTag](s: String): String = 182 | s"$s: ${toTag[A]}" 183 | 184 | /** Lower priority implicits. */ 185 | private[remotely] trait lowpriority { 186 | implicit def codecIsRemote[A:Codec:TypeTag](a: A): Remote[A] = local(a) 187 | } 188 | 189 | /** Provides implicits for promoting values to `Remote[A]`. */ 190 | object implicits extends lowpriority { 191 | 192 | /** Implicitly promote a local value to a `Remote[A]`. */ 193 | implicit def localToRemote[A:Encoder:TypeTag](a: A): Remote[A] = local(a) 194 | } 195 | } 196 | 197 | -------------------------------------------------------------------------------- /core/src/main/scala/SSL.scala: -------------------------------------------------------------------------------- 1 | //: ---------------------------------------------------------------------------- 2 | //: Copyright (C) 2014 Verizon. All Rights Reserved. 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 remotely 18 | 19 | import java.io.{ByteArrayInputStream,InputStream,File, FileInputStream} 20 | import java.security.{KeyFactory,KeyStore,PrivateKey, SecureRandom} 21 | import java.security.spec.{PKCS8EncodedKeySpec} 22 | import java.security.cert.{Certificate,CertificateFactory,PKIXBuilderParameters,X509Certificate,X509CertSelector,TrustAnchor} 23 | import javax.net.ssl._ 24 | import io.netty.handler.ssl._ 25 | import scalaz.stream._ 26 | import scalaz.std.string._ 27 | import scalaz.concurrent.Task 28 | import scalaz.{Kleisli,Monoid} 29 | import scodec.bits.ByteVector 30 | import scalaz.syntax.traverse._ 31 | import scalaz.std.option._ 32 | import Process._ 33 | import collection.JavaConverters._ 34 | 35 | // what are all the configurations we want to support: 36 | // 37 | // 1. everything plaintext 38 | // 2. traditional ssl 39 | // 3. ssl + client cert 40 | 41 | object SSL { 42 | 43 | def emptyKeystore: KeyStore = { 44 | val keystore = KeyStore.getInstance("JKS", "SUN") 45 | //Before a keystore can be accessed, it must be loaded. 46 | //Since we don't read keys from any file, we pass "null" and load certificate later in the code below 47 | keystore.load(null) 48 | keystore 49 | } 50 | 51 | def addPEM(in: InputStream, name: String)(ks: KeyStore): Task[KeyStore] = addCerts(ks, name, certFromPEM(in)) 52 | 53 | implicit val taskUnitMonoid: Monoid[Task[Unit]] = new Monoid[Task[Unit]] { 54 | def zero = Task.now(()) 55 | def append(a: Task[Unit], b: => Task[Unit]) = a flatMap { _ => b } 56 | } 57 | 58 | private[remotely] def addCert(cert: Certificate, name: String, ks: KeyStore): Task[Unit] = Task.delay(ks.setCertificateEntry(name, cert)) 59 | private[remotely] def addKey(key: PrivateKey, certs: Seq[Certificate], name: String, pass: Array[Char], ks: KeyStore):Task[Unit] = Task.delay(ks.setKeyEntry(name, key, pass, certs.toArray[Certificate])) 60 | 61 | private[remotely] def addCerts(ks: KeyStore, name: String, certs: Process[Task,Certificate]): Task[KeyStore] = certs.zipWithIndex.runFoldMap{ case (c,i) => addCert(c,name+i,ks) }.map(_ => ks) 62 | 63 | private[remotely] def stripCruftFromPEM(withHeaders: Boolean): Process1[String,String] = { 64 | def untilBegin: Process1[String,String] = 65 | receive1Or[String,String](halt){ s => 66 | if(s startsWith "-----BEGIN") { 67 | if(withHeaders) emit(s+"\n") else halt 68 | } else { 69 | untilBegin 70 | } 71 | } 72 | 73 | def untilEnd: Process1[String,String] = { 74 | receive1Or[String,String](fail(new IllegalArgumentException("Not a valid KEY, didn't find END marker"))) { s => 75 | if(s startsWith "-----END") 76 | if(withHeaders) emit(s+"\n") else halt 77 | else if(withHeaders) (emit(s + "\n") ++ untilEnd) else (emit(s) ++ untilEnd) 78 | } 79 | } 80 | 81 | (untilBegin ++ untilEnd).foldMap(identity).repeat.filter(_.length > 0) 82 | 83 | } 84 | 85 | def pemString(s: InputStream, withHeaders: Boolean): Process[Task,String] = (io.linesR(s) |> stripCruftFromPEM(withHeaders)) 86 | 87 | def certFromPEM(s: InputStream): Process[Task,Certificate] = 88 | pemString(s, true) map (str => CertificateFactory.getInstance("X.509").generateCertificate(new ByteArrayInputStream(str.getBytes("US-ASCII")))) 89 | 90 | def keyFromPkcs8(s: InputStream): Process[Task,PrivateKey] = 91 | pemString(s, false).flatMap { str => 92 | ByteVector.fromBase64(str).fold[Process[Task,PrivateKey]](Process.fail(new IllegalArgumentException("could not parse PEM data"))){ decoded => 93 | val spec = new PKCS8EncodedKeySpec(decoded.toArray) 94 | emit(KeyFactory.getInstance("RSA").generatePrivate(spec)) 95 | } 96 | } 97 | 98 | 99 | private[remotely] def keyFromEncryptedPkcs8(s: InputStream, pass: Array[Char]): Process[Task,PrivateKey] = 100 | pemString(s, false) map (str => KeyFactory.getInstance("RSA").generatePrivate(new PKCS8EncodedKeySpec(str.getBytes("US-ASCII")))) 101 | } 102 | 103 | case class SslParameters(caBundle: Option[File], 104 | certFile: Option[File], 105 | keyFile: Option[File], 106 | keyPassword: Option[String], 107 | enabledCiphers: Option[List[String]], 108 | enabledProtocols: Option[List[String]], 109 | requireClientAuth: Boolean) { 110 | } 111 | 112 | object SslParameters { 113 | 114 | private[remotely] def trustManagerForBundle(caBundle: File): Task[TrustManagerFactory] = { 115 | Task.delay { 116 | val keystore = SSL.emptyKeystore 117 | val trustSet = new java.util.HashSet[TrustAnchor]() 118 | 119 | // I promise that effects are associative as long as you execute them in the right order 120 | implicit val taskMonoid: Monoid[Task[Unit]] = Monoid.instance((a,b) => a flatMap(_ => b), Task.now(())) 121 | 122 | val doRun = SSL.certFromPEM(new FileInputStream(caBundle)).foldMap { 123 | case cert: X509Certificate => 124 | val name = cert.getSubjectDN().getName() 125 | if(cert.getBasicConstraints != -1) trustSet.add(new TrustAnchor(cert, null)) 126 | SSL.addCert(cert, name, keystore) 127 | case cert => throw new IllegalArgumentException("unexpected cert which is not x509") 128 | } 129 | doRun.run.run // da do run run: https://www.youtube.com/watch?v=uTqnam1zgiw 130 | 131 | val tm = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) 132 | val builder = new PKIXBuilderParameters(trustSet, new X509CertSelector) 133 | builder.setRevocationEnabled(false) 134 | val cptmp: CertPathTrustManagerParameters = new CertPathTrustManagerParameters(builder) 135 | 136 | tm.init(cptmp) 137 | tm 138 | } 139 | } 140 | 141 | private[remotely] def toClientContext(params: Option[SslParameters]): Task[Option[SslContext]] = { 142 | params.traverse { params => 143 | for { 144 | tm <- params.caBundle.fold[Task[TrustManagerFactory]](Task.now(null))(trustManagerForBundle) 145 | } yield SslContextBuilder.forClient(). 146 | sslProvider(SslProvider.JDK). 147 | trustManager(tm). 148 | keyManager(params.certFile.orNull, params.keyFile.orNull, params.keyPassword.orNull). 149 | ciphers(params.enabledCiphers.map(_.asJava).orNull, IdentityCipherSuiteFilter.INSTANCE). 150 | applicationProtocolConfig(ApplicationProtocolConfig.DISABLED). 151 | build() 152 | } 153 | } 154 | 155 | private[remotely] def toServerContext(params: Option[SslParameters]): Task[Option[SslContext]] = { 156 | params.traverse { params => 157 | for { 158 | tm <- params.caBundle.fold[Task[TrustManagerFactory]](Task.now(null))(trustManagerForBundle) 159 | } yield SslContextBuilder. 160 | forServer(params.certFile.orNull, params.keyFile.orNull, params.keyPassword.orNull). 161 | sslProvider(SslProvider.JDK). 162 | trustManager(tm). 163 | ciphers(params.enabledCiphers.map(_.asJava).orNull, IdentityCipherSuiteFilter.INSTANCE). 164 | applicationProtocolConfig(ApplicationProtocolConfig.DISABLED). 165 | build() 166 | } 167 | } 168 | } --------------------------------------------------------------------------------