Browse Source

Initial commit

master
Robby Zambito 1 month ago
commit
5487dfcc8f
29 changed files with 1368 additions and 0 deletions
  1. +15
    -0
      .gitignore
  2. +34
    -0
      app/Module.scala
  3. +52
    -0
      app/controllers/AlgoController.scala
  4. +49
    -0
      app/controllers/AsyncController.scala
  5. +25
    -0
      app/controllers/CountController.scala
  6. +23
    -0
      app/controllers/HomeController.scala
  7. +98
    -0
      app/controllers/VirtualMachineController.scala
  8. +33
    -0
      app/filters/ExampleFilter.scala
  9. +9
    -0
      app/models/DatabaseExecutionContext.scala
  10. +69
    -0
      app/models/VirtualMachineRepository.scala
  11. +110
    -0
      app/services/AlgorandClient.scala
  12. +41
    -0
      app/services/ApplicationTimer.scala
  13. +29
    -0
      app/services/Counter.scala
  14. +10
    -0
      app/services/EventScheduler.scala
  15. +9
    -0
      app/services/User.scala
  16. +51
    -0
      app/services/VirtualMachineManager.scala
  17. +51
    -0
      build.sbt
  18. +411
    -0
      conf/application.conf
  19. +10
    -0
      conf/evolutions/1.sql
  20. +41
    -0
      conf/logback.xml
  21. +34
    -0
      conf/routes
  22. +26
    -0
      conf/vm-template.xml
  23. +1
    -0
      project/build.properties
  24. +7
    -0
      project/plugins.sbt
  25. BIN
      public/images/external.png
  26. BIN
      public/images/favicon.png
  27. BIN
      public/images/header-pattern.png
  28. +3
    -0
      public/javascripts/hello.js
  29. +127
    -0
      public/stylesheets/main.css

+ 15
- 0
.gitignore View File

@@ -0,0 +1,15 @@
logs
project/project
project/target
target
tmp
.history
dist
/.idea
/*.iml
/out
/.idea_modules
/.classpath
/.project
/RUNNING_PID
/.settings

+ 34
- 0
app/Module.scala View File

@@ -0,0 +1,34 @@
import com.google.inject.AbstractModule
import com.typesafe.config.Config
import java.time.Clock
import javax.inject.{Inject, Provider, Singleton}
import models.{DatabaseExecutionContext, VirtualMachineRepository}
import services.{AlgorandClient, ApplicationTimer, AtomicCounter, Counter, VirtualMachineManager}

/**
* This class is a Guice module that tells Guice how to bind several
* different types. This Guice module is created when the Play
* application starts.

* Play will automatically use any class called `Module` that is in
* the root package. You can create modules in other locations by
* adding `play.modules.enabled` settings to the `application.conf`
* configuration file.
*/
class Module extends AbstractModule {

override def configure() = {
// Use the system clock as the default implementation of Clock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
// Ask Guice to create an instance of ApplicationTimer when the
// application starts.
// bind(classOf[ApplicationTimer]).asEagerSingleton()
// bind(classOf[AlgorandClient]).asEagerSingleton()
// bind(classOf[VirtualMachineManager]).asEagerSingleton()
// Set AtomicCounter as the implementation for Counter.
// bind(classOf[Counter]).to(classOf[AtomicCounter])
bind(classOf[DatabaseExecutionContext]).asEagerSingleton()
bind(classOf[VirtualMachineRepository]).asEagerSingleton()
}

}

+ 52
- 0
app/controllers/AlgoController.scala View File

@@ -0,0 +1,52 @@
package controllers

import com.algorand.algosdk.util.Encoder
import javax.inject.{Inject, Singleton}
import play.api.libs.json.Json
import play.api.mvc.{AbstractController, Action, ControllerComponents}
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import services.AlgorandClient

@Singleton
class AlgoController @Inject()(cc: ControllerComponents)
extends AbstractController(cc) {

def getRecommendedParameters = Action.async {
Future {
val params = AlgorandClient.suggestedParameters
val json = Json.obj(
"lastRound" -> params.getLastRound,
"genesisID" -> params.getGenesisID,
"genesishashb64" -> params.getGenesishashb64,
)
Ok(json)
}
}

def sendTransaction = Action.async { implicit request =>

Future(Ok(""))
}

def optInAsset(address: String, assetId: String) = Action.async { implicit request =>
val transaction = request.body.asRaw.get.asBytes().get.toArray

// Publish the purchase transaction to the network
Future(AlgorandClient.algodApi.rawTransaction(transaction)).map { _ =>
val txn = AlgorandClient.createSendAssetTransaction(assetId.toLong, address)
val signedTransaction = AlgorandClient.algoAccount.signTransaction(txn)
signedTransaction
}.map { stx =>
AlgorandClient.algodApi.rawTransaction(Encoder.encodeToMsgPack(stx))
Ok("")
}
}

def balance(address: String) = Action {
val json = Json.obj("balance"-> s"${AlgorandClient.algodApi.accountInformation(address).getAmount}")

Ok(json)
}

}

+ 49
- 0
app/controllers/AsyncController.scala View File

@@ -0,0 +1,49 @@
package controllers

import javax.inject._

import akka.actor.ActorSystem
import play.api.mvc._

import scala.concurrent.duration._
import scala.concurrent.{ExecutionContext, Future, Promise}

/**
* This controller creates an `Action` that demonstrates how to write
* simple asynchronous code in a controller. It uses a timer to
* asynchronously delay sending a response for 1 second.
*
* @param cc standard controller components
* @param actorSystem We need the `ActorSystem`'s `Scheduler` to
* run code after a delay.
* @param exec We need an `ExecutionContext` to execute our
* asynchronous code. When rendering content, you should use Play's
* default execution context, which is dependency injected. If you are
* using blocking operations, such as database or network access, then you should
* use a different custom execution context that has a thread pool configured for
* a blocking API.
*/
@Singleton
class AsyncController @Inject()(cc: ControllerComponents, actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends AbstractController(cc) {

/**
* Creates an Action that returns a plain text message after a delay
* of 1 second.
*
* The configuration in the `routes` file means that this method
* will be called when the application receives a `GET` request with
* a path of `/message`.
*/
def message = Action.async {
getFutureMessage(10.second).map { msg => Ok(msg) }
}

private def getFutureMessage(delayTime: FiniteDuration): Future[String] = {
val promise: Promise[String] = Promise[String]()
actorSystem.scheduler.scheduleOnce(delayTime) {
promise.success("Hi!")
}(actorSystem.dispatcher) // run scheduled tasks using the actor system's dispatcher
promise.future
}

}

+ 25
- 0
app/controllers/CountController.scala View File

@@ -0,0 +1,25 @@
package controllers

import javax.inject._

import play.api.mvc._
import services.Counter

/**
* This controller demonstrates how to use dependency injection to
* bind a component into a controller class. The class creates an
* `Action` that shows an incrementing count to users. The [[Counter]]
* object is injected by the Guice dependency injection system.
*/
@Singleton
class CountController @Inject() (cc: ControllerComponents,
counter: Counter) extends AbstractController(cc) {

/**
* Create an action that responds with the [[Counter]]'s current
* count. The result is plain text. This `Action` is mapped to
* `GET /count` requests by an entry in the `routes` config file.
*/
def count = Action { Ok(counter.nextCount().toString) }

}

+ 23
- 0
app/controllers/HomeController.scala View File

@@ -0,0 +1,23 @@
package controllers

import javax.inject._
import play.api.mvc._

/**
* This controller creates an `Action` to handle HTTP requests to the
* application's home page.
*/
@Singleton
class HomeController @Inject()(cc: ControllerComponents) extends AbstractController(cc) {

/**
* Create an Action to render an HTML page with a welcome message.
* The configuration in the `routes` file means that this method
* will be called when the application receives a `GET` request with
* a path of `/`.
*/
// def index = Action {
// Ok(views.html.algovps())
// }

}

+ 98
- 0
app/controllers/VirtualMachineController.scala View File

@@ -0,0 +1,98 @@
package controllers

import java.time.Instant
import java.util.{Date, UUID}
import javax.inject.{Inject, Singleton}
import models.VirtualMachineRepository
import play.api.libs.json._
import play.api.mvc.{AbstractController, ControllerComponents}
import scala.concurrent.ExecutionContext
import services.{AlgorandClient, VirtualMachineManager}

@Singleton
class VirtualMachineController @Inject()(virtualMachineService: VirtualMachineRepository,
cc: ControllerComponents)(implicit ec: ExecutionContext)
extends AbstractController(cc) {

def createMachine(address: String) = Action.async { implicit request =>
val expirationDate = Date.from(Instant.parse(request.headers("Deletion-Date")))
val transaction = request.body.asRaw.get.asBytes().get.toArray
val vmName = UUID.randomUUID().toString.replaceAll("-", "") // Random name for the VM

// Publish the purchase transaction to the network
AlgorandClient.algodApi.rawTransaction(transaction)

// Spawn the virtual machine with libvirt
VirtualMachineManager.createVirtualMachine(vmName)

// Create a new asset to represent ownership of the virtual machine
val futureAssetId = AlgorandClient.createVirtualMachineAsset(vmName)

// Insert the new virtual machine into the database
futureAssetId map { assetId =>
println(s"Inserting VM $assetId : $vmName")
virtualMachineService.insertNewVirtualMachine(vmName, assetId, expirationDate)

Ok(Json.obj("assetId" -> assetId))
}
}

def getNoVNCLink(vmName: String) =
Action(Ok("http://192.168.122.125:6081/vnc.html?host=algo-vps&port=6081"))

def listVirtualMachines(address: String) = Action.async {
virtualMachineService.listAllAssetIds flatMap { allAssets =>
val userAssetHoldings =
allAssets.map(assetId => (assetId, Option {
AlgorandClient.algodApi
.accountInformation(address)
.getHolding(BigInt(assetId).bigInteger)
}))

val userVmsList =
virtualMachineService.listByAssetId(
userAssetHoldings.filter(_._2.isDefined)
.map { case (i, h) => (i, h.get) }
.filter { case (_, h) => BigInt(h.getAmount) > 0 }
.map { case (i, _) => i }
)

userVmsList

} map { userVmsList =>
val json = Json.obj(
"virtualMachines" ->
userVmsList.map(vm => Json.obj(
"name" -> vm.name,
"assetId" -> vm.assetId,
"poweredOn" -> VirtualMachineManager.isVirtualMachineOn(vm.name),
"dateCreated" -> vm.created,
"paidUntil" -> vm.paidUntil,
"viewerLink" -> "",
)
)
)

Ok(json)
}
}

def turnOffVirtualMachine(vmName: String) = Action {
VirtualMachineManager.turnOffVirtualMachine(vmName)
Ok("")
}

def turnOnVirtualMachine(vmName: String) = Action {
VirtualMachineManager.turnOnVirtualMachine(vmName)
Ok("")
}

def deleteVirtualMachine(vmName: String) = Action {
VirtualMachineManager.deleteVirtualMachine(vmName)
virtualMachineService.deleteVirtualMachine(vmName)
// TODO: Destroy asset

Ok("")
}

}

+ 33
- 0
app/filters/ExampleFilter.scala View File

@@ -0,0 +1,33 @@
package filters

import akka.stream.Materializer
import javax.inject._
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future}

/**
* This is a simple filter that adds a header to all requests. It's
* added to the application's list of filters by the
* [[Filters]] class.
*
* @param mat This object is needed to handle streaming of requests
* and responses.
* @param exec This class is needed to execute code asynchronously.
* It is used below by the `map` method.
*/
@Singleton
class ExampleFilter @Inject()(
implicit override val mat: Materializer,
exec: ExecutionContext) extends Filter {

override def apply(nextFilter: RequestHeader => Future[Result])
(requestHeader: RequestHeader): Future[Result] = {
// Run the next filter in the chain. This will call other filters
// and eventually call the action. Take the result and modify it
// by adding a new header.
nextFilter(requestHeader).map { result =>
result.withHeaders("X-ExampleFilter" -> "foo")
}
}

}

+ 9
- 0
app/models/DatabaseExecutionContext.scala View File

@@ -0,0 +1,9 @@
package models

import akka.actor.ActorSystem
import javax.inject.{Inject, Singleton}
import play.api.libs.concurrent.CustomExecutionContext

@Singleton
class DatabaseExecutionContext @Inject()(system: ActorSystem)
extends CustomExecutionContext(system, "database-dispatcher")

+ 69
- 0
app/models/VirtualMachineRepository.scala View File

@@ -0,0 +1,69 @@
package models

import java.util.Date
import javax.inject.{Inject, Singleton}
import anorm.SqlParser.{date, get, long, str}
import anorm._
import play.api.db.DBApi
import scala.concurrent.Future
import services.AlgorandClient.AssetID

case class VirtualMachine(id: Option[Long] = None,
name: String,
assetId: Long,
created: Date,
paidUntil: Date)

@Singleton
class VirtualMachineRepository @Inject()(dbapi: DBApi) (implicit ec: DatabaseExecutionContext) {
private val db = dbapi.database("default")

private[models] val simple = {
get[Option[Long]]("virtual_machines.id") ~
str("virtual_machines.name") ~
long("virtual_machines.asset_id") ~
date("virtual_machines.created") ~
date("virtual_machines.paid_until") map {
case id ~ name ~ assetId ~ created ~ paidUntil =>
VirtualMachine(id, name, assetId, created, paidUntil)
}
}

def findByAssetId(assetId: Long): Future[Option[VirtualMachine]] = Future {
db.withConnection { implicit connection =>
SQL"SELECT * from virtual_machines WHERE asset_id = $assetId;".as(simple.singleOpt)
}
}(ec)

def listByAssetId(assetIds: Seq[Long]): Future[List[VirtualMachine]] = Future {
if (assetIds.nonEmpty) db.withConnection { implicit connection =>
SQL"SELECT * FROM virtual_machines WHERE asset_id IN (${assetIds});"
.as(simple.*)
} else Nil
}(ec)

def listAllAssetIds: Future[List[Long]] = Future {
db.withConnection { implicit connection =>
SQL"SELECT asset_id FROM virtual_machines;".as(long("asset_id").*)
}
}

def insertNewVirtualMachine(vmName: String,
assetId: AssetID,
paidUntil: Date): Future[Unit] = Future {
db.withConnection { implicit connection =>
SQL"""
INSERT INTO virtual_machines (name, asset_id, paid_until)
VALUES ($vmName, $assetId, $paidUntil);
""".executeInsert()

}
}

def deleteVirtualMachine(vmName: String): Future[Unit] = Future {
db.withConnection { implicit connection =>
SQL"DELETE FROM virtual_machines WHERE name = ${vmName};".executeUpdate()
}
}

}

+ 110
- 0
app/services/AlgorandClient.scala View File

@@ -0,0 +1,110 @@
package services

import com.algorand.algosdk.account.Account
import com.algorand.algosdk.algod.client.AlgodClient
import com.algorand.algosdk.algod.client.api.AlgodApi
import com.algorand.algosdk.algod.client.model.TransactionParams
import com.algorand.algosdk.builder.transaction.{AssetCreateTransactionBuilder, AssetTransferTransactionBuilder}
import com.algorand.algosdk.transaction.Transaction
import com.algorand.algosdk.util.Encoder
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

object AlgorandClient {
private val ALGOD_API_ADDR = "https://testnet-algorand.api.purestake.io/ps1"
private val ALGOD_API_KEY = System.getenv("ALGOD_API_KEY");

type AssetID = BigInt

if (ALGOD_API_KEY == null || ALGOD_API_KEY.isBlank) {
System.err.println("Error: ALGO_API_KEY must be set")
System.exit(1)
}

val client = new AlgodClient()
client.setBasePath(ALGOD_API_ADDR)
client.setApiKey(ALGOD_API_KEY)
client.addDefaultHeader("X-API-Key", ALGOD_API_KEY)

val algodApi = new AlgodApi(client)

private val algoMnemonic = System.getenv("ALGO_WALLET")

if (algoMnemonic == null || algoMnemonic.isBlank) {
System.err.println("Error: ALGO_WALLET must be set")
System.exit(1)
}


val algoAccount = new Account(algoMnemonic)

def suggestedParameters: TransactionParams = algodApi.transactionParams()

/**
* Create a new asset NFT representing ownership of a virtual machine.
* @param assetName the name of the newly created asset.
* @return the ID of the newly created asset.
*/
def createVirtualMachineAsset(assetName: String): Future[AssetID] = Future {
val assetCreationBuilder = AssetCreateTransactionBuilder.Builder()
val sp = suggestedParameters

assetCreationBuilder.sender(algoAccount.getAddress)
assetCreationBuilder.fee(0L)
assetCreationBuilder.firstValid(sp.getLastRound)
assetCreationBuilder.lastValid(sp.getLastRound.add(BigInt(1000).bigInteger))
assetCreationBuilder.genesisHash(sp.getGenesishashb64)
assetCreationBuilder.assetTotal(1L)
assetCreationBuilder.assetUnitName("VM")
assetCreationBuilder.assetName(assetName)
assetCreationBuilder.assetDecimals(0)
// assetCreationBuilder.metadataHashUTF8("")
assetCreationBuilder.manager(algoAccount.getAddress)
assetCreationBuilder.reserve(algoAccount.getAddress)
assetCreationBuilder.freeze(algoAccount.getAddress)
assetCreationBuilder.defaultFrozen(false)
assetCreationBuilder.clawback(algoAccount.getAddress)

val tx = assetCreationBuilder.build()

Account.setFeeByFeePerByte(tx, sp.getFee)

val signedTx = algoAccount.signTransaction(tx)

val txId = algodApi.rawTransaction(Encoder.encodeToMsgPack(signedTx))

// Wait for the transaction to be confirmed
val assetId = {
LazyList.continually(algodApi.pendingTransactionInformation(txId.getTxId))
.flatMap(wtx => Option(wtx.getRound).map(BigInt.apply) match {
case Some(r) if r > 0L => List(BigInt(
algodApi.pendingTransactionInformation(txId.getTxId)
.getTxresults.getCreatedasset
))
case _ =>
algodApi.waitForBlock(suggestedParameters.getLastRound.add(BigInt(1).bigInteger))
Nil
})
.head
}

assetId
}

def createSendAssetTransaction(assetID: Long, receiver: String): Transaction = {
val assetSendBuilder = AssetTransferTransactionBuilder.Builder()
val params = suggestedParameters

assetSendBuilder.assetIndex(assetID)
assetSendBuilder.assetAmount(1L)
assetSendBuilder.assetReceiver(receiver)
assetSendBuilder.flatFee(1000L)
assetSendBuilder.firstValid(params.getLastRound)
assetSendBuilder.lastValid((BigInt(params.getLastRound) + 1000).bigInteger)
assetSendBuilder.genesisHash(params.getGenesishashb64)

assetSendBuilder.build()
}

//ALGOVP5KCBHBU2ZYILN6RDFFZB6S35KHUNVCOEXIUHCZ5O6BHRLTIJM2W4:prison cash sort scissors sport grit question team arrow nest volcano script profit sweet spoon heart obey minute midnight income fork oppose fossil abstract urban
}

+ 41
- 0
app/services/ApplicationTimer.scala View File

@@ -0,0 +1,41 @@
package services

import java.time.{Clock, Instant}
import javax.inject._
import play.api.Logger
import play.api.inject.ApplicationLifecycle
import scala.concurrent.Future

/**
* This class demonstrates how to run code when the
* application starts and stops. It starts a timer when the
* application starts. When the application stops it prints out how
* long the application was running for.
*
* This class is registered for Guice dependency injection in the
* [[Module]] class. We want the class to start when the application
* starts, so it is registered as an "eager singleton". See the code
* in the [[Module]] class to see how this happens.
*
* This class needs to run code when the server stops. It uses the
* application's [[ApplicationLifecycle]] to register a stop hook.
*/
@Singleton
class ApplicationTimer @Inject() (clock: Clock, appLifecycle: ApplicationLifecycle) {

private val logger: Logger = Logger(this.getClass)

// This code is called when the application starts.
private val start: Instant = clock.instant
logger.info(s"ApplicationTimer demo: Starting application at $start.")

// When the application starts, register a stop hook with the
// ApplicationLifecycle object. The code inside the stop hook will
// be run when the application stops.
appLifecycle.addStopHook { () =>
val stop: Instant = clock.instant
val runningTime: Long = stop.getEpochSecond - start.getEpochSecond
logger.info(s"ApplicationTimer demo: Stopping application at ${clock.instant} after ${runningTime}s.")
Future.successful(())
}
}

+ 29
- 0
app/services/Counter.scala View File

@@ -0,0 +1,29 @@
package services

import java.util.concurrent.atomic.AtomicInteger
import javax.inject._

/**
* This trait demonstrates how to create a component that is injected
* into a controller. The trait represents a counter that returns a
* incremented number each time it is called.
*/
trait Counter {
def nextCount(): Int
}

/**
* This class is a concrete implementation of the [[Counter]] trait.
* It is configured for Guice dependency injection in the [[Module]]
* class.
*
* This class has a `Singleton` annotation because we need to make
* sure we only use one counter per application. Without this
* annotation we would get a new instance every time a [[Counter]] is
* injected.
*/
@Singleton
class AtomicCounter extends Counter {
private val atomicCounter = new AtomicInteger()
override def nextCount(): Int = atomicCounter.getAndIncrement()
}

+ 10
- 0
app/services/EventScheduler.scala View File

@@ -0,0 +1,10 @@
package services

class EventScheduler {
// Check VM balances, update expire dates if the balance has increased

// Check all VMs for expired VMs
// If a VM has expired, destroy it through libvirt and delete
// related records in the database

}

+ 9
- 0
app/services/User.scala View File

@@ -0,0 +1,9 @@
package services

import akka.actor._

class User extends Actor {
override def receive = {
case _ =>
}
}

+ 51
- 0
app/services/VirtualMachineManager.scala View File

@@ -0,0 +1,51 @@
package services

import java.util.Date
import org.libvirt.{Connect, ConnectAuthDefault, Domain}
import scala.concurrent._
import ExecutionContext.Implicits.global
import scala.io.Source
import scala.sys.process._

/**
* Uses libvirt to create, delete, power on, and power off virtual machines.
*/
object VirtualMachineManager {
private val ca = new ConnectAuthDefault()
private val conn = new Connect("qemu:///system", ca, 0)
private val vmConfig = Source.fromResource("vm-template.xml").mkString

private def getAllVMs =
conn.listDomains.map(conn.domainLookupByID) ++
conn.listDefinedDomains.map(conn.domainLookupByName)

private def getVirtualMachineByName(vmName: String): Option[Domain] =
getAllVMs.find(_.getName == vmName)

def createVirtualMachine(vmName: String): Future[Unit] = Future {
// Allocate an 8GB drive for the virtual machine.
s"fallocate -l 8G '/home/robby/virtual-machines/$vmName.img'".lazyLines

conn.domainCreateLinux(
vmConfig.replaceAll("VIRTUAL_MACHINE_NAME", vmName),
0
)
}

def deleteVirtualMachine(vmName: String): Future[Unit] = Future {
getVirtualMachineByName(vmName) foreach { _.destroy() }
s"rm /home/robby/virtual-machines/$vmName.img".lazyLines
}

def turnOnVirtualMachine(vmName: String): Future[Unit] = Future {
getVirtualMachineByName(vmName) foreach { _ .create() }
}

def turnOffVirtualMachine(vmName: String): Future[Unit] = Future {
getVirtualMachineByName(vmName) foreach { _ .shutdown() }
}

def isVirtualMachineOn(vmName: String): Boolean = {
getVirtualMachineByName(vmName).exists(_.isActive > 0)
}
}

+ 51
- 0
build.sbt View File

@@ -0,0 +1,51 @@
name := "AlgoVPS"

version := "1.0"

lazy val `algovps` = Project(id = "algovps", base = file(".")).enablePlugins(PlayScala)

resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"

resolvers += "Akka Snapshot Repository" at "https://repo.akka.io/snapshots/"

scalaVersion := "2.13.2"

libraryDependencies ++= Seq(
jdbc,
evolutions,
ehcache,
ws,
specs2 % Test,
guice)

// https://mvnrepository.com/artifact/com.algorand/algosdk
libraryDependencies += "com.algorand" % "algosdk" % "1.3.1"

// https://mvnrepository.com/artifact/org.postgresql/postgresql
libraryDependencies += "org.postgresql" % "postgresql" % "42.2.12"

// https://mvnrepository.com/artifact/org.playframework.anorm/anorm
libraryDependencies += "org.playframework.anorm" %% "anorm" % "2.6.5"

// https://mvnrepository.com/artifact/org.playframework.anorm/anorm-postgres
libraryDependencies += "org.playframework.anorm" %% "anorm-postgres" % "2.6.5"

// https://mvnrepository.com/artifact/net.java.dev.jna/jna
libraryDependencies += "net.java.dev.jna" % "jna" % "5.5.0"

// https://mvnrepository.com/artifact/org.libvirt/libvirt
libraryDependencies += "org.libvirt" % "libvirt" % "0.5.1"

unmanagedResourceDirectories in Test += baseDirectory(_ / "target/web/public/test").value
PlayKeys.externalizeResources := false


//assemblyMergeStrategy in assembly := {
// case PathList(ps @ _*) if ps.last endsWith "module-info.class" => MergeStrategy.rename
//// case PathList(ps @ _*) if ps.last endsWith "reference-overrides.conf" =>
//// MergeStrategy.rename
// case x =>
// val oldStrategy = (assemblyMergeStrategy in assembly).value
// oldStrategy(x)
//
//}

+ 411
- 0
conf/application.conf View File

@@ -0,0 +1,411 @@
# This is the main configuration file for the application.
# https://www.playframework.com/documentation/latest/ConfigFile
# ~~~~~
# Play uses HOCON as its configuration file format. HOCON has a number
# of advantages over other config formats, but there are two things that
# can be used when modifying settings.
#
# You can include other configuration files in this main application.conf file:
#include "extra-config.conf"
#
# You can declare variables and substitute for them:
#mykey = ${some.value}
#
# And if an environment variable exists when there is no other subsitution, then
# HOCON will fall back to substituting environment variable:
#mykey = ${JAVA_HOME}

## Akka
# https://www.playframework.com/documentation/latest/ScalaAkka#Configuration
# https://www.playframework.com/documentation/latest/JavaAkka#Configuration
# ~~~~~
# Play uses Akka internally and exposes Akka Streams and actors in Websockets and
# other streaming HTTP responses.
akka {
# "akka.log-config-on-start" is extraordinarly useful because it log the complete
# configuration at INFO level, including defaults and overrides, so it s worth
# putting at the very top.
#
# Put the following in your conf/logback.xml file:
#
# <logger name="akka.actor" level="INFO" />
#
# And then uncomment this line to debug the configuration.
#
#log-config-on-start = true


database-dispatcher {
# Dispatcher is the name of the event-based dispatcher
type = Dispatcher
# What kind of ExecutionService to use
executor = "fork-join-executor"
# Configuration for the fork join pool
fork-join-executor {
# Min number of threads to cap factor-based parallelism number to
parallelism-min = 2
# Parallelism (threads) ... ceil(available processors * factor)
parallelism-factor = 2.0
# Max number of threads to cap factor-based parallelism number to
parallelism-max = 10
}
# Throughput defines the maximum number of messages to be
# processed per actor before the thread jumps to the next actor.
# Set to 1 for as fair as possible.
throughput = 100
}
}



## Secret key
# https://www.playframework.com/documentation/latest/ApplicationSecret
# ~~~~~
# The secret key is used to sign Play's session cookie.
# This must be changed for production, but we don't recommend you change it in this file.
play.http.secret.key = "ka9I4eC6xG0w[AXT2u2eoaV@Bzwr/=HF>zU?5Q<sF:5mt::qAWYS3UQ5DoHXGIr`"

## Modules
# https://www.playframework.com/documentation/latest/Modules
# ~~~~~
# Control which modules are loaded when Play starts. Note that modules are
# the replacement for "GlobalSettings", which are deprecated in 2.5.x.
# Please see https://www.playframework.com/documentation/latest/GlobalSettings
# for more information.
#
# You can also extend Play functionality by using one of the publically available
# Play modules: https://playframework.com/documentation/latest/ModuleDirectory
play.modules {
# By default, Play will load any class called Module that is defined
# in the root package (the "app" directory), or you can define them
# explicitly below.
# If there are any built-in modules that you want to disable, you can list them here.
#enabled += my.application.Module

# If there are any built-in modules that you want to disable, you can list them here.
disabled += "play.filters.cors.CORSModule"
disabled += "play.filters.csrf.CSRFModule"
}

## IDE
# https://www.playframework.com/documentation/latest/IDE
# ~~~~~
# Depending on your IDE, you can add a hyperlink for errors that will jump you
# directly to the code location in the IDE in dev mode. The following line makes
# use of the IntelliJ IDEA REST interface:
#play.editor="http://localhost:63342/api/file/?file=%s&line=%s"

## Internationalisation
# https://www.playframework.com/documentation/latest/JavaI18N
# https://www.playframework.com/documentation/latest/ScalaI18N
# ~~~~~
# Play comes with its own i18n settings, which allow the user's preferred language
# to map through to internal messages, or allow the language to be stored in a cookie.
play.i18n {
# The application languages
langs = [ "en" ]

# Whether the language cookie should be secure or not
#langCookieSecure = true

# Whether the HTTP only attribute of the cookie should be set to true
#langCookieHttpOnly = true
}

## Filters
# https://www.playframework.com/documentation/latest/ScalaHttpFilters
# https://www.playframework.com/documentation/latest/JavaHttpFilters
# ~~~~~
# Filters run code on every request. They can be used to perform
# common logic for all your actions, e.g. adding common headers.
#
play.filters {

# Enabled filters are run automatically against Play.
# CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default.
#enabled += filters.ExampleFilter

# Disabled filters remove elements from the enabled list.
#disabled += filters.ExampleFilter
disabled += "play.filters.cors.CORSFilter"
disabled += play.filters.csrf.CSRFFilter
disabled += play.filters.headers.SecurityHeadersFilter
disabled += play.filters.hosts.AllowedHostsFilter
}

## Play HTTP settings
# ~~~~~
play.http {
## Router
# https://www.playframework.com/documentation/latest/JavaRouting
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~~
# Define the Router object to use for this application.
# This router will be looked up first when the application is starting up,
# so make sure this is the entry point.
# Furthermore, it's assumed your route file is named properly.
# So for an application router like `my.application.Router`,
# you may need to define a router file `conf/my.application.routes`.
# Default to Routes in the root package (aka "apps" folder) (and conf/routes)
#router = my.application.Router

## Action Creator
# https://www.playframework.com/documentation/latest/JavaActionCreator
# ~~~~~
#actionCreator = null

## ErrorHandler
# https://www.playframework.com/documentation/latest/JavaRouting
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~~
# If null, will attempt to load a class called ErrorHandler in the root package,
#errorHandler = null

## Session & Flash
# https://www.playframework.com/documentation/latest/JavaSessionFlash
# https://www.playframework.com/documentation/latest/ScalaSessionFlash
# ~~~~~
session {
# Sets the cookie to be sent only over HTTPS.
#secure = true

# Sets the cookie to be accessed only by the server.
#httpOnly = true

# Sets the max-age field of the cookie to 5 minutes.
# NOTE: this only sets when the browser will discard the cookie. Play will consider any
# cookie value with a valid signature to be a valid session forever. To implement a server side session timeout,
# you need to put a timestamp in the session and check it at regular intervals to possibly expire it.
#maxAge = 300

# Sets the domain on the session cookie.
#domain = "example.com"
}

flash {
# Sets the cookie to be sent only over HTTPS.
#secure = true

# Sets the cookie to be accessed only by the server.
#httpOnly = true
}
}

## Netty Provider
# https://www.playframework.com/documentation/latest/SettingsNetty
# ~~~~~
play.server.netty {
# Whether the Netty wire should be logged
#log.wire = true

# If you run Play on Linux, you can use Netty's native socket transport
# for higher performance with less garbage.
#transport = "native"
}

## WS (HTTP Client)
# https://www.playframework.com/documentation/latest/ScalaWS#Configuring-WS
# ~~~~~
# The HTTP client primarily used for REST APIs. The default client can be
# configured directly, but you can also create different client instances
# with customized settings. You must enable this by adding to build.sbt:
#
# libraryDependencies += ws // or javaWs if using java
#
play.ws {
# Sets HTTP requests not to follow 302 requests
#followRedirects = false

# Sets the maximum number of open HTTP connections for the client.
#ahc.maxConnectionsTotal = 50

## WS SSL
# https://www.playframework.com/documentation/latest/WsSSL
# ~~~~~
ssl {
# Configuring HTTPS with Play WS does not require programming. You can
# set up both trustManager and keyManager for mutual authentication, and
# turn on JSSE debugging in development with a reload.
#debug.handshake = true
#trustManager = {
# stores = [
# { type = "JKS", path = "exampletrust.jks" }
# ]
#}
}
}

## Cache
# https://www.playframework.com/documentation/latest/JavaCache
# https://www.playframework.com/documentation/latest/ScalaCache
# ~~~~~
# Play comes with an integrated cache API that can reduce the operational
# overhead of repeated requests. You must enable this by adding to build.sbt:
#
# libraryDependencies += cache
#
play.cache {
# If you want to bind several caches, you can bind the individually
#bindCaches = ["db-cache", "user-cache", "session-cache"]
}

## Filters
# https://www.playframework.com/documentation/latest/ScalaHttpFilters
# https://www.playframework.com/documentation/latest/JavaHttpFilters
# ~~~~~
# Filters run code on every request. They can be used to perform
# common logic for all your actions, e.g. adding common headers.
#
play.filters {

# Enabled filters are run automatically against Play.
# CSRFFilter, AllowedHostFilters, and SecurityHeadersFilters are enabled by default.
#enabled += filters.ExampleFilter

#enabled += play.filters.csrf.CSRFFilter

# Disabled filters remove elements from the enabled list.
#disabled += filters.ExampleFilter
}

## Filter Configuration
# https://www.playframework.com/documentation/latest/Filters
# ~~~~~
# There are a number of built-in filters that can be enabled and configured
# to give Play greater security.
#
play.filters {
## CORS filter configuration
# https://www.playframework.com/documentation/latest/CorsFilter
# ~~~~~
# CORS is a protocol that allows web applications to make requests from the browser
# across different domains.
# NOTE: You MUST apply the CORS configuration before the CSRF filter, as CSRF has
# dependencies on CORS settings.
cors {
# Filter paths by a whitelist of path prefixes
#pathPrefixes = ["/some/path", ...]

# The allowed origins. If null, all origins are allowed.
#allowedOrigins = ["https://www.example.com"]

allowedOrigins = ["192.168.122.125", "192.168.122.125:9000", "192.168.122.125:8080", "localhost:8081", "localhost:9000"]


# The allowed HTTP methods. If null, all methods are allowed
#allowedHttpMethods = ["GET", "POST"]
}

## CSRF Filter
# https://www.playframework.com/documentation/latest/ScalaCsrf#Applying-a-global-CSRF-filter
# https://www.playframework.com/documentation/latest/JavaCsrf#Applying-a-global-CSRF-filter
# ~~~~~
# Play supports multiple methods for verifying that a request is not a CSRF request.
# The primary mechanism is a CSRF token. This token gets placed either in the query string
# or body of every form submitted, and also gets placed in the users session.
# Play then verifies that both tokens are present and match.
csrf {
# Sets the cookie to be sent only over HTTPS
#cookie.secure = true

# Defaults to CSRFErrorHandler in the root package.
#errorHandler = MyCSRFErrorHandler
}

## Security headers filter configuration
# https://www.playframework.com/documentation/latest/SecurityHeaders
# ~~~~~
# Defines security headers that prevent XSS attacks.
# If enabled, then all options are set to the below configuration by default:
headers {
# The X-Frame-Options header. If null, the header is not set.
#frameOptions = "DENY"

# The X-XSS-Protection header. If null, the header is not set.
#xssProtection = "1; mode=block"

# The X-Content-Type-Options header. If null, the header is not set.
#contentTypeOptions = "nosniff"

# The X-Permitted-Cross-Domain-Policies header. If null, the header is not set.
#permittedCrossDomainPolicies = "master-only"

# The Content-Security-Policy header. If null, the header is not set.
#contentSecurityPolicy = "default-src 'self'"
}

## Allowed hosts filter configuration
# https://www.playframework.com/documentation/latest/AllowedHostsFilter
# ~~~~~
# Play provides a filter that lets you configure which hosts can access your application.
# This is useful to prevent cache poisoning attacks.
hosts {
# Allow requests to example.com, its subdomains, and localhost:9000.
allowed = ["localhost:9000", "localhost:8081"]
#allowed = ["192.168.122.125", "192.168.122.125:9000", "192.168.122.125:8080"]
}
}

## Evolutions
# https://www.playframework.com/documentation/latest/Evolutions
# ~~~~~
# Evolutions allows database scripts to be automatically run on startup in dev mode
# for database migrations. You must enable this by adding to build.sbt:
#
# libraryDependencies += evolutions
#
play.evolutions {
# You can disable evolutions for a specific datasource if necessary
#db.default.enabled = false
}

## Database Connection Pool
# https://www.playframework.com/documentation/latest/SettingsJDBC
# ~~~~~
# Play doesn't require a JDBC database to run, but you can easily enable one.
#
# libraryDependencies += jdbc
#
play.db {
# The combination of these two settings results in "db.default" as the
# default JDBC pool:
#config = "db"
#default = "default"

# Play uses HikariCP as the default connection pool. You can override
# settings by changing the prototype:
prototype {
# Sets a fixed JDBC connection pool size of 50
#hikaricp.minimumIdle = 50
#hikaricp.maximumPoolSize = 50
}
}

## JDBC Datasource
# https://www.playframework.com/documentation/latest/JavaDatabase
# https://www.playframework.com/documentation/latest/ScalaDatabase
# ~~~~~
# Once JDBC datasource is set up, you can work with several different
# database options:
#
# Slick (Scala preferred option): https://www.playframework.com/documentation/latest/PlaySlick
# JPA (Java preferred option): https://playframework.com/documentation/latest/JavaJPA
# EBean: https://playframework.com/documentation/latest/JavaEbean
# Anorm: https://www.playframework.com/documentation/latest/ScalaAnorm
#
db {
# You can declare as many datasources as you want.
# By convention, the default datasource is named `default`

# https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database
#default.driver = org.h2.Driver
#default.url = "jdbc:h2:mem:play"
default.driver = org.postgresql.Driver
default.url = "jdbc:postgresql://192.168.122.125/postgres"
default.username = "postgres"
default.password = "1234"

# You can turn on SQL logging for any datasource
# https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements
default.logSql=true
}

+ 10
- 0
conf/evolutions/1.sql View File

@@ -0,0 +1,10 @@

CREATE TABLE virtual_machines (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
asset_id INTEGER,
created TIMESTAMP DEFAULT NOW() NOT NULL,
paid_until TIMESTAMP NOT NULL
);

CREATE INDEX virtual_machines_index ON virtual_machines (asset_id);

+ 41
- 0
conf/logback.xml View File

@@ -0,0 +1,41 @@
<!-- https://www.playframework.com/documentation/latest/SettingsLogger -->
<configuration>

<conversionRule conversionWord="coloredLevel" converterClass="play.api.libs.logback.ColoredLevel" />

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
<file>${application.home:-.}/logs/application.log</file>
<encoder>
<pattern>%date [%level] from %logger in %thread - %message%n%xException</pattern>
</encoder>
</appender>

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%coloredLevel %logger{15} - %message%n%xException{10}</pattern>
</encoder>
</appender>

<appender name="ASYNCFILE" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="FILE" />
</appender>

<appender name="ASYNCSTDOUT" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="STDOUT" />
</appender>

<logger name="play" level="INFO" />
<logger name="application" level="DEBUG" />

<!-- Off these ones as they are annoying, and anyway we manage configuration ourselves -->
<logger name="com.avaje.ebean.config.PropertyMapLoader" level="OFF" />
<logger name="com.avaje.ebeaninternal.server.core.XmlConfigLoader" level="OFF" />
<logger name="com.avaje.ebeaninternal.server.lib.BackgroundThread" level="OFF" />
<logger name="com.gargoylesoftware.htmlunit.javascript" level="OFF" />

<root level="WARN">
<appender-ref ref="ASYNCFILE" />
<appender-ref ref="ASYNCSTDOUT" />
</root>

</configuration>

+ 34
- 0
conf/routes View File

@@ -0,0 +1,34 @@

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# An example controller showing a sample home page
#GET / controllers.HomeController.index
# An example controller showing how to use dependency injection
#GET /count controllers.CountController.count
# An example controller showing how to write asynchronous code
#GET /message controllers.AsyncController.message

# Map static resources from the /public folder to the /assets URL path
#GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)

GET /api/algo/*address/balance controllers.AlgoController.balance(address)

GET /api/algo/recommended-parameters controllers.AlgoController.getRecommendedParameters

+nocsrf
POST /api/algo/*address/opt-in-asset/*assetId controllers.AlgoController.optInAsset(address, assetId)

+nocsrf
POST /api/virtual-machines/*address/create-machine controllers.VirtualMachineController.createMachine(address)

GET /api/virtual-machines/*address/list-vms controllers.VirtualMachineController.listVirtualMachines(address)

GET /api/virtual-machines/turn-off/*vmName controllers.VirtualMachineController.turnOffVirtualMachine(vmName)

GET /api/virtual-machines/turn-on/*vmName controllers.VirtualMachineController.turnOnVirtualMachine(vmName)

GET /api/virtual-machines/delete/*vmName controllers.VirtualMachineController.deleteVirtualMachine(vmName)

GET /api/virtual-machines/get-viewer/*vmName controllers.VirtualMachineController.getNoVNCLink(vmName)

+ 26
- 0
conf/vm-template.xml View File

@@ -0,0 +1,26 @@
<domain type='qemu'>
<name>VIRTUAL_MACHINE_NAME</name>
<memory unit="KiB">2097152</memory>
<currentMemory unit="KiB">2097152</currentMemory>
<vcpu>2</vcpu>
<os>
<type arch='x86_64' machine='pc'>hvm</type>
<boot dev='cdrom'/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/home/robby/installISOs/debian10.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<source file='/home/robby/virtual-machines/VIRTUAL_MACHINE_NAME.img'/>
<target dev='hda'/>
</disk>
<interface type='network'>
<source network='default'/>
</interface>
<graphics type='vnc' port='-1' autoport='yes' keymap='en-us'/>
</devices>
</domain>

+ 1
- 0
project/build.properties View File

@@ -0,0 +1 @@
sbt.version=1.3.10

+ 7
- 0
project/plugins.sbt View File

@@ -0,0 +1,7 @@
logLevel := Level.Warn

resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/"

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.1")

addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")

BIN
public/images/external.png View File

Before After
Width: 12  |  Height: 12  |  Size: 278 B

BIN
public/images/favicon.png View File

Before After
Width: 16  |  Height: 16  |  Size: 687 B

BIN
public/images/header-pattern.png View File

Before After
Width: 10  |  Height: 10  |  Size: 175 B

+ 3
- 0
public/javascripts/hello.js View File

@@ -0,0 +1,3 @@
if (window.console) {
console.log("Welcome to your Play application's JavaScript!");
}

+ 127
- 0
public/stylesheets/main.css View File

@@ -0,0 +1,127 @@
/*
* Copyright (C) 2009-2017 Lightbend Inc. <https://www.lightbend.com>
*/
html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;}
table{border-collapse:collapse;border-spacing:0;}
caption,th,td{text-align:left;font-weight:normal;}
form legend{display:none;}
blockquote:before,blockquote:after,q:before,q:after{content:"";}
blockquote,q{quotes:"" "";}
ol,ul{list-style:none;}
hr{display:none;visibility:hidden;}
:focus{outline:0;}
article{}article h1,article h2,article h3,article h4,article h5,article h6{color:#333;font-weight:bold;line-height:1.25;margin-top:1.3em;}
article h1 a,article h2 a,article h3 a,article h4 a,article h5 a,article h6 a{font-weight:inherit;color:#333;}article h1 a:hover,article h2 a:hover,article h3 a:hover,article h4 a:hover,article h5 a:hover,article h6 a:hover{color:#333;}
article h1{font-size:36px;margin:0 0 18px;border-bottom:4px solid #eee;}
article h2{font-size:25px;margin-bottom:9px;border-bottom:2px solid #eee;}
article h3{font-size:18px;margin-bottom:9px;}
article h4{font-size:15px;margin-bottom:3px;}
article h5{font-size:12px;font-weight:normal;margin-bottom:3px;}
article .subheader{color:#777;font-weight:300;margin-bottom:24px;}
article p{line-height:1.3em;margin:1em 0;}
article p img{margin:0;}
article p.lead{font-size:18px;font-size:1.8rem;line-height:1.5;}
article li>p:first-child{margin-top:0;}
article li>p:last-child{margin-bottom:0;}
article ul li,article ol li{position:relative;padding:4px 0 4px 14px;}article ul li ol,article ol li ol,article ul li ul,article ol li ul{margin-left:20px;}
article ul li:before,article ol li:before{position:absolute;top:8px;left:0;content:"►";color:#ccc;font-size:10px;margin-right:5px;}
article>ol{counter-reset:section;}article>ol li:before{color:#ccc;font-size:13px;}
article>ol>li{padding:6px 0 4px 20px;counter-reset:chapter;}article>ol>li:before{content:counter(section) ".";counter-increment:section;}
article>ol>li>ol>li{padding:6px 0 4px 30px;counter-reset:item;}article>ol>li>ol>li:before{content:counter(section) "." counter(chapter);counter-increment:chapter;}
article>ol>li>ol>li>ol>li{padding:6px 0 4px 40px;}article>ol>li>ol>li>ol>li:before{content:counter(section) "." counter(chapter) "." counter(item);counter-increment:item;}
article em,article i{font-style:italic;line-height:inherit;}
article strong,article b{font-weight:bold;line-height:inherit;}
article small{font-size:60%;line-height:inherit;}
article h1 small,article h2 small,article h3 small,article h4 small,article h5 small{color:#777;}
article hr{border:solid #ddd;border-width:1px 0 0;clear:both;margin:12px 0 18px;height:0;}
article abbr,article acronym{text-transform:uppercase;font-size:90%;color:#222;border-bottom:1px solid #ddd;cursor:help;}
article abbr{text-transform:none;}
article img{max-width:100%;}
article pre{margin:10px 0;border:1px solid #ddd;padding:10px;background:#fafafa;color:#666;overflow:auto;border-radius:5px;}
article code{background:#fafafa;color:#666;font-family:inconsolata, monospace;border:1px solid #ddd;border-radius:3px;height:4px;padding:0;}
article a code{color:#80c846;}article a code:hover{color:#6dae38;}
article pre code{border:0;background:inherit;border-radius:0;line-height:inherit;font-size:14px;}
article pre.prettyprint{border:1px solid #ddd;padding:10px;}
article blockquote,article blockquote p,article p.note{line-height:20px;color:#4c4742;}
article blockquote,article .note{margin:0 0 18px;padding:1px 20px;background:#fff7d6;}article blockquote li:before,article .note li:before{color:#e0bc6f;}
article blockquote code,article .note code{background:#f5d899;border:none;color:inherit;}
article blockquote a,article .note a{color:#6dae38;}
article blockquote pre,article .note pre{background:#F5D899 !important;color:#48484C !important;border:none !important;}
article p.note{padding:15px 20px;}
article table{width:100%;}article table td{padding:8px;}
article table tr{background:#F4F4F7;border-bottom:1px solid #eee;}
article table tr:nth-of-type(odd){background:#fafafa;}
article dl dt{font-weight:bold;}
article dl.tabbed{position:relative;}
article dl.tabbed dt{float:left;margin:0 5px 0 0;border:1px solid #ddd;padding:0 20px;line-height:2;border-radius: 5px 5px 0 0;}
article dl.tabbed dt a{display:block;height:30px;color:#333;text-decoration:none;}
article dl.tabbed dt.current{background: #f7f7f7;}
article dl.tabbed dd{position:absolute;width:100%;left:0;top:30px;}
article dl.tabbed dd pre{margin-top:0;border-top-left-radius:0;}
a{color:#80c846;}a:hover{color:#6dae38;}
p{margin:1em 0;}
h1{-webkit-font-smoothing:antialiased;}
h2{font-weight:bold;font-size:28px;}
hr{clear:both;margin:20px 0 25px 0;border:none;border-top:1px solid #444;visibility:visible;display:block;}
section{padding:50px 0;}
body{background:#f5f5f5;background:#fff;color:#555;font:15px "Helvetica Nueue",sans-serif;padding:0px 0 0px;}
.wrapper{width:960px;margin:0 auto;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;padding:60px 0;}.wrapper:after{content:" ";display:block;clear:both;}
.wrapper article{min-height:310px;width:650px;float:left;}
.wrapper aside{width:270px;float:right;}.wrapper aside ul{margin:2px 0 30px;}.wrapper aside ul a{display:block;padding:3px 0 3px 10px;margin:2px 0;border-left:4px solid #eee;}.wrapper aside ul a:hover{border-color:#80c846;}
.wrapper aside h3{font-size:18px;color:#333;font-weight:bold;line-height:2em;margin:9px 0;border-bottom:1px solid #eee;}
.wrapper aside.stick{position:fixed;right:50%;margin-right:-480px;top:120px;bottom:0;overflow:hidden;}
.half{width:50%;float:left;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;}
header{position:fixed;top:0;z-index:1000;width:100%;height:50px;line-height:50px;padding:30px 0;background:#fff;background:rgba(255, 255, 255, 0.95);border-bottom:1px solid #ccc;box-shadow:0 4px 0 rgba(0, 0, 0, 0.1);}header #logo{position:absolute;left:50%;margin-left:-480px;}
header nav{position:absolute;right:50%;margin-right:-480px;}header nav a{padding:0 10px 4px;font-size:21px;font-weight:500;text-decoration:none;}
header nav a.selected{border-bottom:3px solid #E9E9E9;}
header nav a.download{position:relative;background:#80c846;color:white;margin-left:10px;padding:5px 10px 2px;font-weight:700;border-radius:5px;box-shadow:0 3px 0 #6dae38;text-shadow:-1px -1px 0 rgba(0, 0, 0, 0.2);-webkit-transition:all 70ms ease-out;border:0;}header nav a.download:hover{box-shadow:0 3px 0 #6dae38,0 3px 4px rgba(0, 0, 0, 0.3);}
header nav a.download:active{box-shadow:0 1px 0 #6dae38;top:2px;-webkit-transition:none;}
#download,#getLogo{display:none;position:absolute;padding:5px 20px;width:200px;background:#000;background:rgba(0, 0, 0, 0.8);border-radius:5px;color:#999;line-height:15px;}#download a,#getLogo a{color:#ccc;text-decoration:none;}#download a:hover,#getLogo a:hover{color:#fff;}
#getLogo{text-align:center;}#getLogo h3{font-size:16px;color:#80c846;margin:0 0 15px;}
#getLogo figure{border-radius:3px;margin:5px 0;padding:5px;background:#fff;line-height:25px;width:80px;display:inline-block;}#getLogo figure a{color:#999;text-decoration:none;}#getLogo figure a:hover{color:#666;}
#download{top:85px;right:50%;margin-right:-480px;}#download .button{font-size:16px;color:#80c846;}
#getLogo{top:85px;left:50%;padding:20px;margin-left:-480px;}#getLogo ul{margin:5px 0;}
#getLogo li{margin:1px 0;}
#news{background:#f5f5f5;color:#999;font-size:17px;box-shadow:0 1px 0 rgba(0, 0, 0, 0.1);position:relative;z-index:2;padding:3px 0;}#news ul{box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;background:url(/assets/images/news.png) 10px center no-repeat;padding:19px 0 19px 60px;}
#content{padding:30px 0;}
#top{background:#80c846 url(/assets/images/header-pattern.png) fixed;box-shadow:0 -4px 0 rgba(0, 0, 0, 0.1) inset;padding:0;position:relative;}#top .wrapper{padding:30px 0;}
#top h1{float:left;color:#fff;font-size:35px;line-height:48px;text-shadow:2px 2px 0 rgba(0, 0, 0, 0.1);}#top h1 a{text-decoration:none;color:#fff;}
#top nav{float:right;margin-top:10px;line-height:25px;}#top nav .versions,#top nav form{float:left;margin:0 5px;}
#top nav .versions{height:25px;display:inline-block;border:1px solid #6dae38;border-radius:3px;background:#80c846;background:-moz-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #80c846), color-stop(100%, #6dae38));background:-webkit-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-o-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-ms-linear-gradient(top, #80c846 0%, #6dae38 100%);background:linear-gradient(top, #80c846 0%, #6dae38 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#80c846', endColorstr='#6dae38',GradientType=0 );box-shadow:inset 0 -1px 1px #80c846;text-align:center;color:#fff;text-shadow:-1px -1px 0 #6dae38;}#top nav .versions span{padding:0 4px;position:absolute;}#top nav .versions span:before{content:"⬍";color:rgba(0, 0, 0, 0.4);text-shadow:1px 1px 0 #80c846;margin-right:4px;}
#top nav .versions select{opacity:0;position:relative;z-index:9;}
#top .follow{display:inline-block;border:1px solid #6dae38;border-radius:3px;background:#80c846;background:-moz-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #80c846), color-stop(100%, #6dae38));background:-webkit-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-o-linear-gradient(top, #80c846 0%, #6dae38 100%);background:-ms-linear-gradient(top, #80c846 0%, #6dae38 100%);background:linear-gradient(top, #80c846 0%, #6dae38 100%);filter:progid:DXImageTransform.Microsoft.gradient( startColorstr='#80c846', endColorstr='#6dae38',GradientType=0 );box-shadow:inset 0 -1px 1px #80c846;text-align:center;vertical-align:middle;color:#fff;text-shadow:-1px -1px 0 #6dae38;padding:4px 8px;text-decoration:none;position:absolute;top:41px;left:50%;margin-left:210px;width:250px;}#top .follow:before{vertical-align:middle;content:url(/assets/images/twitter.png);margin-right:10px;}
#top input{width:80px;-webkit-transition:width 200ms ease-in-out;-moz-transition:width 200ms ease-in-out;}#top input:focus{width:200px;}
#title{width:500px;float:left;font-size:17px;color:#2d6201;}
#quicklinks{width:350px;margin:-15px 0 0 0;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;float:right;padding:30px;background:#fff;color:#888;box-shadow:0 3px 5px rgba(0, 0, 0, 0.2);}#quicklinks h2{color:#80c846;font-size:20px;margin-top:15px;padding:10px 0 5px 0;border-top:1px solid #eee;}#quicklinks h2:first-child{margin:0;padding:0 0 5px 0;border:0;}
#quicklinks p{margin:0;}
#quicklinks a{color:#444;}#quicklinks a:hover{color:#222;}
.tweet{border-bottom:1px solid #eee;padding:6px 0 20px 60px;position:relative;min-height:50px;margin-bottom:20px;}.tweet img{position:absolute;left:0;top:8px;}
.tweet strong{font-size:14px;font-weight:bold;}
.tweet span{font-size:12px;color:#888;}
.tweet p{padding:0;margin:5px 0 0 0;}
footer{padding:40px 0;background:#363736;background:#eee;border-top:1px solid #e5e5e5;color:#aaa;position:relative;}footer .logo{position:absolute;top:55px;left:50%;margin-left:-480px;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);}
footer:after{content:" ";display:block;clear:both;}
footer .links{width:960px;margin:0 auto;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;margin:0 auto;padding-left:200px;}footer .links:after{content:" ";display:block;clear:both;}
footer .links dl{width:33%;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;padding:0 10px;float:left;}
footer .links dt{color:#80c846;font-weight:bold;}
footer .links a{color:#aaa;text-decoration:none;}footer .links a:hover{color:#888;}
footer .licence{width:960px;margin:0 auto;box-sizing:border-box;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;margin:20px auto 0;padding-top:20px;border-top:2px solid #ddd;font-size:12px;}footer .licence:after{content:" ";display:block;clear:both;}
footer .licence .typesafe,footer .licence .zenexity{float:right;}
footer .licence .typesafe{position:relative;top:-3px;margin-left:10px;}
footer .licence a{color:#999;}
div.coreteam{position:relative;min-height:80px;border-bottom:1px solid #eee;}div.coreteam img{width:50px;position:absolute;left:0;top:0;padding:2px;border:1px solid #ddd;}
div.coreteam a{color:inherit;text-decoration:none;}
div.coreteam h2{padding-left:70px;border:none;font-size:20px;}
div.coreteam p{margin-top:5px;padding-left:70px;}
ul.contributors{padding:0;margin:0;list-style:none;}ul.contributors li{padding:6px 0 !important;margin:0;}ul.contributors li:before{content:' ';}
ul.contributors img{width:25px;padding:1px;border:1px solid #ddd;margin-right:5px;vertical-align:middle;}
ul.contributors a{color:inherit;text-decoration:none;}
ul.contributors span{font-weight:bold;color:#666;}
ul.contributors.others li{display:inline-block;width:32.3333%;}
div.list{float:left;width:33.3333%;margin-bottom:30px;}
h2{clear:both;}
span.by{font-size:14px;font-weight:normal;}
form dl{padding:10px 0;}
dd.info{color:#888;font-size:12px;}
dd.error{color:#c00;}
aside a[href^="http"]:after,.doc a[href^="http"]:after{content:url(/assets/images/external.png);vertical-align:middle;margin-left:5px;}