diff --git a/build.sbt b/build.sbt index 7f281628..3e67b453 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,4 @@ +import com.karasiq.scalajsbundler.compilers.{AssetCompilers, ConcatCompiler} import com.typesafe.sbt.packager.docker.Cmd import sbtcrossproject.CrossPlugin.autoImport.{CrossType, crossProject} @@ -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) diff --git a/core/src/main/resources/reference.conf b/core/src/main/resources/reference.conf index f6bd2b47..019e902d 100644 --- a/core/src/main/resources/reference.conf +++ b/core/src/main/resources/reference.conf @@ -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 diff --git a/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala b/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala index 6856199b..09b8cb51 100644 --- a/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala +++ b/core/src/main/scala/com/karasiq/shadowcloud/ShadowCloud.scala @@ -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 @@ -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) diff --git a/core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala b/core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala new file mode 100644 index 00000000..bfbc9a6c --- /dev/null +++ b/core/src/main/scala/com/karasiq/shadowcloud/ui/ChallengeHub.scala @@ -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() + } +} diff --git a/model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala b/model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala new file mode 100644 index 00000000..cf020b89 --- /dev/null +++ b/model/src/main/scala/com/karasiq/shadowcloud/ui/Challenge.scala @@ -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 + } +} diff --git a/persistence/src/main/resources/reference.conf b/persistence/src/main/resources/reference.conf index 208a68e5..0141bcc8 100644 --- a/persistence/src/main/resources/reference.conf +++ b/persistence/src/main/resources/reference.conf @@ -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" diff --git a/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala b/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala index 7c665bfd..02940a39 100644 --- a/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala +++ b/serialization/src/main/scala/com/karasiq/shadowcloud/serialization/boopickle/SCBooPickleEncoders.scala @@ -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 { @@ -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) } @@ -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) } diff --git a/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala b/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala index 48227847..8fea2a2b 100644 --- a/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala +++ b/server/api-routes/src/main/scala/com/karasiq/shadowcloud/server/http/api/ShadowCloudApiImpl.scala @@ -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 @@ -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 @@ -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 // ----------------------------------------------------------------------- diff --git a/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala b/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala index 50aea376..a5a07332 100644 --- a/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala +++ b/server/autowire-api/src/main/scala/com/karasiq/shadowcloud/api/ShadowCloudApi.scala @@ -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 @@ -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] } diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala index 919a0749..0575759e 100644 --- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala +++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/api/AjaxApi.scala @@ -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} @@ -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} @@ -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 // ----------------------------------------------------------------------- @@ -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() + } } diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/ChallengePanel.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/ChallengePanel.scala new file mode 100644 index 00000000..d7dc36c5 --- /dev/null +++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/ChallengePanel.scala @@ -0,0 +1,76 @@ +package com.karasiq.shadowcloud.webapp.components + +import java.util.UUID + +import akka.util.ByteString +import com.karasiq.bootstrap.Bootstrap.default._ +import com.karasiq.shadowcloud.ui.Challenge +import com.karasiq.shadowcloud.webapp.api.AjaxApi +import com.karasiq.shadowcloud.webapp.components.common.Toastr +import com.karasiq.shadowcloud.webapp.context.AppContext +import com.karasiq.shadowcloud.webapp.utils.{Blobs, RxWithUpdate} +import org.scalajs.dom +import rx.{Rx, Var} +import scalaTags.all._ + +import scala.collection.mutable +import scala.concurrent.Future +import scala.scalajs.concurrent.JSExecutionContext.Implicits.queue + +object ChallengePanel { + def apply()(implicit context: AppContext): ChallengePanel = new ChallengePanel +} + +class ChallengePanel(implicit context: AppContext) extends BootstrapHtmlComponent { + private[this] val list = RxWithUpdate(Seq.empty[Challenge])(_ ⇒ AjaxApi.getChallenges()) + dom.window.setInterval(() ⇒ list.update(), 10000) + + override def renderTag(md: ModifierT*): TagT = { + val map = mutable.HashMap.empty[UUID, TableRow] + + val source = list.toRx + source.triggerLater { + map.keys + .filter(id ⇒ !source.now.exists(_.id == id)) + .foreach(map -= _) + } + + def renderChallenge(challenge: Challenge): TableRow = + map.getOrElseUpdate( + challenge.id, { + Toastr.info(challenge.title, "Challenge") + val string = Var("") + val files = Var(Seq.empty[dom.File]) + + TableRow( + Seq( + challenge.title, + raw(challenge.html), + FormInput.text("", string.reactiveInput), + FormInput.file("", files.reactiveInputRead), + Button(block = true)( + context.locale.submit, + onclick := Callback.onClick { _ ⇒ + val data = files.now.headOption match { + case Some(value) ⇒ + Blobs.toBytes(value) + case None ⇒ + Future.successful(ByteString(string.now)) + } + + data + .flatMap(AjaxApi.solveChallenge(challenge.id, _)) + .foreach(_ ⇒ list.update()) + } + ) + ) + ) + } + ) + + Table( + Rx(Seq(context.locale.name, context.locale.content, context.locale.pasteText, context.locale.file, context.locale.submit)), + source.map(_.map(renderChallenge)) + ) + } +} diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala index bf60f2cf..18d550c7 100644 --- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala +++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/SCFrontend.scala @@ -1,20 +1,18 @@ package com.karasiq.shadowcloud.webapp.components -import scala.util.control.NonFatal - -import org.scalajs.dom - import com.karasiq.bootstrap.Bootstrap.default._ -import scalaTags.all._ - import com.karasiq.shadowcloud.webapp.components.common.AppIcons import com.karasiq.shadowcloud.webapp.components.folder.FoldersPanel import com.karasiq.shadowcloud.webapp.components.keys.KeysContext -import com.karasiq.shadowcloud.webapp.components.region.{RegionContext, RegionsStoragesPanel, RegionSwitcher} +import com.karasiq.shadowcloud.webapp.components.region.{RegionContext, RegionSwitcher, RegionsStoragesPanel} import com.karasiq.shadowcloud.webapp.components.themes.ThemeSelector import com.karasiq.shadowcloud.webapp.context.{AppContext, FolderContext} import com.karasiq.shadowcloud.webapp.controllers.{FileController, FolderController} import com.karasiq.shadowcloud.webapp.utils.RxLocation +import org.scalajs.dom +import scalaTags.all._ + +import scala.util.control.NonFatal object SCFrontend { def apply()(implicit appContext: AppContext): SCFrontend = { @@ -67,7 +65,8 @@ class SCFrontend()(implicit val context: AppContext) { .withTabs( NavigationTab(context.locale.foldersView, "folders", AppIcons.foldersView, renderFoldersPanel()), NavigationTab(context.locale.regionsView, "regions", AppIcons.regionsView, renderRegionsPanel()), - NavigationTab(context.locale.logs, "logs", AppIcons.logs, LogPanel()) + NavigationTab(context.locale.logs, "logs", AppIcons.logs, LogPanel()), + NavigationTab("Challenges", "challengees", AppIcons.rename, ChallengePanel()) ) } diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala index a83ad570..c4e69f57 100644 --- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala +++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/keys/KeysView.scala @@ -81,27 +81,12 @@ class KeysView()(implicit context: AppContext, kc: KeysContext, rc: RegionContex .show() } - def showImportDialog(): Unit = { - AppComponents.importDialog(context.locale.importKey) { result => - val key = ExportUtils.decodeKey(result) - context.api.addKey(key).foreach(_ ⇒ kc.updateAll()) - }.show() - } - val generateButton = Button(ButtonStyle.success, block = true)( context.locale.generateKey, onclick := Callback.onClick(_ ⇒ showGenerateDialog()) ) - val importButton = Button(ButtonStyle.primary, block = true)( - context.locale.importKey, - onclick := Callback.onClick(_ ⇒ showImportDialog()) - ) - - GridSystem.row( - GridSystem.col.md(6)(generateButton), - GridSystem.col.md(6)(importButton) - ) + generateButton } private[this] def showExportDialog(key: KeySet): Unit = { diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala index a9d5b9c6..88145faf 100644 --- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala +++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/components/region/ExportImportModal.scala @@ -49,11 +49,11 @@ object ExportImportModal { val filteredSnapshot = snapshot.copy( snapshot.regions.collect { case (regionId, status) if regions(regionId) ⇒ - regionId → status.copy(storages = status.storages.filter(storages)) + regionId → status.copy(storages = status.storages) }, snapshot.storages.collect { case (storageId, status) if storages(storageId) ⇒ - storageId → status.copy(regions = status.regions.filter(regions)) + storageId → status.copy(regions = status.regions) } ) diff --git a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala index a3cba926..26413a33 100644 --- a/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala +++ b/server/webapp/src/main/scala/com/karasiq/shadowcloud/webapp/utils/Blobs.scala @@ -1,13 +1,14 @@ package com.karasiq.shadowcloud.webapp.utils -import scala.concurrent.{ExecutionContext, Future, Promise} -import scala.scalajs.js -import scala.scalajs.js.typedarray.Uint8Array -import scalatags.JsDom.all._ - +import akka.util.ByteString import org.scalajs.dom -import org.scalajs.dom.{Blob, Event} import org.scalajs.dom.raw._ +import org.scalajs.dom.{Blob, Event} +import scalatags.JsDom.all._ + +import scala.concurrent.{ExecutionContext, Future, Promise} +import scala.scalajs.js +import scala.scalajs.js.typedarray.{ArrayBuffer, TypedArrayBuffer, Uint8Array} /** * Blob/file utility @@ -79,4 +80,20 @@ object Blobs { promise.future } + + def toBytes(blob: Blob): Future[ByteString] = { + val promise = Promise[ByteString] + val reader = new FileReader + reader.readAsArrayBuffer(blob) + reader.onloadend = (_: ProgressEvent) ⇒ { + val buffer = TypedArrayBuffer.wrap(reader.result.asInstanceOf[ArrayBuffer]) + promise.success(ByteString.fromByteBuffer(buffer)) + } + + reader.onerror = (errorEvent: Event) ⇒ { + promise.failure(new IllegalArgumentException(errorEvent.toString)) + } + + promise.future + } } diff --git a/storage/telegram/src/main/resources/reference.conf b/storage/telegram/src/main/resources/reference.conf index b8cc4f49..7ab946a8 100644 --- a/storage/telegram/src/main/resources/reference.conf +++ b/storage/telegram/src/main/resources/reference.conf @@ -1,6 +1,7 @@ shadowcloud.storage { providers.telegram = com.karasiq.shadowcloud.storage.telegram.TelegramStorageProvider telegram { + temp-dir = ${shadowcloud.base-dir}/tgcloud secrets { api-hash = "b18441a1ff607e10a989891a5462e627" api-id = 2040 diff --git a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala index 4440657a..8586b99a 100644 --- a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala +++ b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramScripts.scala @@ -7,25 +7,31 @@ import java.nio.file.attribute.BasicFileAttributes import akka.util.ByteString import com.karasiq.shadowcloud.model.StorageId import com.karasiq.shadowcloud.storage.telegram.TelegramStorageConfig.Secrets -import com.karasiq.shadowcloud.ui.UIProvider +import com.karasiq.shadowcloud.ui.Challenge.AnswerFormat +import com.karasiq.shadowcloud.ui.ChallengeHub +import scala.concurrent.Await +import scala.concurrent.duration.Duration import scala.util.Try object TelegramScripts { - def createSession(storageId: StorageId, secrets: Secrets, uiProvider: UIProvider): ByteString = { - require(uiProvider.canBlock, "Please create session manually and paste it as base64 in storage config session key") - val baseDir = Paths.get(sys.props("user.home"), s"tgcloud-temp-$storageId") + def createSession(storageId: StorageId, secrets: Secrets, tempDir: String, uiProvider: ChallengeHub): ByteString = { + val baseDir = Paths.get(tempDir, storageId) deleteDir(baseDir) extract(baseDir) writeSecrets(baseDir, secrets) - uiProvider.showNotification( - s"""Please execute the following action depending on your OS, then press OK - |Windows: Execute create_session.bat in $baseDir - |Linux/MacOS: Run in terminal: $baseDir/create_session - |""".stripMargin + val future = uiProvider.create( + s"Telegram login ($storageId)", + s"""Please execute the following action depending on your OS
+ |Windows: Execute create_session.bat in $baseDir
+ |Linux/MacOS: Run in terminal: bash $baseDir/create_session
+ |Then upload created ${secrets.entity}.session file + |""".stripMargin, + AnswerFormat.Binary ) - val result = readSession(baseDir, secrets.entity) + + val result = Await.result(future, Duration.Inf) deleteDir(baseDir) result } @@ -54,7 +60,9 @@ object TelegramScripts { val files = Seq( "download_service.py", "requirements.txt", - "telegram_create_session.py" + "telegram_create_session.py", + "create_session", + "create_session.bat" ) Files.createDirectories(directory) files.foreach { f ⇒ diff --git a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala index a24b2c15..788a6aba 100644 --- a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala +++ b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStorageConfig.scala @@ -4,7 +4,7 @@ import com.karasiq.common.configs.ConfigImplicits._ import com.karasiq.shadowcloud.storage.telegram.TelegramStorageConfig.Secrets import com.typesafe.config.Config -case class TelegramStorageConfig(pythonPath: Option[String], entity: String, port: Option[Int], secrets: Secrets) +case class TelegramStorageConfig(pythonPath: Option[String], entity: String, port: Option[Int], secrets: Secrets, tempDir: String) object TelegramStorageConfig { case class Secrets(apiId: Int, apiHash: String, entity: String) @@ -23,7 +23,8 @@ object TelegramStorageConfig { config.optional(_.getString("python-path")), config.withDefault("tgcloud", _.getString("entity")), config.optional(_.getInt("port")), - Secrets(config.getConfig("secrets")) + Secrets(config.getConfig("secrets")), + config.getString("temp-dir") ) } } diff --git a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala index e70c17f6..e58f8c1d 100644 --- a/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala +++ b/storage/telegram/src/main/scala/com/karasiq/shadowcloud/storage/telegram/TelegramStoragePlugin.scala @@ -16,8 +16,8 @@ import com.karasiq.shadowcloud.storage.props.StorageProps import com.karasiq.shadowcloud.storage.utils.StoragePluginBuilder import scala.collection.JavaConverters._ +import scala.concurrent.Await import scala.concurrent.duration._ -import scala.concurrent.{Await, Future} import scala.sys.ShutdownHookThread import scala.util.control.NonFatal import scala.util.{Random, Try} @@ -48,10 +48,7 @@ class TelegramStoragePlugin extends StoragePlugin { val session = Try(sc.sessions.getRawBlocking(storageId, "tgcloud-session")).toOption .orElse(props.rootConfig.optional(_.getString("session")).map(Base64.decode)) .filter(_.nonEmpty) - .getOrElse { - val future = Future(TelegramScripts.createSession(storageId, config.secrets, sc.ui))(sc.ui.executionContext) - Await.result(future, 5 minutes) - } + .getOrElse(TelegramScripts.createSession(storageId, config.secrets, config.tempDir, sc.challenges)) require(session.nonEmpty, "Session is empty") sc.sessions.setRawBlocking(storageId, "tgcloud-session", session)