├── version ├── src ├── test │ ├── resources │ │ ├── reference.conf │ │ └── logback.xml │ └── scala │ │ └── io │ │ └── wasted │ │ └── util │ │ └── test │ │ ├── TcpByteBufSpec.scala │ │ ├── TryoSpec.scala │ │ ├── CryptoSpec.scala │ │ ├── Base64Spec.scala │ │ ├── WactorSpec.scala │ │ ├── LruMapSpec.scala │ │ ├── ScheduleSpec.scala │ │ ├── HashingSpec.scala │ │ ├── HttpSpec.scala │ │ ├── InetPrefixSpec.scala │ │ ├── HttpsSpec.scala │ │ ├── ConfigSpec.scala │ │ ├── HttpRetrySpec.scala │ │ ├── WebSocketSpec.scala │ │ └── RedisSpec.scala ├── main │ └── scala │ │ └── io │ │ └── wasted │ │ └── util │ │ ├── Netty.scala │ │ ├── Tryo.scala │ │ ├── NettyCodec.scala │ │ ├── redis │ │ ├── RateLimiter.scala │ │ └── RedisClient.scala │ │ ├── Base64.scala │ │ ├── HostInformation.scala │ │ ├── http │ │ ├── ExceptionHandler.scala │ │ ├── HttpResponder.scala │ │ ├── Headers.scala │ │ ├── WebSocketClient.scala │ │ ├── WebSocketHandler.scala │ │ ├── HttpServer.scala │ │ ├── HttpClient.scala │ │ ├── NettyHttpCodec.scala │ │ ├── NettyWebSocketCodec.scala │ │ └── Thruput.scala │ │ ├── Logger.scala │ │ ├── Package.scala │ │ ├── Shell.scala │ │ ├── apn │ │ ├── Models.scala │ │ ├── FeedbackService.scala │ │ └── PushService.scala │ │ ├── NettyTcpServer.scala │ │ ├── NettyServerBuilder.scala │ │ ├── LruMap.scala │ │ ├── NettyTcpClient.scala │ │ ├── Hashing.scala │ │ ├── Schedule.scala │ │ ├── Crypto.scala │ │ ├── InetPrefix.scala │ │ ├── NettyByteArrayCodec.scala │ │ ├── NettyStringCodec.scala │ │ ├── Config.scala │ │ ├── Wactor.scala │ │ └── NettyClientBuilder.scala └── site │ ├── javascripts │ └── scale.fix.js │ ├── params.json │ ├── index.html │ ├── index.tmpl │ └── stylesheets │ ├── pygment_trac.css │ └── styles.css ├── project └── plugins.sbt ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── sbt /version: -------------------------------------------------------------------------------- 1 | 0.12.4 2 | -------------------------------------------------------------------------------- /src/test/resources/reference.conf: -------------------------------------------------------------------------------- 1 | test.int=5 2 | test.bool=true 3 | test.string=chicken 4 | test.list=["foo", "bar", "baz"] 5 | test.addrs=["127.0.0.1:8080", "[::]:8081"] -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/TcpByteBufSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | /** 4 | * Created by fbettag on 30/10/15. 5 | */ 6 | class TcpByteBufSpec { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Netty.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import io.netty.channel.nio.NioEventLoopGroup 4 | 5 | private[util] object Netty { 6 | val eventLoop = new NioEventLoopGroup 7 | } 8 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Tryo.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import scala.util.Success 4 | 5 | /** 6 | * This is a simple try/catch wrapped in an Option. 7 | */ 8 | object Tryo { 9 | def apply[T](f: => T): Option[T] = scala.util.Try(f) match { case Success(x) => Some(x) case _ => None } 10 | def apply[T](f: => T, fallback: T): T = scala.util.Try(f) getOrElse fallback 11 | } 12 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.url("artifactory", url("http://scalasbt.artifactoryonline.com/scalasbt/sbt-plugin-releases"))(Resolver.ivyStylePatterns) 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.3.0") 4 | 5 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.11.2") 6 | 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 8 | 9 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.7.4") 10 | 11 | resolvers += "jgit-repo" at "http://download.eclipse.org/jgit/maven" 12 | 13 | addSbtPlugin("com.typesafe.sbt" % "sbt-ghpages" % "0.5.4") 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # swap files 2 | *~ 3 | *# 4 | *.sw[op] 5 | ._* 6 | README.md.old 7 | 8 | # sbt 9 | sbt-launch.jar* 10 | project/boot 11 | project/target 12 | project/plugins/target 13 | project/plugins/project 14 | 15 | # OSX 16 | .DS_Store 17 | 18 | 19 | # Intellij IDEA 20 | *.iws 21 | *.ipr 22 | *.iml 23 | .idea* 24 | 25 | 26 | # Eclipse & Netbeans 27 | .cache 28 | .project 29 | .settings 30 | .classpath 31 | .scala_dependencies 32 | 33 | 34 | # ensime 35 | ensime_port 36 | .ensime.* 37 | 38 | # binary stuff 39 | build 40 | target 41 | lib 42 | 43 | # 44 | src/main/resources/props/* 45 | 46 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/TryoSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.Tryo 4 | import org.scalatest._ 5 | 6 | class TryoSpec extends WordSpec { 7 | val tryoSuccess = Tryo("success!") 8 | val tryoFailure = Tryo(throw new IllegalArgumentException) 9 | 10 | "Tryo(throw new IllegalArgumentException)" should { 11 | "have thrown an Exception and returned None" in { assert(tryoFailure.isEmpty) } 12 | } 13 | 14 | "Tryo(\"success!\")" should { 15 | "have also a result containing 'success!'" in { assert(tryoSuccess.contains("success!")) } 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/site/javascripts/scale.fix.js: -------------------------------------------------------------------------------- 1 | var metas = document.getElementsByTagName('meta'); 2 | var i; 3 | if (navigator.userAgent.match(/iPhone/i)) { 4 | for (i=0; i 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ![netflow.io](http://netflow.io/images/github/netflow.png) 2 | 3 | ======= 4 | 5 | #### In order to contribute to netflow.io, you need to add the following statement into your pull-request: 6 | 7 | By submitting this pull request which includes my name and email address (the email address may be in a non-robot readable format), I agree that the entirety of the contribution is my own original work, that there are no prior claims on this work including, but not limited to, any agreements I may have with my employer or other contracts, and that I license this work under an Apache 2.0 license. 8 | 9 | **Name:** John Doe 10 | 11 | **Email:** john at doe dot org 12 | 13 | ======= 14 | 15 | Thanks for contributing! -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyCodec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import com.twitter.util.{ Duration, Future } 4 | import io.netty.channel.Channel 5 | 6 | trait NettyCodec[Req, Resp] { 7 | def readTimeout: Option[Duration] 8 | def writeTimeout: Option[Duration] 9 | 10 | /** 11 | * Sets up basic Server-Pipeline for this Codec 12 | * @param channel Channel to apply the Pipeline to 13 | */ 14 | def serverPipeline(channel: Channel): Unit 15 | 16 | /** 17 | * Sets up basic Client-Pipeline for this Codec 18 | * @param channel Channel to apply the Pipeline to 19 | */ 20 | def clientPipeline(channel: Channel): Unit 21 | 22 | /** 23 | * Gets called once the TCP Connection has been established 24 | * with this being the API Client connecting to a Server 25 | * @param channel Channel we're connected to 26 | * @param request Request object we want to use 27 | * @return Future Response 28 | */ 29 | def clientConnected(channel: Channel, request: Req): Future[Resp] 30 | } -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/redis/RateLimiter.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | package redis 3 | 4 | import com.twitter.util.{ Duration, Future } 5 | 6 | class OverRateLimitException(val name: String, val window: Duration, val limit: Long, val prefix: Option[String]) 7 | extends Exception("Rate-Limit " + prefix.map("at " + _ + " ").getOrElse("") + "for " + name + " with Limit of " + limit + " in " + window.inSeconds + " seconds") 8 | with scala.util.control.NoStackTrace 9 | 10 | final case class CounterBasedRateLimiter(client: NettyRedisChannel, window: Duration, limit: Long, prefix: Option[String] = None) { 11 | def apply(name: String): Future[Unit] = { 12 | val key = "ratelimit:" + prefix.map(_ + ":").getOrElse("") + name 13 | client.incr(key).flatMap { count => 14 | if (count > limit) Future.exception(new OverRateLimitException(name, window, limit, prefix)) 15 | else { 16 | if (count == 1) client.expire(key, window) 17 | Future.Done 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/site/params.json: -------------------------------------------------------------------------------- 1 | {"name":"Scala-util","google":"UA-37069809-1","body":"wasted.io Scala Utility Library\r\n=======\r\n\r\n## Using our repository\r\n\r\n```\r\n not done yet ;)\r\n```\r\n\r\n## Do-it-yourself\r\n\r\n```\r\n ./sbt publish-local\r\n```\r\n\r\n\r\n## License\r\n\r\n```\r\n Copyright 2012 wasted.io Ltd \r\n\r\n Licensed under the Apache License, Version 2.0 (the \"License\");\r\n you may not use this file except in compliance with the License.\r\n You may obtain a copy of the License at\r\n\r\n http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n Unless required by applicable law or agreed to in writing, software\r\n distributed under the License is distributed on an \"AS IS\" BASIS,\r\n WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n See the License for the specific language governing permissions and\r\n limitations under the License.\r\n```\r\n\r\n","tagline":"Scala Utility Library used at wasted.io","note":"Don't delete this file! It's used internally to help with page regeneration."} -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/CryptoSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.{ Base64, Crypto, CryptoCipher } 4 | import org.scalatest._ 5 | 6 | class CryptoSpec extends WordSpec { 7 | 8 | implicit val cipher = CryptoCipher("AES") 9 | 10 | val ourString = "this must work!!" 11 | val ourSalt = "1111111111111111" 12 | 13 | val encrypted: Array[Byte] = Crypto.encryptBinary(ourSalt, ourString) 14 | val base64Encoded: String = Base64.encodeString(encrypted) 15 | val base64Decoded: Array[Byte] = Base64.decodeBinary(base64Encoded) 16 | val theirString = Crypto.decryptString(ourSalt, Base64.decodeBinary(base64Encoded)) 17 | 18 | "Pregenerated Base64 (" + ourString + ")" should { 19 | "be the same as the decrypted (" + theirString + ")" in { 20 | assert(ourString == theirString) 21 | } 22 | } 23 | 24 | "Encoded Array (" + encrypted.toList.toString + ")" should { 25 | "be the same as the decoded (" + base64Decoded.toList.toString + ")" in { 26 | assert(encrypted.deep == base64Decoded.deep) 27 | } 28 | } 29 | 30 | } 31 | 32 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/Base64Spec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.Base64 4 | import org.scalatest._ 5 | 6 | class Base64Spec extends WordSpec { 7 | val ourString = "it works!" 8 | val ourB64 = "aXQgd29ya3Mh" 9 | val ourB64Array = Array(97, 88, 81, 103, 100, 50, 57, 121, 97, 51, 77, 104).map(_.toByte) 10 | 11 | val theirB64 = Base64.encodeString(ourString) 12 | val theirB64Array = Base64.encodeBinary(ourString) 13 | val theirB64String = Base64.decodeString(theirB64Array) 14 | 15 | "Precalculated Base64 String (" + ourB64 + ")" should { 16 | "be the same as the calculated (" + theirB64 + ")" in { 17 | assert(ourB64 == theirB64) 18 | } 19 | } 20 | 21 | "Precalculated Base64 Array (" + ourB64Array.mkString(", ") + ")" should { 22 | "be the same as the calculated (" + theirB64Array.mkString(", ") + ")" in { 23 | assert(ourB64Array.deep == theirB64Array.deep) 24 | } 25 | } 26 | 27 | "Pre-set string of \"" + ourString + "\"" should { 28 | "be the same as the calculated (" + theirB64String + ")" in { 29 | assert(ourString == theirB64String) 30 | } 31 | } 32 | 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/WactorSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.Wactor 4 | import org.scalatest._ 5 | import org.scalatest.concurrent.AsyncAssertions 6 | import org.scalatest.time.SpanSugar._ 7 | 8 | class WactorSpec extends WordSpec with AsyncAssertions with BeforeAndAfter { 9 | val w = new Waiter 10 | val testInt = 5 11 | val testString = "wohooo!" 12 | 13 | class TestWactor extends Wactor(5) { 14 | def receive = { 15 | case a: String => 16 | assert(a == testString); w.dismiss() 17 | case a: Int => assert(a == testInt); w.dismiss() 18 | } 19 | 20 | override val loggerName = "TestWactor" 21 | override def exceptionCaught(e: Throwable) { e.printStackTrace() } 22 | } 23 | 24 | val actor = new TestWactor 25 | 26 | before(actor ! true) // wakeup 27 | 28 | "TestWactor" should { 29 | "have a message with \"" + testString + "\"" in { 30 | actor ! testString 31 | w.await(timeout(1 second)) 32 | } 33 | 34 | "have a message with Integer " + testInt in { 35 | actor ! testInt 36 | w.await(timeout(1 second)) 37 | } 38 | } 39 | 40 | after(actor ! Wactor.Die) 41 | } 42 | 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![wasted.io](http://wasted.io/images/soon/wasted.png) 2 | 3 | ======= 4 | 5 | ### We do allow pull requests, but please follow the [contribution guidelines](https://github.com/wasted/scala-util/blob/master/CONTRIBUTING.md). 6 | 7 | ## Using our repository 8 | 9 | ``` 10 | resolvers += "wasted.io/repo" at "http://repo.wasted.io/mvn" 11 | libraryDependencies += "io.wasted" %% "wasted-util" % "0.12.4" 12 | ``` 13 | 14 | ## [API Docs](http://wasted.github.com/scala-util/latest/api/#io.wasted.util.package) 15 | 16 | ## [Examples](https://github.com/wasted/scala-util/tree/master/src/test/scala/io/wasted/util/test) 17 | 18 | ## License 19 | 20 | ``` 21 | Copyright 2012-2016, wasted.io Ltd 22 | 23 | Licensed under the Apache License, Version 2.0 (the "License"); 24 | you may not use this file except in compliance with the License. 25 | You may obtain a copy of the License at 26 | 27 | http://www.apache.org/licenses/LICENSE-2.0 28 | 29 | Unless required by applicable law or agreed to in writing, software 30 | distributed under the License is distributed on an "AS IS" BASIS, 31 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | See the License for the specific language governing permissions and 33 | limitations under the License. 34 | ``` 35 | 36 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/LruMapSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.LruMap 4 | import org.scalatest._ 5 | 6 | class LruMapSpec extends WordSpec { 7 | 8 | val lru = LruMap[Int, Int](10) 9 | 10 | "Pre-loaded LruMap with Ints (0 to 10)" should { 11 | for (i <- 0 to 10) lru.put(i, i) 12 | "not contain element 0 anymore" in { assert(lru.get(0).isEmpty) } 13 | "contain element 1" in { assert(lru.get(1).contains(1)) } 14 | "contain element 2" in { assert(lru.get(2).contains(2)) } 15 | "contain element 3" in { assert(lru.get(3).contains(3)) } 16 | "contain element 4" in { assert(lru.get(4).contains(4)) } 17 | "contain element 5" in { assert(lru.get(5).contains(5)) } 18 | "contain element 6" in { assert(lru.get(6).contains(6)) } 19 | "contain element 7" in { assert(lru.get(7).contains(7)) } 20 | "contain element 8" in { assert(lru.get(8).contains(8)) } 21 | "contain element 9" in { assert(lru.get(9).contains(9)) } 22 | "contain element 10" in { assert(lru.get(10).contains(10)) } 23 | } 24 | 25 | "Deleting Lru-Entries" should { 26 | "delete element 5 and verify" in { 27 | lru.remove(5) 28 | assert(lru.get(5).isEmpty) 29 | } 30 | "push element 0 and verify that 1 is still in" in { 31 | lru.put(0, 0) 32 | assert(lru.get(1).contains(1)) 33 | } 34 | } 35 | 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/ScheduleSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.{ Schedule, WheelTimer } 4 | import org.scalatest._ 5 | import org.scalatest.concurrent.AsyncAssertions 6 | import org.scalatest.time.SpanSugar._ 7 | 8 | class ScheduleSpec extends WordSpec with AsyncAssertions { 9 | val w = new Waiter 10 | implicit val wheel = WheelTimer 11 | 12 | var result2 = false 13 | var result3 = false 14 | var testfunc2 = () => if (result2) { 15 | result3 = true 16 | w.dismiss() 17 | } else { 18 | result2 = true 19 | w.dismiss() 20 | } 21 | val cancelAgain = Schedule.again(testfunc2, scala.concurrent.duration.DurationInt(5).millis, scala.concurrent.duration.DurationInt(5).millis) 22 | 23 | "Schedule should have done 3 tests where results" should { 24 | "be true for Schedule.once" in { 25 | Schedule.once(() => w.dismiss(), scala.concurrent.duration.DurationInt(5).millis) 26 | w.await(timeout(500 millis), dismissals(2)) 27 | } 28 | "be true for Schedule.again (first run)" in { 29 | Schedule.once(testfunc2, scala.concurrent.duration.DurationInt(5).millis) 30 | w.await(timeout(500 millis), dismissals(2)) 31 | } 32 | "be true for Schedule.again (second run)" in { 33 | Schedule.once(testfunc2, scala.concurrent.duration.DurationInt(5).millis) 34 | w.await(timeout(500 millis), dismissals(2)) 35 | } 36 | } 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Base64.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | object Base64 { 4 | /** Encodes the given String into a Base64 String. **/ 5 | def encodeString(in: String): String = java.util.Base64.getUrlEncoder.encodeToString(in.getBytes("UTF-8")) 6 | 7 | /** Encodes the given ByteArray into a Base64 String. **/ 8 | def encodeString(in: Array[Byte]): String = java.util.Base64.getUrlEncoder.encodeToString(in) 9 | 10 | /** Encodes the given String into a Base64 ByteArray. **/ 11 | def encodeBinary(in: String): Array[Byte] = java.util.Base64.getUrlEncoder.encode(in.getBytes("UTF-8")) 12 | 13 | /** Encodes the given ByteArray into a Base64 ByteArray. **/ 14 | def encodeBinary(in: Array[Byte]): Array[Byte] = java.util.Base64.getUrlEncoder.encode(in) 15 | 16 | /** Decodes the given Base64-ByteArray into a String. **/ 17 | def decodeString(in: Array[Byte]): String = new String(java.util.Base64.getUrlDecoder.decode(in), "UTF-8") 18 | 19 | /** Decodes the given Base64-String into a String. **/ 20 | def decodeString(in: String): String = new String(java.util.Base64.getUrlDecoder.decode(in), "UTF-8") 21 | 22 | /** Decodes the given Base64-String into a ByteArray. **/ 23 | def decodeBinary(in: String): Array[Byte] = java.util.Base64.getUrlDecoder.decode(in) 24 | 25 | /** Decodes the given Base64-ByteArray into a ByteArray. **/ 26 | def decodeBinary(in: Array[Byte]): Array[Byte] = java.util.Base64.getUrlDecoder.decode(in) 27 | } 28 | -------------------------------------------------------------------------------- /sbt: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | root=$( 4 | cd $(dirname $(readlink $0 || echo $0))/.. 5 | pwd 6 | ) 7 | 8 | sbtver=0.13.9 9 | sbtjar=sbt/bin/sbt-launch.jar 10 | sbtsum=767d963ed266459aa8bf32184599786d 11 | 12 | mkdir -p target 13 | 14 | function download { 15 | echo "downloading sbt $sbtver" 1>&2 16 | curl -L -o target/sbt-${sbtver}.tgz "https://dl.bintray.com/sbt/native-packages/sbt/${sbtver}/sbt-${sbtver}.tgz" 17 | tar -zxpf target/sbt-${sbtver}.tgz -C target/ 18 | } 19 | 20 | function sbtjar_md5 { 21 | openssl md5 < target/${sbtjar} | cut -f2 -d'=' | awk '{print $1}' 22 | } 23 | 24 | if [ ! -f "target/${sbtjar}" ]; then 25 | download 26 | fi 27 | 28 | test -f "target/${sbtjar}" || exit 1 29 | 30 | jarmd5=$(sbtjar_md5) 31 | if [ "${jarmd5}" != "${sbtsum}" ]; then 32 | echo "Bad MD5 checksum on ${sbtjar}!" 1>&2 33 | download 34 | 35 | jarmd5=$(sbtjar_md5) 36 | if [ "${jarmd5}" != "${sbtsum}" ]; then 37 | echo "Bad MD5 checksum *AGAIN*!" 1>&2 38 | exit 1 39 | fi 40 | fi 41 | 42 | test -f ~/.sbtconfig && . ~/.sbtconfig 43 | 44 | if [ -f /usr/jrebel/jrebel.jar ]; then 45 | JREBEL="-noverify -javaagent:/usr/jrebel/jrebel.jar -Drebel.lift_plugin=true" 46 | fi 47 | 48 | java -ea -server $SBT_OPTS $JAVA_OPTS $JREBEL \ 49 | -XX:+AggressiveOpts \ 50 | -XX:+OptimizeStringConcat \ 51 | -XX:+UseConcMarkSweepGC \ 52 | -Xms128M \ 53 | -Xmx2G \ 54 | -Djava.net.preferIPv4Stack=true \ 55 | -jar target/$sbtjar "$@" 56 | 57 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/HashingSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import io.wasted.util.{ Hashing, HashingAlgo, HexingAlgo } 4 | import org.scalatest._ 5 | 6 | class HashingSpec extends WordSpec { 7 | implicit val hashingAlgo = HashingAlgo("HmacSHA256") 8 | implicit val hexingAlgo = HexingAlgo("SHA") 9 | 10 | val ourString = "this must work!!" 11 | val ourHexDigest = "c2bf26e94cab462fa275a3dc41f04cf3e67d470a" 12 | val ourSignature = "6efac23cabff39ec218e18a7a2494591095e74913ada965fbf8ad9d9b9f38d91" 13 | val ourHexSignature = "this works?!" 14 | 15 | val theirHexDigest = Hashing.hexDigest(ourString.getBytes("UTF-8")) 16 | val theirSignature = Hashing.sign(ourString, theirHexDigest) 17 | val theirHexSignature = Hashing.hexEncode(ourHexSignature.getBytes("UTF-8")) 18 | 19 | "Precalculated hex-digest (" + ourHexDigest + ")" should { 20 | "be the same as the calculated (" + theirHexDigest + ")" in { 21 | assert(ourHexDigest == theirHexDigest) 22 | } 23 | } 24 | 25 | "Precalculated hex-encoded (" + ourHexSignature + ")" should { 26 | "be the same as the calculated (" + theirHexSignature + ")" in { 27 | assert(ourHexSignature == new String(Hashing.hexDecode(theirHexSignature), "UTF-8")) 28 | } 29 | } 30 | 31 | "Precalculated sign (" + ourSignature + ")" should { 32 | "be the same as the calculated (" + theirSignature + ")" in { 33 | assert(ourSignature == theirSignature) 34 | } 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/HostInformation.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.net.{ InetAddress, NetworkInterface } 4 | 5 | import scala.collection.JavaConversions._ 6 | 7 | object HostInformation { 8 | 9 | /** 10 | * Get the hash of the jar-file being used. 11 | */ 12 | def jarHash: Option[String] = { 13 | val path = this.getClass.getProtectionDomain.getCodeSource.getLocation.getPath 14 | val decodedPath = java.net.URLDecoder.decode(path, "UTF-8") 15 | new java.io.File(decodedPath).isFile match { 16 | case true => Some(Hashing.hexFileDigest(decodedPath)(HexingAlgo("SHA1"))) 17 | case _ => None 18 | } 19 | } 20 | 21 | /** 22 | * Get all IP Addresses from this system. 23 | */ 24 | def ipAddresses: List[InetAddress] = NetworkInterface.getNetworkInterfaces.flatMap { f => 25 | f.getInetAddresses.map(_.getHostAddress.replaceAll("%.*$", "").toLowerCase) ++ 26 | f.getSubInterfaces.flatMap(_.getInetAddresses.map(_.getHostAddress.replaceAll("%.*$", "").toLowerCase)) 27 | }.toList.sortWith((e1, e2) => (e1 compareTo e2) < 0).map(InetAddress.getByName) 28 | 29 | /** 30 | * Get all MAC Addresses from this system. 31 | */ 32 | def macAddresses: List[String] = NetworkInterface.getNetworkInterfaces.flatMap { 33 | case iface: NetworkInterface if iface.getHardwareAddress != null => 34 | Some(iface.getHardwareAddress.map("%02X" format _).mkString(":")) 35 | case _ => None 36 | }.toList.sortWith((e1, e2) => (e1 compareTo e2) < 0) 37 | } 38 | 39 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/ExceptionHandler.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.http 2 | 3 | import io.netty.channel.ChannelHandlerContext 4 | 5 | /** 6 | * Basic Netty Exception-handling with Logging capabilities. 7 | */ 8 | object ExceptionHandler { 9 | /** 10 | * Precompiled Patterns for performance reasons. 11 | * Filters unimportant/low level exceptions. 12 | */ 13 | private val unimportant = List( 14 | "java.net.ConnectException.*".r, 15 | "java.nio.channels.ClosedChannelException.*".r, 16 | "io.netty.handler.codec.CorruptedFrameException.*".r, 17 | "java.io.IOException.*".r, 18 | "javax.net.ssl.SSLException.*".r, 19 | "java.lang.IllegalArgumentException.*".r) 20 | 21 | /** 22 | * Bad client = closed connection, malformed requests, etc. 23 | * 24 | * Do nothing if the exception is one of the following: 25 | * java.io.IOException: Connection reset by peer 26 | * java.io.IOException: Broken pipe 27 | * java.nio.channels.ClosedChannelException: null 28 | * javax.net.ssl.SSLException: not an SSL/TLS record (Use http://... URL to connect to HTTPS server) 29 | * java.lang.IllegalArgumentException: empty text (Use http://... URL to connect to HTTPS server) 30 | */ 31 | def apply(ctx: ChannelHandlerContext, cause: Throwable): Option[Throwable] = { 32 | val s = cause.toString 33 | if (unimportant.exists(_.findFirstIn(s).isDefined)) None 34 | else { 35 | if (ctx.channel.isOpen) ctx.channel.close() 36 | Some(cause) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/HttpResponder.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.http 2 | 3 | import io.netty.buffer._ 4 | import io.netty.handler.codec.http.HttpVersion._ 5 | import io.netty.handler.codec.http._ 6 | import io.netty.util.CharsetUtil 7 | 8 | /** 9 | * Responder class used in our server applications 10 | * @param token HTTP Server token 11 | * @param allocator ByteBuf Allocator 12 | */ 13 | class HttpResponder(token: String, allocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT) { 14 | def apply( 15 | status: HttpResponseStatus, 16 | body: Option[String] = None, 17 | mime: Option[String] = None, 18 | keepAlive: Boolean = true, 19 | headers: Map[String, String] = Map()): FullHttpResponse = { 20 | val res = body.map { body => 21 | val bytes = body.getBytes(CharsetUtil.UTF_8) 22 | val content = allocator.ioBuffer(bytes.length, bytes.length) 23 | content.writeBytes(bytes).slice() 24 | val res = new DefaultFullHttpResponse(HTTP_1_1, status, content) 25 | HttpUtil.setContentLength(res, content.readableBytes()) 26 | res 27 | } getOrElse { 28 | val res = new DefaultFullHttpResponse(HTTP_1_1, status) 29 | HttpUtil.setContentLength(res, 0) 30 | res 31 | } 32 | 33 | mime.map(res.headers.set(HttpHeaderNames.CONTENT_TYPE, _)) 34 | res.headers.set(HttpHeaderNames.SERVER, token) 35 | headers.foreach { h => res.headers.set(h._1, h._2) } 36 | 37 | res.headers.set(HttpHeaderNames.CONNECTION, if (!keepAlive) HttpHeaderValues.CLOSE else HttpHeaderValues.KEEP_ALIVE) 38 | res.retain() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/Headers.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.http 2 | 3 | import io.netty.handler.codec.http.HttpHeaderNames._ 4 | import io.netty.handler.codec.http.HttpRequest 5 | 6 | import scala.collection.JavaConversions._ 7 | 8 | trait WastedHttpHeaders { 9 | def get(key: io.netty.util.AsciiString): Option[String] = getAll(key.toString).headOption 10 | def get(key: String): Option[String] = getAll(key).headOption 11 | def apply(key: io.netty.util.AsciiString): String = get(key).getOrElse(scala.sys.error("Header doesn't exist")) 12 | def apply(key: String): String = get(key).getOrElse(scala.sys.error("Header doesn't exist")) 13 | def getAll(key: String): Iterable[String] 14 | val length: Int 15 | lazy val cors: Map[String, String] = { 16 | for { 17 | corsMethods <- this.get(ACCESS_CONTROL_REQUEST_METHOD) 18 | corsHeaders <- this.get(ACCESS_CONTROL_REQUEST_HEADERS) 19 | corsOrigin <- this.get(ORIGIN.toString) 20 | } yield Map( 21 | ACCESS_CONTROL_ALLOW_METHODS.toString -> corsMethods, 22 | ACCESS_CONTROL_ALLOW_HEADERS.toString -> corsHeaders, 23 | ACCESS_CONTROL_ALLOW_ORIGIN.toString -> corsOrigin) 24 | } getOrElse Map() 25 | } 26 | 27 | /** 28 | * Parser HTTP Request headers and give back a nice map 29 | * @param corsOrigin Origin for CORS Request if we want to add them onto a HTTP Request 30 | */ 31 | class Headers(corsOrigin: String = "*") { 32 | def get(request: HttpRequest): WastedHttpHeaders = { 33 | val headers: Map[String, Seq[String]] = request.headers.names.map(key => 34 | key.toLowerCase -> Seq(request.headers.get(key))).toMap 35 | 36 | new WastedHttpHeaders { 37 | def getAll(key: String): Iterable[String] = headers.getOrElse(key.toLowerCase, Seq()) 38 | override def toString = headers.toString() 39 | override lazy val length = headers.size 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Logger.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import org.slf4j.LoggerFactory 4 | 5 | /** 6 | * This trait enables classes to do easy logging. 7 | */ 8 | trait Logger { 9 | 10 | /** 11 | * Override this to give your class a custom Logger name. 12 | */ 13 | protected def loggerName = this.getClass.getCanonicalName 14 | 15 | private[this] lazy val logger = LoggerFactory.getLogger(loggerName) 16 | 17 | /** 18 | * Implement this method to get your exceptions handled the way you want. 19 | */ 20 | protected def submitException(trace: String): Unit = {} 21 | 22 | /** 23 | * Prints a message on debug. 24 | */ 25 | def debug(msg: => String, x: Any*) { 26 | if (!logger.isDebugEnabled) return 27 | x.foreach { case msg: Throwable => submitException(msg) case _ => } 28 | logger.debug(msg.format(x: _*)) 29 | } 30 | 31 | /** 32 | * Prints a message on info. 33 | */ 34 | def info(msg: => String, x: Any*) { 35 | if (!logger.isInfoEnabled) return 36 | x.foreach { case msg: Throwable => submitException(msg) case _ => } 37 | logger.info(msg.format(x: _*)) 38 | } 39 | 40 | /** 41 | * Prints a message on warn. 42 | */ 43 | def warn(msg: => String, x: Any*) { 44 | if (!logger.isWarnEnabled) return 45 | x.foreach { case msg: Throwable => submitException(msg) case _ => } 46 | logger.warn(msg.format(x: _*)) 47 | } 48 | 49 | /** 50 | * Prints a message on error. 51 | */ 52 | def error(msg: => String, x: Any*) { 53 | if (!logger.isErrorEnabled) return 54 | x.foreach { case msg: Throwable => submitException(msg) case _ => } 55 | logger.error(msg.format(x: _*)) 56 | } 57 | } 58 | 59 | /** 60 | * Helper to create stand-alone loggers with fixed names. 61 | */ 62 | object Logger { 63 | def apply[T](clazz: Class[T]): Logger = new Logger { 64 | override val loggerName = clazz.getCanonicalName 65 | } 66 | 67 | def apply(name: String): Logger = new Logger { 68 | override val loggerName = name 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Package.scala: -------------------------------------------------------------------------------- 1 | package io.wasted 2 | 3 | import java.io.{ InputStream, PrintWriter, StringWriter } 4 | import java.security.KeyStore 5 | import javax.net.ssl.KeyManagerFactory 6 | 7 | import com.twitter.util.{ Duration => TD } 8 | import io.netty.channel.{ ChannelFuture, ChannelFutureListener } 9 | import io.netty.handler.ssl.SslContextBuilder 10 | 11 | import scala.concurrent.duration.{ Duration => SD } 12 | 13 | /** 14 | * Helpers 15 | */ 16 | package object util { 17 | implicit val hashingAlgo = HashingAlgo() 18 | implicit val hexingAlgo = HexingAlgo() 19 | implicit val cryptoCipher = CryptoCipher() 20 | 21 | /** 22 | * Transforms StackTraces into a String using StringWriter. 23 | */ 24 | def stackTraceToString(throwable: Throwable) = { 25 | val w = new StringWriter 26 | throwable.printStackTrace(new PrintWriter(w)) 27 | w.toString 28 | } 29 | 30 | implicit val implicitStackTraceToString = stackTraceToString _ 31 | 32 | implicit val durationScala2Twitter: SD => TD = (sd) => TD(sd.length, sd.unit) 33 | implicit val durationTwitter2Scala: TD => SD = (td) => SD(td.inTimeUnit._1, td.inTimeUnit._2) 34 | 35 | implicit val channelFutureListener: (ChannelFuture => Any) => ChannelFutureListener = { pf => 36 | new ChannelFutureListener { 37 | override def operationComplete(f: ChannelFuture): Unit = pf(f) 38 | } 39 | } 40 | 41 | object KeyStoreType extends Enumeration { 42 | val P12 = Value("PKCS12") 43 | val JKS = Value("JKS") 44 | } 45 | 46 | implicit class OurSslBuilder(val builder: SslContextBuilder) extends AnyVal { 47 | def keyManager(store: InputStream, secret: String, keyStoreType: KeyStoreType.Value): SslContextBuilder = { 48 | val secretArray = secret.toCharArray 49 | val ks = KeyStore.getInstance(keyStoreType.toString) 50 | ks.load(store, secretArray) 51 | 52 | val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) 53 | kmf.init(ks, secretArray) 54 | 55 | builder.keyManager(kmf) 56 | } 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/HttpSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | import com.twitter.conversions.time._ 7 | import com.twitter.util.Await 8 | import io.netty.handler.codec.http._ 9 | import io.netty.util.CharsetUtil 10 | import io.wasted.util.http._ 11 | import org.scalatest._ 12 | import org.scalatest.concurrent._ 13 | 14 | class HttpSpec extends WordSpec with ScalaFutures with AsyncAssertions with BeforeAndAfter { 15 | 16 | val responder = new HttpResponder("wasted-http") 17 | var server = new AtomicReference[HttpServer[FullHttpRequest, HttpResponse]](null) 18 | 19 | before { 20 | server.set(HttpServer[FullHttpRequest, HttpResponse](NettyHttpCodec()).handler { 21 | case (ctx, req) => 22 | req.map { req => 23 | val resp = if (req.uri == "/bad_gw") HttpResponseStatus.BAD_GATEWAY else HttpResponseStatus.ACCEPTED 24 | responder(resp) 25 | } 26 | }.bind(new InetSocketAddress(8888))) 27 | } 28 | 29 | val client1 = HttpClient(NettyHttpCodec[HttpRequest, FullHttpResponse]().withDecompression(false)) 30 | 31 | "GET Request to http://wasted.io" should { 32 | "contain the phrase \"wasted\" somewhere" in { 33 | val resp1: FullHttpResponse = Await.result(client1.get(new java.net.URI("http://wasted.io/")), 5.seconds) 34 | assert(resp1.content.toString(CharsetUtil.UTF_8).contains("wasted")) 35 | resp1.content.release() 36 | } 37 | } 38 | 39 | val client2 = HttpClient(NettyHttpCodec[HttpRequest, FullHttpResponse]()).withTcpKeepAlive(true) 40 | 41 | "GET Request to embedded Http Server" should { 42 | "returns status code ACCEPTED" in { 43 | val resp2: FullHttpResponse = Await.result(client2.get(new java.net.URI("http://localhost:8888/")), 5.seconds) 44 | assert(resp2.status() equals HttpResponseStatus.ACCEPTED) 45 | resp2.content.release() 46 | } 47 | "returns status code BAD_GATEWAY" in { 48 | val resp3: FullHttpResponse = Await.result(client2.get(new java.net.URI("http://localhost:8888/bad_gw")), 5.seconds) 49 | assert(resp3.status() equals HttpResponseStatus.BAD_GATEWAY) 50 | resp3.content.release() 51 | } 52 | } 53 | 54 | after(server.get.shutdown()) 55 | } 56 | 57 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/InetPrefixSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import java.net.InetAddress 4 | 5 | import io.wasted.util.InetPrefix 6 | import org.scalatest._ 7 | 8 | class InetPrefixSpec extends WordSpec { 9 | 10 | val ipv4network = InetPrefix(InetAddress.getByName("172.16.176.0"), 20) 11 | val ipv4first = InetAddress.getByName("172.16.176.0") 12 | val ipv4last = InetAddress.getByName("172.16.191.255") 13 | val ipv4invalid1 = InetAddress.getByName("172.16.175.5") 14 | val ipv4invalid2 = InetAddress.getByName("172.16.192.0") 15 | 16 | "IPv4 Network 172.16.176.0/20" should { 17 | "contain 172.16.176.0 as first valid address" in { 18 | assert(ipv4network.contains(ipv4first)) 19 | } 20 | "contain 172.16.191.255 as last valid address" in { 21 | assert(ipv4network.contains(ipv4last)) 22 | } 23 | "not contain 172.16.175.5" in { 24 | assert(!ipv4network.contains(ipv4invalid1)) 25 | } 26 | "not contain 172.16.192.0" in { 27 | assert(!ipv4network.contains(ipv4invalid2)) 28 | } 29 | } 30 | 31 | val ipv6first = InetAddress.getByName("2013:4ce8::") 32 | val ipv6network = InetPrefix(ipv6first, 32) 33 | val ipv6last = InetAddress.getByName("2013:4ce8:ffff:ffff:ffff:ffff:ffff:ffff") 34 | val ipv6invalid1 = InetAddress.getByName("2015:1234::") 35 | val ipv6invalid2 = InetAddress.getByName("aaaa:bbb::") 36 | 37 | "IPv6 Network 2013:4ce8::/32" should { 38 | "contain 2013:4ce8:: as first valid address" in { 39 | assert(ipv6network.contains(ipv6first)) 40 | } 41 | "contain 2013:4ce8:ffff:ffff:ffff:ffff:ffff:ffff as last valid address" in { 42 | assert(ipv6network.contains(ipv6last)) 43 | } 44 | "not contain 2015:1234::" in { 45 | assert(!ipv6network.contains(ipv6invalid1)) 46 | } 47 | "not contain aaaa:bbb::" in { 48 | assert(!ipv6network.contains(ipv6invalid2)) 49 | } 50 | } 51 | 52 | val ipv62addr = InetAddress.getByName("0::1") 53 | val ipv62network = InetPrefix(ipv62addr, 128) 54 | "IPv6 Network 0::1/128" should { 55 | "contain itself" in { 56 | assert(ipv62network.contains(ipv62addr)) 57 | } 58 | "not contain 2015:1234::" in { 59 | assert(!ipv62network.contains(ipv6invalid1)) 60 | } 61 | "not contain aaaa:bbb::" in { 62 | assert(!ipv62network.contains(ipv6invalid2)) 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Shell.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.io.{ BufferedReader, File, InputStreamReader } 4 | 5 | /** 6 | * Current working directory 7 | * @param file File object representing the directory 8 | */ 9 | case class CWD(file: File) 10 | 11 | /** 12 | * Companion for the current working directory 13 | */ 14 | object CWD { 15 | /** 16 | * Get a CWD for the given string path 17 | * @param path String path 18 | * @return Option of a CWD 19 | */ 20 | def apply(path: String): Option[CWD] = { 21 | val file = new File(path) 22 | if (file.exists && file.isDirectory) Some(CWD(file)) else None 23 | } 24 | } 25 | 26 | /** 27 | * Result of a Shell Operation 28 | * @param lines Output lines from the operation 29 | * @param exitValue Explains itself 30 | */ 31 | case class ShellOperation(lines: Seq[String], exitValue: Int) 32 | 33 | /** 34 | * Shell companion object 35 | */ 36 | object Shell { 37 | private[this] final val dummyLineFunc = (x: String) => x 38 | 39 | /** 40 | * Run the given cmd 41 | * @param cmd Command 42 | * @param cwd Current Working Directory 43 | * @return Shell Operation 44 | */ 45 | def run(cmd: String)(implicit cwd: CWD): ShellOperation = run(cmd :: Nil, dummyLineFunc) 46 | 47 | /** 48 | * Run the given cmd with params 49 | * @param cmd Command 50 | * @param params Command Line Parameters 51 | * @param cwd Current Working Directory 52 | * @return Shell Operation 53 | */ 54 | def run(cmd: String, params: String*)(implicit cwd: CWD): ShellOperation = run(Seq(cmd) ++ params, dummyLineFunc) 55 | 56 | /** 57 | * Run the given cmd with a Line-Function 58 | * @param cmds Command-Sequence 59 | * @param lineFunc Function to be used on every output-line 60 | * @param cwd Current Working Directory 61 | * @return Shell Operation 62 | */ 63 | def run(cmds: Seq[String], lineFunc: (String) => Any = dummyLineFunc)(implicit cwd: CWD): ShellOperation = { 64 | var output = Seq[String]() 65 | val process = Runtime.getRuntime.exec(cmds.toArray, null, cwd.file) 66 | val resultBuffer = new BufferedReader(new InputStreamReader(process.getInputStream)) 67 | 68 | // Parse the output from rsync line-based and send it to the actor 69 | var line: String = null 70 | do { 71 | line = resultBuffer.readLine 72 | output ++= List(line) 73 | lineFunc(line) 74 | } while (line != null) 75 | 76 | process.waitFor() 77 | resultBuffer.close() 78 | ShellOperation(output, process.exitValue) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/apn/Models.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.apn 2 | 3 | import io.netty.buffer._ 4 | import io.netty.handler.ssl.SslContextBuilder 5 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 6 | import io.netty.util.CharsetUtil 7 | import io.wasted.util.KeyStoreType 8 | 9 | /** 10 | * Apple Push Notification Message. 11 | * 12 | * @param deviceToken Apple Device Token as Hex-String 13 | * @param payload APN Payload 14 | * @param ident Transaction Identifier 15 | * @param expire Expiry 16 | */ 17 | case class Message(deviceToken: String, payload: String, prio: Int, ident: Int = 10, expire: Option[java.util.Date] = None) { 18 | lazy val bytes: ByteBuf = { 19 | val payloadBuf = Unpooled.copiedBuffer(payload, CharsetUtil.UTF_8) 20 | val deviceTokenA: Array[Byte] = deviceToken.grouped(2).map(Integer.valueOf(_, 16).toByte).toArray 21 | 22 | // take 5 times the max-message length 23 | val bufData = PooledByteBufAllocator.DEFAULT.buffer(5 * (3 + 32 + 256 + 4 + 4 + 1)) 24 | 25 | // frame data 26 | bufData.writeByte(1.toByte) 27 | bufData.writeShort(deviceTokenA.length) 28 | bufData.writeBytes(deviceTokenA) 29 | 30 | bufData.writeByte(2.toByte) 31 | bufData.writeShort(payloadBuf.readableBytes) 32 | bufData.writeBytes(payloadBuf) 33 | 34 | bufData.writeByte(3.toByte) 35 | bufData.writeShort(4) 36 | bufData.writeInt(ident) 37 | 38 | bufData.writeByte(4.toByte) 39 | bufData.writeShort(4) 40 | bufData.writeInt(expire.map(_.getTime / 1000).getOrElse(0L).toInt) // expiration 41 | 42 | bufData.writeByte(5.toByte) 43 | bufData.writeShort(1) 44 | bufData.writeByte(prio.toByte) // prio 45 | 46 | // 5 bytes for the header 47 | val bufHeader = PooledByteBufAllocator.DEFAULT.buffer(55) 48 | bufHeader.writeByte(2.toByte) // Command set version 2 49 | bufHeader.writeInt(bufData.readableBytes) 50 | 51 | val buf = Unpooled.copiedBuffer(bufHeader, bufData) 52 | bufData.release 53 | bufHeader.release 54 | buf 55 | } 56 | } 57 | 58 | /** 59 | * Apple Push Notification connection parameters 60 | * @param name Name of this connection 61 | * @param p12 InputStream of the P12 Certificate 62 | * @param secret Secret for the P12 Certificate 63 | * @param sandbox Wether to use Sandbox or Production 64 | * @param timeout Connection timeout, default shouldb e fine 65 | */ 66 | case class Params(name: String, p12: java.io.InputStream, secret: String, sandbox: Boolean = false, timeout: Int = 5) { 67 | lazy val sslCtx = SslContextBuilder.forClient 68 | .trustManager(InsecureTrustManagerFactory.INSTANCE) 69 | .keyManager(p12, secret, KeyStoreType.P12) 70 | .build() 71 | } 72 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/HttpsSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | import com.twitter.conversions.time._ 7 | import com.twitter.util.Await 8 | import io.netty.handler.codec.http._ 9 | import io.netty.handler.ssl.SslContextBuilder 10 | import io.netty.handler.ssl.util.SelfSignedCertificate 11 | import io.netty.util.CharsetUtil 12 | import io.wasted.util.http._ 13 | import org.scalatest._ 14 | import org.scalatest.concurrent._ 15 | 16 | class HttpsSpec extends WordSpec with ScalaFutures with AsyncAssertions with BeforeAndAfter { 17 | 18 | val responder = new HttpResponder("wasted-http") 19 | 20 | val cert = new SelfSignedCertificate() 21 | val sslCtx = SslContextBuilder.forServer(cert.certificate(), cert.privateKey()).build() 22 | var server = new AtomicReference[HttpServer[FullHttpRequest, HttpResponse]](null) 23 | 24 | before { 25 | server.set(HttpServer[FullHttpRequest, HttpResponse](NettyHttpCodec() 26 | .withTls(sslCtx)) 27 | .handler { 28 | case (ctx, req) => 29 | req.map { req => 30 | val resp = if (req.uri() == "/bad_gw") HttpResponseStatus.BAD_GATEWAY else HttpResponseStatus.ACCEPTED 31 | responder(resp) 32 | } 33 | }.bind(new InetSocketAddress(8889))) 34 | } 35 | 36 | val client1 = HttpClient(NettyHttpCodec[HttpRequest, FullHttpResponse]().withInsecureTls()) 37 | 38 | "2 GET Request to https://anycast.io" should { 39 | "contain the phrase \"anycast\" somewhere" in { 40 | val resp1: FullHttpResponse = Await.result(client1.get(new java.net.URI("https://anycast.io:443/")), 5.seconds) 41 | assert(resp1.content.toString(CharsetUtil.UTF_8).contains("anycast")) 42 | resp1.content.release() 43 | } 44 | "another phrase of \"anycast\" somewhere" in { 45 | val resp2: FullHttpResponse = Await.result(client1.get(new java.net.URI("https://anycast.io:443/")), 5.seconds) 46 | assert(resp2.content.toString(CharsetUtil.UTF_8).contains("anycast")) 47 | resp2.content.release() 48 | } 49 | } 50 | 51 | "GET Request to embedded Http Server" should { 52 | "returns status code ACCEPTED" in { 53 | val resp3: FullHttpResponse = Await.result(client1.get(new java.net.URI("https://localhost:8889/")), 5.seconds) 54 | assert(resp3.status() equals HttpResponseStatus.ACCEPTED) 55 | resp3.content.release() 56 | } 57 | "returns status code BAD_GATEWAY" in { 58 | val resp4: FullHttpResponse = Await.result(client1.get(new java.net.URI("https://localhost:8889/bad_gw")), 5.seconds) 59 | assert(resp4.status() equals HttpResponseStatus.BAD_GATEWAY) 60 | resp4.content.release() 61 | } 62 | } 63 | 64 | after(server.get.shutdown()) 65 | } 66 | 67 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/apn/FeedbackService.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.apn 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import io.netty.bootstrap._ 6 | import io.netty.buffer._ 7 | import io.netty.channel.ChannelHandler.Sharable 8 | import io.netty.channel._ 9 | import io.netty.channel.socket.SocketChannel 10 | import io.netty.channel.socket.nio.NioSocketChannel 11 | import io.wasted.util._ 12 | 13 | /** 14 | * Feedback Service response 15 | * @param token Token of the device 16 | * @param expired When the device expired 17 | */ 18 | case class Feedback(token: String, expired: java.util.Date) 19 | 20 | /** 21 | * Apple Push Notification Push class which will handle all delivery. 22 | * 23 | * @param params Connection Parameters 24 | */ 25 | @Sharable 26 | class FeedbackService(params: Params, function: Feedback => AnyVal) 27 | extends SimpleChannelInboundHandler[ByteBuf] with Logger { thisService => 28 | override val loggerName = getClass.getCanonicalName + ":" + params.name 29 | 30 | private final val production = new InetSocketAddress(java.net.InetAddress.getByName("feedback.push.apple.com"), 2196) 31 | private final val sandbox = new InetSocketAddress(java.net.InetAddress.getByName("feedback.sandbox.push.apple.com"), 2196) 32 | val addr: InetSocketAddress = if (params.sandbox) sandbox else production 33 | 34 | private val srv = new Bootstrap() 35 | private val bootstrap = srv.group(Netty.eventLoop) 36 | .channel(classOf[NioSocketChannel]) 37 | .remoteAddress(addr) 38 | .option[java.lang.Boolean](ChannelOption.TCP_NODELAY, true) 39 | .option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) 40 | .option[java.lang.Boolean](ChannelOption.SO_REUSEADDR, true) 41 | .option[java.lang.Integer](ChannelOption.SO_LINGER, 0) 42 | .option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, params.timeout * 1000) 43 | .handler(new ChannelInitializer[SocketChannel] { 44 | override def initChannel(ch: SocketChannel) { 45 | val p = ch.pipeline() 46 | p.addLast("ssl", params.sslCtx.newHandler(ch.alloc())) 47 | p.addLast("handler", thisService) 48 | } 49 | }) 50 | 51 | def run(): Boolean = Tryo(bootstrap.clone.connect().sync().channel()).isDefined 52 | 53 | override def channelRead0(ctx: ChannelHandlerContext, buf: ByteBuf) { 54 | if (buf.readableBytes > 6) { 55 | val ts = buf.readInt() 56 | val length = buf.readShort() 57 | function(Feedback(new String(buf.readBytes(length).array), new java.util.Date(ts * 1000))) 58 | } 59 | } 60 | 61 | override def channelInactive(ctx: ChannelHandlerContext) { 62 | info("APN Feedback disconnected!") 63 | } 64 | 65 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 66 | http.ExceptionHandler(ctx, cause).foreach(_.printStackTrace()) 67 | ctx.close() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyTcpServer.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import com.twitter.conversions.time._ 4 | import com.twitter.util.{ Duration, Future } 5 | import io.netty.buffer.{ ByteBuf, ByteBufAllocator, PooledByteBufAllocator } 6 | import io.netty.channel._ 7 | 8 | object NettyTcpServer { 9 | private[util] val defaultHandler: (Channel, Future[ByteBuf]) => Future[ByteBuf] = { (ch, msg) => 10 | Future.value(null) 11 | } 12 | } 13 | 14 | /** 15 | * wasted.io Scala Server 16 | * @param codec Http Codec 17 | * @param tcpConnectTimeout TCP Connect Timeout 18 | * @param tcpKeepAlive TCP KeepAlive 19 | * @param reuseAddr Reuse-Address 20 | * @param tcpNoDelay TCP No-Delay 21 | * @param soLinger soLinger 22 | * @param sendAllocator ByteBuf send Allocator 23 | * @param recvAllocator ByteBuf recv Allocator 24 | * @param parentLoop Netty Event-Loop for Parents 25 | * @param childLoop Netty Event-Loop for Children 26 | * @param pipeline Setup extra handlers on the Netty Pipeline 27 | * @param handle Service to handle HttpRequests 28 | */ 29 | case class NettyTcpServer[Req, Resp](codec: NettyCodec[Req, Resp], 30 | tcpConnectTimeout: Duration = 5.seconds, 31 | tcpKeepAlive: Boolean = false, 32 | reuseAddr: Boolean = true, 33 | tcpNoDelay: Boolean = true, 34 | soLinger: Int = 0, 35 | sendAllocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT, 36 | recvAllocator: RecvByteBufAllocator = new AdaptiveRecvByteBufAllocator, 37 | parentLoop: EventLoopGroup = Netty.eventLoop, 38 | childLoop: EventLoopGroup = Netty.eventLoop, 39 | pipeline: Channel => Unit = p => (), 40 | handle: (Channel, Future[Req]) => Future[Resp] = NettyTcpServer.defaultHandler) 41 | extends NettyServerBuilder[NettyTcpServer[Req, Resp], Req, Resp] { 42 | 43 | def withSoLinger(soLinger: Int) = copy[Req, Resp](soLinger = soLinger) 44 | def withTcpNoDelay(tcpNoDelay: Boolean) = copy[Req, Resp](tcpNoDelay = tcpNoDelay) 45 | def withTcpKeepAlive(tcpKeepAlive: Boolean) = copy[Req, Resp](tcpKeepAlive = tcpKeepAlive) 46 | def withReuseAddr(reuseAddr: Boolean) = copy[Req, Resp](reuseAddr = reuseAddr) 47 | def withTcpConnectTimeout(tcpConnectTimeout: Duration) = copy[Req, Resp](tcpConnectTimeout = tcpConnectTimeout) 48 | def withPipeline(pipeline: (Channel) => Unit) = copy[Req, Resp](pipeline = pipeline) 49 | def handler(handle: (Channel, Future[Req]) => Future[Resp]) = copy[Req, Resp](handle = handle) 50 | 51 | def withEventLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](parentLoop = eventLoop, childLoop = eventLoop) 52 | def withEventLoop(parentLoop: EventLoopGroup, childLoop: EventLoopGroup) = copy[Req, Resp](parentLoop = parentLoop, childLoop = childLoop) 53 | def withChildLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](childLoop = eventLoop) 54 | def withParentLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](parentLoop = eventLoop) 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/ConfigSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import io.wasted.util.Config 6 | import org.scalatest._ 7 | 8 | class ConfigSpec extends WordSpec { 9 | val ourInt = 5 10 | val ourBool = true 11 | val ourString = "chicken" 12 | val ourList = List("foo", "bar", "baz") 13 | val ourAddrs = List(new InetSocketAddress("127.0.0.1", 8080), new InetSocketAddress("::", 8081)) 14 | 15 | "Preset Integer (" + ourInt + ")" should { 16 | val testVal = Config.getInt("test.int") 17 | "be the same as the config value (" + testVal + ")" in { 18 | assert(testVal.contains(ourInt)) 19 | } 20 | } 21 | 22 | "Preset Boolean (" + ourBool + ")" should { 23 | val testVal = Config.getBool("test.bool") 24 | "be the same as the config value (" + testVal + ")" in { 25 | assert(testVal.contains(ourBool)) 26 | } 27 | } 28 | 29 | "Preset String (" + ourString + ")" should { 30 | val testVal = Config.getString("test.string") 31 | "be the same as the config value (" + testVal + ")" in { 32 | assert(testVal.contains(ourString)) 33 | } 34 | } 35 | 36 | "Preset String-List (" + ourList + ")" should { 37 | val testVal = Config.getStringList("test.list") 38 | "be the same as the config value (" + testVal + ")" in { 39 | assert(testVal.contains(ourList)) 40 | } 41 | } 42 | 43 | "Preset InetSocketAddress-List (" + ourAddrs + ")" should { 44 | val testVal = Config.getInetAddrList("test.addrs") 45 | "be the same as the config value (" + testVal + ")" in { 46 | assert(testVal.contains(ourAddrs)) 47 | } 48 | } 49 | 50 | "Non-existing config-lookups" should { 51 | "for Integers be the None if no default-value was given" in { 52 | assert(Config.getInt("foo").isEmpty) 53 | } 54 | "for Integers be Some(5) if a default-value of 5 was given" in { 55 | assert(Config.getInt("foo", 5) == 5) 56 | } 57 | "for Boolean be the None if no default-value was given" in { 58 | assert(Config.getBool("foo").isEmpty) 59 | } 60 | "for Boolean be Some(true) if a default-value of true was given" in { 61 | assert(Config.getBool("foo", fallback = true)) 62 | } 63 | "for String be the None if no default-value was given" in { 64 | assert(Config.getString("foo").isEmpty) 65 | } 66 | "for String be Some(bar) if a default-value of bar was given" in { 67 | assert(Config.getString("foo", "bar") == "bar") 68 | } 69 | "for String-List be the None if no default-value was given" in { 70 | assert(Config.getStringList("foo").isEmpty) 71 | } 72 | "for String-List be Some(List(\"bar\", \"baz\")) if a default-value was given" in { 73 | assert(Config.getStringList("foo", List("bar", "baz")) == List("bar", "baz")) 74 | } 75 | "for InetSocketAddress-List be the None if no default-value was given" in { 76 | assert(Config.getInetAddrList("foo").isEmpty) 77 | } 78 | "for InetSocketAddress-List be Some(InetSocketAddress(\"1.2.3.4\", 80)) if a default-value was given" in { 79 | assert(Config.getInetAddrList("foo", List("1.2.3.4:80")) == List(new java.net.InetSocketAddress("1.2.3.4", 80))) 80 | } 81 | } 82 | 83 | } 84 | 85 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyServerBuilder.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.net.InetSocketAddress 4 | 5 | import com.twitter.util._ 6 | import io.netty.bootstrap.ServerBootstrap 7 | import io.netty.buffer.ByteBufAllocator 8 | import io.netty.channel._ 9 | import io.netty.channel.socket.SocketChannel 10 | import io.netty.channel.socket.nio.NioServerSocketChannel 11 | 12 | trait NettyServerBuilder[T, Req, Resp] extends Logger { self: T => 13 | def codec: NettyCodec[Req, Resp] 14 | def tcpConnectTimeout: Duration 15 | def tcpKeepAlive: Boolean 16 | def reuseAddr: Boolean 17 | def tcpNoDelay: Boolean 18 | def soLinger: Int 19 | def sendAllocator: ByteBufAllocator 20 | def recvAllocator: RecvByteBufAllocator 21 | def parentLoop: EventLoopGroup 22 | def childLoop: EventLoopGroup 23 | def pipeline: Channel => Unit 24 | def handle: (Channel, Future[Req]) => Future[Resp] 25 | 26 | protected[this] lazy val srv = new ServerBootstrap() 27 | protected[this] lazy val chan = { 28 | val pipelineFactory = new ChannelInitializer[SocketChannel] { 29 | override def initChannel(ch: SocketChannel): Unit = { 30 | codec.serverPipeline(ch) 31 | pipeline(ch) 32 | } 33 | } 34 | val bs = srv.group(parentLoop, childLoop) 35 | .channel(classOf[NioServerSocketChannel]) 36 | .option[java.lang.Boolean](ChannelOption.TCP_NODELAY, tcpNoDelay) 37 | .option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, tcpKeepAlive) 38 | .option[java.lang.Boolean](ChannelOption.SO_REUSEADDR, reuseAddr) 39 | .option[java.lang.Integer](ChannelOption.SO_LINGER, soLinger) 40 | .option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, tcpConnectTimeout.inMillis.toInt) 41 | .childHandler(pipelineFactory) 42 | bs.childOption[ByteBufAllocator](ChannelOption.ALLOCATOR, sendAllocator) 43 | bs.childOption[RecvByteBufAllocator](ChannelOption.RCVBUF_ALLOCATOR, recvAllocator) 44 | bs 45 | } 46 | 47 | private[this] var listeners = Map[InetSocketAddress, ChannelFuture]() 48 | 49 | /** 50 | * Bind synchronous onto the given Socket Address 51 | * @param addr Inet Socket Address to listen on 52 | * @return this HttpServer for chained calling 53 | */ 54 | def bind(addr: InetSocketAddress): T = synchronized { 55 | listeners += addr -> chan.bind(addr).awaitUninterruptibly() 56 | info("Listening on %s:%s", addr.getAddress.getHostAddress, addr.getPort) 57 | self 58 | } 59 | 60 | /** 61 | * Unbind synchronous from the given Socket Address 62 | * @param addr Inet Socket Address to unbind from 63 | * @return this HttpServer for chained calling 64 | */ 65 | def unbind(addr: InetSocketAddress): T = synchronized { 66 | listeners.get(addr).foreach { cf => 67 | cf.await() 68 | cf.channel().close() 69 | cf.channel().closeFuture().await() 70 | listeners = listeners - addr 71 | info("Removed listener on %s:%s", addr.getAddress.getHostAddress, addr.getPort) 72 | } 73 | self 74 | } 75 | 76 | /** 77 | * Shutdown this server and unbind all listeners 78 | */ 79 | def shutdown(): Unit = synchronized { 80 | info("Shutting down") 81 | listeners.foreach { 82 | case (addr, cf) => unbind(addr) 83 | } 84 | listeners = Map.empty 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/HttpRetrySpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.TimeUnit 5 | import java.util.concurrent.atomic.{ AtomicInteger, AtomicReference } 6 | 7 | import com.twitter.util.{ Duration, Future } 8 | import io.netty.handler.codec.http.{ FullHttpRequest, FullHttpResponse, HttpResponse, HttpResponseStatus } 9 | import io.wasted.util.Logger 10 | import io.wasted.util.http._ 11 | import org.scalatest._ 12 | import org.scalatest.concurrent._ 13 | import org.scalatest.time.Span 14 | 15 | class HttpRetrySpec extends FunSuite with ShouldMatchers with AsyncAssertions with BeforeAndAfter with Logger { 16 | 17 | val responder = new HttpResponder("wasted-http") 18 | val retries = 2 19 | val counter = new AtomicInteger() 20 | var server = new AtomicReference[HttpServer[FullHttpRequest, HttpResponse]](null) 21 | 22 | before { 23 | server.set(HttpServer[FullHttpRequest, HttpResponse](NettyHttpCodec()).handler { 24 | case (ctx, req) => 25 | req.map { req => 26 | if (req.uri() == "/sleep") { 27 | warn("at sleeper") 28 | Thread.sleep(50) 29 | } 30 | if (req.uri() == "/retry") { 31 | val reqCount = counter.incrementAndGet() 32 | warn("at " + reqCount) 33 | if (reqCount <= retries) Thread.sleep(100) 34 | } 35 | responder(HttpResponseStatus.OK) 36 | } 37 | }.bind(new InetSocketAddress(8887))) 38 | } 39 | 40 | val client1 = HttpClient[FullHttpResponse]().withSpecifics(NettyHttpCodec()).withTcpKeepAlive(true) 41 | test("Failing Timeout") { 42 | // warmup request 43 | client1.get(new java.net.URI("http://localhost:8887/warmup")).map(_.release()) 44 | 45 | val w = new Waiter // Do this in the main test thread 46 | client1.withGlobalTimeout(Duration(20, TimeUnit.MILLISECONDS)) 47 | .get(new java.net.URI("http://localhost:8887/sleep")).rescue { 48 | case t => 49 | w { () } 50 | w.dismiss() 51 | Future.value(null) 52 | } 53 | w.await() 54 | } 55 | 56 | test("Working Timeout") { 57 | val w = new Waiter // Do this in the main test thread 58 | client1.withRetries(0) 59 | .withTcpConnectTimeout(Duration(10, TimeUnit.MILLISECONDS)) 60 | .withRequestTimeout(Duration(90, TimeUnit.MILLISECONDS)) 61 | .withGlobalTimeout(Duration(100, TimeUnit.MILLISECONDS)) 62 | .get(new java.net.URI("http://localhost:8887/sleep")).map { resp => 63 | w { 64 | resp.status().code() should equal(200) 65 | } 66 | resp.release() 67 | w.dismiss() 68 | } 69 | w.await(PatienceConfiguration.Timeout((Span(1, org.scalatest.time.Seconds)))) 70 | } 71 | 72 | test("Retry") { 73 | val w = new Waiter // Do this in the main test thread 74 | client1.withRetries(retries) 75 | .withTcpConnectTimeout(Duration(10, TimeUnit.MILLISECONDS)) 76 | .withRequestTimeout(Duration(30, TimeUnit.MILLISECONDS)) 77 | .withGlobalTimeout(Duration(500, TimeUnit.MILLISECONDS)) 78 | .get(new java.net.URI("http://localhost:8887/retry")).map { resp => 79 | w { 80 | resp.status().code() should equal(200) 81 | } 82 | resp.release() 83 | w.dismiss() 84 | } 85 | w.await(timeout(Span(5, org.scalatest.time.Seconds))) 86 | } 87 | 88 | after(server.get.shutdown()) 89 | } 90 | 91 | -------------------------------------------------------------------------------- /src/site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wasted-util for Scala 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 |
18 |

wasted-util for Scala

19 |

Scala Utility Library used at wasted.io

20 | 21 |

View the Project on https://github.com/wasted/scala-util

22 | 23 | 24 | 29 |
30 |
31 |

wasted.io Scala Utility Library

32 | 33 |

API Docs

34 | 35 |
http://wasted.github.com/scala-util/latest/api/#io.wasted.util.package
36 | 37 |

Using our repository

38 | 39 |

40 |   resolvers += "wasted.io/repo" at "http://repo.wasted.io/mvn"
41 |   libraryDependencies += "io.wasted" %% "wasted-util" % "0.12.4"
42 |         
43 | 44 |

License

45 | 46 |
  Copyright 2012-2016 wasted.io Ltd <really@wasted.io>
47 | 
48 |           Licensed under the Apache License, Version 2.0 (the "License");
49 |           you may not use this file except in compliance with the License.
50 |           You may obtain a copy of the License at
51 | 
52 |               http://www.apache.org/licenses/LICENSE-2.0
53 | 
54 |           Unless required by applicable law or agreed to in writing, software
55 |           distributed under the License is distributed on an "AS IS" BASIS,
56 |           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
57 |           See the License for the specific language governing permissions and
58 |           limitations under the License.
59 |         
60 |
61 |
62 |

This project is maintained by wasted.io

63 |
64 |
65 | 66 | 70 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/site/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wasted-util for Scala 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 |
17 |
18 |

wasted-util for Scala

19 |

Scala Utility Library used at wasted.io

20 | 21 |

View the Project on https://github.com/wasted/scala-util

22 | 23 | 24 | 29 |
30 |
31 |

wasted.io Scala Utility Library

32 | 33 |

API Docs

34 | 35 |
http://wasted.github.com/scala-util/latest/api/#io.wasted.util.package
36 | 37 |

Using our repository

38 | 39 |

40 |   resolvers += "wasted.io/repo" at "http://repo.wasted.io/mvn"
41 |   libraryDependencies += "io.wasted" %% "wasted-util" % "%%VERSION%%"
42 |         
43 | 44 |

License

45 | 46 |
  Copyright 2012-2016 wasted.io Ltd <really@wasted.io>
47 | 
48 |           Licensed under the Apache License, Version 2.0 (the "License");
49 |           you may not use this file except in compliance with the License.
50 |           You may obtain a copy of the License at
51 | 
52 |               http://www.apache.org/licenses/LICENSE-2.0
53 | 
54 |           Unless required by applicable law or agreed to in writing, software
55 |           distributed under the License is distributed on an "AS IS" BASIS,
56 |           WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
57 |           See the License for the specific language governing permissions and
58 |           limitations under the License.
59 |         
60 |
61 |
62 |

This project is maintained by wasted.io

63 |
64 |
65 | 66 | 70 | 76 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/WebSocketClient.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | package http 3 | 4 | import java.net.{ InetAddress, InetSocketAddress } 5 | 6 | import com.twitter.util.{ Duration, Future } 7 | import io.netty.channel._ 8 | 9 | /** 10 | * wasted.io Scala WebSocket Client 11 | * @param codec Http Codec 12 | * @param remote Remote Host and Port 13 | * @param globalTimeout Global Timeout for the completion of a request 14 | * @param tcpConnectTimeout TCP Connect Timeout 15 | * @param connectTimeout Timeout for establishing the Service 16 | * @param requestTimeout Timeout for each request 17 | * @param tcpKeepAlive TCP KeepAlive. Defaults to false 18 | * @param reuseAddr Reuse-Address. Defaults to true 19 | * @param tcpNoDelay TCP No-Delay. Defaults to true 20 | * @param soLinger soLinger. Defaults to 0 21 | * @param retries On connection or timeouts, how often should we retry? Defaults to 0 22 | * @param eventLoop Netty Event-Loop 23 | */ 24 | final case class WebSocketClient(codec: NettyWebSocketCodec = NettyWebSocketCodec(), 25 | subprotocols: String = null, 26 | allowExtensions: Boolean = true, 27 | remote: List[InetSocketAddress] = List.empty, 28 | globalTimeout: Option[Duration] = None, 29 | tcpConnectTimeout: Option[Duration] = None, 30 | connectTimeout: Option[Duration] = None, 31 | requestTimeout: Option[Duration] = None, 32 | tcpKeepAlive: Boolean = false, 33 | reuseAddr: Boolean = true, 34 | tcpNoDelay: Boolean = true, 35 | soLinger: Int = 0, 36 | retries: Int = 0, 37 | eventLoop: EventLoopGroup = Netty.eventLoop)(implicit wheelTimer: WheelTimer) 38 | extends NettyClientBuilder[java.net.URI, NettyWebSocketChannel] { 39 | 40 | def withExtensions(allowExtensions: Boolean) = copy(allowExtensions = allowExtensions) 41 | def withSubprotocols(subprotocols: String) = copy(subprotocols = subprotocols) 42 | def withSpecifics(codec: NettyWebSocketCodec) = copy(codec = codec) 43 | def withSoLinger(soLinger: Int) = copy(soLinger = soLinger) 44 | def withTcpNoDelay(tcpNoDelay: Boolean) = copy(tcpNoDelay = tcpNoDelay) 45 | def withTcpKeepAlive(tcpKeepAlive: Boolean) = copy(tcpKeepAlive = tcpKeepAlive) 46 | def withReuseAddr(reuseAddr: Boolean) = copy(reuseAddr = reuseAddr) 47 | def withTcpConnectTimeout(tcpConnectTimeout: Duration) = copy(tcpConnectTimeout = Some(tcpConnectTimeout)) 48 | def withEventLoop(eventLoop: EventLoopGroup) = copy(eventLoop = eventLoop) 49 | def withRetries(retries: Int) = copy(retries = retries) 50 | def connectTo(host: String, port: Int) = copy(remote = List(new InetSocketAddress(InetAddress.getByName(host), port))) 51 | def connectTo(hosts: List[InetSocketAddress]) = copy(remote = hosts) 52 | 53 | private[this] def connect(): Future[NettyWebSocketChannel] = { 54 | val rand = scala.util.Random.nextInt(remote.length) 55 | val host = remote(rand) 56 | val proto = if (codec.sslCtx.isEmpty) "ws" else "wss" 57 | val uri = new java.net.URI(proto + "://" + host.getHostString + ":" + host.getPort) 58 | write(uri, uri) 59 | } 60 | 61 | def open(): Future[NettyWebSocketChannel] = { 62 | connect() 63 | } 64 | 65 | protected def getPort(url: java.net.URI): Int = if (url.getPort > 0) url.getPort else url.getScheme match { 66 | case "http" => 80 67 | case "https" => 443 68 | } 69 | 70 | } -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/redis/RedisClient.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | package redis 3 | 4 | import java.net.{ InetAddress, InetSocketAddress } 5 | 6 | import com.twitter.util.{ Duration, Future } 7 | import io.netty.channel._ 8 | 9 | /** 10 | * wasted.io Scala Redis Client 11 | * @param codec Redis Codec 12 | * @param remote Remote Host and Port 13 | * @param hostConnectionLimit Number of open connections for this client. Defaults to 1 14 | * @param globalTimeout Global Timeout for the completion of a request 15 | * @param tcpConnectTimeout TCP Connect Timeout 16 | * @param connectTimeout Timeout for establishing the Service 17 | * @param requestTimeout Timeout for each request 18 | * @param tcpKeepAlive TCP KeepAlive. Defaults to false 19 | * @param reuseAddr Reuse-Address. Defaults to true 20 | * @param tcpNoDelay TCP No-Delay. Defaults to true 21 | * @param soLinger soLinger. Defaults to 0 22 | * @param retries On connection or timeouts, how often should we retry? Defaults to 0 23 | * @param eventLoop Netty Event-Loop 24 | */ 25 | final case class RedisClient(codec: NettyRedisCodec = NettyRedisCodec(), 26 | remote: List[InetSocketAddress] = List.empty, 27 | hostConnectionLimit: Int = 1, 28 | globalTimeout: Option[Duration] = None, 29 | tcpConnectTimeout: Option[Duration] = None, 30 | connectTimeout: Option[Duration] = None, 31 | requestTimeout: Option[Duration] = None, 32 | tcpKeepAlive: Boolean = false, 33 | reuseAddr: Boolean = true, 34 | tcpNoDelay: Boolean = true, 35 | soLinger: Int = 0, 36 | retries: Int = 0, 37 | eventLoop: EventLoopGroup = Netty.eventLoop) 38 | extends NettyClientBuilder[java.net.URI, NettyRedisChannel] { 39 | 40 | def withSpecifics(codec: NettyRedisCodec) = copy(codec = codec) 41 | def withSoLinger(soLinger: Int) = copy(soLinger = soLinger) 42 | def withTcpNoDelay(tcpNoDelay: Boolean) = copy(tcpNoDelay = tcpNoDelay) 43 | def withTcpKeepAlive(tcpKeepAlive: Boolean) = copy(tcpKeepAlive = tcpKeepAlive) 44 | def withReuseAddr(reuseAddr: Boolean) = copy(reuseAddr = reuseAddr) 45 | def withGlobalTimeout(globalTimeout: Duration) = copy(globalTimeout = Some(globalTimeout)) 46 | def withTcpConnectTimeout(tcpConnectTimeout: Duration) = copy(tcpConnectTimeout = Some(tcpConnectTimeout)) 47 | def withConnectTimeout(connectTimeout: Duration) = copy(connectTimeout = Some(connectTimeout)) 48 | def withRequestTimeout(requestTimeout: Duration) = copy(requestTimeout = Some(requestTimeout)) 49 | def withHostConnectionLimit(limit: Int) = copy(hostConnectionLimit = limit) 50 | def withEventLoop(eventLoop: EventLoopGroup) = copy(eventLoop = eventLoop) 51 | def connectTo(host: String, port: Int) = copy(remote = List(new InetSocketAddress(InetAddress.getByName(host), port))) 52 | def connectTo(hosts: List[InetSocketAddress]) = copy(remote = hosts) 53 | 54 | protected def getPort(url: java.net.URI): Int = if (url.getPort > 0) url.getPort else 6379 55 | 56 | private[redis] def connect(): Future[NettyRedisChannel] = { 57 | val rand = scala.util.Random.nextInt(remote.length) 58 | val host = remote(rand) 59 | val uri = new java.net.URI("redis://" + host.getHostString + ":" + host.getPort) 60 | write(uri, uri) 61 | } 62 | 63 | def open(): Future[NettyRedisChannel] = { 64 | connect().map { redisChannel => 65 | redisChannel.setClient(this) 66 | redisChannel 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/WebSocketSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.atomic.AtomicReference 5 | 6 | import com.twitter.conversions.time._ 7 | import com.twitter.util.{ Await, Promise } 8 | import io.netty.buffer.Unpooled 9 | import io.netty.handler.codec.http._ 10 | import io.netty.handler.codec.http.websocketx.{ BinaryWebSocketFrame, TextWebSocketFrame } 11 | import io.netty.util.{ CharsetUtil, ReferenceCountUtil } 12 | import io.wasted.util.http._ 13 | import io.wasted.util.{ Logger, WheelTimer } 14 | import org.scalatest._ 15 | import org.scalatest.concurrent._ 16 | 17 | class WebSocketSpec extends WordSpec with ScalaFutures with AsyncAssertions with BeforeAndAfter with Logger { 18 | implicit val wheelTimer = WheelTimer 19 | 20 | val responder = new HttpResponder("wasted-ws") 21 | var server = new AtomicReference[HttpServer[FullHttpRequest, HttpResponse]](null) 22 | 23 | before { 24 | val socket1 = WebSocketHandler().onConnect { chan => 25 | info("client connected") 26 | }.onDisconnect { chan => 27 | info("client disconnected") 28 | }.handler { 29 | case (ctx, f) => println(f); Some(f.map(_.retain())) 30 | }.withHttpHandler { 31 | case (ctx, req) => 32 | req.map { req => 33 | val resp = if (req.uri == "/bad_gw") HttpResponseStatus.BAD_GATEWAY else HttpResponseStatus.ACCEPTED 34 | responder(resp) 35 | } 36 | } 37 | server.set(HttpServer(NettyHttpCodec[FullHttpRequest, HttpResponse]()) 38 | .handler(socket1.dispatch).bind(new InetSocketAddress(8890))) 39 | } 40 | 41 | val stringT = "worked" 42 | val string = new Promise[String] 43 | val bytesT: Array[Byte] = stringT.getBytes(CharsetUtil.UTF_8) 44 | val bytes = new Promise[String] 45 | 46 | val uri = new java.net.URI("http://localhost:8890/") 47 | 48 | "GET Request to embedded WebSocket Server" should { 49 | "open connection and send some data, close after" in { 50 | val client1 = Await.result(WebSocketClient().connectTo("127.0.0.1", 8890).open(), 5.seconds) 51 | client1.foreach { 52 | case text: TextWebSocketFrame => 53 | string.setValue(text.text()) 54 | text.release() 55 | case binary: BinaryWebSocketFrame => 56 | bytes.setValue(binary.content().toString(CharsetUtil.UTF_8)) 57 | binary.release() 58 | case x => 59 | ReferenceCountUtil.release(x) 60 | error("got " + x) 61 | } 62 | client1 ! new TextWebSocketFrame(stringT) 63 | client1 ! new BinaryWebSocketFrame(Unpooled.wrappedBuffer(bytesT).slice()) 64 | Thread.sleep(5000) 65 | } 66 | "returns the same string as sent" in { 67 | assert(Await.result(string, 2.seconds) equals stringT) 68 | } 69 | "returns the same string as sent in bytes" in { 70 | assert(Await.result(bytes, 2.seconds) equals stringT) 71 | } 72 | } 73 | 74 | val client2 = HttpClient[FullHttpResponse]().withSpecifics(NettyHttpCodec()).withTcpKeepAlive(true) 75 | 76 | "GET Request to embedded Http Server" should { 77 | "returns status code ACCEPTED" in { 78 | val resp2: FullHttpResponse = Await.result(client2.get(uri), 5.seconds) 79 | assert(resp2.status() equals HttpResponseStatus.ACCEPTED) 80 | resp2.content.release() 81 | } 82 | "returns status code BAD_GATEWAY" in { 83 | val resp3: FullHttpResponse = Await.result(client2.get(new java.net.URI("http://localhost:8890/bad_gw")), 5.seconds) 84 | assert(resp3.status() equals HttpResponseStatus.BAD_GATEWAY) 85 | resp3.content.release() 86 | } 87 | } 88 | 89 | after { 90 | server.get.shutdown() 91 | } 92 | } 93 | 94 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/LruMap.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.util.concurrent.Callable 4 | 5 | import com.google.common.cache._ 6 | 7 | case class KeyHolder[K](key: K) 8 | case class ValueHolder[V](value: V) 9 | 10 | /** 11 | * LruMap Companion to make creation easier 12 | */ 13 | object LruMap { 14 | def apply[K, V](maxSize: Int): LruMap[K, V] = 15 | new LruMap[K, V](maxSize, None, None) 16 | 17 | def apply[K, V](maxSize: Int, load: (K) => V): LruMap[K, V] = 18 | new LruMap[K, V](maxSize, Some(load), None) 19 | 20 | def apply[K, V](maxSize: Int, expire: (K, V) => Any): LruMap[K, V] = 21 | new LruMap[K, V](maxSize, None, Some(expire)) 22 | 23 | def apply[K, V](maxSize: Int, load: (K) => V, expire: (K, V) => Any): LruMap[K, V] = 24 | new LruMap[K, V](maxSize, Some(load), Some(expire)) 25 | } 26 | 27 | /** 28 | * LruMap Wrapper for Guava's Cache 29 | * 30 | * @param maxSize Maximum size of this cache 31 | * @param load Function to load objects 32 | * @param expire Function to be called on expired objects 33 | * @param builderConf Function to extend the CacheBuilder 34 | */ 35 | class LruMap[K, V](val maxSize: Int, 36 | load: Option[(K) => V], 37 | expire: Option[(K, V) => Any], 38 | builderConf: Option[CacheBuilder[AnyRef, AnyRef] => CacheBuilder[AnyRef, AnyRef]] = None) { lru => 39 | private[this] val loader: Option[CacheLoader[KeyHolder[K], ValueHolder[V]]] = lru.load.map { loadFunc => 40 | new CacheLoader[KeyHolder[K], ValueHolder[V]] { 41 | def load(key: KeyHolder[K]): ValueHolder[V] = ValueHolder(loadFunc(key.key)) 42 | } 43 | } 44 | 45 | private[this] val removal: Option[RemovalListener[KeyHolder[K], ValueHolder[V]]] = lru.expire.map { expireFunc => 46 | new RemovalListener[KeyHolder[K], ValueHolder[V]] { 47 | def onRemoval(removal: RemovalNotification[KeyHolder[K], ValueHolder[V]]): Unit = 48 | expireFunc(removal.getKey.key, removal.getValue.value) 49 | } 50 | } 51 | 52 | /** 53 | * Underlying Guava Cache 54 | */ 55 | val cache: Cache[KeyHolder[K], ValueHolder[V]] = { 56 | val builder = CacheBuilder.newBuilder().maximumSize(maxSize) 57 | builderConf.map(_(builder)) 58 | (loader, removal) match { 59 | case (Some(loaderO), Some(removalO)) => builder.removalListener(removalO).build(loaderO) 60 | case (Some(loaderO), None) => builder.build(loaderO) 61 | case (None, Some(removalO)) => builder.removalListener(removalO).build() 62 | case _ => builder.build() 63 | } 64 | } 65 | 66 | /** 67 | * Current size of this LRU Map 68 | */ 69 | def size = cache.size.toInt 70 | 71 | /** 72 | * Puts a value 73 | * @param key Key to put the Value for 74 | * @param value Value to put for the Key 75 | */ 76 | def put(key: K, value: V): Unit = cache.put(KeyHolder(key), ValueHolder(value)) 77 | 78 | /** 79 | * Gets a value associated with the given key. 80 | * @param key Key to get the Value for 81 | */ 82 | 83 | def get(key: K): Option[V] = Option(cache.getIfPresent(KeyHolder(key))).map(_.value) 84 | 85 | /** 86 | * Get the value associated with the given key. If no value is already associated, then associate the given value 87 | * with the key and use it as the return value. 88 | * 89 | * @param key Key to put the Value for 90 | * @param value Value to put for the Key 91 | * @return 92 | */ 93 | def getOrElseUpdate(key: K, value: => V): V = { 94 | cache.get(KeyHolder(key), new Callable[ValueHolder[V]] { 95 | def call(): ValueHolder[V] = ValueHolder(value) 96 | }).value 97 | } 98 | 99 | /** 100 | * Remove a value by key 101 | * @param key Key to be removed 102 | */ 103 | def remove(key: K): Unit = cache.invalidate(KeyHolder(key)) 104 | } 105 | 106 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyTcpClient.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.net.{ InetAddress, InetSocketAddress } 4 | 5 | import com.twitter.util.Duration 6 | import io.netty.channel._ 7 | 8 | /** 9 | * wasted.io Scala Netty TCP Client 10 | * @param codec Codec 11 | * @param remote Remote Host and Port 12 | * @param hostConnectionLimit Number of open connections for this client. Defaults to 1 13 | * @param hostConnectionCoreSize Number of connections to keep open for this client. Defaults to 0 14 | * @param globalTimeout Global Timeout for the completion of a request 15 | * @param tcpConnectTimeout TCP Connect Timeout 16 | * @param connectTimeout Timeout for establishing the Service 17 | * @param requestTimeout Timeout for each request 18 | * @param tcpKeepAlive TCP KeepAlive. Defaults to false 19 | * @param reuseAddr Reuse-Address. Defaults to true 20 | * @param tcpNoDelay TCP No-Delay. Defaults to true 21 | * @param soLinger soLinger. Defaults to 0 22 | * @param retries On connection or timeouts, how often should we retry? Defaults to 0 23 | * @param eventLoop Netty Event-Loop 24 | * @param pipeline Setup extra handlers on the Netty Pipeline 25 | */ 26 | case class NettyTcpClient[Req, Resp](codec: NettyCodec[Req, Resp], 27 | remote: List[InetSocketAddress] = List.empty, 28 | hostConnectionLimit: Int = 1, 29 | hostConnectionCoreSize: Int = 0, 30 | globalTimeout: Option[Duration] = None, 31 | tcpConnectTimeout: Option[Duration] = None, 32 | connectTimeout: Option[Duration] = None, 33 | requestTimeout: Option[Duration] = None, 34 | tcpKeepAlive: Boolean = false, 35 | reuseAddr: Boolean = true, 36 | tcpNoDelay: Boolean = true, 37 | soLinger: Int = 0, 38 | retries: Int = 0, 39 | eventLoop: EventLoopGroup = Netty.eventLoop, 40 | pipeline: Channel => Unit = p => ()) extends NettyClientBuilder[Req, Resp] { 41 | def withSoLinger(soLinger: Int) = copy[Req, Resp](soLinger = soLinger) 42 | def withTcpNoDelay(tcpNoDelay: Boolean) = copy[Req, Resp](tcpNoDelay = tcpNoDelay) 43 | def withTcpKeepAlive(tcpKeepAlive: Boolean) = copy[Req, Resp](tcpKeepAlive = tcpKeepAlive) 44 | def withReuseAddr(reuseAddr: Boolean) = copy[Req, Resp](reuseAddr = reuseAddr) 45 | def withGlobalTimeout(globalTimeout: Duration) = copy[Req, Resp](globalTimeout = Some(globalTimeout)) 46 | def withTcpConnectTimeout(tcpConnectTimeout: Duration) = copy[Req, Resp](tcpConnectTimeout = Some(tcpConnectTimeout)) 47 | def withConnectTimeout(connectTimeout: Duration) = copy[Req, Resp](connectTimeout = Some(connectTimeout)) 48 | def withRequestTimeout(requestTimeout: Duration) = copy[Req, Resp](requestTimeout = Some(requestTimeout)) 49 | def withHostConnectionLimit(limit: Int) = copy[Req, Resp](hostConnectionLimit = limit) 50 | def withHostConnectionCoresize(coreSize: Int) = copy[Req, Resp](hostConnectionCoreSize = coreSize) 51 | def withRetries(retries: Int) = copy[Req, Resp](retries = retries) 52 | def withEventLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](eventLoop = eventLoop) 53 | def withPipeline(pipeline: Channel => Unit) = copy[Req, Resp](pipeline = pipeline) 54 | def connectTo(host: String, port: Int) = copy[Req, Resp](remote = List(new InetSocketAddress(InetAddress.getByName(host), port))) 55 | def connectTo(hosts: List[InetSocketAddress]) = copy[Req, Resp](remote = hosts) 56 | 57 | def getPort(uri: java.net.URI): Int = remote.headOption.map(_.getPort).getOrElse { 58 | throw new IllegalArgumentException("No port was given through hosts Parameter") 59 | } 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Hashing.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.io.FileInputStream 4 | import java.security.MessageDigest 5 | import javax.crypto.spec.SecretKeySpec 6 | 7 | case class HashingAlgo(name: String = "HmacSHA256") 8 | case class HexingAlgo(name: String = "SHA") 9 | 10 | /** 11 | * Helper Object for hashing different Strings and Files. 12 | */ 13 | object Hashing { 14 | 15 | /** 16 | * Sign the payload with the given key using the given algorithm. 17 | * 18 | * @param key Key used for hashing 19 | * @param payload Big mystery here.. 20 | * @param alg Algorithm to be used. Possible choices are HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384 and HmacSHA512. 21 | */ 22 | def sign(key: String, payload: String)(implicit alg: HashingAlgo) = { 23 | val mac = javax.crypto.Mac.getInstance(alg.name) 24 | val secret = new SecretKeySpec(key.toCharArray.map(_.toByte), alg.name) 25 | mac.init(secret) 26 | // catch (InvalidKeyException e) 27 | mac.doFinal(payload.toCharArray.map(_.toByte)).map(b => Integer.toString((b & 0xff) + 0x100, 16).substring(1)).mkString 28 | } 29 | 30 | /** 31 | * Create an hex encoded SHA hash from a Byte array using the given algorithm. 32 | * 33 | * @param in ByteArray to be encoded 34 | * @param alg Algorithm to be used. Possible choices are HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384 and HmacSHA512. 35 | */ 36 | def hexDigest(in: Array[Byte])(implicit alg: HexingAlgo): String = { 37 | val binHash = MessageDigest.getInstance(alg.name).digest(in) 38 | hexEncode(binHash) 39 | } 40 | 41 | /** 42 | * Create an hex encoded SHA hash from a File on disk using the given algorithm. 43 | * 44 | * @param file Path to the file 45 | * @param alg Algorithm to be used. Possible choices are HmacMD5, HmacSHA1, HmacSHA256, HmacSHA384 and HmacSHA512. 46 | */ 47 | def hexFileDigest(file: String)(implicit alg: HexingAlgo): String = { 48 | val md = MessageDigest.getInstance(alg.name) 49 | val input = new FileInputStream(file) 50 | val buffer = new Array[Byte](1024) 51 | Stream.continually(input.read(buffer)).takeWhile(_ != -1).foreach(md.update(buffer, 0, _)) 52 | hexEncode(md.digest) 53 | } 54 | 55 | /** 56 | * Encode a ByteArray as hexadecimal characters. 57 | * 58 | * @param in ByteArray to be encoded 59 | */ 60 | def hexEncode(in: Array[Byte]): String = { 61 | val sb = new StringBuilder 62 | val len = in.length 63 | def addDigit(in: Array[Byte], pos: Int, len: Int, sb: StringBuilder) { 64 | if (pos < len) { 65 | val b: Int = in(pos) 66 | val msb = (b & 0xf0) >> 4 67 | val lsb = b & 0x0f 68 | sb.append(if (msb < 10) ('0' + msb).asInstanceOf[Char] else ('a' + (msb - 10)).asInstanceOf[Char]) 69 | sb.append(if (lsb < 10) ('0' + lsb).asInstanceOf[Char] else ('a' + (lsb - 10)).asInstanceOf[Char]) 70 | 71 | addDigit(in, pos + 1, len, sb) 72 | } 73 | } 74 | addDigit(in, 0, len, sb) 75 | sb.toString() 76 | } 77 | 78 | /** 79 | * Decode a ByteArray from hex. 80 | * 81 | * @param str String to be decoded 82 | */ 83 | def hexDecode(str: String): Array[Byte] = { 84 | val max = str.length / 2 85 | val ret = new Array[Byte](max) 86 | var pos = 0 87 | 88 | def byteOf(in: Char): Int = in match { 89 | case '0' => 0 90 | case '1' => 1 91 | case '2' => 2 92 | case '3' => 3 93 | case '4' => 4 94 | case '5' => 5 95 | case '6' => 6 96 | case '7' => 7 97 | case '8' => 8 98 | case '9' => 9 99 | case 'a' | 'A' => 10 100 | case 'b' | 'B' => 11 101 | case 'c' | 'C' => 12 102 | case 'd' | 'D' => 13 103 | case 'e' | 'E' => 14 104 | case 'f' | 'F' => 15 105 | case _ => 0 106 | } 107 | 108 | while (pos < max) { 109 | val two = pos * 2 110 | val ch: Char = str.charAt(two) 111 | val cl: Char = str.charAt(two + 1) 112 | ret(pos) = (byteOf(ch) * 16 + byteOf(cl)).toByte 113 | pos += 1 114 | } 115 | 116 | ret 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Schedule.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.util.concurrent.{ ConcurrentHashMap, TimeUnit } 4 | 5 | import com.twitter.util.JavaTimer 6 | import io.netty.util.{ Timeout, TimerTask } 7 | 8 | import scala.concurrent.duration.Duration 9 | 10 | class WheelTimer(timerMillis: Int, wheelSize: Int) 11 | extends io.netty.util.HashedWheelTimer(timerMillis.toLong, TimeUnit.MILLISECONDS, wheelSize) { 12 | lazy val twitter = new JavaTimer() 13 | } 14 | 15 | object WheelTimer extends WheelTimer(100, 512) 16 | 17 | /** 18 | * Wasted Scheduler based on Netty's HashedWheelTimer 19 | */ 20 | object Schedule extends Logger { 21 | private val repeatTimers = new ConcurrentHashMap[Long, Timeout]() 22 | 23 | /** 24 | * Creates a TimerTask from a Function 25 | * 26 | * @param func Function to be tasked 27 | */ 28 | private def task(func: () => Any): TimerTask = new TimerTask() { 29 | def run(timeout: Timeout): Unit = func() 30 | } 31 | 32 | private def repeatFunc(id: Long, func: () => Any, delay: Duration)(implicit timer: WheelTimer): () => Any = () => { 33 | val to = timer.newTimeout(task(repeatFunc(id, func, delay)), delay.length, delay.unit) 34 | repeatTimers.put(id, to) 35 | func() 36 | } 37 | 38 | /** 39 | * Schedule an event. 40 | * 41 | * @param func Function to be scheduled 42 | * @param initialDelay Initial delay before first firing 43 | * @param delay Optional delay to be used if it is to be rescheduled (again) 44 | */ 45 | def apply(func: () => Any, initialDelay: Duration, delay: Option[Duration] = None)(implicit timer: WheelTimer): Action = 46 | delay match { 47 | case Some(d) => apply(func, initialDelay, d) 48 | case None => new Action(Some(timer.newTimeout(task(func), initialDelay.length, initialDelay.unit))) 49 | } 50 | 51 | /** 52 | * Schedule an event over and over again saving timeout reference. 53 | * 54 | * @param func Function to be scheduled 55 | * @param initialDelay Initial delay before first firing 56 | * @param delay Delay to be called after the first firing 57 | */ 58 | def apply(func: () => Any, initialDelay: Duration, delay: Duration)(implicit timer: WheelTimer): Action = { 59 | val action = new Action(None) 60 | val to = timer.newTimeout(task(repeatFunc(action.id, func, delay)), initialDelay.length, initialDelay.unit) 61 | repeatTimers.put(action.id, to) 62 | action 63 | } 64 | 65 | /** 66 | * Schedule an event once. 67 | * 68 | * @param func Function to be scheduled 69 | * @param initialDelay Initial delay before first firing 70 | */ 71 | def once(func: () => Any, initialDelay: Duration)(implicit timer: WheelTimer): Action = apply(func, initialDelay) 72 | 73 | /** 74 | * Schedule an event over and over again. 75 | * 76 | * @param func Function to be scheduled 77 | * @param initialDelay Initial delay before first firing 78 | * @param delay Delay to be called after the first firing 79 | */ 80 | def again(func: () => Any, initialDelay: Duration, delay: Duration)(implicit timer: WheelTimer): Action = apply(func, initialDelay, delay) 81 | 82 | /** 83 | * This is a proxy-class, which works around the rescheduling issue. 84 | * If a task is scheduled once, it will have Some(Timeout). 85 | * If it is to be scheduled more than once, it will have None, but 86 | * methods will work transparently through a reference in Schedule.repeatTimers. 87 | * 88 | * @param timeout Optional Timeout parameter only provided by Schedule.once 89 | */ 90 | class Action(timeout: Option[Timeout]) { 91 | lazy val id = scala.util.Random.nextLong() 92 | private def getTimeout = timeout orElse Option(repeatTimers.get(id)) 93 | 94 | /** 95 | * Cancel the scheduled event. 96 | */ 97 | def cancel(): Unit = getTimeout match { 98 | case Some(t) => 99 | t.cancel 100 | repeatTimers.remove(id) 101 | case None => 102 | } 103 | 104 | /** 105 | * Get the according TimerTask. 106 | */ 107 | def task(): Option[TimerTask] = getTimeout.map(_.task) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/site/stylesheets/pygment_trac.css: -------------------------------------------------------------------------------- 1 | .highlight { background: #ffffff; } 2 | .highlight .c { color: #999988; font-style: italic } /* Comment */ 3 | .highlight .err { color: #a61717; background-color: #e3d2d2 } /* Error */ 4 | .highlight .k { font-weight: bold } /* Keyword */ 5 | .highlight .o { font-weight: bold } /* Operator */ 6 | .highlight .cm { color: #999988; font-style: italic } /* Comment.Multiline */ 7 | .highlight .cp { color: #999999; font-weight: bold } /* Comment.Preproc */ 8 | .highlight .c1 { color: #999988; font-style: italic } /* Comment.Single */ 9 | .highlight .cs { color: #999999; font-weight: bold; font-style: italic } /* Comment.Special */ 10 | .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ 11 | .highlight .gd .x { color: #000000; background-color: #ffaaaa } /* Generic.Deleted.Specific */ 12 | .highlight .ge { font-style: italic } /* Generic.Emph */ 13 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 14 | .highlight .gh { color: #999999 } /* Generic.Heading */ 15 | .highlight .gi { color: #000000; background-color: #ddffdd } /* Generic.Inserted */ 16 | .highlight .gi .x { color: #000000; background-color: #aaffaa } /* Generic.Inserted.Specific */ 17 | .highlight .go { color: #888888 } /* Generic.Output */ 18 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 19 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 20 | .highlight .gu { color: #800080; font-weight: bold; } /* Generic.Subheading */ 21 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 22 | .highlight .kc { font-weight: bold } /* Keyword.Constant */ 23 | .highlight .kd { font-weight: bold } /* Keyword.Declaration */ 24 | .highlight .kn { font-weight: bold } /* Keyword.Namespace */ 25 | .highlight .kp { font-weight: bold } /* Keyword.Pseudo */ 26 | .highlight .kr { font-weight: bold } /* Keyword.Reserved */ 27 | .highlight .kt { color: #445588; font-weight: bold } /* Keyword.Type */ 28 | .highlight .m { color: #009999 } /* Literal.Number */ 29 | .highlight .s { color: #d14 } /* Literal.String */ 30 | .highlight .na { color: #008080 } /* Name.Attribute */ 31 | .highlight .nb { color: #0086B3 } /* Name.Builtin */ 32 | .highlight .nc { color: #445588; font-weight: bold } /* Name.Class */ 33 | .highlight .no { color: #008080 } /* Name.Constant */ 34 | .highlight .ni { color: #800080 } /* Name.Entity */ 35 | .highlight .ne { color: #990000; font-weight: bold } /* Name.Exception */ 36 | .highlight .nf { color: #990000; font-weight: bold } /* Name.Function */ 37 | .highlight .nn { color: #555555 } /* Name.Namespace */ 38 | .highlight .nt { color: #000080 } /* Name.Tag */ 39 | .highlight .nv { color: #008080 } /* Name.Variable */ 40 | .highlight .ow { font-weight: bold } /* Operator.Word */ 41 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 42 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 43 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 44 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 45 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 46 | .highlight .sb { color: #d14 } /* Literal.String.Backtick */ 47 | .highlight .sc { color: #d14 } /* Literal.String.Char */ 48 | .highlight .sd { color: #d14 } /* Literal.String.Doc */ 49 | .highlight .s2 { color: #d14 } /* Literal.String.Double */ 50 | .highlight .se { color: #d14 } /* Literal.String.Escape */ 51 | .highlight .sh { color: #d14 } /* Literal.String.Heredoc */ 52 | .highlight .si { color: #d14 } /* Literal.String.Interpol */ 53 | .highlight .sx { color: #d14 } /* Literal.String.Other */ 54 | .highlight .sr { color: #009926 } /* Literal.String.Regex */ 55 | .highlight .s1 { color: #d14 } /* Literal.String.Single */ 56 | .highlight .ss { color: #990073 } /* Literal.String.Symbol */ 57 | .highlight .bp { color: #999999 } /* Name.Builtin.Pseudo */ 58 | .highlight .vc { color: #008080 } /* Name.Variable.Class */ 59 | .highlight .vg { color: #008080 } /* Name.Variable.Global */ 60 | .highlight .vi { color: #008080 } /* Name.Variable.Instance */ 61 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 62 | 63 | .type-csharp .highlight .k { color: #0000FF } 64 | .type-csharp .highlight .kt { color: #0000FF } 65 | .type-csharp .highlight .nf { color: #000000; font-weight: normal } 66 | .type-csharp .highlight .nc { color: #2B91AF } 67 | .type-csharp .highlight .nn { color: #000000 } 68 | .type-csharp .highlight .s { color: #A31515 } 69 | .type-csharp .highlight .sc { color: #A31515 } 70 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Crypto.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import javax.crypto.Cipher 4 | import javax.crypto.spec.SecretKeySpec 5 | 6 | case class CryptoCipher(name: String = "AES", jce: Boolean = true) 7 | 8 | /** 9 | * Helper methods for en-/decrypting strings. 10 | */ 11 | object Crypto { 12 | 13 | /** 14 | * Encrypt the given String using the given Cipher and the supplied salt. 15 | * @param salt Secret to encrypt with 16 | * @param payload Payload to be encrypted 17 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding.. 18 | * @return Byte-Array of the encrypted data 19 | */ 20 | def encryptBinary(salt: String, payload: String)(implicit cipher: CryptoCipher): Array[Byte] = 21 | encryptBinary(salt, payload.getBytes("UTF-8"))(cipher) 22 | 23 | /** 24 | * Encrypt the given Payload using the given Cipher and the supplied salt. 25 | * @param salt Secret to encrypt with 26 | * @param payload Payload to be encrypted 27 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 28 | * @return Byte-Array of the encrypted data 29 | */ 30 | def encryptBinary(salt: String, payload: Array[Byte])(implicit cipher: CryptoCipher): Array[Byte] = { 31 | val key = new SecretKeySpec(salt.getBytes("UTF-8"), "AES") 32 | val cipherI = if (cipher.jce) Cipher.getInstance(cipher.name, "SunJCE") else Cipher.getInstance(cipher.name) 33 | cipherI.init(Cipher.ENCRYPT_MODE, key) 34 | cipherI.doFinal(payload) 35 | } 36 | 37 | /** 38 | * Encrypt the given String using the given Cipher and the supplied salt. 39 | * @param salt Secret to encrypt with 40 | * @param payload Payload to be encrypted 41 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 42 | * @return Base64-String of the encrypted data 43 | */ 44 | def encryptString(salt: String, payload: String)(implicit cipher: CryptoCipher): String = 45 | new String(encryptBinary(salt, payload.getBytes("UTF-8"))(cipher), "UTF-8") 46 | 47 | /** 48 | * Encrypt the given Payload using the given Cipher and the supplied salt. 49 | * @param salt Secret to encrypt with 50 | * @param payload Payload to be encrypted 51 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 52 | * @return Base64-String of the encrypted data 53 | */ 54 | def encryptString(salt: String, payload: Array[Byte])(implicit cipher: CryptoCipher): String = 55 | new String(encryptBinary(salt, payload)(cipher), "UTF-8") 56 | 57 | /** 58 | * Decrypt the given Payload using the given Cipher and the supplied salt. 59 | * @param salt Secret to decrypt with 60 | * @param payload Payload to be decrypted 61 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 62 | * @return Byte-Array of the decrypted data 63 | */ 64 | def decryptBinary(salt: String, payload: Array[Byte])(implicit cipher: CryptoCipher): Array[Byte] = { 65 | val key = new SecretKeySpec(salt.getBytes("UTF-8"), cipher.name) 66 | val cipherI = if (cipher.jce) Cipher.getInstance(cipher.name, "SunJCE") else Cipher.getInstance(cipher.name) 67 | cipherI.init(Cipher.DECRYPT_MODE, key) 68 | cipherI.doFinal(payload) 69 | } 70 | 71 | /** 72 | * Decrypt the given String using the given Cipher and the supplied salt. 73 | * @param salt Secret to decrypt with 74 | * @param payload Payload to be decrypted 75 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 76 | * @return Byte-Array of the decrypted data 77 | */ 78 | def decryptBinary(salt: String, payload: String)(implicit cipher: CryptoCipher): Array[Byte] = { 79 | decryptBinary(salt, payload)(cipher) 80 | } 81 | 82 | /** 83 | * Decrypt the given Payload using the given Cipher and the supplied salt. 84 | * @param salt Secret to decrypt with 85 | * @param payload Payload to be decrypted 86 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 87 | * @return String of the decrypted data 88 | */ 89 | def decryptString(salt: String, payload: Array[Byte])(implicit cipher: CryptoCipher): String = { 90 | new String(decryptBinary(salt, payload)(cipher), "UTF-8") 91 | } 92 | 93 | /** 94 | * Decrypt the given String using the given Cipher and the supplied salt. 95 | * @param salt Secret to decrypt with 96 | * @param payload Payload to be decrypted 97 | * @param cipher Cipher to be used. Possible choices are AES, ECB, PKCS5Padding. 98 | * @return String of the decrypted data 99 | */ 100 | def decryptString(salt: String, payload: String)(implicit cipher: CryptoCipher): String = { 101 | decryptString(salt, payload.getBytes("UTF-8"))(cipher) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/InetPrefix.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.net.{ InetAddress, InetSocketAddress, UnknownHostException } 4 | 5 | /** 6 | * Helper Object for creating InetPrefix-objects. 7 | */ 8 | object InetPrefix extends Logger { 9 | def apply(prefix: InetAddress, prefixLen: Int): InetPrefix = { 10 | if (prefix.getAddress.length == 16) 11 | new Inet6Prefix(prefix, prefixLen) 12 | else 13 | new Inet4Prefix(prefix, prefixLen) 14 | } 15 | 16 | /** 17 | * Converts a String to an InetSocketAddress with respects to IPv6 ([123:123::123]:80). 18 | * @param string IP Address to convert 19 | * @return InetSocketAddress 20 | */ 21 | def stringToInetAddr(string: String): Option[InetSocketAddress] = string match { 22 | case ipv4: String if ipv4.matches("""\d+\.\d+\.\d+\.\d+:\d+""") => 23 | val split = ipv4.split(":") 24 | Tryo(new java.net.InetSocketAddress(split(0), split(1).toInt)) 25 | case ipv6: String if ipv6.matches("""\[[0-9a-fA-F:]+\]:\d+""") => 26 | val split = ipv6.split("]:") 27 | val addr = split(0).replaceFirst("\\[", "") 28 | Tryo(new java.net.InetSocketAddress(java.net.InetAddress.getByName(addr), split(1).toInt)) 29 | case _ => None 30 | } 31 | 32 | def inetAddrToLong(addr: InetAddress): Long = 33 | (0L to 3L).toArray.foldRight(0L)((i, ip) => ip | (addr.getAddress()(3 - i.toInt) & 0xff).toLong << i * 8) 34 | 35 | } 36 | 37 | /** 38 | * Common Interface for both protocol types. 39 | */ 40 | trait InetPrefix { 41 | 42 | /** 43 | * Base IP-Address of the Prefix. 44 | */ 45 | val prefix: InetAddress 46 | 47 | /** 48 | * Length of the Prefix. 0-32 for IPv4 and 0-128 for IPv6. 49 | */ 50 | val prefixLen: Int 51 | 52 | /** 53 | * Either 4 for IPv4 or 6 for IPv6. 54 | */ 55 | val ipVersion: Int 56 | 57 | /** 58 | * Check if the given InetAddress is contained in this prefix. 59 | */ 60 | def contains(addr: InetAddress): Boolean 61 | 62 | private def prefixAddr() = prefix.getHostAddress 63 | override val toString = prefixAddr + "/" + prefixLen 64 | } 65 | 66 | /** 67 | * Inet6Prefix object to hold information about an IPv6 Prefix. 68 | * 69 | * Currently only containment checks are implemented. 70 | * 71 | * @param prefix Base-Address for the Prefix 72 | * @param prefixLen Length of this Prefix in CIDR notation 73 | */ 74 | class Inet6Prefix(val prefix: InetAddress, val prefixLen: Int) extends InetPrefix { 75 | val ipVersion = 6 76 | if (prefixLen < 0 || prefixLen > 128) 77 | throw new UnknownHostException(prefixLen + " is not a valid IPv6 Prefix-Length (0-128)") 78 | 79 | private lazy val network: Array[Byte] = prefix.getAddress 80 | private lazy val netmask: Array[Byte] = { 81 | var netmask: Array[Byte] = Array.fill(16)(0xff.toByte) 82 | val maskBytes: Int = prefixLen / 8 83 | if (maskBytes < 16) netmask(maskBytes) = (0xff.toByte << 8 - (prefixLen % 8)).toByte 84 | for (i <- maskBytes + 1 until 128 / 8) netmask(i) = 0 85 | netmask 86 | } 87 | 88 | /** 89 | * Check if the given InetAddress is contained in this IPv6 prefix. 90 | */ 91 | def contains(addr: InetAddress): Boolean = { 92 | if (addr.getAddress.length != 16) { 93 | InetPrefix.debug("Inet6Prefix cannot check against Inet4Address") 94 | return false 95 | } 96 | 97 | val candidate = addr.getAddress 98 | for (i <- netmask.indices) 99 | if ((candidate(i) & netmask(i)) != (network(i) & netmask(i))) return false 100 | true 101 | } 102 | } 103 | 104 | /** 105 | * Inet4Prefix object to hold information about an IPv4 Prefix. 106 | * 107 | * Currently only containment checks are implemented. 108 | * 109 | * @param prefix Base-Address for the Prefix 110 | * @param prefixLen Length of this Prefix in CIDR notation 111 | */ 112 | class Inet4Prefix(val prefix: InetAddress, val prefixLen: Int) extends InetPrefix { 113 | val ipVersion = 4 114 | if (prefixLen < 0 || prefixLen > 32) 115 | throw new UnknownHostException(prefixLen + " is not a valid IPv4 Prefix-Length (0-32)") 116 | 117 | private lazy val netmask: Long = (((1L << 32) - 1) << (32 - prefixLen)) & 0xFFFFFFFFL 118 | private lazy val start: Long = InetPrefix.inetAddrToLong(prefix) & netmask 119 | private lazy val stop: Long = start + (0xFFFFFFFFL >> prefixLen) 120 | 121 | /** 122 | * Check if the given InetAddress is contained in this IPv4 prefix. 123 | * @param addr The InetAddr which is to be checked. 124 | */ 125 | def contains(addr: InetAddress): Boolean = { 126 | if (addr.getAddress.length != 4) { 127 | InetPrefix.debug("Inet4Prefix cannot check against Inet6Address") 128 | return false 129 | } 130 | 131 | val candidate = InetPrefix.inetAddrToLong(addr) 132 | candidate >= start && candidate <= stop 133 | } 134 | } 135 | 136 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/WebSocketHandler.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | package http 3 | 4 | import com.twitter.util.Future 5 | import io.netty.channel.ChannelHandler.Sharable 6 | import io.netty.channel._ 7 | import io.netty.handler.codec.http._ 8 | import io.netty.handler.codec.http.websocketx._ 9 | import io.netty.util.ReferenceCountUtil 10 | 11 | @Sharable 12 | case class WebSocketHandler[Req <: HttpRequest](corsOrigin: String = "*", 13 | handshakerHeaders: Map[String, String] = Map.empty, 14 | connect: (Channel) => Unit = (p) => (), 15 | disconnect: (Channel) => Unit = (p) => (), 16 | httpHandler: Option[(Channel, Future[Req]) => Future[HttpResponse]] = None, 17 | handle: Option[(Channel, Future[WebSocketFrame]) => Option[Future[WebSocketFrame]]] = None) 18 | extends SimpleChannelInboundHandler[WebSocketFrame] { channelHandler => 19 | 20 | def withHandshakerHeaders(handshakerHeaders: Map[String, String]) = copy(handshakerHeaders = handshakerHeaders) 21 | def onConnect(connect: (Channel) => Unit) = copy(connect = connect) 22 | def onDisconnect(disconnect: (Channel) => Unit) = copy(disconnect = disconnect) 23 | def handler(handle: (Channel, Future[WebSocketFrame]) => Option[Future[WebSocketFrame]]) = copy(handle = Some(handle)) 24 | def withHttpHandler(httpHandler: (Channel, Future[FullHttpRequest]) => Future[HttpResponse]) = copy(httpHandler = Some(httpHandler)) 25 | 26 | lazy val headerParser = new Headers(corsOrigin) 27 | lazy val wsHandshakerHeaders: HttpHeaders = if (handshakerHeaders.isEmpty) null else { 28 | val wshh = new DefaultHttpHeaders() 29 | handshakerHeaders.foreach { 30 | case (name, value) => wshh.add(name, value) 31 | } 32 | wshh 33 | } 34 | 35 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { 36 | handle.map(_(ctx.channel, Future.exception(cause))) getOrElse cause.printStackTrace() 37 | } 38 | 39 | override def channelRead0(ctx: ChannelHandlerContext, msg: WebSocketFrame): Unit = { 40 | ReferenceCountUtil.retain(msg) 41 | Future { 42 | handle.flatMap { serverF => 43 | serverF(ctx.channel(), Future.value(msg)).map { resultF => 44 | resultF.map(ctx.channel().writeAndFlush).ensure(msg.release()) 45 | } 46 | }.getOrElse(msg.release()) 47 | } 48 | } 49 | 50 | def dispatch(channel: Channel, freq: Future[Req]): Future[HttpResponse] = freq.flatMap { req => 51 | val headers = headerParser.get(req) 52 | // WebSocket Handshake needed? 53 | if (headers.get(HttpHeaderNames.UPGRADE).exists(_.toLowerCase == HttpHeaderValues.WEBSOCKET.toLowerCase.toString)) { 54 | val securityProto = headers.get(HttpHeaderNames.SEC_WEBSOCKET_PROTOCOL).orNull 55 | val proto = if (channel.pipeline.get(HttpServer.Handlers.ssl) != null) "wss" else "ws" 56 | // Handshake 57 | val location = proto + "://" + req.headers.get(HttpHeaderNames.HOST) + "/" 58 | val factory = new WebSocketServerHandshakerFactory(location, securityProto, false) 59 | val handshaker: WebSocketServerHandshaker = factory.newHandshaker(req) 60 | if (handshaker == null) { 61 | val resp = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UPGRADE_REQUIRED) 62 | resp.headers().set(HttpHeaderNames.SEC_WEBSOCKET_VERSION, WebSocketVersion.V13.toHttpHeaderValue) 63 | Future.value(resp) 64 | } else { 65 | val promise = channel.newPromise() 66 | promise.addListener(handshakeCompleteListener) 67 | channel.pipeline().replace(HttpServer.Handlers.handler, HttpServer.Handlers.handler, channelHandler) 68 | handshaker.handshake(channel, req, wsHandshakerHeaders, promise) 69 | Future.value(null) 70 | } 71 | } else if (req.method() == HttpMethod.OPTIONS) { 72 | // Handles WebSocket and CORS OPTIONS requests 73 | val resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK) 74 | headers.cors.foreach { 75 | case (name, value) => resp.headers().set(name, value) 76 | } 77 | Future.value(resp) 78 | } else httpHandler.map(_(channel, freq)) getOrElse { 79 | Future.value(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)) 80 | } 81 | } 82 | 83 | final private val channelClosedListener = new ChannelFutureListener { 84 | override def operationComplete(p1: ChannelFuture): Unit = { 85 | disconnect(p1.channel()) 86 | } 87 | } 88 | 89 | final private val handshakeCompleteListener = new ChannelFutureListener { 90 | override def operationComplete(p1: ChannelFuture): Unit = { 91 | connect(p1.channel()) 92 | p1.channel().closeFuture().addListener(channelClosedListener) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/site/stylesheets/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:1000px; 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:630px; 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 | } 256 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyByteArrayCodec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import com.twitter.concurrent.{ Broker, Offer } 6 | import com.twitter.util.{ Duration, Future, Promise } 7 | import io.netty.buffer.ByteBuf 8 | import io.netty.channel._ 9 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 10 | import io.netty.handler.ssl.{ SslContext, SslContextBuilder } 11 | import io.netty.handler.timeout.{ ReadTimeoutHandler, WriteTimeoutHandler } 12 | import io.wasted.util.http.HttpClient.Handlers 13 | 14 | /** 15 | * ByteArray Channel 16 | * @param out Outbound Broker 17 | * @param in Inbound Offer 18 | * @param channel Netty Channel 19 | */ 20 | final case class NettyByteArrayChannel(out: Broker[ByteBuf], in: Offer[ByteBuf], private val channel: Channel) { 21 | def close(): Future[Unit] = { 22 | val closed = Promise[Unit]() 23 | channel.closeFuture().addListener(new ChannelFutureListener { 24 | override def operationComplete(f: ChannelFuture): Unit = { 25 | closed.setDone() 26 | } 27 | }) 28 | closed.raiseWithin(Duration(2, TimeUnit.SECONDS))(WheelTimer.twitter) 29 | } 30 | 31 | val onDisconnect = Promise[Unit]() 32 | channel.closeFuture().addListener(new ChannelFutureListener { 33 | override def operationComplete(f: ChannelFuture): Unit = onDisconnect.setDone() 34 | }) 35 | 36 | def !(s: ByteBuf): Unit = out ! s 37 | def !(s: Array[Byte]): Unit = out ! channel.alloc().buffer(s.length).writeBytes(s) 38 | def foreach(run: ByteBuf => Unit) = in.foreach(run) 39 | } 40 | 41 | /** 42 | * wasted.io Scala ByteArray Codec 43 | * 44 | * For composition you may use ByteArrayCodec().withTls(..) 45 | * 46 | * @param readTimeout Channel Read Timeout 47 | * @param writeTimeout Channel Write Timeout 48 | * @param sslCtx Netty SSL Context 49 | */ 50 | final case class NettyByteArrayCodec(readTimeout: Option[Duration] = None, 51 | writeTimeout: Option[Duration] = None, 52 | sslCtx: Option[SslContext] = None) extends NettyCodec[ByteBuf, NettyByteArrayChannel] { 53 | 54 | def withTls(sslCtx: SslContext) = copy(sslCtx = Some(sslCtx)) 55 | def withReadTimeout(timeout: Duration) = copy(readTimeout = Some(timeout)) 56 | def withWriteTimeout(timeout: Duration) = copy(writeTimeout = Some(timeout)) 57 | 58 | def withInsecureTls() = { 59 | val ctx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build() 60 | copy(sslCtx = Some(ctx)) 61 | } 62 | 63 | /** 64 | * Sets up basic server Pipeline 65 | * @param channel Channel to apply the Pipeline to 66 | */ 67 | def serverPipeline(channel: Channel): Unit = () 68 | 69 | /** 70 | * Sets up basic client Pipeline 71 | * @param channel Channel to apply the Pipeline to 72 | */ 73 | def clientPipeline(channel: Channel): Unit = () 74 | 75 | /** 76 | * Handle the connected channel and send the request 77 | * @param channel Channel we're connected to 78 | * @param request Object we want to send 79 | * @return Future Output Broker and Inbound Offer and Close Function 80 | */ 81 | def clientConnected(channel: Channel, request: ByteBuf): Future[NettyByteArrayChannel] = { 82 | val inBroker = new Broker[ByteBuf] 83 | val outBroker = new Broker[ByteBuf] 84 | val result = Promise[NettyByteArrayChannel] 85 | 86 | readTimeout.foreach { readTimeout => 87 | channel.pipeline.addFirst(Handlers.readTimeout, new ReadTimeoutHandler(readTimeout.inMillis.toInt) { 88 | override def readTimedOut(ctx: ChannelHandlerContext) { 89 | ctx.channel.close 90 | result.setException(new IllegalStateException("Read timed out")) 91 | } 92 | }) 93 | } 94 | writeTimeout.foreach { writeTimeout => 95 | channel.pipeline.addFirst(Handlers.writeTimeout, new WriteTimeoutHandler(writeTimeout.inMillis.toInt) { 96 | override def writeTimedOut(ctx: ChannelHandlerContext) { 97 | ctx.channel.close 98 | result.setException(new IllegalStateException("Write timed out")) 99 | } 100 | }) 101 | } 102 | 103 | channel.pipeline.addLast(Handlers.handler, new SimpleChannelInboundHandler[ByteBuf] { 104 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 105 | http.ExceptionHandler(ctx, cause) 106 | result.setException(cause) 107 | ctx.channel.close 108 | } 109 | 110 | def channelRead0(ctx: ChannelHandlerContext, msg: ByteBuf) { 111 | // if our future has not been filled yet (first response), we reply with the brokers 112 | if (!result.isDefined) result.setValue(NettyByteArrayChannel(outBroker, inBroker.recv, channel)) 113 | 114 | // we wire the inbound packet to the Broker 115 | inBroker ! msg.retain() 116 | } 117 | }) 118 | 119 | // we wire the outbound broker to send to the channel 120 | outBroker.recv.foreach(buf => channel.writeAndFlush(buf)) 121 | Option(request).map(outBroker ! _) 122 | 123 | result 124 | } 125 | } -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyStringCodec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import com.twitter.concurrent.{ Broker, Offer } 6 | import com.twitter.util.{ Duration, Future, Promise } 7 | import io.netty.channel._ 8 | import io.netty.handler.codec.string.{ StringDecoder, StringEncoder } 9 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 10 | import io.netty.handler.ssl.{ SslContext, SslContextBuilder } 11 | import io.netty.handler.timeout.{ ReadTimeoutHandler, WriteTimeoutHandler } 12 | import io.netty.util.CharsetUtil 13 | import io.wasted.util.http.HttpClient.Handlers 14 | 15 | /** 16 | * String Channel 17 | * @param out Outbound Broker 18 | * @param in Inbound Offer 19 | * @param channel Netty Channel 20 | */ 21 | final case class NettyStringChannel(out: Broker[String], in: Offer[String], private val channel: Channel) { 22 | def close(): Future[Unit] = { 23 | val closed = Promise[Unit]() 24 | channel.closeFuture().addListener(new ChannelFutureListener { 25 | override def operationComplete(f: ChannelFuture): Unit = { 26 | closed.setDone() 27 | } 28 | }) 29 | closed.raiseWithin(Duration(2, TimeUnit.SECONDS))(WheelTimer.twitter) 30 | } 31 | 32 | val onDisconnect = Promise[Unit]() 33 | channel.closeFuture().addListener(new ChannelFutureListener { 34 | override def operationComplete(f: ChannelFuture): Unit = onDisconnect.setDone() 35 | }) 36 | 37 | def !(s: String): Unit = out ! s 38 | def foreach(run: String => Unit) = in.foreach(run) 39 | } 40 | 41 | /** 42 | * wasted.io Scala String Codec 43 | * 44 | * For composition you may use StringCodec().withTls(..) 45 | * 46 | * @param readTimeout Channel Read Timeout 47 | * @param writeTimeout Channel Write Timeout 48 | * @param sslCtx Netty SSL Context 49 | */ 50 | final case class NettyStringCodec(readTimeout: Option[Duration] = None, 51 | writeTimeout: Option[Duration] = None, 52 | sslCtx: Option[SslContext] = None) extends NettyCodec[String, NettyStringChannel] { 53 | 54 | def withTls(sslCtx: SslContext) = copy(sslCtx = Some(sslCtx)) 55 | def withReadTimeout(timeout: Duration) = copy(readTimeout = Some(timeout)) 56 | def withWriteTimeout(timeout: Duration) = copy(writeTimeout = Some(timeout)) 57 | 58 | def withInsecureTls() = { 59 | val ctx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build() 60 | copy(sslCtx = Some(ctx)) 61 | } 62 | 63 | /** 64 | * Sets up basic server Pipeline 65 | * @param channel Channel to apply the Pipeline to 66 | */ 67 | def serverPipeline(channel: Channel): Unit = clientPipeline(channel) 68 | 69 | /** 70 | * Sets up basic client Pipeline 71 | * @param channel Channel to apply the Pipeline to 72 | */ 73 | def clientPipeline(channel: Channel): Unit = { 74 | val pipeline = channel.pipeline() 75 | pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8)) 76 | pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8)) 77 | } 78 | 79 | /** 80 | * Handle the connected channel and send the request 81 | * @param channel Channel we're connected to 82 | * @param request Object we want to send 83 | * @return Future Output Broker and Inbound Offer and Close Function 84 | */ 85 | def clientConnected(channel: Channel, request: String): Future[NettyStringChannel] = { 86 | val inBroker = new Broker[String] 87 | val outBroker = new Broker[String] 88 | val result = Promise[NettyStringChannel] 89 | 90 | readTimeout.foreach { readTimeout => 91 | channel.pipeline.addFirst(Handlers.readTimeout, new ReadTimeoutHandler(readTimeout.inMillis.toInt) { 92 | override def readTimedOut(ctx: ChannelHandlerContext) { 93 | ctx.channel.close 94 | result.setException(new IllegalStateException("Read timed out")) 95 | } 96 | }) 97 | } 98 | writeTimeout.foreach { writeTimeout => 99 | channel.pipeline.addFirst(Handlers.writeTimeout, new WriteTimeoutHandler(writeTimeout.inMillis.toInt) { 100 | override def writeTimedOut(ctx: ChannelHandlerContext) { 101 | ctx.channel.close 102 | result.setException(new IllegalStateException("Write timed out")) 103 | } 104 | }) 105 | } 106 | 107 | channel.pipeline.addLast(Handlers.handler, new SimpleChannelInboundHandler[String] { 108 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 109 | http.ExceptionHandler(ctx, cause) 110 | result.setException(cause) 111 | ctx.channel.close 112 | } 113 | 114 | def channelRead0(ctx: ChannelHandlerContext, msg: String) { 115 | // if our future has not been filled yet (first response), we reply with the brokers 116 | if (!result.isDefined) result.setValue(NettyStringChannel(outBroker, inBroker.recv, channel)) 117 | 118 | // we wire the inbound packet to the Broker 119 | inBroker ! msg 120 | } 121 | }) 122 | 123 | // we wire the outbound broker to send to the channel 124 | outBroker.recv.foreach(buf => channel.writeAndFlush(buf)) 125 | Option(request).map(outBroker ! _) 126 | 127 | result 128 | } 129 | } -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/HttpServer.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.http 2 | 3 | import com.twitter.conversions.time._ 4 | import com.twitter.util._ 5 | import io.netty.buffer._ 6 | import io.netty.channel._ 7 | import io.netty.handler.codec.http._ 8 | import io.netty.util.ReferenceCountUtil 9 | import io.wasted.util._ 10 | 11 | object HttpServer { 12 | private[http] val defaultHandler: (Channel, Future[HttpMessage]) => Future[HttpResponse] = { (ch, msg) => 13 | Future.value(new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND)) 14 | } 15 | 16 | val closeListener = new ChannelFutureListener { 17 | override def operationComplete(f: ChannelFuture): Unit = { 18 | f.channel().disconnect() 19 | } 20 | } 21 | 22 | /** 23 | * These can be used to modify the pipeline afterwards without having to guess their names 24 | */ 25 | object Handlers { 26 | val ssl = "ssl" 27 | val codec = "codec" 28 | val dechunker = "dechunker" 29 | val compressor = "compressor" 30 | val timeout = "timeout" 31 | val handler = "handler" 32 | } 33 | } 34 | 35 | /** 36 | * wasted.io Scala Http Server 37 | * @param codec Http Codec 38 | * @param httpValidateHeaders Validate HTTP Headers 39 | * @param tcpConnectTimeout TCP Connect Timeout 40 | * @param tcpKeepAlive TCP KeepAlive 41 | * @param reuseAddr Reuse-Address 42 | * @param tcpNoDelay TCP No-Delay 43 | * @param soLinger soLinger 44 | * @param sendAllocator ByteBuf send Allocator 45 | * @param recvAllocator ByteBuf recv Allocator 46 | * @param parentLoop Netty Event-Loop for Parents 47 | * @param childLoop Netty Event-Loop for Children 48 | * @param customPipeline Setup extra handlers on the Netty Pipeline 49 | * @param handle Service to handle HttpRequests 50 | */ 51 | final case class HttpServer[Req <: HttpMessage, Resp <: HttpResponse](codec: NettyHttpCodec[Req, Resp] = NettyHttpCodec[Req, Resp](), 52 | httpValidateHeaders: Boolean = true, 53 | tcpConnectTimeout: Duration = 5.seconds, 54 | tcpKeepAlive: Boolean = false, 55 | reuseAddr: Boolean = true, 56 | tcpNoDelay: Boolean = true, 57 | soLinger: Int = 0, 58 | sendAllocator: ByteBufAllocator = PooledByteBufAllocator.DEFAULT, 59 | recvAllocator: RecvByteBufAllocator = new AdaptiveRecvByteBufAllocator, 60 | parentLoop: EventLoopGroup = Netty.eventLoop, 61 | childLoop: EventLoopGroup = Netty.eventLoop, 62 | customPipeline: Channel => Unit = p => (), 63 | handle: (Channel, Future[Req]) => Future[Resp] = HttpServer.defaultHandler) 64 | extends NettyServerBuilder[HttpServer[Req, Resp], Req, Resp] with Logger { 65 | 66 | def withSoLinger(soLinger: Int) = copy[Req, Resp](soLinger = soLinger) 67 | def withTcpNoDelay(tcpNoDelay: Boolean) = copy[Req, Resp](tcpNoDelay = tcpNoDelay) 68 | def withTcpKeepAlive(tcpKeepAlive: Boolean) = copy[Req, Resp](tcpKeepAlive = tcpKeepAlive) 69 | def withReuseAddr(reuseAddr: Boolean) = copy[Req, Resp](reuseAddr = reuseAddr) 70 | def withTcpConnectTimeout(tcpConnectTimeout: Duration) = copy[Req, Resp](tcpConnectTimeout = tcpConnectTimeout) 71 | def withPipeline(pipeline: (Channel) => Unit) = copy[Req, Resp](customPipeline = pipeline) 72 | def handler(handle: (Channel, Future[Req]) => Future[Resp]) = copy[Req, Resp](handle = handle) 73 | 74 | def withEventLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](parentLoop = eventLoop, childLoop = eventLoop) 75 | def withEventLoop(parentLoop: EventLoopGroup, childLoop: EventLoopGroup) = copy[Req, Resp](parentLoop = parentLoop, childLoop = childLoop) 76 | def withChildLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](childLoop = eventLoop) 77 | def withParentLoop(eventLoop: EventLoopGroup) = copy[Req, Resp](parentLoop = eventLoop) 78 | 79 | val pipeline: Channel => Unit = (channel: Channel) => { 80 | customPipeline(channel) 81 | channel.pipeline().addLast(HttpServer.Handlers.handler, new SimpleChannelInboundHandler[Req] { 82 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 83 | error(cause.getMessage) 84 | debug(cause) 85 | handle(ctx.channel(), Future.exception(cause)) 86 | } 87 | 88 | def channelRead0(ctx: ChannelHandlerContext, req: Req) { 89 | ReferenceCountUtil.retain(req) 90 | handle(ctx.channel(), Future.value(req)).rescue { 91 | case t => 92 | error(t.getMessage) 93 | debug(t) 94 | val resp = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR) 95 | Future.value(resp) 96 | }.map { 97 | case resp: HttpResponse => 98 | val ka = { 99 | if (HttpUtil.isKeepAlive(req)) HttpHeaderValues.KEEP_ALIVE 100 | else HttpHeaderValues.CLOSE 101 | } 102 | resp.headers().set(HttpHeaderNames.CONNECTION, ka) 103 | val written = ctx.channel().writeAndFlush(resp) 104 | if (!HttpUtil.isKeepAlive(req)) written.addListener(HttpServer.closeListener) 105 | case resp => ctx.channel().writeAndFlush(resp) 106 | } ensure (ReferenceCountUtil.release(req)) 107 | } 108 | }) 109 | } 110 | 111 | } 112 | 113 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Config.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.TimeUnit 5 | 6 | import com.typesafe.config.ConfigFactory 7 | 8 | import scala.collection.JavaConverters._ 9 | import scala.concurrent.duration._ 10 | 11 | /** 12 | * Wrapper around Typesafe [[http://typesafehub.github.com/config/latest/api/com/typesafe/config/ConfigFactory.html ConfigFactory]]. 13 | * Cache results in vals as i expect most of these operations to be expensive at some point or another. 14 | */ 15 | object Config { 16 | val conf = ConfigFactory.load() 17 | 18 | /** 19 | * Gets the Bytes from Config 20 | * @param name Config directive 21 | * @return Option for a Long 22 | */ 23 | def getBytes(name: String): Option[Long] = Tryo(conf.getBytes(name)) 24 | 25 | /** 26 | * Gets the Bytes from Config and uses a fallback 27 | * @param name Config directive 28 | * @param fallback Fallback value if nothing is found 29 | * @return Option for a Long 30 | */ 31 | def getBytes(name: String, fallback: Long): Long = getBytes(name) getOrElse fallback 32 | 33 | /** 34 | * Gets the Duration from Config 35 | * @param name Config directive 36 | * @return Option for a Duration 37 | */ 38 | def getDuration(name: String): Option[Duration] = Tryo(conf.getDuration(name, TimeUnit.MILLISECONDS).millis) 39 | 40 | /** 41 | * Gets the Duration from Config and uses a fallback 42 | * @param name Config directive 43 | * @param fallback Fallback value if nothing is found 44 | * @return Option for a Duration 45 | */ 46 | def getDuration(name: String, fallback: Duration): Duration = getDuration(name) getOrElse fallback 47 | 48 | /** 49 | * Gets the Int from Config 50 | * @param name Config directive 51 | * @return Option for an Integer 52 | */ 53 | def getInt(name: String): Option[Int] = Tryo(conf.getInt(name)) 54 | 55 | /** 56 | * Gets the Int from Config and uses a fallback 57 | * @param name Config directive 58 | * @param fallback Fallback value if nothing is found 59 | * @return Option for a Integer 60 | */ 61 | def getInt(name: String, fallback: Int): Int = getInt(name) getOrElse fallback 62 | 63 | /** 64 | * Gets the Double from Config 65 | * @param name Config directive 66 | * @return Option for an Double 67 | */ 68 | def getDouble(name: String): Option[Double] = Tryo(conf.getDouble(name)) 69 | 70 | /** 71 | * Gets the Double from Config and uses a fallback 72 | * @param name Config directive 73 | * @param fallback Fallback value if nothing is found 74 | * @return Option for a Double 75 | */ 76 | def getDouble(name: String, fallback: Double): Double = getDouble(name) getOrElse fallback 77 | 78 | /** 79 | * Gets the Boolean from Config 80 | * @param name Config directive 81 | * @return Option for a Boolean 82 | */ 83 | def getBool(name: String): Option[Boolean] = Tryo(conf.getBoolean(name)) 84 | 85 | /** 86 | * Gets the Bool from Config and uses a fallback 87 | * @param name Config directive 88 | * @param fallback Fallback value if nothing is found 89 | * @return Option for a Bool 90 | */ 91 | def getBool(name: String, fallback: Boolean): Boolean = getBool(name) getOrElse fallback 92 | 93 | /** 94 | * Gets the String from Config 95 | * @param name Config directive 96 | * @return Option for a String 97 | */ 98 | def get(name: String): Option[String] = Tryo(conf.getString(name)) 99 | 100 | /** 101 | * Gets the String from Config and uses a fallback 102 | * @param name Config directive 103 | * @param fallback Fallback value if nothing is found 104 | * @return Option for a String 105 | */ 106 | def get(name: String, fallback: String): String = getString(name) getOrElse fallback 107 | 108 | /** 109 | * Gets the String from Config 110 | * @param name Config directive 111 | * @return Option for aString 112 | */ 113 | def getString(name: String): Option[String] = Tryo(conf.getString(name)) 114 | 115 | /** 116 | * Gets the String from Config and uses a fallback 117 | * @param name Config directive 118 | * @param fallback Fallback value if nothing is found 119 | * @return Option for a String 120 | */ 121 | def getString(name: String, fallback: String): String = getString(name) getOrElse fallback 122 | 123 | /** 124 | * Get a list of Strings out of your configuration. 125 | * 126 | * Example: 127 | * In your configuration you have a key 128 | * listen.on=["foo", "bar", "baz"] 129 | * 130 | * @param name Name of the configuration directive 131 | * @return Seq of Strings 132 | */ 133 | def getStringList(name: String): Option[Seq[String]] = Tryo(conf.getStringList(name).asScala.toList) match { 134 | case Some(l: List[String] @unchecked) if l.nonEmpty => Some(l) 135 | case _ => None 136 | } 137 | 138 | /** 139 | * Get a list of Strings out of your configuration. 140 | * 141 | * Example: 142 | * In your configuration you have a key 143 | * listen.on=["foo", "bar", "baz"] 144 | * 145 | * @param name Name of the configuration directive 146 | * @param fallback Fallback value if nothing is found 147 | * @return Seq of Strings 148 | */ 149 | def getStringList(name: String, fallback: Seq[String]): Seq[String] = getStringList(name) getOrElse fallback 150 | 151 | /** 152 | * Get a list of InetSocketAddresses back where you configured multiple IPs in your configuration. 153 | * 154 | * Example: 155 | * In your configuration you have a key 156 | * listen.on=["127.0.0.1:8080" ,"[2002:123::123]:5", ..] 157 | * 158 | * @param name Name of the configuration directive 159 | * @return Seq of InetSocketAddresses to be used 160 | */ 161 | def getInetAddrList(name: String): Option[Seq[InetSocketAddress]] = { 162 | val valid = Tryo(conf.getStringList(name).asScala.toList) getOrElse List() flatMap InetPrefix.stringToInetAddr 163 | if (valid.nonEmpty) Some(valid) else None 164 | } 165 | 166 | /** 167 | * Get a list of InetSocketAddresses back where you configured multiple IPs in your configuration. 168 | * 169 | * Example: 170 | * In your configuration you have a key 171 | * listen.on=["127.0.0.1:8080" ,"[2002:123::123]:5", ..] 172 | * 173 | * @param name Name of the configuration directive 174 | * @param fallback Fallback value if nothing is found 175 | * @return Seq of InetSocketAddresses to be used 176 | */ 177 | def getInetAddrList(name: String, fallback: Seq[String]): Seq[InetSocketAddress] = 178 | getInetAddrList(name) getOrElse fallback.flatMap(InetPrefix.stringToInetAddr) 179 | } 180 | 181 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/Wactor.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.util.concurrent.atomic.AtomicInteger 4 | import java.util.concurrent.{ ConcurrentLinkedQueue, Executor, Executors, ForkJoinPool } 5 | 6 | import scala.concurrent.duration.Duration 7 | import scala.util.{ Failure, Success, Try } 8 | 9 | /** 10 | * Wasted lightweight Actor implementation based on Viktor Klang's mini-Actor (https://gist.github.com/2362563). 11 | * 12 | * @param ec ExecutionContext to be used 13 | */ 14 | abstract class Wactor(maxQueueSize: Int = -1)(implicit ec: Executor = Wactor.ecForkJoin) extends Wactor.Address with Runnable with Logger { 15 | protected def receive: PartialFunction[Any, Any] 16 | 17 | /** 18 | * Netty-style exceptionCaught method which will get all exceptions caught while running a job. 19 | */ 20 | protected def exceptionCaught(e: Throwable) { 21 | e.printStackTrace() 22 | } 23 | 24 | // Our little indicator if this actor is on or not 25 | val on = new AtomicInteger(0) 26 | 27 | // Queue/LRU management counter 28 | private val queueSize = new AtomicInteger(0) 29 | 30 | // Our awesome little mailboxes, free of blocking and evil 31 | private final val mboxHigh = new ConcurrentLinkedQueue[Any] 32 | private final val mboxNormal = new ConcurrentLinkedQueue[Any] 33 | import io.wasted.util.Wactor._ 34 | 35 | // Rebindable top of the mailbox, bootstrapped to Dispatch behavior 36 | private var behavior: Behavior = Dispatch(this, receive) 37 | 38 | // Add a message with normal priority 39 | final override def !(msg: Any): Unit = behavior match { 40 | case dead @ Die.`like` => dead(msg) // Efficiently bail out if we're _known_ to be dead 41 | case _ => 42 | var dontQueueBecauseWeAreNotHighEnough = false // ;) 43 | // if our queue is considered full, discard the head 44 | if (maxQueueSize > 0 && queueSize.incrementAndGet > maxQueueSize) { 45 | dontQueueBecauseWeAreNotHighEnough = mboxNormal.poll == null 46 | queueSize.decrementAndGet 47 | } 48 | // Enqueue the message onto the mailbox only if we are high enough 49 | if (!dontQueueBecauseWeAreNotHighEnough) mboxNormal.offer(msg) 50 | async() // try to schedule for execution 51 | } 52 | 53 | // Add a message with high priority 54 | final override def !!(msg: Any): Unit = behavior match { 55 | case dead @ Die.`like` => dead(msg) // Efficiently bail out if we're _known_ to be dead 56 | case _ => 57 | // if our queue is considered full, discard the head of our **normal inbox** 58 | if (maxQueueSize > 0 && queueSize.incrementAndGet > maxQueueSize) { 59 | if (mboxNormal.poll == null) mboxHigh.poll 60 | queueSize.decrementAndGet 61 | } 62 | mboxHigh.offer(msg) // Enqueue the message onto the mailbox 63 | async() // try to schedule for execution 64 | } 65 | 66 | final def run(): Unit = try { 67 | if (on.get == 1) behavior = behavior({ 68 | queueSize.decrementAndGet 69 | val ret = if (mboxHigh.isEmpty) mboxNormal.poll else mboxHigh.poll 70 | if (ret == Die) { 71 | mboxNormal.clear() 72 | mboxHigh.clear() 73 | } 74 | ret 75 | })(behavior) 76 | } finally { 77 | // Switch ourselves off, and then see if we should be rescheduled for execution 78 | on.set(0) 79 | async() 80 | } 81 | 82 | // If there's something to process, and we're not already scheduled 83 | private final def async() { 84 | if (!(mboxHigh.isEmpty && mboxNormal.isEmpty) && on.compareAndSet(0, 1)) 85 | try { ec.execute(this) } catch { case e: Throwable => on.set(0); throw e } 86 | } 87 | } 88 | 89 | /** 90 | * Wasted lightweight Actor companion 91 | */ 92 | object Wactor extends Logger { 93 | private[util] lazy val ecForkJoin: Executor = new ForkJoinPool 94 | private[util] lazy val ecThreadPool: Executor = Executors.newCachedThreadPool 95 | private[util]type Behavior = Any => Effect 96 | private[util] sealed trait Effect extends (Behavior => Behavior) 97 | 98 | /* Effect which tells the actor to keep the current behavior. */ 99 | private[util] case object Stay extends Effect { 100 | def apply(old: Behavior): Behavior = old 101 | } 102 | 103 | /* Effect hwich tells the actor to use a new behavior. */ 104 | case class Become(like: Behavior) extends Effect { 105 | def apply(old: Behavior): Behavior = like 106 | } 107 | 108 | /* Default dispatch Behavior. */ 109 | object Dispatch { 110 | val fallbackPF: PartialFunction[Any, Any] = { case _ => } 111 | def apply(actor: Wactor, pf: PartialFunction[Any, Any]) = (msg: Any) => { 112 | val newpf: (Any => Any) = pf orElse fallbackPF 113 | Try(newpf(msg)) match { 114 | case Success(e) => 115 | case Failure(e) => actor.exceptionCaught(e) 116 | } 117 | Stay 118 | } 119 | } 120 | 121 | /* Behavior to tell the actor he's dead. */ 122 | final val Die = Become(msg => { 123 | error("Dropping msg [" + msg + "] due to severe case of death.") 124 | Stay 125 | }) 126 | 127 | /* The notion of an Address to where you can post messages to. */ 128 | trait Address { 129 | def !(msg: Any): Unit 130 | def !!(msg: Any): Unit = this ! msg 131 | 132 | /** 133 | * Schedule an event once on this Wactor. 134 | * 135 | * @param msg Message to be sent to the actor 136 | * @param initialDelay Initial delay before first firing 137 | */ 138 | def scheduleOnce(msg: Any, initialDelay: Duration)(implicit timer: WheelTimer): Schedule.Action = 139 | Schedule.once(() => { this ! msg }, initialDelay) 140 | 141 | /** 142 | * Schedule an event once on this Wactor for High Priority. 143 | * 144 | * @param msg Message to be sent to the actor 145 | * @param initialDelay Initial delay before first firing 146 | */ 147 | def scheduleOnceHigh(msg: Any, initialDelay: Duration)(implicit timer: WheelTimer): Schedule.Action = 148 | Schedule.once(() => { this !! msg }, initialDelay) 149 | 150 | /** 151 | * Schedule an event over and over again on this Wactor for High Priority. 152 | * 153 | * @param msg Message to be sent to the actor 154 | * @param initialDelay Initial delay before first firing 155 | * @param delay Delay to be called after the first firing 156 | */ 157 | def scheduleHigh(msg: Any, initialDelay: Duration, delay: Duration)(implicit timer: WheelTimer): Schedule.Action = 158 | Schedule.again(() => { this !! msg }, initialDelay, delay) 159 | 160 | /** 161 | * Schedule an event over and over again on this Wactor. 162 | * 163 | * @param msg Message to be sent to the actor 164 | * @param initialDelay Initial delay before first firing 165 | * @param delay Delay to be called after the first firing 166 | */ 167 | def schedule(msg: Any, initialDelay: Duration, delay: Duration)(implicit timer: WheelTimer): Schedule.Action = 168 | Schedule.again(() => { this ! msg }, initialDelay, delay) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/apn/PushService.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.apn 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.ConcurrentLinkedQueue 5 | import java.util.concurrent.atomic.AtomicReference 6 | 7 | import io.netty.bootstrap._ 8 | import io.netty.buffer._ 9 | import io.netty.channel.ChannelHandler.Sharable 10 | import io.netty.channel._ 11 | import io.netty.channel.socket.SocketChannel 12 | import io.netty.channel.socket.nio.NioSocketChannel 13 | import io.wasted.util._ 14 | 15 | import scala.annotation.tailrec 16 | import scala.concurrent.ExecutionContext.Implicits.global 17 | import scala.concurrent.Future 18 | import scala.concurrent.duration._ 19 | 20 | /** 21 | * Declares the different connection states 22 | */ 23 | object ConnectionState extends Enumeration { 24 | val fresh, connected, disconnected = Value 25 | } 26 | 27 | /** 28 | * Apple Push Notification Push class which will handle all delivery 29 | * 30 | * @param params Connection Parameters 31 | * @param eventLoop Optional custom event loop for proxy applications 32 | * @param wheelTimer WheelTimer for scheduling 33 | */ 34 | @Sharable 35 | class PushService(params: Params, eventLoop: EventLoopGroup = Netty.eventLoop)(implicit val wheelTimer: WheelTimer) 36 | extends SimpleChannelInboundHandler[ByteBuf] with Logger { thisService => 37 | override val loggerName = getClass.getCanonicalName + ":" + params.name 38 | def addr: InetSocketAddress = if (params.sandbox) sandbox else production 39 | 40 | private final val production = { 41 | new java.net.InetSocketAddress(java.net.InetAddress.getByName("gateway.push.apple.com"), 2195) 42 | } 43 | private final val sandbox = { 44 | new java.net.InetSocketAddress(java.net.InetAddress.getByName("gateway.sandbox.push.apple.com"), 2195) 45 | } 46 | 47 | private val srv = new Bootstrap() 48 | private val bootstrap = srv.group(eventLoop) 49 | .channel(classOf[NioSocketChannel]) 50 | .remoteAddress(addr) 51 | .option[java.lang.Boolean](ChannelOption.TCP_NODELAY, true) 52 | .option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) 53 | .option[java.lang.Boolean](ChannelOption.SO_REUSEADDR, true) 54 | .option[java.lang.Integer](ChannelOption.SO_LINGER, 0) 55 | .option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, params.timeout * 1000) 56 | .handler(new ChannelInitializer[SocketChannel] { 57 | override def initChannel(ch: SocketChannel) { 58 | val p = ch.pipeline() 59 | Tryo(params.sslCtx.newHandler(ch.alloc())) match { 60 | case Some(handler) => p.addLast("ssl", handler) 61 | case _ => 62 | error("Unable to create SSL Context") 63 | ch.close() 64 | } 65 | } 66 | }) 67 | 68 | /* separate state for flushing */ 69 | private val flushing = new java.util.concurrent.atomic.AtomicBoolean(false) 70 | 71 | /* statistical tracking */ 72 | private val written = new java.util.concurrent.atomic.AtomicLong(0L) 73 | def sentMessages = written.get 74 | 75 | private final def sentFuture(msg: Message) = new ChannelFutureListener() { 76 | override def operationComplete(cf: ChannelFuture) { 77 | if (cf.isSuccess) written.addAndGet(1L) 78 | else queued.add(msg) 79 | } 80 | } 81 | 82 | /* queues for our data */ 83 | private val queued = new ConcurrentLinkedQueue[Message]() 84 | def send(message: Message): Boolean = channel.get match { 85 | case Some(chan) if state.get == ConnectionState.connected => 86 | chan.writeAndFlush(message.bytes.retain).addListener(sentFuture(message)) 87 | if (!queued.isEmpty && !flushing.get) deliverQueued() 88 | true 89 | case _ => queued.add(message) 90 | } 91 | 92 | /* reference to channel and state */ 93 | private val ping = new AtomicReference[Option[Schedule.Action]](None) 94 | private val channel = new AtomicReference[Option[Channel]](None) 95 | private val state = new AtomicReference[ConnectionState.Value](ConnectionState.fresh) 96 | def connectionState = state.get 97 | 98 | /** 99 | * Connects to the Apple Push Servers 100 | */ 101 | def connect(): Unit = synchronized { 102 | if (state.get == ConnectionState.connected) return 103 | channel.set(Tryo(bootstrap.clone.connect().sync().channel())) 104 | if (channel.get.isEmpty) { 105 | state.set(ConnectionState.disconnected) 106 | Schedule.once(() => connect(), 5.seconds) 107 | } else { 108 | state.set(ConnectionState.connected) 109 | ping.set(Some(Schedule.again(() => channel.get.foreach { case chan if !chan.isActive => reconnect() case _ => }, 5.seconds, 5.seconds))) 110 | } 111 | } 112 | 113 | /** 114 | * Disconnects from the Apple Push Server 115 | */ 116 | def disconnect(): Unit = synchronized { 117 | channel.get.foreach(_.close()) 118 | channel.set(None) 119 | ping.get.foreach(_.cancel()) 120 | ping.set(None) 121 | state.set(ConnectionState.disconnected) 122 | } 123 | 124 | /** 125 | * Reconnect to Apple Push Servers 126 | */ 127 | private def reconnect(): Unit = synchronized { 128 | disconnect() 129 | info("Reconnecting..") 130 | connect() 131 | } 132 | 133 | /** 134 | * Delivery messages to Apple Push Servers. 135 | */ 136 | private def deliverQueued(): Unit = Future { 137 | if (!flushing.compareAndSet(false, true) && state.get == ConnectionState.connected && !queued.isEmpty) { 138 | channel.get.foreach(write) 139 | flushing.set(false) 140 | } 141 | } 142 | 143 | @tailrec 144 | private def write(channel: Channel): Unit = { 145 | val msg = queued.poll() 146 | channel.writeAndFlush(msg.bytes.retain).addListener(sentFuture(msg)) 147 | if (!queued.isEmpty) write(channel) 148 | } 149 | 150 | override def channelRead0(ctx: ChannelHandlerContext, buf: ByteBuf) { 151 | val readable = buf.readableBytes 152 | val cmd = buf.getByte(0).toInt 153 | if (readable == 6 && cmd == 8) { 154 | val errorCode = buf.getByte(1).toInt 155 | val id = buf.getInt(2) 156 | errorCode match { 157 | case 0 => // "No errors encountered" 158 | case 1 => error("Processing error on %s", id) 159 | case 2 => error("Missing device token on %s", id) 160 | case 3 => error("Missing topic on %s", id) 161 | case 4 => error("Missing payload on %s", id) 162 | case 5 => error("Invalid token size on %s", id) 163 | case 6 => error("Invalid topic size on %s", id) 164 | case 7 => error("Invalid payload size on %s", id) 165 | case 8 => error("Invalid token on %s", id) 166 | case 10 => info("Shutdown on %s", id) 167 | case 255 => debug("None (unknown) on %s", id) 168 | case x => debug("Unknown error %s on %s", x, id) 169 | } 170 | } 171 | } 172 | 173 | override def channelActive(ctx: ChannelHandlerContext) { 174 | info("APN connected!") 175 | } 176 | 177 | override def channelInactive(ctx: ChannelHandlerContext) { 178 | info("APN disconnected!") 179 | reconnect() 180 | } 181 | 182 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 183 | http.ExceptionHandler(ctx, cause) match { 184 | case Some(e) => e.printStackTrace() 185 | case _ => 186 | } 187 | ctx.close() 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/NettyClientBuilder.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | 3 | import java.net.InetSocketAddress 4 | import java.util.concurrent.atomic.AtomicLong 5 | 6 | import com.twitter.util._ 7 | import io.netty.bootstrap.Bootstrap 8 | import io.netty.channel._ 9 | import io.netty.channel.socket.SocketChannel 10 | import io.netty.channel.socket.nio.NioSocketChannel 11 | import io.netty.util.ReferenceCountUtil 12 | 13 | trait NettyClientBuilder[Req, Resp] extends Logger { 14 | def codec: NettyCodec[Req, Resp] 15 | def remote: List[InetSocketAddress] 16 | //def hostConnectionLimit: Int 17 | //def hostConnectionCoreSize: Int 18 | def globalTimeout: Option[Duration] 19 | def tcpConnectTimeout: Option[Duration] 20 | def connectTimeout: Option[Duration] 21 | def requestTimeout: Option[Duration] 22 | def tcpKeepAlive: Boolean 23 | def reuseAddr: Boolean 24 | def tcpNoDelay: Boolean 25 | def soLinger: Int 26 | def retries: Int 27 | def eventLoop: EventLoopGroup 28 | 29 | implicit val timer = WheelTimer.twitter 30 | protected[this] lazy val requestCounter = new AtomicLong() 31 | protected[this] lazy val clnt = new Bootstrap 32 | protected[this] lazy val bootstrap = { 33 | // produce a warnings if timeouts do not add up 34 | val reqT = requestTimeout.map(_ * (retries + 1)) 35 | val outterTotalT: Iterable[Duration] = List(reqT, connectTimeout).flatten 36 | if (outterTotalT.nonEmpty && globalTimeout.exists(_ < outterTotalT.reduceRight(_ + _))) { 37 | val logger = Logger(getClass.getCanonicalName) 38 | val str = "GlobalTimeout is lower than ConnectTimeout + RequestTimeout * (Retries + 1)" 39 | logger.warn(str + ". See debug for Trace") 40 | logger.debug(new IllegalArgumentException(str)) 41 | } 42 | 43 | val innerReadReqT = codec.readTimeout.map(_ * (retries + 1)) 44 | val innerReadT: Iterable[Duration] = List(reqT, innerReadReqT).flatten 45 | if (innerReadT.nonEmpty && globalTimeout.exists(_ < innerReadT.reduceRight(_ + _))) { 46 | val logger = Logger(getClass.getCanonicalName) 47 | val str = "GlobalTimeout is lower than ConnectTimeout + ReadTimeout * (Retries + 1)" 48 | logger.warn(str + ". See debug for Trace") 49 | logger.debug(new IllegalArgumentException(str)) 50 | } 51 | 52 | val innerWriteReqT = codec.writeTimeout.map(_ * (retries + 1)) 53 | val innerWriteT: Iterable[Duration] = List(reqT, innerWriteReqT).flatten 54 | if (innerWriteT.nonEmpty && globalTimeout.exists(_ < innerWriteT.reduceRight(_ + _))) { 55 | val logger = Logger(getClass.getCanonicalName) 56 | val str = "GlobalTimeout is lower than ConnectTimeout + WriteTimeout * (Retries + 1)" 57 | logger.warn(str + ". See debug for Trace") 58 | logger.debug(new IllegalArgumentException(str)) 59 | } 60 | val handler = new ChannelInitializer[SocketChannel] { 61 | override def initChannel(ch: SocketChannel): Unit = codec.clientPipeline(ch) 62 | } 63 | val baseGrp = clnt.group(eventLoop) 64 | .channel(classOf[NioSocketChannel]) 65 | .option[java.lang.Boolean](ChannelOption.TCP_NODELAY, tcpNoDelay) 66 | .option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, tcpKeepAlive) 67 | .option[java.lang.Boolean](ChannelOption.SO_REUSEADDR, reuseAddr) 68 | .option[java.lang.Integer](ChannelOption.SO_LINGER, soLinger) 69 | 70 | val tcpCtGrp = tcpConnectTimeout.map { tcpCT => 71 | baseGrp.option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, tcpCT.inMillis.toInt) 72 | }.getOrElse(baseGrp) 73 | 74 | tcpCtGrp.handler(handler) 75 | } 76 | 77 | /** 78 | * Write a Request directly through to the given URI. 79 | * The request to generate the response should be used to prepare 80 | * the request only once the connection is established. 81 | * This reduces the context-switching for allocation/deallocation 82 | * on failed connects. 83 | * 84 | * @param uri What could this be? 85 | * @param req Request object 86 | * @return Future Resp 87 | */ 88 | def write(uri: java.net.URI, req: Req): Future[Resp] = write(uri.toString, req) 89 | 90 | /** 91 | * Write a Request directly through to the given URI. 92 | * The request to generate the response should be used to prepare 93 | * the request only once the connection is established. 94 | * This reduces the context-switching for allocation/deallocation 95 | * on failed connects. 96 | * 97 | * @param uri What could this be? 98 | * @param req Request object 99 | * @return Future Resp 100 | */ 101 | def write(uri: String, req: Req): Future[Resp] = { 102 | val result = run(uri, req) 103 | globalTimeout.map(result.raiseWithin).getOrElse(result) 104 | } 105 | 106 | /** 107 | * Run the request against one of the specified remotes 108 | * @param uri What could this be? 109 | * @param req Request object 110 | * @param counter Request counter 111 | * @return Future Resp 112 | */ 113 | protected[this] def run(uri: String, req: Req, counter: Int = 0): Future[Resp] = { 114 | // if it is the first time fire this request, we retain it twice for retries 115 | ReferenceCountUtil.retain(req, if (counter == 0) 1 else 2) 116 | getConnection(uri).flatMap { chan => 117 | val resp = codec.clientConnected(chan, req) 118 | requestTimeout.map(resp.raiseWithin).getOrElse(resp).onFailure { 119 | case t: Throwable => resp.map(ReferenceCountUtil.release(_)) // release on failure 120 | } 121 | }.rescue { 122 | case t: Throwable if counter <= retries => 123 | run(uri, req, counter + 1) 124 | case t: Throwable => Future.exception(t) 125 | }.ensure { 126 | // clean up the old retain 127 | if (counter == 0) ReferenceCountUtil.release(req) 128 | } 129 | } 130 | 131 | /** 132 | * Get a connection from the pool and regard the hostConnectionLimit 133 | * @param uri URI we want to connect to 134 | * @return Future Channel 135 | */ 136 | protected[this] def getConnection(uri: String): Future[Channel] = { 137 | // TODO this is not implemented yet, looking for a nice way to keep track of connections 138 | connect(uri) 139 | } 140 | 141 | /** 142 | * Connects to the given URI and returns a Channel using a round-robin remote selection 143 | * @param uri URI we want to connect to 144 | * @return Future Channel 145 | */ 146 | protected[this] def connect(uri: String): Future[Channel] = { 147 | val connectPromise = Promise[Channel]() 148 | 149 | val safeUri = new java.net.URI(uri.split("/", 4).take(3).mkString("/")) 150 | 151 | val connected = remote match { 152 | case Nil => bootstrap.clone().connect(safeUri.getHost, getPort(safeUri)) 153 | case hosts => 154 | // round robin connection 155 | val sock = hosts((requestCounter.incrementAndGet() % hosts.length).toInt) 156 | bootstrap.connect(sock) 157 | } 158 | connected.addListener { cf: ChannelFuture => 159 | if (!cf.isSuccess) connectPromise.setException(cf.cause()) 160 | else connectPromise.setValue(cf.channel) 161 | } 162 | connectTimeout.map(connectPromise.raiseWithin).getOrElse(connectPromise) 163 | } 164 | 165 | /** 166 | * Gets the port for the given URI. 167 | * @param uri The URI where we want the Port number for 168 | * @return Port Number 169 | */ 170 | protected[this] def getPort(uri: java.net.URI): Int 171 | } 172 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/HttpClient.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.http 2 | 3 | import java.net.{InetAddress, InetSocketAddress, URI} 4 | 5 | import com.twitter.util.{Duration, Future} 6 | import io.netty.buffer._ 7 | import io.netty.channel._ 8 | import io.netty.handler.codec.http._ 9 | import io.wasted.util._ 10 | 11 | object HttpClient { 12 | 13 | /** 14 | * These can be used to modify the pipeline afterwards without having to guess their names 15 | */ 16 | object Handlers { 17 | val ssl = "ssl" 18 | val codec = "codec" 19 | val aggregator = "aggregator" 20 | val decompressor = "decompressor" 21 | val readTimeout = "readTimeout" 22 | val writeTimeout = "writeTimeout" 23 | val handler = "handler" 24 | } 25 | } 26 | 27 | /** 28 | * wasted.io Scala Http Client 29 | * @param codec Http Codec 30 | * @param remote Remote Host and Port 31 | * @param hostConnectionLimit Number of open connections for this client. Defaults to 1 32 | * @param hostConnectionCoreSize Number of connections to keep open for this client. Defaults to 0 33 | * @param globalTimeout Global Timeout for the completion of a request 34 | * @param tcpConnectTimeout TCP Connect Timeout 35 | * @param connectTimeout Timeout for establishing the Service 36 | * @param requestTimeout Timeout for each request 37 | * @param tcpKeepAlive TCP KeepAlive. Defaults to false 38 | * @param reuseAddr Reuse-Address. Defaults to true 39 | * @param tcpNoDelay TCP No-Delay. Defaults to true 40 | * @param soLinger soLinger. Defaults to 0 41 | * @param retries On connection or timeouts, how often should we retry? Defaults to 0 42 | * @param eventLoop Netty Event-Loop 43 | */ 44 | case class HttpClient[T <: HttpObject](codec: NettyHttpCodec[HttpRequest, T] = NettyHttpCodec[HttpRequest, T](), 45 | remote: List[InetSocketAddress] = List.empty, 46 | hostConnectionLimit: Int = 1, 47 | hostConnectionCoreSize: Int = 0, 48 | globalTimeout: Option[Duration] = None, 49 | tcpConnectTimeout: Option[Duration] = None, 50 | connectTimeout: Option[Duration] = None, 51 | requestTimeout: Option[Duration] = None, 52 | tcpKeepAlive: Boolean = false, 53 | reuseAddr: Boolean = true, 54 | tcpNoDelay: Boolean = true, 55 | soLinger: Int = 0, 56 | retries: Int = 0, 57 | eventLoop: EventLoopGroup = Netty.eventLoop) extends NettyClientBuilder[HttpRequest, T] { 58 | def withSpecifics(codec: NettyHttpCodec[HttpRequest, T]) = copy[T](codec = codec) 59 | def withSoLinger(soLinger: Int) = copy[T](soLinger = soLinger) 60 | def withTcpNoDelay(tcpNoDelay: Boolean) = copy[T](tcpNoDelay = tcpNoDelay) 61 | def withTcpKeepAlive(tcpKeepAlive: Boolean) = copy[T](tcpKeepAlive = tcpKeepAlive) 62 | def withReuseAddr(reuseAddr: Boolean) = copy[T](reuseAddr = reuseAddr) 63 | def withGlobalTimeout(globalTimeout: Duration) = copy[T](globalTimeout = Some(globalTimeout)) 64 | def withTcpConnectTimeout(tcpConnectTimeout: Duration) = copy[T](tcpConnectTimeout = Some(tcpConnectTimeout)) 65 | def withConnectTimeout(connectTimeout: Duration) = copy[T](connectTimeout = Some(connectTimeout)) 66 | def withRequestTimeout(requestTimeout: Duration) = copy[T](requestTimeout = Some(requestTimeout)) 67 | def withHostConnectionLimit(limit: Int) = copy[T](hostConnectionLimit = limit) 68 | def withHostConnectionCoresize(coreSize: Int) = copy[T](hostConnectionCoreSize = coreSize) 69 | def withRetries(retries: Int) = copy[T](retries = retries) 70 | def withEventLoop(eventLoop: EventLoopGroup) = copy[T](eventLoop = eventLoop) 71 | def connectTo(host: String, port: Int) = copy[T](remote = List(new InetSocketAddress(InetAddress.getByName(host), port))) 72 | def connectTo(hosts: List[InetSocketAddress]) = copy[T](remote = hosts) 73 | 74 | protected def getPortString(url: java.net.URI): String = if (url.getPort == -1) "" else ":" + url.getPort 75 | protected def getPort(url: java.net.URI): Int = if (url.getPort > 0) url.getPort else url.getScheme match { 76 | case "http" => 80 77 | case "https" => 443 78 | } 79 | 80 | /** 81 | * Run a GET-Request on the given URI. 82 | * 83 | * @param url What could this be? 84 | * @param headers The mysteries keep piling up! 85 | */ 86 | def get(url: java.net.URI, headers: Map[String, String] = Map()): Future[T] = { 87 | val path = if (url.getQuery == null) url.getPath else url.getPath + "?" + url.getQuery 88 | val req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, path) 89 | req.headers.set(HttpHeaderNames.HOST, url.getHost + getPortString(url)) 90 | req.headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE) 91 | headers.foreach(f => req.headers.set(f._1, f._2)) 92 | write(url, req) 93 | } 94 | 95 | /** 96 | * Send a PUT/POST-Request on the given URI with body. 97 | * 98 | * @param url This is getting weird.. 99 | * @param mime The MIME type of the request 100 | * @param body ByteArray to be send 101 | * @param headers I don't like to explain trivial stuff 102 | * @param method HTTP Method to be used 103 | */ 104 | def post(url: java.net.URI, mime: String, body: Seq[Byte] = Seq(), headers: Map[String, String] = Map(), method: HttpMethod): Future[T] = { 105 | val content = Unpooled.wrappedBuffer(body.toArray) 106 | val path = if (url.getQuery == null) url.getPath else url.getPath + "?" + url.getQuery 107 | val req = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, method, path, content) 108 | req.headers.set(HttpHeaderNames.HOST, url.getHost + getPortString(url)) 109 | req.headers.set(HttpHeaderNames.CONNECTION, HttpHeaderValues.CLOSE) 110 | req.headers.set(HttpHeaderNames.CONTENT_TYPE, mime) 111 | req.headers.set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes) 112 | headers.foreach(f => req.headers.set(f._1, f._2)) 113 | write(url, req) 114 | } 115 | 116 | /** 117 | * Allow for sending raw http requests 118 | * @param req big mystery 119 | * @param hostAndPort optional host and port to send to 120 | */ 121 | def raw(req: HttpRequest, hostAndPort: Option[(String, Int)] = None): Future[T] = { 122 | assert(remote.nonEmpty || hostAndPort.isDefined, "Either remotes need to be specified on creation or hostAndPort needs to be given") 123 | val whereTo = hostAndPort.getOrElse { 124 | val r = remote.head 125 | r.getHostName -> r.getPort 126 | } 127 | val proto = if (codec.sslCtx.isDefined) "https" else "http" 128 | write(new URI(proto + "://" + whereTo._1 + ":" + whereTo._2), req) 129 | } 130 | 131 | /** 132 | * Send a message to thruput.io endpoint. 133 | * 134 | * @param url thruput.io Endpoint 135 | * @param auth Authentication Key for thruput.io platform 136 | * @param sign Signing Key for thruput.io platform 137 | * @param payload Payload to be sent 138 | */ 139 | def thruput(url: java.net.URI, auth: java.util.UUID, sign: java.util.UUID, payload: String): Future[T] = { 140 | val headers = Map("X-Io-Auth" -> auth.toString, "X-Io-Sign" -> io.wasted.util.Hashing.sign(sign.toString, payload)) 141 | post(url, "application/json", payload.map(_.toByte), headers, HttpMethod.PUT) 142 | } 143 | } 144 | 145 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/NettyHttpCodec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | package http 3 | 4 | import com.twitter.conversions.storage._ 5 | import com.twitter.util._ 6 | import io.netty.channel.{Channel, ChannelHandlerContext, SimpleChannelInboundHandler} 7 | import io.netty.handler.codec.http._ 8 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 9 | import io.netty.handler.ssl.{SslContext, SslContextBuilder} 10 | import io.netty.handler.timeout.{ReadTimeoutHandler, WriteTimeoutHandler} 11 | import io.netty.util.ReferenceCountUtil 12 | 13 | /** 14 | * wasted.io Scala Http Codec 15 | * 16 | * For composition you may use HttpCodec[HttpObject]().withChunking(true)... 17 | * 18 | * @param compressionLevel GZip compression level 19 | * @param decompression GZip decompression? 20 | * @param keepAlive TCP KeepAlive. Defaults to false 21 | * @param chunked Should we pass through chunks? 22 | * @param chunking Should we allow chunking? 23 | * @param maxChunkSize Maximum chunk size 24 | * @param maxRequestSize Maximum request size 25 | * @param maxResponseSize Maximum response size 26 | * @param maxInitialLineLength Maximum line length for GET/POST /foo... 27 | * @param maxHeaderSize Maximum header size 28 | * @param readTimeout Channel Read Timeout 29 | * @param writeTimeout Channel Write Timeout 30 | * @param sslCtx Netty SSL Context 31 | */ 32 | final case class NettyHttpCodec[Req <: HttpMessage, Resp <: HttpObject](compressionLevel: Int = -1, 33 | decompression: Boolean = true, 34 | keepAlive: Boolean = false, 35 | chunked: Boolean = false, 36 | chunking: Boolean = true, 37 | maxChunkSize: StorageUnit = 5.megabytes, 38 | maxRequestSize: StorageUnit = 5.megabytes, 39 | maxResponseSize: StorageUnit = 5.megabytes, 40 | maxInitialLineLength: StorageUnit = 4096.bytes, 41 | maxHeaderSize: StorageUnit = 8192.bytes, 42 | readTimeout: Option[Duration] = None, 43 | writeTimeout: Option[Duration] = None, 44 | sslCtx: Option[SslContext] = None) extends NettyCodec[Req, Resp] { 45 | 46 | def withChunking(chunking: Boolean, chunked: Boolean = false, maxChunkSize: StorageUnit = 5.megabytes) = 47 | copy[Req, Resp](chunking = chunking, chunked = chunked, maxChunkSize = maxChunkSize) 48 | 49 | def withCompression(compressionLevel: Int) = copy[Req, Resp](compressionLevel = compressionLevel) 50 | def withDecompression(decompression: Boolean) = copy[Req, Resp](decompression = decompression) 51 | def withMaxRequestSize(maxRequestSize: StorageUnit) = copy[Req, Resp](maxRequestSize = maxRequestSize) 52 | def withMaxResponseSize(maxResponseSize: StorageUnit) = copy[Req, Resp](maxResponseSize = maxResponseSize) 53 | def withMaxInitialLineLength(maxInitialLineLength: StorageUnit) = copy[Req, Resp](maxInitialLineLength = maxInitialLineLength) 54 | def withMaxHeaderSize(maxHeaderSize: StorageUnit) = copy[Req, Resp](maxHeaderSize = maxHeaderSize) 55 | def withTls(sslCtx: SslContext) = copy[Req, Resp](sslCtx = Some(sslCtx)) 56 | def withKeepAlive(keepAlive: Boolean) = copy[Req, Resp](keepAlive = keepAlive) 57 | def withReadTimeout(timeout: Duration) = copy[Req, Resp](readTimeout = Some(timeout)) 58 | def withWriteTimeout(timeout: Duration) = copy[Req, Resp](writeTimeout = Some(timeout)) 59 | 60 | def withInsecureTls() = { 61 | val ctx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build() 62 | copy[Req, Resp](sslCtx = Some(ctx)) 63 | } 64 | 65 | /** 66 | * Sets up basic Server-Pipeline for this Codec 67 | * @param channel Channel to apply the Pipeline to 68 | */ 69 | def serverPipeline(channel: Channel): Unit = { 70 | val p = channel.pipeline() 71 | sslCtx.foreach(e => p.addLast(HttpServer.Handlers.ssl, e.newHandler(channel.alloc()))) 72 | val maxInitialBytes = maxInitialLineLength.inBytes.toInt 73 | val maxHeaderBytes = maxHeaderSize.inBytes.toInt 74 | val maxContentLength = this.maxResponseSize.inBytes.toInt 75 | p.addLast(HttpServer.Handlers.codec, new HttpServerCodec(maxInitialBytes, maxHeaderBytes, maxChunkSize.inBytes.toInt)) 76 | if (compressionLevel > 0) { 77 | p.addLast(HttpServer.Handlers.compressor, new HttpContentCompressor(compressionLevel)) 78 | } 79 | if (chunking && !chunked) { 80 | p.addLast(HttpServer.Handlers.dechunker, new HttpObjectAggregator(maxContentLength)) 81 | } 82 | } 83 | 84 | /** 85 | * Sets up basic HTTP Pipeline 86 | * @param channel Channel to apply the Pipeline to 87 | */ 88 | def clientPipeline(channel: Channel): Unit = { 89 | val pipeline = channel.pipeline() 90 | sslCtx.foreach(e => pipeline.addLast(HttpServer.Handlers.ssl, e.newHandler(channel.alloc()))) 91 | val maxInitialBytes = this.maxInitialLineLength.inBytes.toInt 92 | val maxHeaderBytes = this.maxHeaderSize.inBytes.toInt 93 | val maxChunkSize = this.maxChunkSize.inBytes.toInt 94 | val maxContentLength = this.maxResponseSize.inBytes.toInt 95 | pipeline.addLast(HttpClient.Handlers.codec, new HttpClientCodec(maxInitialBytes, maxHeaderBytes, maxChunkSize)) 96 | if (chunking && !chunked) { 97 | pipeline.addLast(HttpClient.Handlers.aggregator, new HttpObjectAggregator(maxContentLength)) 98 | } 99 | if (decompression) { 100 | pipeline.addLast(HttpClient.Handlers.decompressor, new HttpContentDecompressor()) 101 | } 102 | } 103 | 104 | /** 105 | * Handle the connected channel and send the request 106 | * @param channel Channel we're connected to 107 | * @param request Object we want to send 108 | * @return 109 | */ 110 | def clientConnected(channel: Channel, request: Req): Future[Resp] = { 111 | val result = Promise[Resp] 112 | readTimeout.foreach { readTimeout => 113 | channel.pipeline.addFirst(HttpClient.Handlers.readTimeout, new ReadTimeoutHandler(readTimeout.inMillis.toInt) { 114 | override def readTimedOut(ctx: ChannelHandlerContext) { 115 | ctx.channel.close 116 | result.setException(new IllegalStateException("Read timed out")) 117 | } 118 | }) 119 | } 120 | writeTimeout.foreach { writeTimeout => 121 | channel.pipeline.addFirst(HttpClient.Handlers.writeTimeout, new WriteTimeoutHandler(writeTimeout.inMillis.toInt) { 122 | override def writeTimedOut(ctx: ChannelHandlerContext) { 123 | ctx.channel.close 124 | result.setException(new IllegalStateException("Write timed out")) 125 | } 126 | }) 127 | } 128 | 129 | channel.pipeline.addLast(HttpClient.Handlers.handler, new SimpleChannelInboundHandler[Resp] { 130 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 131 | ExceptionHandler(ctx, cause) 132 | result.setException(cause) 133 | ctx.channel.close 134 | } 135 | 136 | def channelRead0(ctx: ChannelHandlerContext, msg: Resp) { 137 | if (!result.isDefined && result.isInterrupted.isEmpty) result.setValue(ReferenceCountUtil.retain(msg)) 138 | if (keepAlive && HttpUtil.isKeepAlive(request)) {} else channel.close() 139 | } 140 | }) 141 | val ka = if (keepAlive && HttpUtil.isKeepAlive(request)) 142 | HttpHeaderValues.KEEP_ALIVE else HttpHeaderValues.CLOSE 143 | request.headers().set(HttpHeaderNames.CONNECTION, ka) 144 | 145 | channel.writeAndFlush(request) 146 | result 147 | } 148 | } -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/NettyWebSocketCodec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util 2 | package http 3 | 4 | import java.util.concurrent.TimeUnit 5 | 6 | import com.twitter.concurrent.{ Broker, Offer } 7 | import com.twitter.conversions.storage._ 8 | import com.twitter.conversions.time._ 9 | import com.twitter.util._ 10 | import io.netty.buffer.ByteBuf 11 | import io.netty.channel._ 12 | import io.netty.handler.codec.http._ 13 | import io.netty.handler.codec.http.websocketx.{ BinaryWebSocketFrame, WebSocketClientHandshakerFactory, WebSocketFrame, WebSocketVersion } 14 | import io.netty.handler.ssl.util.InsecureTrustManagerFactory 15 | import io.netty.handler.ssl.{ SslContext, SslContextBuilder } 16 | import io.netty.handler.timeout.{ ReadTimeoutHandler, WriteTimeoutHandler } 17 | 18 | /** 19 | * String Channel 20 | * @param out Outbound Broker 21 | * @param in Inbound Offer 22 | * @param channel Netty Channel 23 | */ 24 | final case class NettyWebSocketChannel(out: Broker[WebSocketFrame], in: Offer[WebSocketFrame], private val channel: Channel) { 25 | def close(): Future[Unit] = { 26 | val closed = Promise[Unit]() 27 | channel.closeFuture().addListener(new ChannelFutureListener { 28 | override def operationComplete(f: ChannelFuture): Unit = { 29 | closed.setDone() 30 | } 31 | }) 32 | closed.raiseWithin(Duration(2, TimeUnit.SECONDS))(WheelTimer.twitter) 33 | } 34 | 35 | val onDisconnect = Promise[Unit]() 36 | channel.closeFuture().addListener(new ChannelFutureListener { 37 | override def operationComplete(f: ChannelFuture): Unit = onDisconnect.setDone() 38 | }) 39 | 40 | def !(s: ByteBuf): Unit = out ! new BinaryWebSocketFrame(s) 41 | def !(s: Array[Byte]): Unit = out ! new BinaryWebSocketFrame(channel.alloc().buffer(s.length).writeBytes(s)) 42 | def !(s: WebSocketFrame): Unit = out ! s 43 | def foreach(run: WebSocketFrame => Unit) = in.foreach(run) 44 | } 45 | 46 | /** 47 | * wasted.io Scala WebSocket Codec 48 | * 49 | * For composition you may use NettyWebSocketCodec()... 50 | * 51 | * @param compressionLevel GZip compression level 52 | * @param decompression GZip decompression? 53 | * @param subprotocols Subprotocols which to support 54 | * @param allowExtensions Allow Extensions 55 | * @param keepAlive TCP KeepAlive. Defaults to false 56 | * @param maxChunkSize Maximum chunk size 57 | * @param maxRequestSize Maximum request size 58 | * @param maxResponseSize Maximum response size 59 | * @param maxInitialLineLength Maximum line length for GET/POST /foo... 60 | * @param maxHeaderSize Maximum header size 61 | * @param readTimeout Channel Read Timeout 62 | * @param writeTimeout Channel Write Timeout 63 | * @param sslCtx Netty SSL Context 64 | */ 65 | final case class NettyWebSocketCodec(compressionLevel: Int = -1, 66 | subprotocols: String = null, 67 | allowExtensions: Boolean = true, 68 | decompression: Boolean = true, 69 | keepAlive: Boolean = false, 70 | maxChunkSize: StorageUnit = 5.megabytes, 71 | maxRequestSize: StorageUnit = 5.megabytes, 72 | maxResponseSize: StorageUnit = 5.megabytes, 73 | maxInitialLineLength: StorageUnit = 4096.bytes, 74 | maxHeaderSize: StorageUnit = 8192.bytes, 75 | readTimeout: Option[Duration] = None, 76 | writeTimeout: Option[Duration] = None, 77 | sslCtx: Option[SslContext] = None) 78 | extends NettyCodec[java.net.URI, NettyWebSocketChannel] { 79 | 80 | def withCompression(compressionLevel: Int) = copy(compressionLevel = compressionLevel) 81 | def withDecompression(decompression: Boolean) = copy(decompression = decompression) 82 | def withMaxRequestSize(maxRequestSize: StorageUnit) = copy(maxRequestSize = maxRequestSize) 83 | def withMaxResponseSize(maxResponseSize: StorageUnit) = copy(maxResponseSize = maxResponseSize) 84 | def withMaxInitialLineLength(maxInitialLineLength: StorageUnit) = copy(maxInitialLineLength = maxInitialLineLength) 85 | def withMaxHeaderSize(maxHeaderSize: StorageUnit) = copy(maxHeaderSize = maxHeaderSize) 86 | def withTls(sslCtx: SslContext) = copy(sslCtx = Some(sslCtx)) 87 | def withKeepAlive(keepAlive: Boolean) = copy(keepAlive = keepAlive) 88 | def withReadTimeout(timeout: Duration) = copy(readTimeout = Some(timeout)) 89 | def withWriteTimeout(timeout: Duration) = copy(writeTimeout = Some(timeout)) 90 | 91 | def withInsecureTls() = { 92 | val ctx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build() 93 | copy(sslCtx = Some(ctx)) 94 | } 95 | 96 | /** 97 | * Sets up basic Server-Pipeline for this Codec 98 | * @param channel Channel to apply the Pipeline to 99 | */ 100 | def serverPipeline(channel: Channel): Unit = { 101 | val p = channel.pipeline() 102 | sslCtx.foreach(e => p.addLast(HttpServer.Handlers.ssl, e.newHandler(channel.alloc()))) 103 | val maxInitialBytes = maxInitialLineLength.inBytes.toInt 104 | val maxHeaderBytes = maxHeaderSize.inBytes.toInt 105 | p.addLast(HttpServer.Handlers.codec, new HttpServerCodec(maxInitialBytes, maxHeaderBytes, maxChunkSize.inBytes.toInt)) 106 | if (compressionLevel > 0) { 107 | p.addLast(HttpServer.Handlers.compressor, new HttpContentCompressor(compressionLevel)) 108 | } 109 | } 110 | 111 | /** 112 | * Sets up basic HTTP Pipeline 113 | * @param channel Channel to apply the Pipeline to 114 | */ 115 | def clientPipeline(channel: Channel): Unit = { 116 | val pipeline = channel.pipeline() 117 | sslCtx.foreach(e => pipeline.addLast(HttpServer.Handlers.ssl, e.newHandler(channel.alloc()))) 118 | val maxInitialBytes = this.maxInitialLineLength.inBytes.toInt 119 | val maxHeaderBytes = this.maxHeaderSize.inBytes.toInt 120 | val maxChunkSize = this.maxChunkSize.inBytes.toInt 121 | pipeline.addLast(HttpClient.Handlers.codec, new HttpClientCodec(maxInitialBytes, maxHeaderBytes, maxChunkSize)) 122 | if (decompression) { 123 | pipeline.addLast(HttpClient.Handlers.decompressor, new HttpContentDecompressor()) 124 | } 125 | pipeline.addLast(HttpClient.Handlers.aggregator, new HttpObjectAggregator(maxChunkSize)) 126 | } 127 | 128 | /** 129 | * Handle the connected channel and send the request 130 | * @param channel Channel we're connected to 131 | * @param uri URI We want to use 132 | * @return 133 | */ 134 | def clientConnected(channel: Channel, uri: java.net.URI): Future[NettyWebSocketChannel] = { 135 | val inBroker = new Broker[WebSocketFrame] 136 | val outBroker = new Broker[WebSocketFrame] 137 | val result = Promise[NettyWebSocketChannel] 138 | 139 | readTimeout.foreach { readTimeout => 140 | channel.pipeline.addFirst(HttpClient.Handlers.readTimeout, new ReadTimeoutHandler(readTimeout.inMillis.toInt) { 141 | override def readTimedOut(ctx: ChannelHandlerContext) { 142 | ctx.channel.close 143 | if (!result.isDefined) result.setException(new IllegalStateException("Read timed out")) 144 | } 145 | }) 146 | } 147 | writeTimeout.foreach { writeTimeout => 148 | channel.pipeline.addFirst(HttpClient.Handlers.writeTimeout, new WriteTimeoutHandler(writeTimeout.inMillis.toInt) { 149 | override def writeTimedOut(ctx: ChannelHandlerContext) { 150 | ctx.channel.close 151 | if (!result.isDefined) result.setException(new IllegalStateException("Write timed out")) 152 | } 153 | }) 154 | } 155 | 156 | val headers = new DefaultHttpHeaders() 157 | val handshaker = WebSocketClientHandshakerFactory.newHandshaker( 158 | uri, WebSocketVersion.V13, subprotocols, allowExtensions, headers) 159 | 160 | channel.pipeline().addLast(new SimpleChannelInboundHandler[FullHttpResponse] { 161 | override def channelRead0(ctx: ChannelHandlerContext, msg: FullHttpResponse) { 162 | if (!handshaker.isHandshakeComplete) { 163 | handshaker.finishHandshake(ctx.channel(), msg) 164 | ctx.channel().pipeline().remove(this) 165 | } else ctx.channel().close() // this should not happen 166 | } 167 | 168 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 169 | if (!result.isDefined) result.setException(cause) 170 | ctx.close() 171 | } 172 | }) 173 | 174 | channel.pipeline().addLast(HttpClient.Handlers.handler, new SimpleChannelInboundHandler[WebSocketFrame] { 175 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { 176 | if (!result.isDefined) result.setException(cause) 177 | } 178 | 179 | override def channelRead0(ctx: ChannelHandlerContext, msg: WebSocketFrame): Unit = { 180 | // we wire the inbound packet to the Broker 181 | inBroker ! msg.retain() 182 | } 183 | }) 184 | 185 | val promise = channel.newPromise() 186 | handshaker.handshake(channel, promise) 187 | promise.addListener(new ChannelFutureListener { 188 | override def operationComplete(f: ChannelFuture): Unit = { 189 | // don't overflow the server immediately after handshake 190 | implicit val timer = WheelTimer 191 | Schedule(() => { 192 | // we wire the outbound broker to send to the channel 193 | outBroker.recv.foreach(buf => channel.writeAndFlush(buf)) 194 | // return the future 195 | if (!result.isDefined) result.setValue(NettyWebSocketChannel(outBroker, inBroker.recv, channel)) 196 | }, 100.millis) 197 | } 198 | }) 199 | 200 | result 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/main/scala/io/wasted/util/http/Thruput.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.http 2 | 3 | import java.net.{ InetSocketAddress, URI } 4 | import java.util.UUID 5 | import java.util.concurrent.atomic.AtomicReference 6 | import javax.net.ssl.SSLEngine 7 | 8 | import io.netty.bootstrap._ 9 | import io.netty.buffer._ 10 | import io.netty.channel._ 11 | import io.netty.channel.socket.SocketChannel 12 | import io.netty.channel.socket.nio.NioSocketChannel 13 | import io.netty.handler.codec.http._ 14 | import io.netty.handler.codec.http.websocketx._ 15 | import io.netty.handler.ssl.SslHandler 16 | import io.netty.util.CharsetUtil 17 | import io.wasted.util._ 18 | 19 | import scala.util.{ Failure, Success, Try } 20 | 21 | object Thruput { 22 | object State extends Enumeration { 23 | val Connecting, Connected, Reconnecting, Disconnected = Value 24 | } 25 | } 26 | 27 | /** 28 | * Thruput WebSocket Client class which will handle all delivery. 29 | * 30 | * @param uri Endpoint URI for the Thruput Client 31 | * @param auth Authentication Key for the Thruput platform 32 | * @param sign Signing Key for the Thruput platform 33 | * @param from Username to use (optional) 34 | * @param room Room to use (optional) 35 | * @param callback Callback for every non-connection WebSocketFrame (Binary and Text) 36 | * @param states Callback for connection state changes 37 | * @param timeout Connect timeout in seconds 38 | * @param engine Optional SSLEngine 39 | */ 40 | class Thruput( 41 | uri: URI, 42 | auth: UUID, 43 | sign: UUID, 44 | from: Option[String] = None, 45 | room: Option[String] = None, 46 | val callback: (ByteBufHolder) => Any = (x) => x.release, 47 | states: (Thruput.State.Value) => Any = (x) => x, 48 | timeout: Int = 5, 49 | engine: Option[SSLEngine] = None) extends Wactor(100000) { 50 | TP => 51 | override val loggerName = getClass.getCanonicalName + ":" + uri.toString + auth 52 | private def session = UUID.randomUUID 53 | private var channel: Option[Channel] = None 54 | private[http] var handshakeFuture: ChannelPromise = _ 55 | private var disconnected = false 56 | private var connecting = false 57 | private var reconnecting = false 58 | private val _state = new AtomicReference[Thruput.State.Value](Thruput.State.Disconnected) 59 | def state = _state.get() 60 | 61 | private def setState(state: Thruput.State.Value): Unit = { 62 | this._state.set(state) 63 | states(state) 64 | } 65 | 66 | private val bootstrap = new Bootstrap().group(Netty.eventLoop) 67 | .channel(classOf[NioSocketChannel]) 68 | .remoteAddress(new InetSocketAddress(uri.getHost, uri.getPort)) 69 | .option[java.lang.Boolean](ChannelOption.TCP_NODELAY, true) 70 | .option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) 71 | .option[java.lang.Boolean](ChannelOption.SO_REUSEADDR, true) 72 | .option[java.lang.Integer](ChannelOption.SO_LINGER, 0) 73 | .option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, timeout * 1000) 74 | .handler(new ChannelInitializer[SocketChannel] { 75 | override def initChannel(ch: SocketChannel) { 76 | val p = ch.pipeline() 77 | engine.map(e => p.addLast("ssl", new SslHandler(e))) 78 | p.addLast("decoder", new HttpResponseDecoder) 79 | p.addLast("encoder", new HttpRequestEncoder) 80 | p.addLast("aggregator", new HttpObjectAggregator(8192)) 81 | p.addLast("ws-handler", new ThruputResponseAdapter(uri, TP)) 82 | } 83 | }) 84 | 85 | // This method is used to send WebSocketFrames async 86 | private def writeToChannel(channel: Channel, wsf: WebSocketFrame) { 87 | wsf.retain 88 | wsf match { 89 | case a: TextWebSocketFrame => debug("Sending: " + a.text()) 90 | case other => debug("Sending: " + other.getClass.getSimpleName) 91 | } 92 | val eventLoop = channel.eventLoop() 93 | eventLoop.inEventLoop match { 94 | case true => channel.writeAndFlush(wsf) 95 | case false => 96 | eventLoop.execute(new Runnable() { 97 | override def run(): Unit = channel.writeAndFlush(wsf) 98 | }) 99 | } 100 | } 101 | 102 | private sealed trait Action { def run(): Unit } 103 | 104 | private case object Connect extends Action { 105 | def run() { 106 | connecting = true 107 | channel match { 108 | case Some(ch) => 109 | debug("Connection already established") 110 | case _ => 111 | Try { 112 | val ch = bootstrap.clone.connect().sync().channel() 113 | handshakeFuture.sync() 114 | 115 | val body = (room, from) match { 116 | case (Some(uroom), Some(ufrom)) => """{"room":"%s","thruput":true,"from":"%s"}""".format(uroom, ufrom) 117 | case (_, Some(ufrom)) => """{"from":"%s","thruput":true}""".format(ufrom) 118 | case _ => """{"thruput":true}""" 119 | } 120 | writeToChannel(ch, new TextWebSocketFrame("""{"auth":"%s","sign":"%s","body":%s,"session":"%s"}""".format( 121 | auth.toString, io.wasted.util.Hashing.sign(sign.toString, body), body, session.toString))) 122 | ch 123 | } match { 124 | case Success(ch) => 125 | channel = Some(ch) 126 | setState(Thruput.State.Connected) 127 | case Failure(e) => 128 | connect() 129 | setState(Thruput.State.Connecting) 130 | } 131 | } 132 | connecting = false 133 | disconnected = false 134 | } 135 | } 136 | 137 | private case object Disconnect extends Action { 138 | def run() { 139 | disconnected = true 140 | channel match { 141 | case Some(ch) => 142 | writeToChannel(ch, new CloseWebSocketFrame()) 143 | 144 | // WebSocketClientHandler will close the connection when the server 145 | // responds to the CloseWebSocketFrame. 146 | ch.closeFuture().sync() 147 | setState(Thruput.State.Disconnected) 148 | case _ => debug("Connection not established") 149 | } 150 | channel = None 151 | } 152 | } 153 | 154 | private case object Reconnect extends Action { 155 | def run() { 156 | reconnecting = true 157 | setState(Thruput.State.Reconnecting) 158 | Disconnect.run() 159 | Connect.run() 160 | reconnecting = false 161 | } 162 | } 163 | 164 | def receive = { 165 | case action: Action => action.run() 166 | case msg: WebSocketFrame => 167 | if (reconnecting || disconnected || connecting) TP ! msg 168 | else channel match { 169 | case Some(ch) => 170 | Try(writeToChannel(ch, msg)) match { 171 | case Success(f) => 172 | case Failure(e) => 173 | TP ! msg 174 | } 175 | case _ => 176 | connect() 177 | TP ! msg 178 | } 179 | } 180 | 181 | /** 182 | * Reconnect this Thruput Client Socket. (only used by Handler) 183 | */ 184 | def reconnect() { 185 | if (reconnecting || disconnected || connecting) return 186 | TP !! Reconnect 187 | } 188 | 189 | /** 190 | * Connect this Thruput Client Socket. 191 | */ 192 | def connect() { 193 | if (channel.isDefined) return 194 | TP !! Connect 195 | } 196 | 197 | /** 198 | * Disconnect this Thruput Client Socket. 199 | */ 200 | def disconnect() { 201 | TP !! Disconnect 202 | } 203 | 204 | def write(wsf: WebSocketFrame) { 205 | TP ! wsf 206 | } 207 | 208 | def write(string: String) { 209 | TP ! new TextWebSocketFrame(string) 210 | } 211 | 212 | /** 213 | * Shutdown this client. 214 | */ 215 | def shutdown() { 216 | disconnect() 217 | disconnected = true 218 | on.set(0) 219 | } 220 | } 221 | 222 | /** 223 | * Empty Netty Response Adapter which is used for Thruput high-performance delivery. 224 | */ 225 | class ThruputResponseAdapter(uri: URI, client: Thruput) extends SimpleChannelInboundHandler[Object] { 226 | private val handshaker = WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()) 227 | 228 | override def handlerAdded(ctx: ChannelHandlerContext) { 229 | client.handshakeFuture = ctx.newPromise() 230 | } 231 | 232 | override def channelActive(ctx: ChannelHandlerContext) { 233 | handshaker.handshake(ctx.channel) 234 | } 235 | 236 | override def channelInactive(ctx: ChannelHandlerContext) { 237 | client.info("WebSocket Client disconnected!") 238 | client.reconnect() 239 | } 240 | 241 | override def channelRead0(ctx: ChannelHandlerContext, msg: Object) { 242 | val ch = ctx.channel() 243 | 244 | msg match { 245 | case response: FullHttpResponse if !handshaker.isHandshakeComplete => 246 | handshaker.finishHandshake(ch, response) 247 | client.info("WebSocket Client connected!") 248 | client.handshakeFuture.setSuccess() 249 | case response: FullHttpResponse => 250 | throw new Exception("Unexpected FullHttpResponse (status=" + response.status() + ", content=" + response.content().toString(CharsetUtil.UTF_8) + ")") 251 | case frame: BinaryWebSocketFrame => 252 | client.debug("WebSocket BinaryFrame received message") 253 | client.callback(frame.retain) 254 | case frame: TextWebSocketFrame => 255 | client.debug("WebSocket TextFrame received message: " + frame.text()) 256 | client.callback(frame.retain) 257 | case frame: PongWebSocketFrame => 258 | client.debug("WebSocket Client received pong") 259 | case frame: CloseWebSocketFrame => 260 | client.debug("WebSocket Client received closing") 261 | ch.close() 262 | case o: Object => 263 | client.error("Unsupported response type! " + o.toString) 264 | ch.close() 265 | } 266 | } 267 | 268 | override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable) { 269 | if (!client.handshakeFuture.isDone) client.handshakeFuture.setFailure(cause) 270 | ExceptionHandler(ctx, cause) match { 271 | case Some(e) => 272 | case _ => client.reconnect() 273 | } 274 | ctx.close() 275 | } 276 | } 277 | -------------------------------------------------------------------------------- /src/test/scala/io/wasted/util/test/RedisSpec.scala: -------------------------------------------------------------------------------- 1 | package io.wasted.util.test 2 | 3 | import com.twitter.conversions.time._ 4 | import com.twitter.util.{ Await, Future } 5 | import io.wasted.util.Logger 6 | import io.wasted.util.redis._ 7 | import org.scalatest._ 8 | 9 | class RedisSpec extends WordSpec with Logger { 10 | 11 | val clientF = RedisClient().connectTo("localhost", 6379).open() 12 | 13 | val baseKey = "mytest" 14 | val baseStr = "myval" 15 | 16 | var client: NettyRedisChannel = _ 17 | 18 | "Testing redis client functionality" should { 19 | "connect" in { 20 | client = Await.result(clientF, 1.second) 21 | } 22 | "simple set key/value" in { 23 | Await.result(client.set(baseKey, baseStr), 1.second) 24 | } 25 | "simple get key/value" in { 26 | assert(Await.result(client.get(baseKey), 1.second).contains(baseStr), "did not match our value") 27 | assert(Await.result(client.get("non-existing"), 1.second).isEmpty, "did not match our value") 28 | } 29 | 30 | "strlen" in { 31 | assert(Await.result(client.strLen(baseKey), 1.second) == baseStr.length, "did not count right?") 32 | } 33 | 34 | "type" in { 35 | assert(Await.result(client.`type`(baseKey), 1.second) == "string") 36 | } 37 | 38 | "time" in { 39 | assert(Await.result(client.time(), 1.second)._1 <= new java.util.Date().getTime / 1000, "Time on redis is further along?") 40 | } 41 | 42 | "simple append key/value" in { 43 | assert(Await.result(client.append(baseKey, baseStr), 1.second) == (baseStr + baseStr).length, "value too long") 44 | } 45 | 46 | "dbSize" in { 47 | assert(Await.result(client.dbSize(), 1.second) == 1L, "not a fresh database?") 48 | } 49 | 50 | "incr" in { 51 | assert(Await.result(client.incr("int"), 1.second) == 1L, "not a fresh database?") 52 | } 53 | 54 | "incrBy" in { 55 | assert(Await.result(client.incrBy("int", 2), 1.second) == 3L, "not a fresh database?") 56 | } 57 | 58 | "decr" in { 59 | assert(Await.result(client.decr("int"), 1.second) == 2L, "not a fresh database?") 60 | } 61 | 62 | "decrBy" in { 63 | assert(Await.result(client.decrBy("int", 2), 1.second) == 0L, "not a fresh database?") 64 | } 65 | 66 | "incrByFloat" in { 67 | assert(Await.result(client.incrByFloat("int", 2.5f), 1.second) == 2.5f, "not a fresh database?") 68 | } 69 | 70 | "echo" in { 71 | assert(Await.result(client.echo("oink"), 1.second) == "oink", "did not echo oink") 72 | } 73 | 74 | "exists" in { 75 | assert(Await.result(client.exists("int"), 1.second), "field 'int' did not exist") 76 | } 77 | 78 | "expire" in { 79 | assert(Await.result(client.expire("int", 5), 1.second), "expire did not set correctly") 80 | } 81 | 82 | "expireAt" in { 83 | assert(Await.result(client.expireAt(baseKey, new java.util.Date().getTime / 1000 + 10), 1.second), "expire did not set correctly") 84 | } 85 | 86 | "ttl" in { 87 | assert(Await.result(client.ttl("int"), 1.second) == 5L, "expire did not set correctly to 5") 88 | } 89 | 90 | "hMSet" in { 91 | Await.result(client.hMSet("hashmap", Map("key1" -> "value1", "key2" -> "value2")), 1.second) 92 | } 93 | 94 | "hMGet" in { 95 | assert(Await.result(client.hMGet("hashmap", "key2" :: "key1" :: Nil)) == Map("key2" -> "value2", "key1" -> "value1"), 1.second) 96 | } 97 | 98 | "hSet" in { 99 | assert(Await.result(client.hSet("hash", "key1", "value1"), 1.second), "Hash did not set properly") 100 | } 101 | 102 | "hGet" in { 103 | assert(Await.result(client.hGet("hash", "key1"), 1.second).contains("value1"), "Hash did not set correctly") 104 | assert(Await.result(client.hGet("hash", "key2"), 1.second).isEmpty, "Hash did not get correctly") 105 | } 106 | 107 | "hGetAll" in { 108 | assert(Await.result(client.hGetAll("hash"), 1.second) == Map("key1" -> "value1"), "Not all keys in hash") 109 | } 110 | 111 | "hKeys" in { 112 | assert(Await.result(client.hKeys("hash"), 1.second) == List("key1"), "Weird keys in hash") 113 | } 114 | 115 | "hVals" in { 116 | assert(Await.result(client.hVals("hash"), 1.second) == List("value1"), "Weird values in hash") 117 | } 118 | 119 | "hIncrBy" in { 120 | assert(Await.result(client.hIncrBy("hash", "incr", 2), 1.second) == 2L, "hIncrBy is wrong") 121 | } 122 | 123 | "hIncrByFloat" in { 124 | assert(Await.result(client.hIncrByFloat("hash", "incrF", 2.5f), 1.second) == 2.5f, "hIncrByFloat is wrong") 125 | } 126 | 127 | "hLen" in { 128 | assert(Await.result(client.hLen("hash"), 1.second) == 3L, "hash count is wrong") 129 | } 130 | 131 | /*"hStrLen" in { 132 | assert(Await.result(client.hStrLen("hash", "key1"), 1.second) == 6L, "hash string length is wrong") 133 | }*/ 134 | 135 | "hSetNx" in { 136 | assert(Await.result(client.hSetNx("hash", "key1", "value2"), 1.second) == false, "hash set nx should not have set") 137 | assert(Await.result(client.hSetNx("hash", "key2", "value2"), 1.second), "hash set nx should have set") 138 | } 139 | 140 | "hExists" in { 141 | assert(Await.result(client.hExists("hash", "nx"), 1.second) == false, "Should not have key2") 142 | assert(Await.result(client.hExists("hash", "key1"), 1.second), "Should have key1") 143 | } 144 | 145 | "hDel" in { 146 | assert(Await.result(client.hDel("hash", "nx"), 1.second) == false, "Should not be able to delete key2") 147 | assert(Await.result(client.hDel("hash", "key1"), 1.second), "Should be able to delete key1") 148 | } 149 | 150 | "mSetNx" in { 151 | assert(Await.result(client.mSetNx(Map("mset1" -> "shit", "int" -> "5")), 1.second) == false, "Should not have set because of int!") 152 | } 153 | 154 | "mSet" in { 155 | Await.result(client.mSet(Map("mset1" -> "shit", "mset2" -> "shit")), 1.second) 156 | } 157 | 158 | "mGet" in { 159 | assert(Await.result(client.mGet("mset1" :: "mset2" :: Nil), 1.second) == Map("mset1" -> "shit", "mset2" -> "shit"), "Should have returned our Map") 160 | } 161 | 162 | "sAdd" in { 163 | assert(Await.result(client.sAdd("set1", "member1" :: "member2" :: "member3" :: Nil), 1.second) == 3, "wrong number of members added") 164 | assert(Await.result(client.sAdd("set2", "member1" :: "member2" :: Nil), 1.second) == 2, "wrong number of members added") 165 | 166 | } 167 | 168 | "sCard" in { 169 | assert(Await.result(client.sCard("set1"), 1.second) == 3, "wrong number of members counted") 170 | assert(Await.result(client.sCard("set2"), 1.second) == 2, "wrong number of members counted") 171 | } 172 | 173 | "sDiff" in { 174 | assert(Await.result(client.sDiff("set1", "set2"), 1.second) == List("member3"), "wrong number of members diff'd") 175 | } 176 | 177 | "sDiffStore" in { 178 | assert(Await.result(client.sDiffStore("setDiff", "set1", "set2" :: Nil), 1.second) == 1, "wrong number of members diff'd") 179 | } 180 | 181 | "sInter" in { 182 | assert(Await.result(client.sInter("set1", "set2"), 1.second) == List("member2", "member1"), "wrong number of members inter'd") 183 | } 184 | 185 | "sInterStore" in { 186 | assert(Await.result(client.sInterStore("setInter", "set1", "set2" :: Nil), 1.second) == 2, "wrong number of members inter'd") 187 | } 188 | 189 | "sIsMember" in { 190 | assert(Await.result(client.sIsMember("set1", "member3"), 1.second), "member3 not member of set1") 191 | assert(Await.result(client.sIsMember("set2", "member3"), 1.second) == false, "member3 is member of set2") 192 | } 193 | 194 | "sMembers" in { 195 | assert(Await.result(client.sMembers("setInter"), 1.second) == List("member1", "member2"), "wrong members inter'd") 196 | } 197 | 198 | "sMove" in { 199 | assert(Await.result(client.sMove("set1", "set2", "member3"), 1.second), "did not move member3") 200 | } 201 | 202 | "sPop" in { 203 | assert(Await.result(client.sPop("set1"), 1.second).startsWith("member"), "wrong member pop'd") 204 | } 205 | 206 | "sRandMember" in { 207 | assert(Await.result(client.sRandMember("setInter"), 1.second).startsWith("member"), "wrong members rand'd") 208 | } 209 | 210 | "sRem" in { 211 | assert(Await.result(client.sRem("set2", "member3" :: Nil), 1.second) == 1, "other than 1 member rm'd") 212 | } 213 | 214 | "sUnion" in { 215 | assert(Await.result(client.sUnion("set1", "set2"), 1.second).diff(List("member1", "member2")).isEmpty, "wrong number of members union'd") 216 | } 217 | 218 | "sUnionInter" in { 219 | assert(Await.result(client.sUnionStore("setInter", "set1" :: "set2" :: Nil), 1.second) == 2, "wrong number of members union'd") 220 | } 221 | 222 | "setEx" in { 223 | Await.result(client.setEx("setInter", "new", 5), 1.second) 224 | } 225 | 226 | "persist" in { 227 | assert(Await.result(client.persist("setInter"), 1.second)) 228 | } 229 | 230 | "setNx" in { 231 | assert(Await.result(client.setNx("setInter", "evennever"), 1.second) == false, "was able to update setInter") 232 | } 233 | 234 | "getSet" in { 235 | assert(Await.result(client.getSet("setInter", "evennewer"), 1.second) == "new", "getSet is weird") 236 | } 237 | 238 | "lPush" in { 239 | Await.result(client.lPush("mylist", "bar" :: "baz" :: Nil), 1.second) 240 | } 241 | 242 | "lRange" in { 243 | assert(Await.result(client.lRange("mylist", 1, 1), 1.second) == List("bar"), "lRange is weird") 244 | } 245 | 246 | "lTrim" in { 247 | Await.result(client.lTrim("mylist", 1, 1), 1.second) 248 | } 249 | 250 | "lIndex" in { 251 | assert(Await.result(client.lIndex("mylist", 0), 1.second).contains("bar"), "lIndex returned wrong value") 252 | assert(Await.result(client.lIndex("newlist", 5), 1.second).isEmpty, "lIndex returned wrong value") 253 | } 254 | 255 | "lInsertBefore" in { 256 | assert(Await.result(client.lInsertBefore("mylist", "bar", "shit"), 1.second) == 2L, "lInsert Before inserted wrong") 257 | } 258 | 259 | "lInsertAfter" in { 260 | assert(Await.result(client.lInsertAfter("mylist", "shit", "doodle"), 1.second) == 3L, "lInsert After inserted wrong") 261 | } 262 | 263 | "lLen" in { 264 | assert(Await.result(client.lLen("mylist"), 1.second) == 3L, "lLen returned wrong list length") 265 | } 266 | 267 | "lPop" in { 268 | assert(Await.result(client.lPop("mylist"), 1.second).contains("shit"), "lPop pop'd wrong element") 269 | assert(Await.result(client.lPop("newlist"), 1.second).isEmpty, "lPop pop'd wrong element") 270 | } 271 | 272 | "lPushX" in { 273 | assert(Await.result(client.lPushX("mylist", "bar"), 1.second) == 3L, "lPushX returned weird value") 274 | } 275 | 276 | "lRem" in { 277 | assert(Await.result(client.lRem("mylist", -2, "bar"), 1.second) == 2L, "wrong number of items lRem'd") 278 | } 279 | 280 | "lSet" in { 281 | Await.result(client.lSet("mylist", 0, "newitem"), 1.second) 282 | } 283 | 284 | "rPop" in { 285 | assert(Await.result(client.rPop("mylist"), 1.second).contains("newitem"), "rPop pop'd the wrong item") 286 | assert(Await.result(client.rPop("newlist"), 1.second).isEmpty, "rPop pop'd the wrong item") 287 | } 288 | 289 | "rPoplPush" in { 290 | Await.result(client.lPush("newlist", "fizzle" :: Nil), 1.second) 291 | assert(Await.result(client.rPoplPush("newlist", "mylist"), 1.second) == "fizzle", "rPoplPush'd wrong item!") 292 | } 293 | 294 | "rPush" in { 295 | assert(Await.result(client.rPush("newlist", "brittle" :: Nil), 1.second) == 1L, "rPush push'd at wrong index!") 296 | } 297 | 298 | "rPushX" in { 299 | assert(Await.result(client.rPushX("mylist", "newitem"), 1.second) == 2L, "rPushX returned weird value") 300 | } 301 | 302 | "pExpire" in { 303 | assert(Await.result(client.pExpire("newlist", 50000), 1.second), "pExpire did not work") 304 | } 305 | 306 | "pExpireAt" in { 307 | assert(Await.result(client.pExpireAt("newlist", new java.util.Date().getTime + 40000), 1.second), "pExpireAt did not work") 308 | } 309 | 310 | "pSetEx" in { 311 | Await.result(client.pSetEx("psetex", 10000, "crap"), 1.second) 312 | } 313 | 314 | "pTtl" in { 315 | assert(Await.result(client.pTtl("psetex"), 1.second) > 0, "pTtl returned wrong") 316 | } 317 | 318 | "pfAdd" in { 319 | assert(Await.result(client.pfAdd("hill1", "foo" :: "bar" :: "zap" :: "a" :: Nil), 1.second), "HyperLogLog internal register was not altered") 320 | assert(Await.result(client.pfAdd("hill2", "a" :: "b" :: "c" :: "foo" :: Nil), 1.second), "HyperLogLog internal register was not altered") 321 | } 322 | 323 | "pfMerge" in { 324 | Await.result(client.pfMerge("hill3", "hill1" :: "hill2" :: Nil), 1.second) 325 | } 326 | 327 | "pfCount" in { 328 | assert(Await.result(client.pfCount("hill3"), 1.second) == 6L, "HyperLogLog internal register was not merged") 329 | } 330 | 331 | "setBit" in { 332 | assert(Await.result(client.setBit("mykey", 7, true), 1.second) == 0L, "bit not set correctly") 333 | assert(Await.result(client.setBit("mykey", 7, false), 1.second) == 1L, "bit not set correctly") 334 | } 335 | 336 | "setRange" in { 337 | Await.result(client.set("key1", "Hello World"), 1.second) 338 | assert(Await.result(client.setRange("key1", 6, "redis"), 1.second) == 11, "setRange weirded out") 339 | assert(Await.result(client.get("key1"), 1.second).contains("Hello redis"), "setRange did not work") 340 | } 341 | 342 | "getBit" in { 343 | assert(Await.result(client.getBit("mykey", 7), 1.second) == false, "getBit returned a true?") 344 | } 345 | 346 | "getRange" in { 347 | assert(Await.result(client.getRange("key1", 0, 4), 1.second) == "Hello", "getRange did not return hello") 348 | } 349 | 350 | "bitCount" in { 351 | Await.result(client.set("key1", "foobar"), 1.second) 352 | assert(Await.result(client.bitCount("key1"), 1.second) == 26) 353 | assert(Await.result(client.bitCount("key1", 0, 0), 1.second) == 4) 354 | assert(Await.result(client.bitCount("key1", 1, 1), 1.second) == 6) 355 | } 356 | 357 | "randomKey" in { 358 | assert(Await.result(client.randomKey(), 1.second).isDefined) 359 | } 360 | 361 | "bitPos" in { 362 | Await.result(client.set("key1", new String(Array(0xff, 0xf0, 0x00).map(_.toChar))), 1.second) 363 | assert(Await.result(client.bitPos("key1", false), 1.second) == 2) 364 | } 365 | 366 | "bitOp" in { 367 | Await.result(client.set("key1", "foobar"), 1.second) 368 | Await.result(client.set("key2", "abcdef"), 1.second) 369 | assert(Await.result(client.bitOp("dest", RedisBitOperation.AND, Seq("key1", "key2")), 1.second) == 6L) 370 | assert(Await.result(client.bitOp("dest", RedisBitOperation.OR, Seq("key1", "key2")), 1.second) == 6L) 371 | assert(Await.result(client.bitOp("dest", RedisBitOperation.XOR, Seq("key1", "key2")), 1.second) == 6L) 372 | assert(Await.result(client.bitOp("dest", RedisBitOperation.NOT, "key1"), 1.second) == 6L) 373 | } 374 | 375 | "rename" in { 376 | Await.result(client.rename("key1", "key3"), 1.second) 377 | } 378 | 379 | "renameNx" in { 380 | assert(Await.result(client.renameNx("key3", "key1"), 1.second), "did overwrite Siciliy?") 381 | assert(Await.result(client.renameNx("key1", "key2"), 1.second) == false, "did overwrite key1") 382 | } 383 | 384 | "zAdd" in { 385 | assert(Await.result(client.zAdd("myzset", 1, "one", false), 1.second) == 1L) 386 | assert(Await.result(client.zAdd("myzset", 2, "two", true), 1.second) == 2L) 387 | } 388 | 389 | "zCard" in { 390 | assert(Await.result(client.zCard("myzset"), 1.second) == 2L) 391 | } 392 | 393 | "zCount" in { 394 | assert(Await.result(client.zAdd("myzset", 3, "three", false), 1.second) == 1.0) 395 | assert(Await.result(client.zCount("myzset"), 1.second) == 3L) 396 | assert(Await.result(client.zCount("myzset", "(1", "3"), 1.second) == 2L) 397 | } 398 | 399 | "zIncrBy" in { 400 | assert(Await.result(client.zIncrBy("myzset", 2, "one"), 1.second) == 3L) 401 | } 402 | 403 | /*"zInterStore" in { 404 | assert(Await.result(client.zInterStore(), 1.second).isDefined) 405 | }*/ 406 | 407 | "zLexCount" in { 408 | assert(Await.result(client.zAdd("lexzset", List(0d -> "a", 0d -> "b", 0d -> "c", 0d -> "d", 0d -> "e"))) == 5d) 409 | assert(Await.result(client.zLexCount("lexzset"), 1.second) == 5L) 410 | } 411 | 412 | "zRange" in { 413 | assert(Await.result(client.zRange("lexzset", 0, -1), 1.second).length == 5) 414 | } 415 | 416 | "zRangeByLex" in { 417 | assert(Await.result(client.zRangeByLex("lexzset", "-", "[c"), 1.second).length == 3) 418 | } 419 | 420 | "zRevRangeByLex" in { 421 | assert(Await.result(client.zRevRangeByLex("lexzset", "[c", "-"), 1.second).length == 3) 422 | } 423 | 424 | "zRangeByScore" in { 425 | assert(Await.result(client.zRangeByScore("lexzset"), 1.second) == List("a", "b", "c", "d", "e")) 426 | } 427 | 428 | "zRank" in { 429 | assert(Await.result(client.zRank("myzset", "three"), 1.second) == 2L) 430 | } 431 | 432 | "zRem" in { 433 | assert(Await.result(client.zRem("myzset", "three"), 1.second) > 0) 434 | } 435 | 436 | "zRemRangeByLex" in { 437 | assert(Await.result(client.zRemRangeByLex("lexzset", "[alpha", "[omega"), 1.second) == 4) 438 | } 439 | 440 | "zRemRangeByRank" in { 441 | assert(Await.result(client.zRemRangeByRank("lexzset", 0, 1), 1.second) == 1) 442 | } 443 | 444 | "zRemRangeByScore" in { 445 | assert(Await.result(client.zRemRangeByScore("lexzset", "-inf", "(2"), 1.second) == 0) 446 | } 447 | 448 | "zRevRange" in { 449 | assert(Await.result(client.zRevRange("myzset", 0, -1), 1.second) == List("one", "two")) 450 | } 451 | 452 | "zRevRangeByScore" in { 453 | assert(Await.result(client.zRevRangeByScore("myzset"), 1.second) == List("one", "two")) 454 | } 455 | 456 | "zRevRank" in { 457 | assert(Await.result(client.zRevRank("myzset", "two"), 1.second) == 1L) 458 | } 459 | 460 | "zScore" in { 461 | assert(Await.result(client.zScore("myzset", "two"), 1.second) == 2d) 462 | } 463 | 464 | /*"zUnionStore" in { 465 | assert(Await.result(client.zUnionStore(), 1.second).isDefined) 466 | } 467 | 468 | "zScan" in { 469 | assert(Await.result(client.zScan(), 1.second).isDefined) 470 | }*/ 471 | 472 | /* 473 | "geoAdd" in { 474 | val palermo = RedisGeoObject(13.361389, 38.115556, "Palermo") 475 | val catania = RedisGeoObject(15.087269, 37.502669, "Catania") 476 | assert(Await.result(client.geoAdd("Sicily", palermo :: catania :: Nil), 1.second) == 2L) 477 | } 478 | 479 | "geoDist" in { 480 | assert(Await.result(client.geoDist("Sicily", "Palermo", "Catania", RedisGeoUnit.Meters), 1.second) == 166274.15156960039) 481 | } 482 | 483 | "geoHash" in { 484 | assert(Await.result(client.geoHash("Sicily", "Palermo" :: "Catania" :: Nil), 1.second) == Seq("sqc8b49rny0", "sqdtr74hyu0")) 485 | }*/ 486 | 487 | "client list" in { 488 | Await.result(client.clientList().map(x => warn(x)), 1.second) 489 | } 490 | 491 | "ping" in { 492 | Await.result(client.ping(), 1.second) 493 | } 494 | 495 | "keys" in { 496 | assert(Await.result(client.keys("*")).length > 0, "no more keys left before flush?") 497 | } 498 | 499 | "client list2" in { 500 | Await.result(client.clientList().map(x => warn(x)), 1.second) 501 | } 502 | 503 | "move and select" in { 504 | assert(Await.result(client.move("key1", 1), 5.second), "could not move Sicily to database 1") 505 | Await.result(client.select(1), 1.second) 506 | assert(Await.result(client.move("key1", 0), 5.second), "could not move Sicily to database 0") 507 | Await.result(client.select(0), 1.second) 508 | } 509 | 510 | "Counted Rate Limiter" in { 511 | val limiter = CounterBasedRateLimiter(client, 5.seconds, 10) 512 | val f = (0 until 10).toList.map { x => 513 | Thread.sleep(x * 5) 514 | limiter("test2") 515 | } 516 | Await.result(Future.collect(f)) 517 | try { 518 | Await.result(limiter("test2")) 519 | throw new IllegalStateException("This should not happen! Over limit") 520 | } catch { 521 | case t: OverRateLimitException => // yay sucess 522 | case t: Throwable => throw t 523 | } 524 | Thread.sleep(5000) 525 | Await.result(limiter("test2")) 526 | } 527 | 528 | "flushDB" in { 529 | Await.result(client.flushDB()) 530 | } 531 | 532 | "disconnect" in { 533 | Await.result(client.close()) 534 | } 535 | 536 | } 537 | 538 | } 539 | 540 | --------------------------------------------------------------------------------