Skip to content

Commit

Permalink
Challenges api
Browse files Browse the repository at this point in the history
  • Loading branch information
Karasiq committed Oct 26, 2020
1 parent 836aa70 commit ea8cf8a
Show file tree
Hide file tree
Showing 19 changed files with 317 additions and 101 deletions.
10 changes: 7 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import com.karasiq.scalajsbundler.compilers.{AssetCompilers, ConcatCompiler}
import com.typesafe.sbt.packager.docker.Cmd
import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject}

Expand Down Expand Up @@ -360,12 +361,15 @@ lazy val `server-static-routes` = (project in file("server") / "static-routes")
WebDeps.toastrJS,
WebDeps.pellJS,
WebDeps.multiSelectJS,
scalaJsApplication(webapp, fastOpt = false, launcher = false).value
scalaJsApplication(webapp, fastOpt = true, launcher = false).value
)
},
scalaJsBundlerCompile in Compile := (scalaJsBundlerCompile in Compile)
.dependsOn(fullOptJS in Compile in webapp)
.value
.dependsOn(fastOptJS in Compile in webapp)
.value,
scalaJsBundlerCompilers in Compile := AssetCompilers {
case com.karasiq.scalajsbundler.dsl.Mimes.javascript ConcatCompiler
}.<<=(AssetCompilers.default)
)
.dependsOn(`server-api-routes`)
.enablePlugins(SJSAssetBundlerPlugin)
Expand Down
2 changes: 2 additions & 0 deletions core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
include "sc-akka-serialization.conf"

shadowcloud {
base-dir = ${user.home}/.shadowcloud

default-storage {
immutable = false // Prohibits delete of data
// chunk-key = hash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import com.karasiq.shadowcloud.streams.chunk.ChunkProcessingStreams
import com.karasiq.shadowcloud.streams.file.FileStreams
import com.karasiq.shadowcloud.streams.metadata.MetadataStreams
import com.karasiq.shadowcloud.streams.region.RegionStreams
import com.karasiq.shadowcloud.ui.UIProvider
import com.karasiq.shadowcloud.ui.passwords.PasswordProvider
import com.karasiq.shadowcloud.ui.{ChallengeHub, UIProvider}
import com.karasiq.shadowcloud.utils.{ProviderInstantiator, SCProviderInstantiator}
import com.typesafe.config.Config

Expand Down Expand Up @@ -180,6 +180,8 @@ class ShadowCloudExtension(_actorSystem: ExtendedActorSystem) extends Extension
// -----------------------------------------------------------------------
// User interface
// -----------------------------------------------------------------------
object challenges extends ChallengeHub

object ui extends UIProvider with PasswordProvider {
private[this] lazy val passProvider: PasswordProvider = provInstantiator.getInstance(config.ui.passwordProvider)
private[this] lazy val uiProvider: UIProvider = provInstantiator.getInstance(config.ui.uiProvider)
Expand Down
55 changes: 55 additions & 0 deletions core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.karasiq.shadowcloud.ui

import java.time.Instant
import java.util.UUID

import akka.actor.ActorSystem
import akka.event.Logging
import akka.util.ByteString
import com.karasiq.shadowcloud.ui.Challenge.AnswerFormat

import scala.collection.concurrent.TrieMap
import scala.concurrent.duration._
import scala.concurrent.{Future, Promise, TimeoutException}

class ChallengeHub(implicit as: ActorSystem) {
import as.dispatcher
private[this] val log = Logging(as, classOf[ChallengeHub])
private[this] val challenges = TrieMap.empty[UUID, (Challenge, Promise[ByteString], Deadline)]
private[this] val schedule = as.scheduler.scheduleAtFixedRate(1 second, 1 second) { ()
challenges.values.collect {
case (challenge, promise, deadline) if deadline.isOverdue()
log.warning("Challenge timed out: {}", challenge.title)
promise.tryFailure(new TimeoutException(s"Challenge timed out: ${challenge.title}"))
}
}

def list(): Seq[Challenge] =
challenges.values.map(_._1).toVector.sortBy(_.time.toEpochMilli)

def create(
title: String,
html: String = "",
answerFormat: AnswerFormat = AnswerFormat.String,
deadline: FiniteDuration = 5 minutes
): Future[ByteString] = {
val challenge = Challenge(UUID.randomUUID(), Instant.now(), title, html, answerFormat)
val promise = Promise[ByteString]
log.info("Challenge created: {}", title)
challenges(challenge.id) = (challenge, promise, Deadline.now + deadline)
promise.future.onComplete(_ challenges -= challenge.id)
promise.future
}

def solve(id: UUID, response: ByteString = ByteString.empty): Unit = challenges.remove(id).foreach {
case (challenge, promise, _)
log.info("Challenge solved: {}", challenge.title)
promise.trySuccess(response)
}

override def finalize(): Unit = {
schedule.cancel()
challenges.values.foreach { case (_, p, _) p.tryFailure(new RuntimeException("Challenge hub closed")) }
super.finalize()
}
}
19 changes: 19 additions & 0 deletions model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.karasiq.shadowcloud.ui

import java.time.Instant
import java.util.UUID

import com.karasiq.shadowcloud.model.SCEntity
import com.karasiq.shadowcloud.ui.Challenge.AnswerFormat

@SerialVersionUID(0L)
final case class Challenge(id: UUID, time: Instant, title: String, html: String, answerFormat: AnswerFormat) extends SCEntity

object Challenge {
sealed trait AnswerFormat
object AnswerFormat {
case object Ack extends AnswerFormat
case object String extends AnswerFormat
case object Binary extends AnswerFormat
}
}
2 changes: 1 addition & 1 deletion persistence/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
shadowcloud.persistence.h2 {
path = ${user.home}/.shadowcloud/shadowcloud
path = ${shadowcloud.base-dir}/shadowcloud
cipher = AES
compress = true
init-script = "classpath:sc-persistence-h2-init.sql"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
package com.karasiq.shadowcloud.serialization.boopickle

import java.time.Instant

import akka.Done
import akka.util.ByteString
import boopickle._
import scalapb.{GeneratedEnum, GeneratedEnumCompanion, GeneratedMessage, GeneratedMessageCompanion}

import com.karasiq.shadowcloud.config.SerializedProps
import com.karasiq.shadowcloud.index.{ChunkIndex, FolderIndex, IndexData}
import com.karasiq.shadowcloud.index.diffs.{ChunkIndexDiff, FolderDiff, FolderIndexDiff, IndexDiff}
import com.karasiq.shadowcloud.index.{ChunkIndex, FolderIndex, IndexData}
import com.karasiq.shadowcloud.model._
import com.karasiq.shadowcloud.model.crypto._
import com.karasiq.shadowcloud.model.keys.{KeyChain, KeyProps, KeySet}
import com.karasiq.shadowcloud.model.utils._
import com.karasiq.shadowcloud.model.utils.GCReport.{RegionGCState, StorageGCState}
import com.karasiq.shadowcloud.model.utils.RegionStateReport.{RegionStatus, StorageStatus}
import com.karasiq.shadowcloud.model.utils._
import com.karasiq.shadowcloud.ui.Challenge
import scalapb.{GeneratedEnum, GeneratedEnumCompanion, GeneratedMessage, GeneratedMessageCompanion}

//noinspection TypeAnnotation
trait SCBooPickleEncoders extends Base with BasicImplicitPicklers with TransformPicklers with TuplePicklers {
Expand Down Expand Up @@ -45,48 +47,60 @@ trait SCBooPickleEncoders extends Base with BasicImplicitPicklers with Transform
}
}

implicit val akkaDoneFormat: Pickler[Done] = ConstPickler(Done)
implicit val akkaDoneFormat1 = ConstPickler(Done)
implicit val pathFormat = generatePickler[Path]
implicit val serializedPropsFormat = generatePickler[SerializedProps]
implicit val encryptionMethodFormat = generatePickler[EncryptionMethod]
implicit val hashingMethodFormat = generatePickler[HashingMethod]
implicit val symmetricEncryptionParametersFormat = generatePickler[SymmetricEncryptionParameters]
implicit object instantFormat extends Pickler[Instant] {
override def pickle(obj: Instant)(implicit state: PickleState): Unit = {
state.enc.writeLong(obj.toEpochMilli)
}

override def unpickle(implicit state: UnpickleState): Instant = {
Instant.ofEpochMilli(state.dec.readLong)
}
}

implicit val akkaDoneFormat: Pickler[Done] = ConstPickler(Done)
implicit val akkaDoneFormat1 = ConstPickler(Done)
implicit val pathFormat = generatePickler[Path]
implicit val serializedPropsFormat = generatePickler[SerializedProps]
implicit val encryptionMethodFormat = generatePickler[EncryptionMethod]
implicit val hashingMethodFormat = generatePickler[HashingMethod]
implicit val symmetricEncryptionParametersFormat = generatePickler[SymmetricEncryptionParameters]
implicit val asymmetricEncryptionParametersFormat = generatePickler[AsymmetricEncryptionParameters]
implicit val encryptionParametersFormat = generatePickler[EncryptionParameters]
implicit val encryptionParametersFormat = generatePickler[EncryptionParameters]

implicit val signMethodFormat = generatePickler[SignMethod]
implicit val signMethodFormat = generatePickler[SignMethod]
implicit val signParametersFormat = generatePickler[SignParameters]
implicit val timestampFormat = generatePickler[Timestamp]
implicit val dataFormat = generatePickler[Data]
implicit val checksumFormat = generatePickler[Checksum]
implicit val chunkFormat = generatePickler[Chunk]
implicit val fileFormat = generatePickler[File]
implicit val folderFormat = generatePickler[Folder]

implicit val chunkIndexFormat = generatePickler[ChunkIndex]
implicit val folderIndexFormat = generatePickler[FolderIndex]
implicit val folderDiffFormat = generatePickler[FolderDiff]
implicit val folderIndexDiffFormat = generatePickler[FolderIndexDiff]
implicit val chunkIndexDiffFormat = generatePickler[ChunkIndexDiff]
implicit val indexDiffFormat = generatePickler[IndexDiff]
implicit val indexDataFormat = generatePickler[IndexData]
implicit val fileAvailabilityFormat = generatePickler[FileAvailability]
implicit val storageGCStateFormat = generatePickler[StorageGCState]
implicit val regionGCStateFormat = generatePickler[RegionGCState]
implicit val gCReportFormat = generatePickler[GCReport]
implicit val syncReportFormat = generatePickler[SyncReport]
implicit val storageStatusFormat = generatePickler[StorageStatus]
implicit val regionStatusFormat = generatePickler[RegionStatus]
implicit val regionStateReportFormat = generatePickler[RegionStateReport]
implicit val storageHealthFormat = generatePickler[StorageHealth]
implicit val regionHealthFormat = generatePickler[RegionHealth]
implicit val indexScopeFormat = generatePickler[IndexScope]
implicit val keySetFormat = generatePickler[KeySet]
implicit val keyPropsFormat = generatePickler[KeyProps]
implicit val keyChainFormat = generatePickler[KeyChain]

implicit def generatedMessagePickler[T <: GeneratedMessage with scalapb.Message[T] : GeneratedMessageCompanion]: Pickler[T] = new Pickler[T] {
implicit val timestampFormat = generatePickler[Timestamp]
implicit val dataFormat = generatePickler[Data]
implicit val checksumFormat = generatePickler[Checksum]
implicit val chunkFormat = generatePickler[Chunk]
implicit val fileFormat = generatePickler[File]
implicit val folderFormat = generatePickler[Folder]

implicit val chunkIndexFormat = generatePickler[ChunkIndex]
implicit val folderIndexFormat = generatePickler[FolderIndex]
implicit val folderDiffFormat = generatePickler[FolderDiff]
implicit val folderIndexDiffFormat = generatePickler[FolderIndexDiff]
implicit val chunkIndexDiffFormat = generatePickler[ChunkIndexDiff]
implicit val indexDiffFormat = generatePickler[IndexDiff]
implicit val indexDataFormat = generatePickler[IndexData]
implicit val fileAvailabilityFormat = generatePickler[FileAvailability]
implicit val storageGCStateFormat = generatePickler[StorageGCState]
implicit val regionGCStateFormat = generatePickler[RegionGCState]
implicit val gCReportFormat = generatePickler[GCReport]
implicit val syncReportFormat = generatePickler[SyncReport]
implicit val storageStatusFormat = generatePickler[StorageStatus]
implicit val regionStatusFormat = generatePickler[RegionStatus]
implicit val regionStateReportFormat = generatePickler[RegionStateReport]
implicit val storageHealthFormat = generatePickler[StorageHealth]
implicit val regionHealthFormat = generatePickler[RegionHealth]
implicit val indexScopeFormat = generatePickler[IndexScope]
implicit val keySetFormat = generatePickler[KeySet]
implicit val keyPropsFormat = generatePickler[KeyProps]
implicit val keyChainFormat = generatePickler[KeyChain]
implicit val challengeAnswerFormatFormat = generatePickler[Challenge.AnswerFormat]
implicit val challengeFormat = generatePickler[Challenge]

implicit def generatedMessagePickler[T <: GeneratedMessage with scalapb.Message[T]: GeneratedMessageCompanion]: Pickler[T] = new Pickler[T] {
def pickle(obj: T)(implicit state: PickleState): Unit = {
state.enc.writeByteArray(obj.toByteArray)
}
Expand All @@ -96,7 +110,7 @@ trait SCBooPickleEncoders extends Base with BasicImplicitPicklers with Transform
}
}

implicit def generatedEnumPickler[T <: GeneratedEnum : GeneratedEnumCompanion]: Pickler[T] = new Pickler[T] {
implicit def generatedEnumPickler[T <: GeneratedEnum: GeneratedEnumCompanion]: Pickler[T] = new Pickler[T] {
def pickle(obj: T)(implicit state: PickleState): Unit = {
state.enc.writeInt(obj.value)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package com.karasiq.shadowcloud.server.http.api

import java.util.UUID

import akka.Done
import akka.stream.scaladsl.Sink
import akka.util.ByteString
import com.karasiq.shadowcloud.ShadowCloudExtension
import com.karasiq.shadowcloud.actors.RegionGC.GCStrategy
import com.karasiq.shadowcloud.actors.internal.RegionTracker
Expand All @@ -18,6 +21,7 @@ import com.karasiq.shadowcloud.model.utils.{IndexScope, RegionStateReport}
import com.karasiq.shadowcloud.storage.props.StorageProps
import com.karasiq.shadowcloud.storage.replication.ChunkWriteAffinity
import com.karasiq.shadowcloud.streams.region.RegionRepairStream
import com.karasiq.shadowcloud.ui.Challenge

import scala.concurrent.Future
import scala.language.implicitConversions
Expand Down Expand Up @@ -281,6 +285,15 @@ private[server] final class ShadowCloudApiImpl(sc: ShadowCloudExtension) extends
} yield Done
}

override def getChallenges(): Future[Seq[Challenge]] = Future.successful {
sc.challenges.list()
}

override def solveChallenge(id: UUID, answer: ByteString): Future[Done] = Future.successful {
sc.challenges.solve(id, answer)
Done
}

// -----------------------------------------------------------------------
// Utils
// -----------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package com.karasiq.shadowcloud.api

import java.util.UUID

import akka.Done
import akka.util.ByteString
import com.karasiq.shadowcloud.config.SerializedProps
import com.karasiq.shadowcloud.metadata.Metadata
import com.karasiq.shadowcloud.model._
import com.karasiq.shadowcloud.model.keys.{KeyChain, KeyId, KeySet}
import com.karasiq.shadowcloud.model.utils._
import com.karasiq.shadowcloud.ui.Challenge

import scala.concurrent.Future

Expand Down Expand Up @@ -68,4 +72,10 @@ trait ShadowCloudApi {
def deleteFiles(regionId: RegionId, path: Path): Future[Set[File]]
def deleteFile(regionId: RegionId, file: File): Future[File]
def repairFile(regionId: RegionId, file: File, storages: Seq[StorageId], scope: IndexScope = IndexScope.default): Future[Done]

// -----------------------------------------------------------------------
// Сhallenges
// -----------------------------------------------------------------------
def getChallenges(): Future[Seq[Challenge]]
def solveChallenge(id: UUID, answer: ByteString): Future[Done]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
package com.karasiq.shadowcloud.webapp.api

import java.util.UUID

import akka.Done
import akka.util.ByteString
import autowire._
import com.karasiq.shadowcloud.api.js.SCAjaxBooPickleApiClient
import com.karasiq.shadowcloud.api.{SCApiMeta, ShadowCloudApi}
Expand All @@ -9,6 +13,7 @@ import com.karasiq.shadowcloud.metadata.Metadata.Tag
import com.karasiq.shadowcloud.model._
import com.karasiq.shadowcloud.model.keys.{KeyId, KeySet}
import com.karasiq.shadowcloud.model.utils.IndexScope
import com.karasiq.shadowcloud.ui.Challenge
import com.karasiq.shadowcloud.webapp.context.AppContext

import scala.concurrent.{ExecutionContext, Future}
Expand All @@ -25,10 +30,10 @@ object AjaxApi extends ShadowCloudApi with FileApi with SCApiMeta {
val payloadContentType = clientFactory.payloadContentType

import encoding.implicits._ // Should not be deleted

private[this] val apiClient = clientFactory[ShadowCloudApi]
private[this] implicit val implicitExecutionContext: ExecutionContext = AppContext.JsExecutionContext

private[this] val apiClient = clientFactory[ShadowCloudApi]

// -----------------------------------------------------------------------
// Regions
// -----------------------------------------------------------------------
Expand Down Expand Up @@ -212,4 +217,12 @@ object AjaxApi extends ShadowCloudApi with FileApi with SCApiMeta {
def repairFile(regionId: RegionId, file: File, storages: Seq[StorageId], scope: IndexScope) = {
apiClient.repairFile(regionId, file, storages, scope).call()
}

override def getChallenges(): Future[Seq[Challenge]] = {
apiClient.getChallenges().call()
}

override def solveChallenge(id: UUID, answer: ByteString): Future[Done] = {
apiClient.solveChallenge(id, answer).call()
}
}
Loading

0 comments on commit ea8cf8a

Please sign in to comment.