Compare commits

...

98 Commits

Author SHA1 Message Date
Keagan McClelland
894fa21002 Agent/sync/tor upgrade (#234)
* adds tor upgrade sync

* use the right version

* I hate all of you
2021-03-06 19:11:05 -07:00
Matt Hill
d611c69b0c add messaging for missing LAN 2021-03-06 11:29:20 -07:00
Keagan McClelland
d430986403 adds availability to installed full 2021-03-05 17:24:36 -07:00
Aiden McClelland
c8aafbdbc9 appmgr: fix lan service deserialization 2021-03-05 16:33:53 -07:00
Matt Hill
2e3e1401f5 Update ui/src/app/services/api/mock-app-fixures.ts
Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-05 15:42:31 -07:00
Matt Hill
deb0b1e561 rework LAN display and service launchability 2021-03-05 15:42:31 -07:00
Aiden McClelland
daf701a76c appmgr: disable zeroconf if no lan 2021-03-05 15:39:43 -07:00
Keagan McClelland
43035e7271 adds 80 2021-03-05 14:22:12 -07:00
Keagan McClelland
e37db33d62 fixes custom parse 2021-03-05 14:22:12 -07:00
Keagan McClelland
adab9e7fca optional lan 2021-03-05 14:22:12 -07:00
Keagan McClelland
a21bd91460 actually install libavahi 2021-03-04 14:40:35 -07:00
Aiden McClelland
acc2722586 appmgr: add proxy headers for lan services 2021-03-04 12:18:54 -07:00
Keagan McClelland
d8d6541b11 fix mocks 2021-03-03 16:03:04 -07:00
Keagan McClelland
1a7d40afa9 render correctly 2021-03-03 16:03:04 -07:00
Keagan McClelland
b152a93dd8 should fix the logs api 2021-03-03 16:03:04 -07:00
Keagan McClelland
ae90b70348 exposes start alerts 2021-03-03 16:03:04 -07:00
Aiden McClelland
21f982e9a6 appmgr: start-alert 2021-03-03 16:03:04 -07:00
Aiden McClelland
4100d4ca97 ui: fallback to json 2021-03-03 11:13:31 -07:00
Aiden McClelland
f5ae93c999 ui: better error message 2021-03-03 11:13:31 -07:00
Aiden McClelland
2f5ad4d82b stringify error 2021-03-03 11:13:31 -07:00
Keagan McClelland
6e2a332bcd adds sync for libavahi-client3 2021-03-03 10:29:09 -07:00
Aiden McClelland
4a2e496e8a ui: better error handing and messaging 2021-03-03 10:29:09 -07:00
Matt Hill
e69a936fb8 show newlines in action res and longer timeout 2021-03-03 10:29:09 -07:00
Aiden McClelland
d9894d4082 appmgr: pin yajrc to c2952a4a 2021-03-03 10:29:09 -07:00
Aiden McClelland
6b3fa54551 appmgr: fix deleted certs 2021-03-03 10:29:09 -07:00
Aiden McClelland
9f47a34b11 appmgr: logs for adding certs 2021-03-03 10:29:09 -07:00
Matt Hill
531dec936d markdown support for prebaked wizard component notes 2021-03-03 10:29:09 -07:00
Aiden McClelland
1d7684f4d4 appmgr: fsync fullchain 2021-03-03 10:29:09 -07:00
Aiden McClelland
cfacbcabd3 appmgr: make down for 0.2.9 more resilient 2021-03-03 10:29:09 -07:00
Keagan McClelland
4fcdf5f832 fixes issue with action 2021-03-03 10:29:09 -07:00
Keagan McClelland
2189c5643d fixes warning rendering 2021-03-03 10:29:09 -07:00
Aiden McClelland
aada5755de appmgr: sync lan services after tor reload 2021-03-03 10:29:09 -07:00
Keagan McClelland
60d31163c5 remove redundant imports 2021-03-03 10:29:09 -07:00
Aiden McClelland
fd6a1897c8 appmgr: fix portable 2021-03-03 10:29:09 -07:00
Aiden McClelland
62e0f742ba appmgr: create app metadata dir explicitly 2021-03-03 10:29:09 -07:00
Keagan McClelland
c42ff81a38 avoid unnecessary appmgr startup invocation 2021-03-03 10:29:09 -07:00
Keagan McClelland
cc49a73954 fix avahi-daemon edge case 2021-03-03 10:29:09 -07:00
Keagan McClelland
29a4506a40 fixes edge case where upgrade to 0.2.9 would not correctly set up lan 2021-03-03 10:29:09 -07:00
Keagan McClelland
efa60bf4ab fixes clap shit 2021-03-03 10:29:09 -07:00
Matt Hill
1c2fd192df dont block OS update for absent release notes 2021-03-03 10:29:09 -07:00
Matt Hill
0a9349bbc1 change to rocket 2021-03-03 10:29:09 -07:00
Keagan McClelland
653961da64 batches all lan addresses together
removes dbg

fixes clap docs

use actual log

removes service level enabling and disabling of lan

adds reset endpoint

reset lan on install/uninstall
2021-03-03 10:29:09 -07:00
Matt Hill
6585d91816 refresh LAN and more 2021-03-03 10:29:09 -07:00
Aiden McClelland
3e3097945f appmgr: actions override entrypoint 2021-03-03 10:29:09 -07:00
Aiden McClelland
c0f5f09767 appmgr: put linux syscall behind flag 2021-03-03 10:29:09 -07:00
Aiden McClelland
1c8889a60c appmgr: feature flag for avahi 2021-03-03 10:29:09 -07:00
Aiden McClelland
218bae3b46 appmgr: fix CA paths 2021-03-03 10:29:09 -07:00
Matt Hill
92c297648c remove lan stuff 2021-03-03 10:29:09 -07:00
Matt Hill
68eccdb63c fix launching UI from list page 2021-03-03 10:29:09 -07:00
Aiden McClelland
ee1c66d0c2 appmgr: bugfix: use fullchain cert 2021-03-03 10:29:09 -07:00
Keagan McClelland
c52f75c9e3 adds lanEnabled, and unconditionally returns lan address 2021-03-03 10:29:09 -07:00
Matt Hill
b46c75e391 optional properties 2021-03-03 10:29:09 -07:00
Keagan McClelland
7fb8f88c8d pins yajrc to git 2021-03-03 10:29:09 -07:00
Keagan McClelland
c83baec363 Appmgr/feature/lan (#209)
* WIP: Lan with blocking

* stuff

* appmgr: non-ui lan services

* dbus linker errors

* appmgr: allocate on stack

* dns resolves finally

* cleanup

* appmgr: generate ssl if missing

* appmgr: remove -p for purge

Co-authored-by: Aiden McClelland <me@drbonez.dev>
2021-03-03 10:29:09 -07:00
Aiden McClelland
882cfde5f3 appmgr: add warning to actions 2021-03-03 10:29:09 -07:00
Aiden McClelland
53720130b3 appmgr: add support for actions 2021-03-03 10:29:09 -07:00
Matt Hill
7c321bbf6b better conditional for go to service 2021-03-03 10:29:09 -07:00
kn0wmad
bd060670e4 Update lan.page.ts
Fixes broken links
2021-03-03 10:29:09 -07:00
adamgoth
7ff538a526 ui: remove redundant conditional 2021-03-03 10:29:09 -07:00
adamgoth
3c74f3d46e ui: add go to service button on marketplace listing 2021-03-03 10:29:09 -07:00
Keagan McClelland
54ae7f82d6 adds action listings to AIS response 2021-03-03 10:29:09 -07:00
Keagan McClelland
39867478d0 fixed conflicts 2021-03-03 10:29:09 -07:00
Keagan McClelland
8e2642a741 actually render json 2021-03-03 10:29:09 -07:00
Keagan McClelland
a4f7d53a6b finishes lan support for the agent 2021-03-03 10:29:09 -07:00
Aaron Greenspan
397236c68e ui: concatObValues + comments 2021-03-03 10:29:09 -07:00
Matt Hill
8ce43d808e start alerts 2021-03-03 10:29:09 -07:00
Aaron Greenspan
e1200c2991 ui: fix distinctUntilChanged() 2021-03-03 10:29:09 -07:00
Aaron Greenspan
0937c81e46 ui: mocks off 2021-03-03 10:29:09 -07:00
Aaron Greenspan
02ab63da81 UI/feature/actions (#195)
* ui: actions page

* rework actions page

* add warning to Actions

Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
2021-03-03 10:29:09 -07:00
Aaron Greenspan
5cf7d1ff88 UI/feature/enable disable lan (#192)
* ui: skip startup notifications in mocks

* ui: enable-disable lan toggle in ui

* ui: remove this.lanAddress for this.app.lanAddress
2021-03-03 10:29:09 -07:00
Aaron Greenspan
a20970fa17 ui: conditionally render lan if ui 2021-03-03 10:29:09 -07:00
Aaron Greenspan
30dd62285b Update agent/src/Handler/V0.hs
Co-authored-by: Keagan McClelland <keagan.mcclelland@gmail.com>
2021-03-03 10:29:09 -07:00
Aaron Greenspan
3065323e79 agent: adds lan address to server specs 2021-03-03 10:29:09 -07:00
Aaron Greenspan
e1a6a3d9ed ui: fix lan launchable 2021-03-03 10:29:09 -07:00
Aiden McClelland
c0e08df221 appmgr: ignore AlreadyExists error 2021-03-03 10:29:09 -07:00
Aiden McClelland
108213f920 trim hostname 2021-03-03 10:29:09 -07:00
Keagan McClelland
a8e229821f pin appmgr 0.2.9 2021-03-03 10:29:09 -07:00
Aaron Greenspan
a6b7d657a0 ui: 0.2.8 ~> 0.2.9 2021-03-03 10:29:09 -07:00
Keagan McClelland
77b8d0b2a0 updates agent 0.2.9 metadata 2021-03-03 10:29:09 -07:00
Aiden McClelland
9503f754ad fix stupid fucking comment that keagan won't let go 2021-03-03 10:29:09 -07:00
adamgoth
540868220d ui: use ionic positioning instead of custom CSS 2021-03-03 10:29:09 -07:00
adamgoth
dd8037fda1 ui: copy button for tor address spec 2021-03-03 10:29:09 -07:00
Aiden McClelland
6f09738b49 appmgr: write nginx conf when writing tor conf (#177)
* appmgr: write nginx conf when writing tor conf

* appmgr: fix hardcoded certs

* appmgr: add down for 0.2.9
2021-03-03 10:29:09 -07:00
Aaron Greenspan
808fff4187 ui: remove dead imports, fix mock type 2021-03-03 10:29:09 -07:00
Aaron Greenspan
a9735fd777 ui: adds agent logs page in server show 2021-03-03 10:29:09 -07:00
Aaron Greenspan
327c79350e ui: os-welcome 029 2021-03-03 10:29:09 -07:00
Keagan McClelland
44def3be85 release notes plumbing 2021-03-03 10:29:09 -07:00
Keagan McClelland
18df87b8f5 adds log fetch feature to agent 2021-03-03 10:29:09 -07:00
Matt Hill
97a85d6e01 0.2.9 welcome message 2021-03-03 10:29:09 -07:00
Aiden McClelland
3d4930acb4 don't delete lock file 2021-03-03 10:29:09 -07:00
Aaron Greenspan
58468dd53f ui: remove action button on wizard while loading 2021-03-03 10:29:09 -07:00
Matt Hill
50a2be243a implement base ui for LAN services 2021-03-03 10:29:09 -07:00
Aaron Greenspan
0d7b087665 ui: remove release notes modal 2021-03-03 10:29:09 -07:00
Aaron Greenspan
0e87cce8de ui: moves release notes to AAS 2021-03-03 10:29:09 -07:00
Aaron Greenspan
537f2d91b8 ui: rename dev-notes to notes 2021-03-03 10:29:09 -07:00
Aaron Greenspan
79604182c8 ui: add embassy os release notes 2021-03-03 10:29:09 -07:00
Aaron Greenspan
68faa17ab6 ui: fix emver check in startup alerts notifier 2021-03-03 10:29:09 -07:00
Lucy Cifferello
13a6d7f0c7 add local file for better image resolution 2021-02-26 16:09:09 -07:00
132 changed files with 2765 additions and 1514 deletions

View File

@@ -11,7 +11,7 @@
EmbassyOS is a mass-market, graphical operating system designed to facilitate the discovery, installation, configuration, private self-hosting, and reliable operation of open-source software services and applications. It aims to eliminate trust and custodianship from personal computing.
![EmbassyOS image](https://sesoodan.sirv.com/eos.png?w=600)
![EmbassyOS image](eos.png?w=600)
## ⚠️ Caution
Some technologies supported by this software, such as [Lightning](https://lightning.network/), are considered in active development and might experience issues. Do not commit any funds you are not willing to loose. Be #reckless at your own risk.

View File

@@ -13,6 +13,7 @@
/v0/specs SpecsR GET
/v0/metrics MetricsR GET
/v0/logs LogsR GET
/v0/sshKeys SshKeysR GET POST
/v0/sshKeys/#Text SshKeyByFingerprintR DELETE
/v0/password PasswordR PATCH
@@ -38,6 +39,9 @@
/v0/apps/#AppId/backup/stop StopBackupR POST
/v0/apps/#AppId/backup/restore RestoreBackupR POST
/v0/apps/#AppId/autoconfig/#AppId AutoconfigureR POST
/v0/apps/#AppId/actions ActionR POST
/v0/network/lan/reset ResetLanR POST
/v0/disks DisksR GET
/v0/disks/eject EjectR POST

View File

@@ -33,6 +33,6 @@ database:
database: "start9_agent.sqlite3"
poolsize: "_env:YESOD_SQLITE_POOLSIZE:10"
app-mgr-version-spec: "=0.2.8"
app-mgr-version-spec: "=0.2.9"
#analytics: UA-YOURCODE

View File

@@ -0,0 +1 @@
SELECT TRUE;

View File

@@ -1,5 +1,5 @@
name: ambassador-agent
version: 0.2.8
version: 0.2.9
default-extensions:
- NoImplicitPrelude
@@ -65,6 +65,7 @@ dependencies:
- http-types
- interpolate
- iso8601-time
- json-rpc
- lens
- lens-aeson
- lifted-async

View File

@@ -54,21 +54,21 @@ import Yesod.Persist.Core
import Constants
import qualified Daemon.AppNotifications as AppNotifications
import Daemon.RefreshProcDev
import qualified Daemon.SslRenew as SSLRenew
import Daemon.TorHealth
import Daemon.ZeroConf
import Foundation
import Lib.Algebra.State.RegistryUrl
import Lib.Background
import Lib.Database
import Lib.External.Metrics.ProcDev
import Lib.SelfUpdate
import Lib.Sound
import Lib.SystemPaths
import Lib.Tor ( newTorManager )
import Lib.WebServer
import Model
import Settings
import Lib.Background
import qualified Daemon.SslRenew as SSLRenew
import Lib.Tor (newTorManager)
import Daemon.TorHealth
appMain :: IO ()
appMain = do
@@ -118,6 +118,7 @@ makeFoundation appSettings = do
def <- getDefaultProcDevMetrics
appProcDevMomentCache <- newIORef (now, mempty, def)
appLastTorRestart <- newIORef now
appLanThread <- forkIO (sleep 10) >>= newMVar
-- We need a log function to create a connection pool. We need a connection
-- pool to create our foundation. And we need our foundation to get a

View File

@@ -18,6 +18,9 @@ import Lib.ProductKey
import Lib.SystemPaths
import Settings
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Control.Carrier.Lift
import Lib.Error
start9AgentServicePrefix :: IsString a => a
start9AgentServicePrefix = "start9-"
@@ -53,4 +56,10 @@ publishAgentToAvahi = do
"_http._tcp"
agentPort
lift Avahi.reload
lift $ threadDelay 10_000_000
tid <- asks appLanThread >>= liftIO . takeMVar
liftIO $ killThread tid
tid' <- liftIO $ forkIO (runM . void . runExceptT @S9Error $ AppMgr2.runAppMgrCliC AppMgr2.lanEnable)
asks appLanThread >>= liftIO . flip putMVar tid'

View File

@@ -75,6 +75,7 @@ data AgentCtx = AgentCtx
, appBackgroundJobs :: TVar JobCache
, appIconTags :: TVar (HM.HashMap AppId (Digest MD5))
, appLastTorRestart :: IORef UTCTime
, appLanThread :: MVar ThreadId
}
setWebProcessThreadId :: ThreadId -> AgentCtx -> IO ()

View File

@@ -7,78 +7,82 @@
{-# LANGUAGE TypeApplications #-}
module Handler.Apps where
import Startlude hiding ( modify
, execState
import Startlude hiding ( Reader
, asks
, Reader
, runReader
, catchError
, forkFinally
, empty
, execState
, forkFinally
, modify
, runReader
)
import Control.Carrier.Reader
import Control.Carrier.Error.Church
import Control.Carrier.Lift
import Control.Carrier.Reader
import qualified Control.Concurrent.Async.Lifted
as LAsync
import qualified Control.Concurrent.Lifted as Lifted
import qualified Control.Exception.Lifted as Lifted
import Control.Concurrent.STM.TVar
import Control.Effect.Empty hiding ( guard )
import Control.Effect.Labelled ( HasLabelled
, Labelled
, runLabelled
)
import qualified Control.Exception.Lifted as Lifted
import Control.Lens hiding ( (??) )
import Control.Monad.Logger
import Control.Monad.Trans.Control ( MonadBaseControl )
import Crypto.Hash
import Data.Aeson
import Data.Aeson.Lens
import Data.Aeson.Types ( parseMaybe )
import qualified Data.ByteString.Lazy as LBS
import Data.IORef
import qualified Data.HashMap.Lazy as HML
import qualified Data.HashMap.Strict as HM
import Data.IORef
import qualified Data.List.NonEmpty as NE
import Data.Singletons
import Data.Singletons.Prelude.Bool ( SBool(..)
, If
import Data.Singletons.Prelude.Bool ( If
, SBool(..)
)
import Data.Singletons.Prelude.List ( Elem )
import qualified Data.Text as Text
import Database.Persist
import Database.Persist.Sql ( ConnectionPool )
import Database.Persist.Sqlite ( runSqlPool )
import Exinst
import Network.HTTP.Types
import qualified Network.JSONRPC as JSONRPC
import Yesod.Core.Content
import Yesod.Core.Json
import Yesod.Core.Handler hiding ( cached )
import Yesod.Core.Json
import Yesod.Core.Types ( JSONResponse(..) )
import Yesod.Persist.Core
import Foundation
import Handler.Backups
import Handler.Icons
import Handler.Network
import Handler.Types.Apps
import Handler.Util
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Algebra.State.RegistryUrl
import Lib.Background
import Lib.Error
import qualified Lib.External.AppManifest as AppManifest
import qualified Lib.External.AppMgr as AppMgr
import qualified Lib.External.Registry as Reg
import qualified Lib.External.AppManifest as AppManifest
import Lib.IconCache
import qualified Lib.Notifications as Notifications
import Lib.SystemPaths
import Lib.TyFam.ConditionalData
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.NetAddress
import Lib.Types.ServerApp
import Model
import Settings
import Crypto.Hash
pureLog :: Show a => a -> Handler a
pureLog = liftA2 (*>) ($logInfo . show) pure
@@ -244,18 +248,30 @@ getInstalledAppsLogic = do
, appInstalledPreviewStatus = AppStatusTmp Installing
, appInstalledPreviewVersionInstalled = storeAppVersionInfoVersion
, appInstalledPreviewTorAddress = Nothing
, appInstalledPreviewUi = False
, appInstalledPreviewLanAddress = Nothing
, appInstalledPreviewTorUi = False
, appInstalledPreviewLanUi = False
}
installedPreviews = flip
HML.mapWithKey
remapped
\appId (s, v, AppMgr2.InfoRes {..}) -> AppInstalledPreview
{ appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
, appInstalledPreviewStatus = s
, appInstalledPreviewVersionInstalled = v
, appInstalledPreviewTorAddress = infoResTorAddress
, appInstalledPreviewUi = AppManifest.uiAvailable infoResManifest
}
\appId (s, v, AppMgr2.InfoRes {..}) ->
let
mLanAddress = do -- Maybe
addrBase <- infoResTorAddress
let
lanConfs = mapMaybe AppManifest.portMapEntryLan
$ AppManifest.appManifestPortMapping infoResManifest
guard (not . null $ lanConfs)
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
in AppInstalledPreview { appInstalledPreviewBase = AppBase appId infoResTitle (iconUrl appId v)
, appInstalledPreviewStatus = s
, appInstalledPreviewVersionInstalled = v
, appInstalledPreviewTorAddress = infoResTorAddress
, appInstalledPreviewLanAddress = mLanAddress
, appInstalledPreviewTorUi = AppManifest.torUiAvailable infoResManifest
, appInstalledPreviewLanUi = AppManifest.lanUiAvailable infoResManifest
}
pure $ HML.elems $ HML.union installingPreviews installedPreviews
@@ -286,9 +302,14 @@ getInstalledAppByIdLogic appId = do
, appInstalledFullInstructions = Nothing
, appInstalledFullLastBackup = backupTime
, appInstalledFullTorAddress = Nothing
, appInstalledFullLanAddress = Nothing
, appInstalledFullTorUi = False
, appInstalledFullLanUi = False
, appInstalledFullConfiguredRequirements = []
, appInstalledFullUninstallAlert = Nothing
, appInstalledFullRestoreAlert = Nothing
, appInstalledFullStartAlert = Nothing
, appInstalledFullActions = []
}
serverApps <- AppMgr2.list [AppMgr2.flags|-s -d|]
let remapped = remapAppMgrInfo jobCache serverApps
@@ -316,18 +337,30 @@ getInstalledAppByIdLogic appId = do
(HM.lookup depId installCache $> AppStatusTmp Installing)
<|> (view _1 <$> HM.lookup depId remapped)
pure $ dependencyInfoToDependencyRequirement (AsInstalled STrue) (base, depStatus, depInfo)
manifest <- lift $ LAsync.wait manifest'
manifest <- (lift $ LAsync.wait manifest') >>= \case
Nothing -> throwError $ NotFoundE "manifest" (show appId)
Just x -> pure x
instructions <- lift $ LAsync.wait instructions'
backupTime <- lift $ LAsync.wait backupTime'
let lanAddress = do
addrBase <- infoResTorAddress
let lanConfs = mapMaybe AppManifest.portMapEntryLan $ AppManifest.appManifestPortMapping manifest
guard (not . null $ lanConfs)
pure $ LanAddress . (".onion" `Text.replace` ".local") . unTorAddress $ addrBase
pure AppInstalledFull { appInstalledFullBase = AppBase appId infoResTitle (iconUrl appId version)
, appInstalledFullStatus = status
, appInstalledFullVersionInstalled = version
, appInstalledFullInstructions = instructions
, appInstalledFullLastBackup = backupTime
, appInstalledFullTorAddress = infoResTorAddress
, appInstalledFullLanAddress = lanAddress
, appInstalledFullTorUi = AppManifest.torUiAvailable manifest
, appInstalledFullLanUi = AppManifest.lanUiAvailable manifest
, appInstalledFullConfiguredRequirements = HM.elems requirements
, appInstalledFullUninstallAlert = manifest >>= AppManifest.appManifestUninstallAlert
, appInstalledFullRestoreAlert = manifest >>= AppManifest.appManifestRestoreAlert
, appInstalledFullUninstallAlert = AppManifest.appManifestUninstallAlert manifest
, appInstalledFullRestoreAlert = AppManifest.appManifestRestoreAlert manifest
, appInstalledFullStartAlert = AppManifest.appManifestStartAlert manifest
, appInstalledFullActions = AppManifest.appManifestActions manifest
}
runMaybeT (installing <|> installed) `orThrowM` NotFoundE "appId" (show appId)
@@ -361,7 +394,9 @@ postUninstallAppLogic appId dryrun = do
breakageIds <- HM.keys . AppMgr2.unBreakageMap <$> AppMgr2.remove flags appId
bs <- pure (traverse (hydrate $ (AppMgr2.infoResTitle &&& AppMgr2.infoResVersion) <$> serverApps) breakageIds)
`orThrowM` InternalE "Reported app breakage for app that isn't installed, contact support"
when (not $ coerce dryrun) $ clearIcon appId
when (not $ coerce dryrun) $ do
clearIcon appId
postResetLanLogic
pure $ WithBreakages bs ()
type InstallResponse :: Bool -> Type
@@ -465,6 +500,7 @@ postInstallNewAppLogic appId appVersion dryrun = do
(void $ Notifications.emit k infoResVersion (Notifications.RestartFailed e))
pool
)
postResetLanLogic
postStartServerAppR :: AppId -> Handler ()
@@ -769,3 +805,21 @@ dependencyInfoToDependencyRequirement asInstalled (base, status, AppMgr2.Depende
let appDependencyRequirementReasonOptional = dependencyInfoReasonOptional
appDependencyRequirementDefault = dependencyInfoRequired
in AppDependencyRequirement { .. }
postActionR :: AppId -> Handler (JSONResponse JSONRPC.Response)
postActionR appId = do
req <- requireCheckJsonBody
fmap JSONResponse . intoHandler $ postActionLogic appId req
postActionLogic :: (Has (Error S9Error) sig m, Has AppMgr2.AppMgr sig m)
=> AppId
-> JSONRPC.Request
-> m JSONRPC.Response
postActionLogic appId (JSONRPC.Request { getReqMethod, getReqId }) = do
hm <- AppMgr2.action appId getReqMethod
case (HM.lookup "result" hm, HM.lookup "error" hm >>= parseMaybe parseJSON) of
(Just v , _ ) -> pure (JSONRPC.Response JSONRPC.V2 v getReqId)
(_ , Just e ) -> pure (JSONRPC.ResponseError JSONRPC.V2 e getReqId)
(Nothing, Nothing) -> throwError
$ AppMgrParseE "action" (decodeUtf8 . LBS.toStrict $ encode (Object hm)) "Invalid JSONRPC Response"
postActionLogic _ r = throwError $ InvalidRequestE (toJSON r) "Invalid JSONRPC Request"

View File

@@ -0,0 +1,32 @@
module Handler.Network where
import Startlude hiding ( Reader
, asks
, runReader
)
import Control.Carrier.Lift ( runM )
import Control.Effect.Error
import Control.Carrier.Reader
import Lib.Error
import Yesod.Core ( getYesod )
import Foundation
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.Types.Core
postResetLanR :: Handler ()
postResetLanR = do
ctx <- getYesod
runM . handleS9ErrC . runReader ctx $ postResetLanLogic
postResetLanLogic :: (MonadIO m, Has (Reader AgentCtx) sig m, Has (Error S9Error) sig m) => m ()
postResetLanLogic = do
threadVar <- asks appLanThread
mtid <- liftIO . tryTakeMVar $ threadVar
case mtid of
Nothing -> throwError $ TemporarilyForbiddenE (AppId "LAN") "reset" "being reset"
Just tid -> liftIO $ do
killThread tid
newTid <- forkIO (void . runM . runExceptT @S9Error . AppMgr2.runAppMgrCliC $ AppMgr2.lanEnable)
putMVar threadVar newTid

View File

@@ -28,6 +28,9 @@ import Lib.SystemPaths hiding ( (</>) )
import Lib.Tor
import Settings
import Control.Carrier.Lift ( runM )
import System.Process
import qualified UnliftIO
import System.FileLock
getVersionR :: Handler AppVersionRes
getVersionR = pure . AppVersionRes $ agentVersion
@@ -35,8 +38,7 @@ getVersionR = pure . AppVersionRes $ agentVersion
getVersionLatestR :: Handler VersionLatestRes
getVersionLatestR = handleS9ErrT $ do
s <- getsYesod appSettings
v <- interp s $ Reg.getLatestAgentVersion
pure $ VersionLatestRes v
uncurry VersionLatestRes <$> interp s Reg.getLatestAgentVersion
where interp s = ExceptT . liftIO . runError . injectFilesystemBaseFromContext s . runRegistryUrlIOC
@@ -48,6 +50,8 @@ getSpecsR = handleS9ErrT $ do
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
specsNetworkId <- lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
specsTorAddress <- lift . runM . injectFilesystemBaseFromContext settings $ getAgentHiddenServiceUrl
specsLanAddress <-
fmap (<> ".local") . lift . runM . injectFilesystemBaseFromContext settings $ getStart9AgentHostname
let specsAgentVersion = agentVersion
returnJsonEncoding SpecsRes { .. }
@@ -69,3 +73,9 @@ patchServerR = do
getGitR :: Handler Text
getGitR = pure $embedGitRevision
getLogsR :: Handler (JSONResponse [Text])
getLogsR = do
let debugLock = "/root/agent/tmp/debug.lock"
UnliftIO.bracket (liftIO $ lockFile debugLock Exclusive) (liftIO . unlockFile) $ const $ do
liftIO $ callCommand "journalctl -u agent --since \"1 hour ago\" > /root/agent/tmp/debug.log"
liftIO $ JSONResponse . lines <$> readFile "/root/agent/tmp/debug.log"

View File

@@ -9,6 +9,7 @@ import Data.Aeson
import Data.Aeson.Flatten
import Data.Singletons
import qualified Lib.External.AppManifest as Manifest
import Lib.TyFam.ConditionalData
import Lib.Types.Core
import Lib.Types.Emver
@@ -45,7 +46,9 @@ data AppInstalledPreview = AppInstalledPreview
, appInstalledPreviewStatus :: AppStatus
, appInstalledPreviewVersionInstalled :: Version
, appInstalledPreviewTorAddress :: Maybe TorAddress
, appInstalledPreviewUi :: Bool
, appInstalledPreviewLanAddress :: Maybe LanAddress
, appInstalledPreviewTorUi :: Bool
, appInstalledPreviewLanUi :: Bool
}
deriving (Eq, Show)
instance ToJSON AppInstalledPreview where
@@ -53,7 +56,9 @@ instance ToJSON AppInstalledPreview where
[ "status" .= appInstalledPreviewStatus
, "versionInstalled" .= appInstalledPreviewVersionInstalled
, "torAddress" .= (unTorAddress <$> appInstalledPreviewTorAddress)
, "ui" .= appInstalledPreviewUi
, "lanAddress" .= (unLanAddress <$> appInstalledPreviewLanAddress)
, "torUi" .= appInstalledPreviewTorUi
, "lanUi" .= appInstalledPreviewLanUi
]
data InstallNewAppReq = InstallNewAppReq
@@ -129,11 +134,16 @@ data AppInstalledFull = AppInstalledFull
, appInstalledFullStatus :: AppStatus
, appInstalledFullVersionInstalled :: Version
, appInstalledFullTorAddress :: Maybe TorAddress
, appInstalledFullLanAddress :: Maybe LanAddress
, appInstalledFullTorUi :: Bool
, appInstalledFullLanUi :: Bool
, appInstalledFullInstructions :: Maybe Text
, appInstalledFullLastBackup :: Maybe UTCTime
, appInstalledFullConfiguredRequirements :: [Stripped AppDependencyRequirement]
, appInstalledFullUninstallAlert :: Maybe Text
, appInstalledFullRestoreAlert :: Maybe Text
, appInstalledFullStartAlert :: Maybe Text
, appInstalledFullActions :: [Manifest.Action]
}
instance ToJSON AppInstalledFull where
toJSON AppInstalledFull {..} = object
@@ -141,6 +151,9 @@ instance ToJSON AppInstalledFull where
, "lastBackup" .= appInstalledFullLastBackup
, "configuredRequirements" .= appInstalledFullConfiguredRequirements
, "torAddress" .= (unTorAddress <$> appInstalledFullTorAddress)
, "lanAddress" .= (unLanAddress <$> appInstalledFullLanAddress)
, "torUi" .= appInstalledFullTorUi
, "lanUi" .= appInstalledFullLanUi
, "id" .= appBaseId appInstalledFullBase
, "title" .= appBaseTitle appInstalledFullBase
, "iconURL" .= appBaseIconUrl appInstalledFullBase
@@ -148,6 +161,8 @@ instance ToJSON AppInstalledFull where
, "status" .= appInstalledFullStatus
, "uninstallAlert" .= appInstalledFullUninstallAlert
, "restoreAlert" .= appInstalledFullRestoreAlert
, "startAlert" .= appInstalledFullStartAlert
, "actions" .= appInstalledFullActions
]
data AppVersionInfo = AppVersionInfo

View File

@@ -15,11 +15,13 @@ import Lib.Types.Emver
import Model
data VersionLatestRes = VersionLatestRes
{ versionLatestVersion :: Version
{ versionLatestVersion :: Version
, versionLatestReleaseNotes :: Maybe Text
}
deriving (Eq, Show)
instance ToJSON VersionLatestRes where
toJSON VersionLatestRes {..} = object $ ["versionLatest" .= versionLatestVersion]
toJSON VersionLatestRes {..} =
object $ ["versionLatest" .= versionLatestVersion, "releaseNotes" .= versionLatestReleaseNotes]
instance ToTypedContent VersionLatestRes where
toTypedContent = toTypedContent . toJSON
instance ToContent VersionLatestRes where
@@ -31,14 +33,15 @@ data ServerRes = ServerRes
, serverStatus :: Maybe AppStatus
, serverStatusAt :: UTCTime
, serverVersionInstalled :: Version
, serverNotifications :: [ Entity Notification ]
, serverNotifications :: [Entity Notification]
, serverWifi :: WifiList
, serverSsh :: [ SshKeyFingerprint ]
, serverSsh :: [SshKeyFingerprint]
, serverAlternativeRegistryUrl :: Maybe Text
, serverSpecs :: SpecsRes
, serverWelcomeAck :: Bool
, serverAutoCheckUpdates :: Bool
} deriving (Eq, Show)
}
deriving (Eq, Show)
type JsonEncoding a = Encoding
jsonEncode :: (Monad m, ToJSON a) => a -> m (JsonEncoding a)

View File

@@ -16,6 +16,7 @@ data SpecsRes = SpecsRes
, specsNetworkId :: Text
, specsAgentVersion :: Version
, specsTorAddress :: Text
, specsLanAddress :: Text
}
deriving (Eq, Show)
@@ -23,6 +24,7 @@ instance ToJSON SpecsRes where
toJSON SpecsRes {..} = object
[ "EmbassyOS Version" .= specsAgentVersion
, "Tor Address" .= specsTorAddress
, "LAN Address" .= specsLanAddress
, "Network ID" .= specsNetworkId
, "CPU" .= specsCPU
, "Memory" .= specsMem
@@ -33,6 +35,7 @@ instance ToJSON SpecsRes where
. fold
$ [ "EmbassyOS Version" .= specsAgentVersion
, "Tor Address" .= specsTorAddress
, "LAN Address" .= specsLanAddress
, "Network ID" .= specsNetworkId
, "CPU" .= specsCPU
, "Memory" .= specsMem

View File

@@ -93,6 +93,7 @@ getSpecs settings = do
specsDisk <- fmap show . metricDiskSize <$> getDfMetrics
specsNetworkId <- runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
specsTorAddress <- runM $ injectFilesystemBaseFromContext settings getAgentHiddenServiceUrl
specsLanAddress <- fmap (<> ".local") . runM $ injectFilesystemBaseFromContext settings getStart9AgentHostname
let specsAgentVersion = agentVersion
pure $ SpecsRes { .. }

View File

@@ -29,7 +29,7 @@ import qualified Data.String as String
import Lib.Algebra.Domain.AppMgr.Types
import Lib.Algebra.Domain.AppMgr.TH
import Lib.Error
import Lib.External.AppManifest
import qualified Lib.External.AppManifest as Manifest
import Lib.TyFam.ConditionalData
import Lib.Types.Core ( AppId(..)
, AppContainerStatus(..)
@@ -50,6 +50,7 @@ import Control.Monad.Trans.Control ( defaultLiftBaseWith
, MonadBaseControl(..)
)
import qualified Data.ByteString.Char8 as C8
import System.Process
type InfoRes :: Either OnlyInfoFlag [IncludeInfoFlag] -> Type
@@ -66,7 +67,7 @@ data InfoRes a = InfoRes
(Either_ (DefaultEqSym1 'OnlyDependencies) (ElemSym1 'IncludeDependencies) a)
(HM.HashMap AppId DependencyInfo)
, infoResManifest
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) AppManifest
:: Include (Either_ (DefaultEqSym1 'OnlyManifest) (ElemSym1 'IncludeManifest) a) Manifest.AppManifest
, infoResStatus :: Include (Either_ (DefaultEqSym1 'OnlyStatus) (ElemSym1 'IncludeStatus) a) AppContainerStatus
}
instance SingI (a :: Either OnlyInfoFlag [IncludeInfoFlag]) => FromJSON (InfoRes a) where
@@ -270,6 +271,8 @@ data AppMgr (m :: Type -> Type) k where
-- Tor ::_
Update ::DryRun -> AppId -> Maybe VersionRange -> AppMgr m BreakageMap
-- Verify ::_
LanEnable ::AppMgr m ()
Action ::AppId -> Text -> AppMgr m (HM.HashMap Text Value)
makeSmartConstructors ''AppMgr
newtype AppMgrCliC m a = AppMgrCliC { runAppMgrCliC :: m a }
@@ -421,6 +424,16 @@ instance (Has (Error S9Error) sig m, Algebra sig m, MonadIO m) => Algebra (AppMg
ExitFailure 6 ->
throwError $ NotFoundE "appId@version" ([i|#{appId}#{maybe "" (('@':) . show) version}|])
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
(L LanEnable ) -> liftIO $ callProcess "appmgr" ["lan", "enable"] $> ctx
(L (Action appId action)) -> do
let args = ["actions", show appId, toS action]
(ec, out) <- readProcessInheritStderr "appmgr" args ""
case ec of
ExitSuccess -> case eitherDecodeStrict out of
Left e -> throwError $ AppMgrParseE (toS $ String.unwords args) (decodeUtf8 out) e
Right x -> pure $ ctx $> x
ExitFailure 6 -> throwError $ NotFoundE "appId" (show appId)
ExitFailure n -> throwError $ AppMgrE (toS $ String.unwords args) n
R other -> AppMgrCliC $ alg (runAppMgrCliC . hdl) other ctx
where
versionSpec :: (IsString a, Semigroup a, ConvertText String a) => Maybe VersionRange -> a -> a

View File

@@ -9,12 +9,12 @@ import Data.Aeson
import qualified Data.HashMap.Strict as HM
import qualified Data.Yaml as Yaml
import Control.Monad.Fail ( MonadFail(fail) )
import Lib.Error
import Lib.SystemPaths
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.Emver.Orphans ( )
import Control.Monad.Fail ( MonadFail(fail) )
data ImageType = ImageTypeTar
deriving (Eq, Show)
@@ -47,6 +47,33 @@ instance FromJSON AssetMapping where
assetMappingOverwrite <- o .: "overwrite"
pure $ AssetMapping { .. }
data Action = Action
{ actionId :: Text
, actionName :: Text
, actionDescription :: Text
, actionWarning :: Maybe Text
, actionAllowedStatuses :: [AppContainerStatus]
}
deriving Show
instance FromJSON Action where
parseJSON = withObject "AppAction" $ \o -> do
actionId <- o .: "id"
actionName <- o .: "name"
actionDescription <- o .: "description"
actionWarning <- o .:? "warning"
actionAllowedStatuses <- o .: "allowed-statuses"
pure Action { .. }
instance ToJSON Action where
toJSON Action {..} =
object
$ [ "id" .= actionId
, "name" .= actionName
, "description" .= actionDescription
, "allowedStatuses" .= actionAllowedStatuses
]
<> maybeToList (("warning" .=) <$> actionWarning)
data AppManifest where
AppManifest ::{ appManifestId :: AppId
, appManifestVersion :: Version
@@ -54,7 +81,7 @@ data AppManifest where
, appManifestDescShort :: Text
, appManifestDescLong :: Text
, appManifestReleaseNotes :: Text
, appManifestPortMapping :: HM.HashMap Word16 Word16
, appManifestPortMapping :: [PortMapEntry]
, appManifestImageType :: ImageType
, appManifestMount :: FilePath
, appManifestAssets :: [AssetMapping]
@@ -62,10 +89,20 @@ data AppManifest where
, appManifestDependencies :: HM.HashMap AppId VersionRange
, appManifestUninstallAlert :: Maybe Text
, appManifestRestoreAlert :: Maybe Text
, appManifestStartAlert :: Maybe Text
, appManifestActions :: [Action]
} -> AppManifest
deriving instance Show AppManifest
uiAvailable :: AppManifest -> Bool
uiAvailable AppManifest {..} = isJust $ HM.lookup 80 appManifestPortMapping
torUiAvailable :: AppManifest -> Bool
torUiAvailable AppManifest {..} = any (== 80) $ portMapEntryTor <$> appManifestPortMapping
lanUiAvailable :: AppManifest -> Bool
lanUiAvailable AppManifest {..} = any id $ fmap portMapEntryLan appManifestPortMapping <&> \case
Just Standard -> True
Just (Custom 443) -> True
Just (Custom 80 ) -> True
_ -> False
instance FromJSON AppManifest where
parseJSON = withObject "App Manifest " $ \o -> do
@@ -75,7 +112,7 @@ instance FromJSON AppManifest where
appManifestDescShort <- o .: "description" >>= (.: "short")
appManifestDescLong <- o .: "description" >>= (.: "long")
appManifestReleaseNotes <- o .: "release-notes"
appManifestPortMapping <- o .: "ports" >>= fmap HM.fromList . traverse parsePortMapping
appManifestPortMapping <- o .: "ports"
appManifestImageType <- o .: "image" >>= (.: "type")
appManifestMount <- o .: "mount"
appManifestAssets <- o .: "assets" >>= traverse parseJSON
@@ -83,13 +120,34 @@ instance FromJSON AppManifest where
appManifestDependencies <- o .:? "dependencies" .!= HM.empty >>= traverse parseDepInfo
appManifestUninstallAlert <- o .:? "uninstall-alert"
appManifestRestoreAlert <- o .:? "restore-alert"
appManifestStartAlert <- o .:? "start-alert"
appManifestActions <- o .: "actions"
pure $ AppManifest { .. }
where
parsePortMapping = withObject "Port Mapping" $ \o -> liftA2 (,) (o .: "tor") (o .: "internal")
parseDepInfo = withObject "Dep Info" $ (.: "version")
where parseDepInfo = withObject "Dep Info" $ (.: "version")
getAppManifest :: (MonadIO m, HasFilesystemBase sig m) => AppId -> S9ErrT m (Maybe AppManifest)
getAppManifest appId = do
base <- ask @"filesystemBase"
ExceptT $ first (ManifestParseE appId) <$> liftIO
(Yaml.decodeFileEither . toS $ (appMgrAppPath appId <> "manifest.yaml") `relativeTo` base)
data LanConfiguration = Standard | Custom Word16 deriving (Eq, Show)
instance FromJSON LanConfiguration where
parseJSON = liftA2 (<|>) standard custom
where
standard =
withText "Standard Lan" \t -> if t == "standard" then pure Standard else fail "Not Standard Lan Conf"
custom = withObject "Custom Lan" $ \o -> do
Custom <$> (o .: "custom" >>= (.: "port"))
data PortMapEntry = PortMapEntry
{ portMapEntryInternal :: Word16
, portMapEntryTor :: Word16
, portMapEntryLan :: Maybe LanConfiguration
}
deriving (Eq, Show)
instance FromJSON PortMapEntry where
parseJSON = withObject "Port Map Entry" $ \o -> do
portMapEntryInternal <- o .: "internal"
portMapEntryTor <- o .: "tor"
portMapEntryLan <- o .:? "lan"
pure PortMapEntry { .. }

View File

@@ -150,12 +150,13 @@ getAppVersionForSpec appId spec = do
v <- o .: "version"
pure v
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m Version
getLatestAgentVersion :: (Has RegistryUrl sig m, Has (Error S9Error) sig m, MonadIO m) => m (Version, Maybe Text)
getLatestAgentVersion = do
val <- registryRequest agentVersionPath
parseOrThrow agentVersionPath val $ withObject "version response" $ \o -> do
v <- o .: "version"
pure v
v <- o .: "version"
rn <- o .:? "release-notes"
pure (v, rn)
where agentVersionPath = "sys/version/agent"
getLatestAgentVersionForSpec :: (Has RegistryUrl sig m, Has (Lift IO) sig m, Has (Error S9Error) sig m)

View File

@@ -11,9 +11,9 @@ import Startlude hiding ( check
import qualified Startlude.ByteStream as ByteStream
import qualified Startlude.ByteStream.Char8 as ByteStream
import Control.Carrier.Lift ( runM )
import qualified Control.Effect.Reader.Labelled
as Fused
import Control.Carrier.Lift ( runM )
import Control.Monad.Trans.Reader ( mapReaderT )
import Control.Monad.Trans.Resource
import Data.Attoparsec.Text
@@ -21,51 +21,52 @@ import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as B8
import qualified Data.Conduit as Conduit
import qualified Data.Conduit.Combinators as Conduit
import qualified Data.Conduit.Tar as Conduit
import Data.Conduit.Shell hiding ( arch
, hostname
, patch
, stream
, hostname
)
import qualified Data.Conduit.Tar as Conduit
import Data.FileEmbed
import qualified Data.HashMap.Strict as HM
import Data.IORef
import Data.String.Interpolate.IsString
import qualified Data.Yaml as Yaml
import Exinst
import System.FilePath ( splitPath
import qualified Streaming.Conduit as Conduit
import qualified Streaming.Prelude as Stream
import qualified Streaming.Zip as Stream
import System.Directory
import System.FilePath ( (</>)
, joinPath
, (</>)
, splitPath
)
import System.FilePath.Posix ( takeDirectory )
import System.Directory
import System.IO.Error
import System.Posix.Files
import System.Process ( callCommand )
import qualified Streaming.Prelude as Stream
import qualified Streaming.Conduit as Conduit
import qualified Streaming.Zip as Stream
import Constants
import Control.Effect.Error hiding ( run )
import Daemon.ZeroConf ( getStart9AgentHostname )
import qualified Data.Text as T
import Foundation
import Handler.Network
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Lib.ClientManifest
import Lib.Error
import qualified Lib.External.AppMgr as AppMgr
import Lib.External.Registry
import Lib.Sound
import Lib.Ssl
import Lib.Tor
import Lib.Types.Core
import Lib.Types.NetAddress
import Lib.Types.Emver
import Lib.SystemCtl
import Lib.SystemPaths hiding ( (</>) )
import Lib.Tor
import Lib.Types.Core
import Lib.Types.Emver
import Lib.Types.NetAddress
import Settings
import Util.File
import qualified Lib.Algebra.Domain.AppMgr as AppMgr2
import Daemon.ZeroConf ( getStart9AgentHostname )
import qualified Data.Text as T
import Control.Effect.Error hiding ( run )
data Synchronizer = Synchronizer
@@ -96,15 +97,16 @@ parseKernelVersion = do
pure $ KernelVersion (Version (major', minor', patch', 0)) arch
synchronizer :: Synchronizer
synchronizer = sync_0_2_8
synchronizer = sync_0_2_9
{-# INLINE synchronizer #-}
sync_0_2_8 :: Synchronizer
sync_0_2_8 = Synchronizer
"0.2.8"
sync_0_2_9 :: Synchronizer
sync_0_2_9 = Synchronizer
"0.2.9"
[ syncCreateAgentTmp
, syncCreateSshDir
, syncRemoveAvahiSystemdDependency
, syncInstallLibAvahi
, syncInstallAppMgr
, syncFullUpgrade
, sync32BitKernel
@@ -113,6 +115,7 @@ sync_0_2_8 = Synchronizer
, syncInstallDuplicity
, syncInstallExfatFuse
, syncInstallExfatUtils
, syncUpgradeTor
, syncInstallAmbassadorUI
, syncOpenHttpPorts
, syncUpgradeLifeline
@@ -240,6 +243,19 @@ syncInstallExfatUtils = SyncOp "Install exfat-utils" check migrate False
shell "apt-get update"
shell "apt-get install -y exfat-utils"
syncInstallLibAvahi :: SyncOp
syncInstallLibAvahi = SyncOp "Install libavahi-client" check migrate False
where
check =
liftIO
$ (run (shell [i|dpkg -l|] $| shell [i|grep libavahi-client3|] $| conduit await) $> False)
`catch` \(e :: ProcessException) -> case e of
ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e
migrate = liftIO . run $ do
shell "apt-get update"
shell "apt-get install -y libavahi-client3"
syncWriteConf :: Text -> ByteString -> SystemPath -> SyncOp
syncWriteConf name contents' confLocation = SyncOp [i|Write #{name} Conf|] check migrate False
where
@@ -423,6 +439,7 @@ syncInstallAppMgr = SyncOp "Install AppMgr" check migrate False
avs <- asks $ appMgrVersionSpec . appSettings
av <- AppMgr.installNewAppMgr avs
unless (av <|| avs) $ throwE $ AppMgrVersionE av avs
postResetLanLogic -- to accommodate 0.2.x -> 0.2.9 where previous appmgr didn't correctly set up lan
syncUpgradeLifeline :: SyncOp
syncUpgradeLifeline = SyncOp "Upgrade Lifeline" check migrate False
@@ -480,7 +497,7 @@ replaceDerivativeCerts :: (HasFilesystemBase sig m, Fused.Has (Error S9Error) si
replaceDerivativeCerts = do
sid <- getStart9AgentHostname
let hostname = sid <> ".local"
tor <- getAgentHiddenServiceUrl
torAddr <- getAgentHiddenServiceUrl
caKeyPath <- toS <$> getAbsoluteLocationFor rootCaKeyPath
caConfPath <- toS <$> getAbsoluteLocationFor rootCaOpenSslConfPath
@@ -531,7 +548,7 @@ replaceDerivativeCerts = do
, duration = 365
}
hostname
tor
torAddr
liftIO $ do
putStrLn @Text "openssl logs"
putStrLn @Text "exit code: "
@@ -563,6 +580,21 @@ syncRestarterService = SyncOp "Install Restarter Service" check migrate True
liftIO $ callCommand "systemctl enable restarter.service"
liftIO $ callCommand "systemctl enable restarter.timer"
syncUpgradeTor :: SyncOp
syncUpgradeTor = SyncOp "Install Tor 0.3.5.12-1" check migrate False
where
check =
liftIO
$ ( run (shell [i|dpkg -l|] $| shell [i|grep tor|] $| shell [i|grep 0.3.5.12-1|] $| conduit await)
$> False
)
`catch` \(e :: ProcessException) -> case e of
ProcessException _ (ExitFailure 1) -> pure True
_ -> throwIO e
migrate = liftIO . run $ do
shell "apt-get update"
shell "apt-get install -y tor=0.3.5.12-1"
failUpdate :: S9Error -> ExceptT Void (ReaderT AgentCtx IO) ()
failUpdate e = do
ref <- asks appIsUpdateFailed

View File

@@ -7,9 +7,7 @@ import Network.HTTP.Client
import Network.Connection
import Lib.SystemPaths
import Network.HTTP.Client.TLS ( mkManagerSettings
, newTlsManagerWith
)
import Network.HTTP.Client.TLS ( mkManagerSettings )
import Data.Default
getAgentHiddenServiceUrl :: (HasFilesystemBase sig m, MonadIO m) => m Text

View File

@@ -7,6 +7,10 @@ newtype TorAddress = TorAddress { unTorAddress :: Text } deriving (Eq)
instance Show TorAddress where
show = toS . unTorAddress
newtype LanAddress = LanAddress { unLanAddress :: Text } deriving (Eq)
instance Show LanAddress where
show = toS . unLanAddress
newtype LanIp = LanIp { unLanIp :: Text } deriving (Eq)
instance Show LanIp where
show = toS . unLanIp

View File

@@ -45,6 +45,7 @@ import Handler.Backups
import Handler.Hosts
import Handler.Icons
import Handler.Login
import Handler.Network
import Handler.Notifications
import Handler.PasswordUpdate
import Handler.PowerOff

View File

@@ -66,12 +66,65 @@ assets:
hidden-service-version: v3
|]
mastodon330Manifest :: ByteString
mastodon330Manifest = [i|
---
id: mastodon
version: 3.3.0.1
title: Mastodon
description:
short: "A free, open-source social network server."
long: "Mastodon is a free, open-source social network server based on ActivityPub where users can follow friends and discover new ones. On Mastodon, users can publish anything they want: links, pictures, text, video. All Mastodon servers are interoperable as a federated network (users on one server can seamlessly communicate with users from another one, including non-Mastodon software that implements ActivityPub)!"
release-notes: Added an acation to reset the admin password
install-alert: "After starting mastodon for the first time, it can take a long time (several minutes) to be ready.\nPlease be patient. On future starts of the service, it will be faster, but still takes longer than other services.\nMake sure to sign up for a user before giving out your link. The first user to sign up is set as the admin user.\n"
uninstall-alert: ~
restore-alert: ~
start-alert: "It may take several minutes after startup for this service to be ready for use.\n"
has-instructions: true
os-version-required: ">=0.2.8"
os-version-recommended: ">=0.2.8"
ports:
- internal: 80
tor: 80
lan: standard
- internal: 443
tor: 443
lan:
custom:
port: 443
- internal: 3000
tor: 3000
lan: ~
- internal: 4000
tor: 4000
lan: ~
image:
type: tar
shm-size-mb: ~
mount: /root/persistence
public: ~
shared: ~
assets: []
hidden-service-version: v3
dependencies: {}
actions:
- id: reset-admin-password
name: Reset Admin Password
description: This action will reset your admin password to a random value
allowed-statuses:
- RUNNING
command:
- docker_entrypoint.sh
- reset_admin_password.sh
|]
spec :: Spec
spec = do
describe "parsing app manifest ports" $ do
it "should yield true for cups 0.2.3" $ do
res <- decodeThrow @IO @(AppManifest 0) cups023Manifest
uiAvailable res `shouldBe` True
it "should yield false for cups 0.2.3 Mod" $ do
res <- decodeThrow @IO @(AppManifest 0) cups023ManifestModNoUI
uiAvailable res `shouldBe` False
describe "parsing app manifest ports" $ do
it "should parse mastodon 3.3.0" $ do
res <- decodeThrow @IO @AppManifest mastodon330Manifest
print res
lanUiAvailable res `shouldBe` True
torUiAvailable res `shouldBe` True

1070
appmgr/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
authors = ["Aiden McClelland <me@drbonez.dev>"]
edition = "2018"
name = "appmgr"
version = "0.2.8"
version = "0.2.9"
[lib]
name = "appmgrlib"
@@ -13,23 +13,27 @@ name = "appmgr"
path = "src/main.rs"
[features]
default = []
avahi = ["avahi-sys"]
default = ["avahi"]
portable = []
production = []
[dependencies]
argonautica = "0.2.0"
async-trait = "0.1.42"
avahi-sys = { git = "https://github.com/Start9Labs/avahi-sys", branch = "feature/dynamic-linking", features = ["dynamic"], optional = true }
base32 = "0.4.0"
clap = "2.33"
ctrlc = "3.1.7"
ed25519-dalek = "1.0.1"
emver = { version = "0.1.0", features = ["serde"] }
failure = "0.1.8"
file-lock = "1.1"
futures = "0.3.8"
git-version = "0.3.4"
http = "0.2.3"
itertools = "0.9.0"
lazy_static = "1.4"
libc = "0.2.86"
linear-map = { version = "1.2", features = ["serde_impl"] }
log = "0.4.11"
nix = "0.19.1"
@@ -41,6 +45,8 @@ rand = "0.7.3"
regex = "1.4.2"
reqwest = { version = "0.10.9", features = ["stream", "json"] }
rpassword = "5.0.0"
rust-argon2 = "0.8.3"
scopeguard = "1.1" # because avahi-sys fucks your shit up
serde = { version = "1.0.118", features = ["derive", "rc"] }
serde_cbor = "0.11.1"
serde_json = "1.0.59"
@@ -49,3 +55,4 @@ simple-logging = "2.0"
tokio = { version = "0.3.5", features = ["full"] }
tokio-compat-02 = "0.1.2"
tokio-tar = { version = "0.3.0", git = "https://github.com/dr-bonez/tokio-tar.git", rev = "1ba710f3" }
yajrc = { version = "0.1.0", git = "https://github.com/dr-bonez/yajrc", rev = "c2952a4a21c50f7be6f8003afa37ee77deb66d56" }

View File

@@ -6,4 +6,4 @@ shopt -s expand_aliases
alias 'rust-arm-builder'='docker run --rm -it -v "$HOME/.cargo/registry":/root/.cargo/registry -v "$(pwd)":/home/rust/src start9/rust-arm-cross:latest'
cd ..
rust-arm-builder sh -c "(cd appmgr && cargo build --release)"
rust-arm-builder sh -c "(cd appmgr && cargo build)"

116
appmgr/src/actions.rs Normal file
View File

@@ -0,0 +1,116 @@
use std::os::unix::process::ExitStatusExt;
use std::process::Stdio;
use linear_map::set::LinearSet;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, Error as IoError};
use yajrc::RpcError;
use crate::apps::DockerStatus;
pub const STATUS_NOT_ALLOWED: i32 = -2;
pub const INVALID_COMMAND: i32 = -3;
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct Action {
pub id: String,
pub name: String,
pub description: String,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub warning: Option<String>,
pub allowed_statuses: LinearSet<DockerStatus>,
pub command: Vec<String>,
}
async fn tee<R: AsyncRead + Unpin, W: AsyncWrite + Unpin>(
mut r: R,
mut w: W,
) -> Result<Vec<u8>, IoError> {
let mut res = Vec::new();
let mut buf = vec![0; 2048];
let mut bytes;
while {
bytes = r.read(&mut buf).await?;
bytes != 0
} {
res.extend_from_slice(&buf[..bytes]);
w.write_all(&buf[..bytes]).await?;
}
w.flush().await?;
Ok(res)
}
impl Action {
pub async fn perform(&self, app_id: &str) -> Result<String, RpcError> {
let man = crate::apps::manifest(app_id)
.await
.map_err(failure::Error::from)
.map_err(failure::Error::compat)?;
let status = crate::apps::status(app_id, true)
.await
.map_err(failure::Error::from)
.map_err(failure::Error::compat)?
.status;
if !self.allowed_statuses.contains(&status) {
return Err(RpcError {
code: STATUS_NOT_ALLOWED,
message: format!(
"{} is in status {:?} which is not allowed by {}",
app_id, status, self.id
),
data: None,
});
}
let mut cmd = if status == DockerStatus::Running {
let mut cmd = tokio::process::Command::new("docker");
cmd.arg("exec").arg(&app_id).args(&self.command);
cmd
} else {
let mut cmd = tokio::process::Command::new("docker");
let entrypoint = self.command.get(0).ok_or_else(|| RpcError {
code: INVALID_COMMAND,
message: "Command Cannot Be Empty".to_owned(),
data: None,
})?;
cmd.arg("run")
.arg("--rm")
.arg("--name")
.arg(format!("{}_{}", app_id, self.id))
.arg("--mount")
.arg(format!(
"type=bind,src={}/{},dst={}",
crate::VOLUMES,
app_id,
man.mount.display()
))
.arg("--entrypoint")
.arg(entrypoint)
.arg(format!("start9/{}", app_id))
.args(&self.command[1..]);
// TODO: 0.3.0: net, tor, shm
cmd
};
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
let mut child = cmd.spawn()?;
let (stdout, stderr) = futures::try_join!(
tee(child.stdout.take().unwrap(), tokio::io::sink()),
tee(child.stderr.take().unwrap(), tokio::io::sink())
)?;
let status = child.wait().await?;
if status.success() {
String::from_utf8(stdout).map_err(From::from)
} else {
Err(RpcError {
code: status
.code()
.unwrap_or_else(|| status.signal().unwrap_or(0) + 128),
message: String::from_utf8(stderr)?,
data: None,
})
}
}
}

View File

@@ -1,9 +1,10 @@
use std::path::Path;
use argonautica::{Hasher, Verifier};
use argon2::Config;
use emver::Version;
use futures::try_join;
use futures::TryStreamExt;
use rand::Rng;
use serde::Serialize;
use crate::util::from_yaml_async_reader;
@@ -46,10 +47,7 @@ pub async fn create_backup<P: AsRef<Path>>(
let mut hash = String::new();
f.read_to_string(&mut hash).await?;
crate::ensure_code!(
Verifier::new()
.with_password(password)
.with_hash(hash)
.verify()
argon2::verify_encoded(&hash, password.as_bytes())
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
crate::error::INVALID_BACKUP_PASSWORD,
"Invalid Backup Decryption Password"
@@ -58,10 +56,8 @@ pub async fn create_backup<P: AsRef<Path>>(
{
// save password
use tokio::io::AsyncWriteExt;
let mut hasher = Hasher::default();
hasher.opt_out_of_secret_key(true);
let hash = hasher.with_password(password).hash().no_code()?;
let salt = rand::thread_rng().gen::<[u8; 32]>();
let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); // this is safe because apparently the API was poorly designed
let mut f = tokio::fs::File::create(pw_path).await?;
f.write_all(hash.as_bytes()).await?;
f.flush().await?;
@@ -160,10 +156,7 @@ pub async fn restore_backup<P: AsRef<Path>>(
let mut hash = String::new();
f.read_to_string(&mut hash).await?;
crate::ensure_code!(
Verifier::new()
.with_password(password)
.with_hash(hash)
.verify()
argon2::verify_encoded(&hash, password.as_bytes())
.with_code(crate::error::INVALID_BACKUP_PASSWORD)?,
crate::error::INVALID_BACKUP_PASSWORD,
"Invalid Backup Decryption Password"

View File

@@ -0,0 +1,10 @@
[req]
default_bits = 4096
default_md = sha256
distinguished_name = req_distinguished_name
prompt = no
[req_distinguished_name]
CN = {hostname}.local
O = Start9 Labs
OU = Embassy

View File

@@ -1864,6 +1864,9 @@ mod test {
hidden_service_version: crate::tor::HiddenServiceVersion::V3,
dependencies: deps,
extra: LinearMap::new(),
install_alert: None,
restore_alert: None,
uninstall_alert: None,
})
.unwrap();
let config = spec

View File

@@ -256,6 +256,19 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
"Package Name Does Not Match Expected"
);
}
log::info!(
"Creating metadata directory: {}/apps/{}",
crate::PERSISTENCE_DIR,
manifest.id
);
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
let app_dir_path = app_dir.path();
if app_dir_path.exists() {
tokio::fs::remove_dir_all(&app_dir_path).await?;
}
tokio::fs::create_dir_all(&app_dir_path).await?;
let (ip, tor_addr, tor_key) = crate::tor::set_svc(
&manifest.id,
crate::tor::NewService {
@@ -270,12 +283,6 @@ pub async fn install_v0<R: AsyncRead + Unpin + Send + Sync>(
log::info!("Creating volume {}/{}.", crate::VOLUMES, manifest.id);
tokio::fs::create_dir_all(Path::new(crate::VOLUMES).join(&manifest.id)).await?;
let app_dir = PersistencePath::from_ref("apps").join(&manifest.id);
let app_dir_path = app_dir.path();
if app_dir_path.exists() {
tokio::fs::remove_dir_all(&app_dir_path).await?;
}
tokio::fs::create_dir_all(&app_dir_path).await?;
let _lock = app_dir.lock(true).await?;
log::info!("Saving manifest.");
let mut manifest_out = app_dir.join("manifest.yaml").write(None).await?;

93
appmgr/src/lan.rs Normal file
View File

@@ -0,0 +1,93 @@
use crate::Error;
use avahi_sys;
use futures::future::pending;
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct AppId {
pub un_app_id: String,
}
pub async fn enable_lan() -> Result<(), Error> {
unsafe {
let app_list = crate::apps::list_info().await?;
let simple_poll = avahi_sys::avahi_simple_poll_new();
let poll = avahi_sys::avahi_simple_poll_get(simple_poll);
let mut stack_err = 0;
let err_c: *mut i32 = &mut stack_err;
let avahi_client = avahi_sys::avahi_client_new(
poll,
avahi_sys::AvahiClientFlags::AVAHI_CLIENT_NO_FAIL,
None,
std::ptr::null_mut(),
err_c,
);
let group =
avahi_sys::avahi_entry_group_new(avahi_client, Some(noop), std::ptr::null_mut());
let hostname_raw = avahi_sys::avahi_client_get_host_name_fqdn(avahi_client);
let hostname_bytes = std::ffi::CStr::from_ptr(hostname_raw).to_bytes_with_nul();
const HOSTNAME_LEN: usize = 1 + 15 + 1 + 5; // leading byte, main address, dot, "local"
debug_assert_eq!(hostname_bytes.len(), HOSTNAME_LEN);
let mut hostname_buf = [0; HOSTNAME_LEN + 1];
hostname_buf[1..].copy_from_slice(hostname_bytes);
// assume fixed length prefix on hostname due to local address
hostname_buf[0] = 15; // set the prefix length to 15 for the main address
hostname_buf[16] = 5; // set the prefix length to 5 for "local"
for (app_id, app_info) in app_list {
let man = crate::apps::manifest(&app_id).await?;
if man
.ports
.iter()
.filter(|p| p.lan.is_some())
.next()
.is_none()
{
continue;
}
let tor_address = if let Some(addr) = app_info.tor_address {
addr
} else {
continue;
};
let lan_address = tor_address
.strip_suffix(".onion")
.ok_or_else(|| failure::format_err!("Invalid Tor Address: {:?}", tor_address))?
.to_owned()
+ ".local";
let lan_address_ptr = std::ffi::CString::new(lan_address)
.expect("Could not cast lan address to c string");
let _ = avahi_sys::avahi_entry_group_add_record(
group,
avahi_sys::AVAHI_IF_UNSPEC,
avahi_sys::AVAHI_PROTO_UNSPEC,
avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_USE_MULTICAST
| avahi_sys::AvahiPublishFlags_AVAHI_PUBLISH_ALLOW_MULTIPLE,
lan_address_ptr.as_ptr(),
avahi_sys::AVAHI_DNS_CLASS_IN as u16,
avahi_sys::AVAHI_DNS_TYPE_CNAME as u16,
avahi_sys::AVAHI_DEFAULT_TTL,
hostname_buf.as_ptr().cast(),
hostname_buf.len(),
);
log::info!("Published {:?}", lan_address_ptr);
}
avahi_sys::avahi_entry_group_commit(group);
ctrlc::set_handler(move || {
// please the borrow checker with the below semantics
// avahi_sys::avahi_entry_group_free(group);
// avahi_sys::avahi_client_free(avahi_client);
// drop(Box::from_raw(err_c));
std::process::exit(0);
})
.expect("Error setting signal handler");
}
pending().await
}
unsafe extern "C" fn noop(
_group: *mut avahi_sys::AvahiEntryGroup,
_state: avahi_sys::AvahiEntryGroupState,
_userdata: *mut core::ffi::c_void,
) {
}

View File

@@ -20,6 +20,7 @@ lazy_static::lazy_static! {
pub static ref QUIET: tokio::sync::RwLock<bool> = tokio::sync::RwLock::new(!std::env::var("APPMGR_QUIET").map(|a| a == "0").unwrap_or(true));
}
pub mod actions;
pub mod apps;
pub mod backup;
pub mod config;
@@ -30,6 +31,8 @@ pub mod error;
pub mod index;
pub mod inspect;
pub mod install;
#[cfg(feature = "avahi")]
pub mod lan;
pub mod logs;
pub mod manifest;
pub mod pack;

View File

@@ -162,6 +162,17 @@ async fn inner_main() -> Result<(), Error> {
),
);
#[cfg(feature = "avahi")]
#[allow(unused_mut)]
let mut app = app.subcommand(
SubCommand::with_name("lan")
.about("Configures LAN services")
.subcommand(
SubCommand::with_name("enable")
.about("Publishes the LAN addresses for all services"),
),
);
#[cfg(not(feature = "portable"))]
let mut app = app
.subcommand(
@@ -399,7 +410,6 @@ async fn inner_main() -> Result<(), Error> {
.about("Removes an installed app")
.arg(
Arg::with_name("purge")
.short("p")
.long("purge")
.help("Deletes all application data"),
)
@@ -820,6 +830,16 @@ async fn inner_main() -> Result<(), Error> {
)
.subcommand(
SubCommand::with_name("repair-app-status").about("Restarts crashed apps"), // TODO: remove
)
.subcommand(
SubCommand::with_name("actions")
.about("Perform an action for a service")
.arg(
Arg::with_name("SERVICE")
.help("ID of the service to perform an action on")
.required(true),
)
.arg(Arg::with_name("ACTION").help("ID of the action to perform")),
);
let matches = app.clone().get_matches();
@@ -1178,6 +1198,15 @@ async fn inner_main() -> Result<(), Error> {
std::process::exit(1);
}
},
#[cfg(feature = "avahi")]
#[cfg(not(feature = "portable"))]
("lan", Some(sub_m)) => match sub_m.subcommand() {
("enable", _) => crate::lan::enable_lan().await?,
_ => {
println!("{}", sub_m.usage());
std::process::exit(1);
}
},
#[cfg(not(feature = "portable"))]
("info", Some(sub_m)) => {
let name = sub_m.value_of("ID").unwrap();
@@ -1547,6 +1576,34 @@ async fn inner_main() -> Result<(), Error> {
("repair-app-status", _) => {
control::repair_app_status().await?;
}
#[cfg(not(feature = "portable"))]
("actions", Some(sub_m)) => {
use yajrc::{GenericRpcMethod, RpcResponse};
let man = apps::manifest(sub_m.value_of("SERVICE").unwrap()).await?;
let action_id = sub_m.value_of("ACTION").unwrap();
println!(
"{}",
serde_json::to_string(&RpcResponse::<GenericRpcMethod>::from_result(
man.actions
.iter()
.filter(|a| &a.id == &action_id)
.next()
.ok_or_else(|| {
failure::format_err!(
"action {} does not exist for {}",
action_id,
man.id
)
})
.with_code(error::NOT_FOUND)?
.perform(&man.id)
.await
.map(serde_json::Value::String)
))
.with_code(error::SERDE_ERROR)?
)
}
("pack", Some(sub_m)) => {
pack(
sub_m.value_of("PATH").unwrap(),

View File

@@ -2,6 +2,7 @@ use std::path::PathBuf;
use linear_map::LinearMap;
use crate::actions::Action;
use crate::dependencies::Dependencies;
use crate::tor::HiddenServiceVersion;
use crate::tor::PortMapping;
@@ -43,6 +44,8 @@ pub struct ManifestV0 {
#[serde(default)]
pub restore_alert: Option<String>,
#[serde(default)]
pub start_alert: Option<String>,
#[serde(default)]
pub has_instructions: bool,
#[serde(default = "emver::VersionRange::any")]
pub os_version_required: emver::VersionRange,
@@ -63,6 +66,8 @@ pub struct ManifestV0 {
pub hidden_service_version: HiddenServiceVersion,
#[serde(default)]
pub dependencies: Dependencies,
#[serde(default)]
pub actions: Vec<Action>,
#[serde(flatten)]
pub extra: LinearMap<String, serde_yaml::Value>,
}

View File

@@ -0,0 +1,18 @@
server {{
listen 443 ssl;
server_name {hostname}.local;
ssl_certificate /root/appmgr/apps/{app_id}/cert-local.fullchain.crt.pem;
ssl_certificate_key /root/appmgr/apps/{app_id}/cert-local.key.pem;
location / {{
proxy_pass http://{app_ip}:{internal_port}/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}}
}}
server {{
listen 80;
server_name {hostname}.local;
return 301 https://$host$request_uri;
}}

View File

@@ -0,0 +1,8 @@
server {{
listen {port};
server_name {hostname}.local;
location / {{
proxy_pass http://{app_ip}:{internal_port}/;
proxy_set_header Host $host;
}}
}}

View File

@@ -217,6 +217,13 @@ pub async fn verify(path: &str) -> Result<(), failure::Error> {
if let Some(shared) = &manifest.shared {
validate_path(shared)?;
}
for action in &manifest.actions {
ensure!(
!action.command.is_empty(),
"Command Cannot Be Empty: {}",
action.id
);
}
log::info!("Opening config spec from archive.");
let config_spec = entries
.next()

View File

@@ -8,17 +8,62 @@ use failure::ResultExt as _;
use tokio::io::AsyncReadExt;
use tokio::io::AsyncWriteExt;
use crate::util::{PersistencePath, YamlUpdateHandle};
use crate::util::{Invoke, PersistencePath, YamlUpdateHandle};
use crate::{Error, ResultExt as _};
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum LanOptions {
Standard,
Custom { port: u16 },
}
#[derive(Debug, Clone, Copy, serde::Serialize)]
pub struct PortMapping {
pub internal: u16,
pub tor: u16,
pub lan: Option<LanOptions>, // only for http interfaces
}
impl<'de> serde::de::Deserialize<'de> for PortMapping {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
pub struct PortMappingIF {
pub internal: u16,
pub tor: u16,
#[serde(default, deserialize_with = "deserialize_some")]
pub lan: Option<Option<LanOptions>>,
}
fn deserialize_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: serde::de::Deserialize<'de>,
D: serde::de::Deserializer<'de>,
{
serde::de::Deserialize::deserialize(deserializer).map(Some)
}
let input_format: PortMappingIF = serde::de::Deserialize::deserialize(deserializer)?;
Ok(PortMapping {
internal: input_format.internal,
tor: input_format.tor,
lan: if let Some(lan) = input_format.lan {
lan
} else if input_format.tor == 80 {
Some(LanOptions::Standard)
} else {
None
},
})
}
}
pub const ETC_TOR_RC: &'static str = "/etc/tor/torrc";
pub const HIDDEN_SERVICE_DIR_ROOT: &'static str = "/var/lib/tor";
pub const ETC_HOSTNAME: &'static str = "/etc/hostname";
pub const ETC_NGINX_SERVICES_CONF: &'static str = "/etc/nginx/sites-available/start9-services.conf";
#[derive(Debug, Clone, Copy, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "lowercase")]
@@ -179,6 +224,167 @@ pub async fn write_services(hidden_services: &ServicesMap) -> Result<(), Error>
Ok(())
}
pub async fn write_lan_services(hidden_services: &ServicesMap) -> Result<(), Error> {
let mut f = tokio::fs::File::create(ETC_NGINX_SERVICES_CONF).await?;
for (app_id, service) in &hidden_services.map {
let hostname = tokio::fs::read_to_string(
Path::new(HIDDEN_SERVICE_DIR_ROOT)
.join(format!("app-{}", app_id))
.join("hostname"),
)
.await
.with_context(|e| format!("{}/app-{}/hostname: {}", HIDDEN_SERVICE_DIR_ROOT, app_id, e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
let hostname_str = hostname
.trim()
.strip_suffix(".onion")
.ok_or_else(|| failure::format_err!("invalid tor hostname"))
.no_code()?;
for mapping in &service.ports {
match &mapping.lan {
Some(LanOptions::Standard) => {
log::info!("Writing LAN certificates for {}", app_id);
let base_path = PersistencePath::from_ref("apps").join(&app_id);
let key_path = base_path.join("cert-local.key.pem").path();
if tokio::fs::metadata(&key_path).await.is_err() {
tokio::process::Command::new("openssl")
.arg("ecparam")
.arg("-genkey")
.arg("-name")
.arg("prime256v1")
.arg("-noout")
.arg("-out")
.arg(&key_path)
.invoke("OpenSSL GenKey")
.await?;
}
let conf_path = base_path.join("cert-local.csr.conf").path();
if tokio::fs::metadata(&conf_path).await.is_err() {
tokio::fs::write(
&conf_path,
format!(
include_str!("cert-local.csr.conf.template"),
hostname = hostname_str
),
)
.await?;
}
let req_path = base_path.join("cert-local.csr").path();
if tokio::fs::metadata(&req_path).await.is_err() {
tokio::process::Command::new("openssl")
.arg("req")
.arg("-config")
.arg(&conf_path)
.arg("-key")
.arg(&key_path)
.arg("-new")
.arg("-addext")
.arg(format!(
"subjectAltName=DNS:{hostname}.local",
hostname = hostname_str
))
.arg("-out")
.arg(&req_path)
.invoke("OpenSSL Req")
.await?;
}
let cert_path = base_path.join("cert-local.crt.pem").path();
if tokio::fs::metadata(&cert_path).await.is_err() {
tokio::process::Command::new("openssl")
.arg("ca")
.arg("-batch")
.arg("-config")
.arg("/root/agent/ca/intermediate/openssl.conf")
.arg("-rand_serial")
.arg("-keyfile")
.arg("/root/agent/ca/intermediate/private/embassy-int-ca.key.pem")
.arg("-cert")
.arg("/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem")
.arg("-extensions")
.arg("server_cert")
.arg("-days")
.arg("365")
.arg("-notext")
.arg("-in")
.arg(&req_path)
.arg("-out")
.arg(&cert_path)
.invoke("OpenSSL CA")
.await?;
}
let fullchain_path = base_path.join("cert-local.fullchain.crt.pem");
if !fullchain_path.exists().await {
log::info!("Writing fullchain to: {}", fullchain_path.path().display());
let mut fullchain_file = fullchain_path.write(None).await?;
tokio::io::copy(
&mut tokio::fs::File::open(&cert_path).await?,
&mut *fullchain_file,
)
.await?;
tokio::io::copy(
&mut tokio::fs::File::open(
"/root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
)
.await
.with_context(|e| {
format!(
"{}: /root/agent/ca/intermediate/certs/embassy-int-ca.crt.pem",
e
)
})
.with_code(crate::error::FILESYSTEM_ERROR)?,
&mut *fullchain_file,
)
.await?;
tokio::io::copy(
&mut tokio::fs::File::open(
"/root/agent/ca/certs/embassy-root-ca.cert.pem",
)
.await
.with_context(|e| {
format!("{}: /root/agent/ca/certs/embassy-root-ca.cert.pem", e)
})
.with_code(crate::error::FILESYSTEM_ERROR)?,
&mut *fullchain_file,
)
.await?;
fullchain_file.commit().await?;
log::info!("{} written successfully", fullchain_path.path().display());
}
f.write_all(
format!(
include_str!("nginx-standard.conf.template"),
hostname = hostname_str,
app_ip = service.ip,
internal_port = mapping.internal,
app_id = app_id,
)
.as_bytes(),
)
.await?;
f.sync_all().await?;
}
Some(LanOptions::Custom { port }) => {
f.write_all(
format!(
include_str!("nginx.conf.template"),
hostname = hostname_str,
app_ip = service.ip,
port = port,
internal_port = mapping.internal,
)
.as_bytes(),
)
.await?
}
None => (),
}
}
}
Ok(())
}
pub async fn read_tor_address(name: &str, timeout: Option<Duration>) -> Result<String, Error> {
log::info!("Retrieving Tor hidden service address for {}.", name);
let addr_path = Path::new(HIDDEN_SERVICE_DIR_ROOT)
@@ -287,8 +493,8 @@ pub async fn set_svc(
Err(e)
}
})?;
#[cfg(target_os = "linux")]
nix::unistd::sync();
hidden_services.commit().await?;
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"])
@@ -302,19 +508,32 @@ pub async fn set_svc(
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok((
ip,
if is_listening {
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
} else {
None
},
if is_listening {
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
} else {
None
},
))
let addr = if is_listening {
Some(read_tor_address(name, Some(Duration::from_secs(30))).await?)
} else {
None
};
let key = if is_listening {
Some(read_tor_key(name, ver, Some(Duration::from_secs(30))).await?)
} else {
None
};
write_lan_services(&hidden_services).await?;
log::info!("Reloading Nginx.");
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
hidden_services.commit().await?;
Ok((ip, addr, key))
}
pub async fn rm_svc(name: &str) -> Result<(), Error> {
@@ -333,7 +552,6 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
}
log::info!("Removing Tor hidden service {} from {}.", name, ETC_TOR_RC);
write_services(&hidden_services).await?;
hidden_services.commit().await?;
log::info!("Reloading Tor.");
let svc_exit = std::process::Command::new("service")
.args(&["tor", "reload"])
@@ -344,6 +562,21 @@ pub async fn rm_svc(name: &str) -> Result<(), Error> {
"Failed to Reload Tor: {}",
svc_exit.code().unwrap_or(0)
);
write_lan_services(&hidden_services).await?;
log::info!("Reloading Nginx.");
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
hidden_services.commit().await?;
Ok(())
}

View File

@@ -137,6 +137,7 @@ impl PersistenceFile {
if let Some(mut file) = self.file.take() {
file.flush().await?;
file.shutdown().await?;
file.sync_all().await?;
drop(file);
}
if let Some(path) = self.needs_commit.take() {
@@ -156,10 +157,6 @@ impl PersistenceFile {
.await
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
tokio::fs::remove_file(format!("{}.lock", path.path().display()))
.await
.with_context(|e| format!("{}.lock: {}", path.path().display(), e))
.with_code(crate::error::FILESYSTEM_ERROR)?;
}
Ok(())

View File

@@ -24,8 +24,9 @@ mod v0_2_5;
mod v0_2_6;
mod v0_2_7;
mod v0_2_8;
mod v0_2_9;
pub use v0_2_8::Version as Current;
pub use v0_2_9::Version as Current;
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(untagged)]
@@ -46,6 +47,7 @@ enum Version {
V0_2_6(Wrapper<v0_2_6::Version>),
V0_2_7(Wrapper<v0_2_7::Version>),
V0_2_8(Wrapper<v0_2_8::Version>),
V0_2_9(Wrapper<v0_2_9::Version>),
Other(emver::Version),
}
@@ -156,6 +158,7 @@ pub async fn init() -> Result<(), failure::Error> {
Version::V0_2_6(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_7(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_8(v) => v.0.migrate_to(&Current::new()).await?,
Version::V0_2_9(v) => v.0.migrate_to(&Current::new()).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
}
@@ -172,7 +175,8 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
.collect();
let url = format!("{}/appmgr?spec={}", &*crate::SYS_REGISTRY_URL, req_str);
log::info!("Fetching new version from {}", url);
let response = reqwest::get(&url).compat()
let response = reqwest::get(&url)
.compat()
.await
.with_code(crate::error::NETWORK_ERROR)?
.error_for_status()
@@ -244,6 +248,7 @@ pub async fn self_update(requirement: emver::VersionRange) -> Result<(), Error>
Version::V0_2_6(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_7(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_8(v) => Current::new().migrate_to(&v.0).await?,
Version::V0_2_9(v) => Current::new().migrate_to(&v.0).await?,
Version::Other(_) => (),
// TODO find some way to automate this?
};

View File

@@ -0,0 +1,75 @@
use std::os::unix::process::ExitStatusExt;
use super::*;
const V0_2_9: emver::Version = emver::Version::new(0, 2, 9, 0);
pub struct Version;
#[async_trait]
impl VersionT for Version {
type Previous = v0_2_8::Version;
fn new() -> Self {
Version
}
fn semver(&self) -> &'static emver::Version {
&V0_2_9
}
async fn up(&self) -> Result<(), Error> {
crate::tor::write_lan_services(
&crate::tor::services_map(&PersistencePath::from_ref(crate::SERVICES_YAML)).await?,
)
.await?;
tokio::fs::os::unix::symlink(
crate::tor::ETC_NGINX_SERVICES_CONF,
"/etc/nginx/sites-enabled/start9-services.conf",
)
.await
.or_else(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(e)
}
})?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}
async fn down(&self) -> Result<(), Error> {
tokio::fs::remove_file("/etc/nginx/sites-enabled/start9-services.conf")
.await
.or_else(|e| match e {
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
e => Err(e),
})?;
tokio::fs::remove_file(crate::tor::ETC_NGINX_SERVICES_CONF)
.await
.or_else(|e| match e {
e if e.kind() == std::io::ErrorKind::NotFound => Ok(()),
e => Err(e),
})?;
let svc_exit = std::process::Command::new("service")
.args(&["nginx", "reload"])
.status()?;
crate::ensure_code!(
svc_exit.success(),
crate::error::GENERAL_ERROR,
"Failed to Reload Nginx: {}",
svc_exit
.code()
.or_else(|| { svc_exit.signal().map(|a| 128 + a) })
.unwrap_or(0)
);
Ok(())
}
}

BIN
eos.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 657 KiB

View File

@@ -3,6 +3,7 @@ set -e
echo "turn off mocks"
echo "$( jq '.useMocks = false' use-mocks.json )" > use-mocks.json
echo "$( jq '.skipStartupAlerts = false' use-mocks.json )" > use-mocks.json
echo "FILTER: rm -rf www"
rm -rf www

View File

@@ -1,6 +1,6 @@
manifest-version: 0
app-id: start9-ambassador
app-version: 0.2.8
app-version: 0.2.9
uri-rewrites:
- =/api -> http://{{start9-ambassador}}:5959/authenticate
- /api/ -> http://{{start9-ambassador}}:5959/

8
ui/package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "embassy-ui",
"version": "0.2.8",
"version": "0.2.9",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -7903,9 +7903,9 @@
}
},
"marked": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/marked/-/marked-1.2.5.tgz",
"integrity": "sha512-2AlqgYnVPOc9WDyWu7S5DJaEZsfk6dNh/neatQ3IHUW4QLutM/VPSH9lG7bif+XjFWc9K9XR3QvR+fXuECmfdA=="
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-2.0.0.tgz",
"integrity": "sha512-NqRSh2+LlN2NInpqTQnS614Y/3NkVMFFU6sJlRFEpxJ/LHuK/qJECH7/fXZjk4VZstPW/Pevjil/VtSONsLc7Q=="
},
"md5.js": {
"version": "1.3.5",

View File

@@ -1,6 +1,6 @@
{
"name": "embassy-ui",
"version": "0.2.8",
"version": "0.2.9",
"description": "GUI for EmbassyOS",
"author": "Start9 Labs",
"homepage": "https://github.com/Start9Labs/embassy-ui",
@@ -36,7 +36,7 @@
"json-pointer": "^0.6.1",
"jsonpointerx": "^1.0.30",
"jsontokens": "^3.0.0",
"marked": "^1.2.0",
"marked": "^2.0.0",
"rxjs": "^6.6.3",
"uuid": "^8.3.1",
"zone.js": "^0.11.2"

View File

@@ -70,7 +70,6 @@
<ion-icon name="arrow-forward"></ion-icon>
<ion-icon name="arrow-up"></ion-icon>
<ion-icon name="bookmark-outline"></ion-icon>
<ion-icon name="cart-outline"></ion-icon>
<ion-icon name="chevron-down"></ion-icon>
<ion-icon name="chevron-up"></ion-icon>
<ion-icon name="close"></ion-icon>
@@ -86,20 +85,22 @@
<ion-icon name="eye-off-outline"></ion-icon>
<ion-icon name="eye-outline"></ion-icon>
<ion-icon name="file-tray-stacked-outline"></ion-icon>
<ion-icon name="flash-outline"></ion-icon>
<ion-icon name="grid-outline"></ion-icon>
<ion-icon name="help-circle-outline"></ion-icon>
<ion-icon name="home-outline"></ion-icon>
<ion-icon name="information-circle-outline"></ion-icon>
<ion-icon name="list-outline"></ion-icon>
<ion-icon name="newspaper-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="notifications-outline"></ion-icon>
<ion-icon name="rocket-outline"></ion-icon>
<ion-icon name="power"></ion-icon>
<ion-icon name="pulse"></ion-icon>
<ion-icon name="qr-code-outline"></ion-icon>
<ion-icon name="globe-outline"></ion-icon>
<ion-icon name="reload-outline"></ion-icon>
<ion-icon name="refresh-outline"></ion-icon>
<ion-icon name="save-outline"></ion-icon>
<ion-icon name="storefront-outline"></ion-icon>
<ion-icon name="terminal-outline"></ion-icon>
<ion-icon name="trash-outline"></ion-icon>
<ion-icon name="warning-outline"></ion-icon>

View File

@@ -42,7 +42,7 @@ export class AppComponent {
{
title: 'Marketplace',
url: '/services/marketplace',
icon: 'cart-outline',
icon: 'storefront-outline',
},
{
title: 'Notifications',

View File

@@ -11,7 +11,7 @@
<ion-slides *ngIf="!($error$ | async)" id="slide-show" style="--bullet-background: white" pager="false">
<ion-slide *ngFor="let slide of params.slideDefinitions">
<dependencies #components *ngIf="slide.selector === 'dependencies'" [params]="slide.params"></dependencies>
<developer-notes #components *ngIf="slide.selector === 'developer-notes'" [params]="slide.params"></developer-notes>
<notes #components *ngIf="slide.selector === 'notes'" [params]="slide.params"></notes>
<dependents #components *ngIf="slide.selector === 'dependents'" [params]="slide.params" [finished]="finished"></dependents>
<complete #components *ngIf="slide.selector === 'complete'" [params]="slide.params" [finished]="finished"></complete>
</ion-slide>
@@ -43,8 +43,8 @@
<ion-text *ngIf="cancel.text as t">{{t}}</ion-text>
<ion-icon *ngIf="!cancel.text" name="close-outline"></ion-icon>
</ion-button>
<ion-button slot="end" *ngIf="currentSlideDef.nextButton as nextButton" (click)="finished({})" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
<ion-button slot="end" *ngIf="currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" [disabled]="$anythingLoading$ | async" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.nextButton as nextButton" (click)="finished({})" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="nextButton.length > 16">{{nextButton}}</ion-text></ion-button>
<ion-button slot="end" *ngIf="!($anythingLoading$ | async) && currentSlideDef.finishButton as finishButton" (click)="finished({ final: true })" fill="outline" class="toolbar-button" color="primary"><ion-text [class.smaller-text]="finishButton.length > 16">{{finishButton}}</ion-text></ion-button>
</ng-container>
<ng-container *ngIf="$error$ | async">
<ion-button slot="start" (click)="finished({ final: true })" style="text-transform: capitalize; font-weight: bolder;" color="danger">Dismiss</ion-button>

View File

@@ -7,7 +7,7 @@ import { SharingModule } from 'src/app/modules/sharing.module'
import { DependenciesComponentModule } from './dependencies/dependencies.component.module'
import { DependentsComponentModule } from './dependents/dependents.component.module'
import { CompleteComponentModule } from './complete/complete.component.module'
import { DeveloperNotesComponentModule } from './developer-notes/developer-notes.component.module'
import { NotesComponentModule } from './notes/notes.component.module'
@NgModule({
declarations: [
@@ -21,7 +21,7 @@ import { DeveloperNotesComponentModule } from './developer-notes/developer-notes
DependenciesComponentModule,
DependentsComponentModule,
CompleteComponentModule,
DeveloperNotesComponentModule,
NotesComponentModule,
],
exports: [InstallWizardComponent],
})

View File

@@ -47,6 +47,7 @@
font-size: small;
border-width: 0px 0px 1px 0px;
border-color: #393b40;
text-align: left;
}
@media (min-width:500px) {
@@ -57,6 +58,7 @@
font-size: medium;
border-width: 0px 0px 1px 0px;
border-color: #393b40;
text-align: left;
}
}

View File

@@ -1,13 +1,13 @@
import { Component, Input, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { Component, Input, NgZone, OnInit, QueryList, ViewChild, ViewChildren } from '@angular/core'
import { IonContent, IonSlides, ModalController } from '@ionic/angular'
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs'
import { map } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup'
import { capitalizeFirstLetter } from 'src/app/util/misc.util'
import { capitalizeFirstLetter, pauseFor } from 'src/app/util/misc.util'
import { CompleteComponent } from './complete/complete.component'
import { DependenciesComponent } from './dependencies/dependencies.component'
import { DependentsComponent } from './dependents/dependents.component'
import { DeveloperNotesComponent } from './developer-notes/developer-notes.component'
import { NotesComponent } from './notes/notes.component'
import { Colorable, Loadable } from './loadable'
import { WizardAction } from './wizard-types'
@@ -50,7 +50,7 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
$currentColor$: BehaviorSubject<string> = new BehaviorSubject('medium')
$error$ = new BehaviorSubject(undefined)
constructor (private readonly modalController: ModalController) { super() }
constructor (private readonly modalController: ModalController, private readonly zone: NgZone) { super() }
ngOnInit () { }
ngAfterViewInit () {
@@ -80,15 +80,15 @@ export class InstallWizardComponent extends Cleanup implements OnInit {
private async slide () {
if (this.slideComponents[this.slideIndex + 1] === undefined) { return this.finished({ final: true }) }
this.slideIndex += 1
this.currentSlide.load()
await this.slideContainer.lockSwipes(false)
await Promise.all([
this.contentContainer.scrollToTop(),
this.slideContainer.slideNext(500),
])
await this.slideContainer.lockSwipes(true)
this.slideContainer.update()
this.zone.run(async () => {
this.slideComponents[this.slideIndex + 1].load()
await pauseFor(50)
this.slideIndex += 1
await this.slideContainer.lockSwipes(false)
await this.contentContainer.scrollToTop()
await this.slideContainer.slideNext(500)
await this.slideContainer.lockSwipes(true)
})
}
}
@@ -114,8 +114,8 @@ export type SlideDefinition = SlideCommon & (
selector: 'complete',
params: CompleteComponent['params']
} | {
selector: 'developer-notes',
params: DeveloperNotesComponent['params']
selector: 'notes',
params: NotesComponent['params']
}
)

View File

@@ -2,11 +2,9 @@
<div style="margin-top: 25px;">
<div style="margin: 15px; display: flex; justify-content: center; align-items: center;">
<ion-label [color]="$color$ | async" style="font-size: xx-large; font-weight: bold;">
Warning
{{params.title}}
</ion-label>
</div>
<div class="long-message">
{{params.developerNotes}}
</div>
<div class="long-message" [innerHTML]="params.notes | markdown"></div>
</div>
</div>

View File

@@ -1,13 +1,13 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { DeveloperNotesComponent } from './developer-notes.component'
import { NotesComponent } from './notes.component'
import { IonicModule } from '@ionic/angular'
import { RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({
declarations: [
DeveloperNotesComponent,
NotesComponent,
],
imports: [
CommonModule,
@@ -15,6 +15,6 @@ import { SharingModule } from 'src/app/modules/sharing.module'
RouterModule.forChild([]),
SharingModule,
],
exports: [DeveloperNotesComponent],
exports: [NotesComponent],
})
export class DeveloperNotesComponentModule { }
export class NotesComponentModule { }

View File

@@ -4,24 +4,24 @@ import { Colorable, Loadable } from '../loadable'
import { WizardAction } from '../wizard-types'
@Component({
selector: 'developer-notes',
templateUrl: './developer-notes.component.html',
selector: 'notes',
templateUrl: './notes.component.html',
styleUrls: ['../install-wizard.component.scss'],
})
export class DeveloperNotesComponent implements OnInit, Loadable, Colorable {
export class NotesComponent implements OnInit, Loadable, Colorable {
@Input() params: {
action: WizardAction
developerNotes: string
notes: string
title: string
titleColor: string
}
$loading$ = new BehaviorSubject(false)
$color$ = new BehaviorSubject('warning')
$color$ = new BehaviorSubject('light')
$cancel$ = new Subject<void>()
load () { }
constructor () { }
ngOnInit () {
console.log('Developer Notes', this.params)
}
ngOnInit () { this.$color$.next(this.params.titleColor) }
}

View File

@@ -1,5 +1,6 @@
import { Injectable } from '@angular/core'
import { AppModel, AppStatus } from 'src/app/models/app-model'
import { OsUpdateService } from 'src/app/services/os-update.service'
import { exists } from 'src/app/util/misc.util'
import { AppDependency, DependentBreakage, AppInstalledPreview } from '../../models/app-types'
import { ApiService } from '../../services/api/api.service'
@@ -7,7 +8,11 @@ import { InstallWizardComponent, SlideDefinition, TopbarParams } from './install
@Injectable({ providedIn: 'root' })
export class WizardBaker {
constructor (private readonly apiService: ApiService, private readonly appModel: AppModel) { }
constructor (
private readonly apiService: ApiService,
private readonly updateService: OsUpdateService,
private readonly appModel: AppModel
) { }
install (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
@@ -23,8 +28,8 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, developerNotes: installAlert,
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Install', params: {
action, title, version, serviceRequirements,
@@ -52,8 +57,8 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, developerNotes: installAlert,
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update', params: {
action, title, version, serviceRequirements,
@@ -70,6 +75,26 @@ export class WizardBaker {
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
updateOS (values: {
version: string, releaseNotes: string
}): InstallWizardComponent['params'] {
const { version, releaseNotes } = values
const action = 'update'
const title = 'EmbassyOS'
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Update OS', params: {
action, notes: releaseNotes || 'No release notes for this version', title: 'Release Notes', titleColor: 'dark',
}},
{ selector: 'complete', finishButton: 'Dismiss', cancelButton: { whileLoading: { } }, params: {
action, verb: 'beginning update for', title, executeAction: () => this.updateService.updateEmbassyOS(version),
}},
]
return { toolbar, slideDefinitions: slideDefinitions.filter(exists) }
}
downgrade (values: {
id: string, title: string, version: string, serviceRequirements: AppDependency[], installAlert?: string
}): InstallWizardComponent['params'] {
@@ -84,8 +109,8 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
installAlert ? { selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, developerNotes: installAlert,
installAlert ? { selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Next', params: {
action, notes: installAlert, title: 'Warning', titleColor: 'warning',
}} : undefined,
{ selector: 'dependencies', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Downgrade', params: {
action, title, version, serviceRequirements,
@@ -115,8 +140,8 @@ export class WizardBaker {
const toolbar: TopbarParams = { action, title, version }
const slideDefinitions: SlideDefinition[] = [
{ selector: 'developer-notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
action, developerNotes: uninstallAlert || defaultUninstallationWarning(title) },
{ selector: 'notes', cancelButton: { afterLoading: { text: 'Cancel' } }, nextButton: 'Continue', params: {
action, notes: uninstallAlert || defaultUninstallationWarning(title), title: 'Warning', titleColor: 'warning' },
},
{ selector: 'dependents', cancelButton: { whileLoading: { }, afterLoading: { text: 'Cancel' } }, nextButton: 'Uninstall', params: {
action, verb: 'uninstalling', title, fetchBreakages: () => this.apiService.uninstallApp(id, true).then( ({ breakages }) => breakages ),

View File

@@ -1,5 +1,5 @@
<ion-item button lines="none" *ngIf="updateAvailable$ | async as version" (click)="confirmUpdate(version)">
<ion-item button lines="none" *ngIf="updateAvailable$ | async as res" (click)="confirmUpdate(res)">
<ion-label>
New EmbassyOS Version {{version | displayEmver}} Available!
New EmbassyOS Version {{res.versionLatest | displayEmver}} Available!
</ion-label>
</ion-item>

View File

@@ -1,9 +1,10 @@
import { Component } from '@angular/core'
import { OsUpdateService } from 'src/app/services/os-update.service'
import { Observable } from 'rxjs'
import { AlertController } from '@ionic/angular'
import { LoaderService } from 'src/app/services/loader.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { ModalController } from '@ionic/angular'
import { WizardBaker } from '../install-wizard/prebaked-wizards'
import { wizardModal } from '../install-wizard/install-wizard.component'
import { ReqRes } from 'src/app/services/api/api.service'
@Component({
selector: 'update-os-banner',
@@ -11,38 +12,24 @@ import { displayEmver } from 'src/app/pipes/emver.pipe'
styleUrls: ['./update-os-banner.component.scss'],
})
export class UpdateOsBannerComponent {
updateAvailable$: Observable<undefined | string>
updateAvailable$: Observable<undefined | ReqRes.GetVersionLatestRes>
constructor (
private readonly osUpdateService: OsUpdateService,
private readonly alertCtrl: AlertController,
private readonly loader: LoaderService,
private readonly modalCtrl: ModalController,
private readonly wizardBaker: WizardBaker,
) {
this.updateAvailable$ = this.osUpdateService.watchForUpdateAvailable$()
}
ngOnInit () { }
async confirmUpdate (versionLatest: string) {
const alert = await this.alertCtrl.create({
header: `Update EmbassyOS`,
message: `Update EmbassyOS to version ${displayEmver(versionLatest)}?`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Update',
handler: () => this.update(versionLatest),
},
],
})
await alert.present()
}
private async update (versionLatest: string) {
return this.loader.displayDuringP(
this.osUpdateService.updateEmbassyOS(versionLatest),
async confirmUpdate (res: ReqRes.GetVersionLatestRes) {
await wizardModal(
this.modalCtrl,
this.wizardBaker.updateOS({
version: res.versionLatest,
releaseNotes: res.releaseNotes,
}),
)
}
}

View File

@@ -1,15 +0,0 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { AppReleaseNotesPage } from './app-release-notes.page'
import { SharingModule } from 'src/app/modules/sharing.module'
@NgModule({
imports: [
CommonModule,
IonicModule,
SharingModule,
],
declarations: [AppReleaseNotesPage],
})
export class AppReleaseNotesPageModule { }

View File

@@ -1,14 +0,0 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button (click)="dismiss()">
<ion-icon slot="icon-only" name="close"></ion-icon>
</ion-button>
</ion-buttons>
<ion-title style="font-size: 16px;">{{ version }} Release Notes</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div [innerHTML]="releaseNotes | markdown"></div>
</ion-content>

View File

@@ -1,20 +0,0 @@
import { Component, Input } from '@angular/core'
import { ModalController } from '@ionic/angular'
@Component({
selector: 'app-release-notes',
templateUrl: './app-release-notes.page.html',
styleUrls: ['./app-release-notes.page.scss'],
})
export class AppReleaseNotesPage {
@Input() releaseNotes: string
@Input() version: string
constructor (
private readonly modalCtrl: ModalController,
) { }
dismiss () {
this.modalCtrl.dismiss()
}
}

View File

@@ -1,37 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-title >
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to {{ version }}!</ion-label>
<ion-label style="font-size: 20px;" class="ion-text-wrap">Welcome to 0.2.9!</ion-label>
</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div style="display: flex; flex-direction: column; justify-content: space-between; height: 100%">
<div>
<h2>Highlights</h2>
<p>
0.2.8 is a small but important update designed to enhance awareness around potential pitfalls of using certain services.
It introduces warnings for installing, uninstalling, backing up, and restoring backups of stateful services such as LND or c-lightning.
Additionally, it draws a distinction between services that are designed to be launched inside the browser and those that are designed to run in the background
</p>
<p>
0.2.8 also introduces automatic update checks. With this enabled, each time you visit your embassy you will be notified if new Embassy OS or service versions are available. This setting can be edited in your Embassy Config page.
<ion-item lines="none" style="--border-radius: var(--icon-border-radius); margin-top: 15px">
<ion-label>Auto Check for Updates</ion-label>
<ion-toggle slot="end" [(ngModel)]="autoCheckUpdates"></ion-toggle>
</ion-item>
</p>
<div style="margin-top: 30px">
<h5 style="color: var(--ion-color-danger)">Important</h5>
<p>
If you have LND or c-lightning installed, please update them to the latest versions.
An oversight in Start9s USB backups system has created a situation where <b>restoring</b> a LND or c-lightning backup could potentially result in permanent loss of channel funds.
To be clear, <ion-text style="font-weight: 'bold';">DO NOT</ion-text> attempt to <b>restore</b> a LND or c-lightning backup until you have updated to the latest versions.
</p>
</div>
</div>
<h2>Highlights</h2>
<p class="main-content">
0.2.9 introduces LAN support for services running on the Embassy. A service's LAN address (.local URL) can be accessed while connected to the same network.
This is useful for two reasons: (1) LAN connections are significantly faster than Tor, and (2) if the Tor network is experiencing connectivity issues, you will not be locked out of your services.
</p>
<div class="close-button">
<ion-button fill="outline" (click)="dismiss()">

View File

@@ -3,6 +3,10 @@
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 100px;
}
.main-content {
height: 100%;
color: var(--ion-color-medium);
}

View File

@@ -12,24 +12,17 @@ import { ConfigService } from 'src/app/services/config.service'
export class OSWelcomePage {
@Input() version: string
autoCheckUpdates = true
constructor (
private readonly modalCtrl: ModalController,
private readonly apiService: ApiService,
private readonly serverModel: ServerModel,
private readonly config: ConfigService,
) { }
async dismiss () {
this.apiService
.patchServerConfig('autoCheckUpdates', this.autoCheckUpdates)
.then(() => this.serverModel.update({ autoCheckUpdates: this.autoCheckUpdates }))
.then(() => this.apiService.acknowledgeOSWelcome(this.config.version))
.catch(console.error)
this.apiService.acknowledgeOSWelcome(this.config.version).catch(console.error)
// return false to skip subsequent alert modals (e.g. check for updates modals)
// return true to show subsequent alert modals
return this.modalCtrl.dismiss(this.autoCheckUpdates)
return this.modalCtrl.dismiss(true)
}
}

View File

@@ -149,6 +149,7 @@ function emptyAppInstalledFull (): Omit<AppInstalledFull, keyof AppInstalledPrev
lastBackup: null,
configuredRequirements: null,
hasFetchedFull: false,
actions: [],
}
}

View File

@@ -34,21 +34,36 @@ export interface AppAvailableVersionSpecificInfo {
// installed
export interface AppInstalledPreview extends BaseApp {
lanAddress?: string
torAddress: string
versionInstalled: string
ui: boolean
lanUi: boolean
torUi: boolean
// FE state only
hasUI: boolean
launchable: boolean
}
export interface AppInstalledFull extends AppInstalledPreview {
instructions: string | null
lastBackup: string | null
configuredRequirements: AppDependency[] | null // null if not yet configured
hasFetchedFull: boolean
startAlert?: string
uninstallAlert?: string
restoreAlert?: string
actions: Actions
// FE state only
hasFetchedFull: boolean
}
// dependencies
export type Actions = ServiceAction[]
export interface ServiceAction {
id: string,
name: string,
description: string,
warning?: string
allowedStatuses: AppStatus[]
}
export interface AppDependency extends InstalledAppDependency {
// explanation of why it *is* optional. null represents it is required.
optional: string | null

View File

@@ -115,7 +115,7 @@ export interface S9Server {
name: string
origin: string
versionInstalled: string
versionLatest: string | undefined
versionLatest: string | undefined // not on the api as of 0.2.8
status: ServerStatus
badge: number
alternativeRegistryUrl: string | null

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppActionsPage } from './app-actions.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
{
path: '',
component: AppActionsPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
QRComponentModule,
SharingModule,
],
declarations: [AppActionsPage],
})
export class AppActionsPageModule { }

View File

@@ -0,0 +1,41 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Actions</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
<ng-container *ngIf="!($loading$ | async) && {
title: app.title | async,
versionInstalled: app.versionInstalled | async,
status: app.status | async,
actions: app.actions | async
} as vars">
<ion-item *ngIf="error" style="margin-bottom: 16px;">
<ion-text class="ion-text-wrap" color="danger">{{ error }}</ion-text>
</ion-item>
<!-- no metrics -->
<ion-item *ngIf="!vars.actions.length">
<ion-label class="ion-text-wrap">
<p>No Actions for {{ vars.title }} {{ vars.versionInstalled }}.</p>
</ion-label>
</ion-item>
<!-- actions -->
<ion-item-group>
<ion-item button *ngFor="let action of vars.actions" (click)="handleAction(action)" >
<ion-label class="ion-text-wrap">
<h2><ion-text color="primary">{{ action.name }}</ion-text><ion-icon *ngIf="!(action.allowedStatuses | includes: vars.status)" color="danger" name="close-outline"></ion-icon></h2>
<p><ion-text color="dark">{{ action.description }}</ion-text></p>
</ion-label>
</ion-item>
</ion-item-group>
</ng-container>
</ion-content>

View File

@@ -0,0 +1,120 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { ApiService, isRpcFailure, isRpcSuccess } from 'src/app/services/api/api.service'
import { BehaviorSubject } from 'rxjs'
import { AlertController } from '@ionic/angular'
import { ModelPreload } from 'src/app/models/model-preload'
import { LoaderService, markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { ServiceAction, AppInstalledFull } from 'src/app/models/app-types'
import { PropertySubject } from 'src/app/util/property-subject.util'
import { map } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup'
import { AppStatus } from 'src/app/models/app-model'
import { HttpErrorResponse } from '@angular/common/http'
@Component({
selector: 'app-actions',
templateUrl: './app-actions.page.html',
styleUrls: ['./app-actions.page.scss'],
})
export class AppActionsPage extends Cleanup {
error = ''
$loading$ = new BehaviorSubject(true)
appId: string
app: PropertySubject<AppInstalledFull>
constructor(
private readonly route: ActivatedRoute,
private readonly apiService: ApiService,
private readonly alertCtrl: AlertController,
private readonly preload: ModelPreload,
private readonly loaderService: LoaderService,
) { super() }
ngOnInit() {
this.appId = this.route.snapshot.paramMap.get('appId')
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId)).pipe(
map(app => this.app = app),
).subscribe({ error: e => this.error = e.message })
}
async handleAction(action: ServiceAction) {
if (action.allowedStatuses.includes(this.app.status.getValue())) {
const alert = await this.alertCtrl.create({
header: 'Confirm',
message: `Are you sure you want to execute action "${action.name}"? ${action.warning ? action.warning : ""}`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Execute',
handler: () => {
this.executeAction(action)
},
},
],
})
await alert.present()
} else {
const joinStatuses = (statuses: AppStatus[]) => {
const last = statuses.pop()
let s = statuses.join(', ')
if (last) {
if (statuses.length > 1) { // oxford comma
s += ','
}
s += ` or ${last}`
}
return s
}
const alert = await this.alertCtrl.create({
header: 'Forbidden',
message: `Action "${action.name}" can only be executed when service is ${joinStatuses(action.allowedStatuses)}`,
buttons: ['OK'],
cssClass: 'alert-error-message',
})
await alert.present()
}
}
private async executeAction(action: ServiceAction) {
try {
const res = await this.loaderService.displayDuringP(
this.apiService.serviceAction(this.appId, action),
)
if (isRpcFailure(res)) {
this.presentAlertActionFail(res.error.code, res.error.message)
}
if (isRpcSuccess(res)) {
const successAlert = await this.alertCtrl.create({
header: 'Execution Complete',
message: res.result.split('\n').join('</br ></br />'),
buttons: ['OK'],
cssClass: 'alert-success-message',
})
return await successAlert.present()
}
} catch (e) {
if (e instanceof HttpErrorResponse) {
this.presentAlertActionFail(e.status, e.message)
} else {
this.presentAlertActionFail(-1, e.message || JSON.stringify(e))
}
}
}
private async presentAlertActionFail(code: number, message: string): Promise<void> {
const failureAlert = await this.alertCtrl.create({
header: 'Execution Failed',
message: `Error code ${code}. ${message}`,
buttons: ['OK'],
cssClass: 'alert-error-message',
})
return await failureAlert.present()
}
}

View File

@@ -12,7 +12,6 @@ import { RecommendationButtonComponentModule } from 'src/app/components/recommen
import { InstallWizardComponentModule } from 'src/app/components/install-wizard/install-wizard.component.module'
import { ErrorMessageComponentModule } from 'src/app/components/error-message/error-message.component.module'
import { InformationPopoverComponentModule } from 'src/app/components/information-popover/information-popover.component.module'
import { AppReleaseNotesPageModule } from 'src/app/modals/app-release-notes/app-release-notes.module'
const routes: Routes = [
{
@@ -35,7 +34,6 @@ const routes: Routes = [
InstallWizardComponentModule,
ErrorMessageComponentModule,
InformationPopoverComponentModule,
AppReleaseNotesPageModule,
],
declarations: [AppAvailableShowPage],
})

View File

@@ -53,13 +53,18 @@
Install
</ion-button>
<div *ngIf="vars.versionInstalled && vars.status !== 'INSTALLING' ">
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
Update to {{ vars.versionViewing | displayEmver }}
</ion-button>
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
Downgrade to {{ vars.versionViewing | displayEmver }}
<div *ngIf="vars.versionInstalled">
<ion-button class="main-action-button" expand="block" fill="outline" [routerLink]="['/services', 'installed', vars.id]">
Go to Service
</ion-button>
<div *ngIf="vars.status !== 'INSTALLING' ">
<ion-button *ngIf="installedStatus === 'installed-below'" class="main-action-button" expand="block" fill="outline" color="success" (click)="update('update')">
Update to {{ vars.versionViewing | displayEmver }}
</ion-button>
<ion-button *ngIf="installedStatus === 'installed-above'" class="main-action-button" expand="block" fill="outline" color="warning" (click)="update('downgrade')">
Downgrade to {{ vars.versionViewing | displayEmver }}
</ion-button>
</div>
</div>
<ion-item-group>
@@ -81,6 +86,14 @@
</ion-item>
</ng-container>
<ion-item-divider style="color: var(--ion-color-dark); font-weight: bold;">New in {{ vars.versionViewing | displayEmver }}</ion-item-divider>
<ion-item lines="none">
<ion-label *ngIf="!($newVersionLoading$ | async)" style="display: flex; align-items: center; justify-content: space-between;" class="ion-text-wrap" >
<div id='release-notes'color="dark" [innerHTML]="vars.releaseNotes | markdown"></div>
</ion-label>
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
</ion-item>
<ion-item-divider class="divider">Description</ion-item-divider>
<ion-item lines="none">
<ion-label class="ion-text-wrap">
@@ -90,13 +103,6 @@
</ion-label>
</ion-item>
<ion-item-divider>Release Notes</ion-item-divider>
<ion-item lines="none" button details="true" [disabled]="" (click)="presentModalReleaseNotes()" [disabled]="$newVersionLoading$ | async">
<ion-icon slot="start" name="newspaper-outline" color="medium"></ion-icon>
<ion-label *ngIf="!($newVersionLoading$ | async)"><ion-text color="medium">New in {{ vars.versionViewing | displayEmver }}</ion-text></ion-label>
<ion-spinner style="display: block; margin: auto;" name="lines" color="dark" *ngIf="$newVersionLoading$ | async"></ion-spinner>
</ion-item>
<ng-container *ngIf="(vars.serviceRequirements)?.length">
<ion-item-divider class="divider">Service Dependencies
<ion-button style="position: relative; right: 10px;" size="small" fill="clear" color="medium" (click)="presentPopover(serviceDependencyDefintion, $event)">

View File

@@ -1,22 +1,7 @@
// .recommendation-container {
// margin-top: 3px;
// display: grid;
// grid-template-columns: auto auto;
// justify-content: start;
// grid-column-gap: 5px;
// align-items: center;
// }
.recommendation-text {
font-style: italic;
}
// @media (min-width:1000px) {
// .recommendation-text {
// font-size: small;
// }
// }
.recommendation-error {
color: var(--ion-color-danger);
}
@@ -31,4 +16,9 @@
font-size: medium;
padding-left: 10px;
font-weight: unset;
}
#release-notes {
overflow: auto;
max-height: 160px;
}

View File

@@ -1,10 +1,10 @@
import { Component, HostListener, NgZone } from '@angular/core'
import { Component, NgZone } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { AppAvailableFull, AppAvailableVersionSpecificInfo, AppDependency } from 'src/app/models/app-types'
import { AppAvailableFull, AppAvailableVersionSpecificInfo } from 'src/app/models/app-types'
import { ApiService } from 'src/app/services/api/api.service'
import { AlertController, ModalController, NavController, PopoverController } from '@ionic/angular'
import { markAsLoadingDuring$ } from 'src/app/services/loader.service'
import { BehaviorSubject, from, merge, Observable, of, Subscription } from 'rxjs'
import { BehaviorSubject, from, Observable, of } from 'rxjs'
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
import { Recommendation } from 'src/app/components/recommendation-button/recommendation-button.component'
import { wizardModal } from 'src/app/components/install-wizard/install-wizard.component'
@@ -13,8 +13,6 @@ import { AppModel } from 'src/app/models/app-model'
import { initPropertySubject, peekProperties, PropertySubject } from 'src/app/util/property-subject.util'
import { Cleanup } from 'src/app/util/cleanup'
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
import { pauseFor } from 'src/app/util/misc.util'
import { AppReleaseNotesPage } from 'src/app/modals/app-release-notes/app-release-notes.page'
import { Emver } from 'src/app/services/emver.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
@@ -40,8 +38,6 @@ export class AppAvailableShowPage extends Cleanup {
serviceDependencyDefintion = '<span style="font-style: italic">Service Dependencies</span> are other services that this service recommends or requires in order to run.'
showMoreReleaseNotes = false
constructor (
private readonly route: ActivatedRoute,
private readonly apiService: ApiService,
@@ -192,20 +188,6 @@ export class AppAvailableShowPage extends Cleanup {
}
}
async presentModalReleaseNotes () {
const { releaseNotes, versionViewing } = peekProperties(this.$app$)
const modal = await this.modalCtrl.create({
component: AppReleaseNotesPage,
componentProps: {
releaseNotes,
version: versionViewing,
},
})
await modal.present()
}
private fetchRecommendation (): Observable<any> {
this.recommendation = history.state && history.state.installationRecommendation

View File

@@ -22,26 +22,32 @@
<ion-grid>
<ion-row>
<ion-col *ngFor="let app of apps" sizeXs="4" sizeSm="3" sizeMd="2" sizeLg="2">
<ng-container *ngIf="{ tor: app.subject.torAddress | async, status: app.subject.status | async, ui: app.subject.ui | async, iconURL: app.subject.iconURL | async | iconParse, title: app.subject.title | async } as vars" >
<ng-container *ngIf="{
status: app.subject.status | async,
hasUI: app.subject.hasUI | async,
launchable: app.subject.launchable | async,
iconURL: app.subject.iconURL | async | iconParse,
title: app.subject.title | async
} as vars">
<ion-card class="installed-card" [class.installed-card-on]="vars.status === 'RUNNING'" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
<div class="launch-container" *ngIf="vars.ui && !isConsulate">
<div class="launch-button-triangle" (click)="launchUiTab(vars.tor, $event)" [class.disabled]="vars.status !== AppStatus.RUNNING || !isTor">
<ion-icon class="launch-button-triangle-icon" name="globe-outline"></ion-icon>
<ion-card class="installed-card" style="position:relative" [routerLink]="['/services', 'installed', app.id]">
<div class="launch-container" *ngIf="vars.hasUI">
<div class="launch-button-triangle" (click)="launchUiTab(app.id, $event)" [class.disabled]="!vars.launchable">
<ion-icon name="rocket-outline"></ion-icon>
</div>
</div>
</div>
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="app.subject.title | async" />
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
<ion-card-header>
<status [appStatus]="vars.status" size="small"></status>
<p>{{ vars.title }}</p>
</ion-card-header>
</ion-card>
<img style="position: absolute" class="main-img" [src]="vars.iconURL" [alt]="vars.title" />
<img class="main-img" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=">
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'green'" src="assets/img/running-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'red'" src="assets/img/issue-bulb.png"/>
<img class="bulb-on" *ngIf="vars.status | displayBulb: 'yellow'" src="assets/img/warning-bulb.png"/>
<img class="bulb-off" *ngIf="vars.status | displayBulb: 'off'" src="assets/img/off-bulb.png"/>
<ion-card-header>
<status [appStatus]="vars.status" size="small"></status>
<p>{{ vars.title }}</p>
</ion-card-header>
</ion-card>
</ng-container>
</ion-col>
</ion-row>
@@ -53,7 +59,7 @@
<p class="ion-text-wrap">Get started by installing your first service.</p>
</div>
<ion-button [routerLink]="['/services','marketplace']" style="width: 50%;" fill="outline">
<ion-icon slot="start" name="cart-outline"></ion-icon>
<ion-icon slot="start" name="storefront-outline"></ion-icon>
Marketplace
</ion-button>
</div>

View File

@@ -35,19 +35,15 @@ export class AppInstalledListPage extends Cleanup {
segmentValue: 'services' | 'embassy' = 'services'
showCertDownload : boolean
isConsulate: boolean
isTor: boolean
constructor (
private readonly serverModel: ServerModel,
private readonly appModel: AppModel,
private readonly preload: ModelPreload,
private readonly syncDaemon: SyncDaemon,
config: ConfigService,
private readonly config: ConfigService,
) {
super()
this.isConsulate = config.isConsulateAndroid || config.isConsulateIos
this.isTor = config.isTor()
}
ngOnDestroy () {
@@ -96,14 +92,21 @@ export class AppInstalledListPage extends Cleanup {
this.error = e.message
},
})
}
async launchUiTab (address: string, event: Event) {
async launchUiTab (id: string, event: Event) {
event.preventDefault()
event.stopPropagation()
address = address.startsWith('http') ? address : `http://${address}`
return window.open(address, '_blank')
const app = this.apps.find(app => app.id === id).subject
let uiAddress: string
if (this.config.isTor()) {
uiAddress = `http://${app.torAddress.getValue()}`
} else {
uiAddress = `https://${app.lanAddress.getValue()}`
}
return window.open(uiAddress, '_blank')
}
async doRefresh (event: any) {

View File

@@ -20,10 +20,11 @@
hasFetchedFull: app.hasFetchedFull | async,
iconURL: app.iconURL | async,
title: app.title | async,
ui: app.ui | async
hasUI: app.hasUI | async,
launchable: app.launchable | async,
lanAddress: app.lanAddress | async
} as vars" class="ion-padding-bottom">
<ion-spinner *ngIf="$loading$ | async" class="center" name="lines" color="warning"></ion-spinner>
<ng-container *ngIf="!($loading$ | async)">
<ion-refresher *ngIf="app && app.id" slot="fixed" (ionRefresh)="doRefresh($event)">
<ion-refresher-content pullingIcon="lines" refreshingSpinner="lines"></ion-refresher-content>
@@ -53,62 +54,67 @@
</ion-label>
</ion-item>
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px">
<ion-item class="no-cushion-item" lines=none style="margin-bottom: 10px;">
<ion-label class="status-readout">
<status size="bold-large" [appStatus]="vars.status"></status>
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
<ion-button *ngIf="vars.status === AppStatus.NEEDS_CONFIG" expand="block" fill="outline" [routerLink]="['config']">
Configure
</ion-button>
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" (click)="stop()">
<ion-button *ngIf="[AppStatus.RUNNING, AppStatus.CRASHED, AppStatus.PAUSED, AppStatus.RESTARTING] | includes: vars.status" expand="block" fill="outline" color="danger" (click)="stop()">
Stop
</ion-button>
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
<ion-button *ngIf="vars.status === AppStatus.CREATING_BACKUP" expand="block" fill="outline" (click)="presentAlertStopBackup()">
Stop Backup
</ion-button>
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
<ion-button *ngIf="vars.status === AppStatus.DEAD" expand="block" fill="outline" (click)="uninstall()">
Force Uninstall
</ion-button>
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
<ion-button *ngIf="vars.status === AppStatus.BROKEN_DEPENDENCIES" expand="block" fill="outline" (click)="scrollToRequirements()">
Fix
</ion-button>
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" (click)="start()">
<ion-button *ngIf="vars.status === AppStatus.STOPPED" expand="block" fill="outline" color="success" (click)="tryStart()">
Start
</ion-button>
</ion-label>
</ion-item>
<ion-item class="no-cushion-item" *ngIf="vars.ui && !isConsulate" lines="none">
<ion-label style="margin-bottom: 10px; margin-top: 0px; display: flex; justify-content: left; align-items: center;" class="ion-text-wrap">
<ion-button fill="clear" size="small" class="launch-explanation-button" (click)="presentLaunchPopover(vars.status, $event)">
<ion-icon color="medium" name="information-circle-outline">
</ion-icon>
</ion-button>
<ion-button [disabled]="vars.status !== 'RUNNING' || !isTor" class="launch-button" [class.launch-button-off]="vars.status !== 'RUNNING' || !isTor" (click)="launchUiTab()">
<ion-icon style="position: absolute; z-index: 1; left: 0;" name="globe-outline"></ion-icon>
<ion-text>LAUNCH</ion-text>
</ion-button>
</ion-label>
</ion-item>
<ion-button size="small" *ngIf="vars.hasUI" [disabled]="!vars.launchable" class="launch-button" expand="block" (click)="launchUiTab()">
Launch Web Interface
<ion-icon slot="end" name="rocket-outline"></ion-icon>
</ion-button>
</div>
<ng-container *ngIf="app && app.id && vars.status !== 'INSTALLING'">
<ion-item-group class="ion-padding-bottom">
<ion-item-divider>Tor Address</ion-item-divider>
<ion-item lines="none">
<ion-label style="display: flex; justify-content: space-between; align-items: center;" class="ion-text-wrap">
<p style="color: var(--ion-color-dark)">{{ vars.torAddress }}</p>
<ion-button slot="end" fill="clear" (click)="copyTor()">
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
</ion-button>
<!-- addresses -->
<ion-item>
<ion-label class="ion-text-wrap">
<h2>Tor Address</h2>
<p>{{ vars.torAddress }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copyTor()">
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
</ion-button>
</ion-item>
<ion-item lines="none">
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p *ngIf="!hideLAN">{{ vars.lanAddress }}</p>
<p *ngIf="hideLAN"><ion-text color="warning">No LAN address for {{ vars.title }} {{ vars.versionInstalled }}</ion-text></p>
</ion-label>
<ion-button *ngIf="!hideLAN" slot="end" fill="clear" (click)="copyLAN()">
<ion-icon slot="icon-only" name="copy-outline" color="primary"></ion-icon>
</ion-button>
</ion-item>
<ion-item-divider>Backups</ion-item-divider>
<!-- backups -->
<ion-item-divider></ion-item-divider>
<!-- create backup -->
<ion-item button [disabled]="[AppStatus.RESTORING_BACKUP, AppStatus.CREATING_BACKUP, AppStatus.INSTALLING, AppStatus.RESTARTING, AppStatus.STOPPING] | includes: vars.status" (click)="presentModalBackup('create')">
<ion-icon slot="start" name="save-outline" color="primary"></ion-icon>
<ion-label style="display: flex; flex-direction: column;">
<ion-text color="primary">Create new Backup</ion-text>
<ion-text color="primary">Create Backup</ion-text>
<ion-text color="medium" style="font-size: x-small">
Last Backup: {{vars.lastBackup ? (vars.lastBackup | date: 'short') : 'never'}}
</ion-text>
@@ -120,12 +126,7 @@
<ion-label><ion-text color="primary">Restore from Backup</ion-text></ion-label>
</ion-item>
<ion-item-divider>General</ion-item-divider>
<!-- instructions -->
<ion-item button details="true" (click)="checkForUpdates()">
<ion-icon slot="start" name="refresh-outline" color="primary"></ion-icon>
<ion-label><ion-text style="font-weight: 500;" color="primary">Check for Updates</ion-text></ion-label>
</ion-item>
<ion-item-divider></ion-item-divider>
<!-- instructions -->
<ion-item [routerLink]="['instructions']">
<ion-icon slot="start" name="list-outline" color="primary"></ion-icon>
@@ -141,6 +142,11 @@
<ion-icon slot="start" name="information-circle-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Properties</ion-text></ion-label>
</ion-item>
<!-- actions -->
<ion-item [routerLink]="['actions']">
<ion-icon slot="start" name="flash-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Actions</ion-text></ion-label>
</ion-item>
<!-- logs -->
<ion-item [disabled]="vars.status === AppStatus.INSTALLING" [routerLink]="['logs']">
<ion-icon slot="start" name="newspaper-outline" color="primary"></ion-icon>
@@ -148,7 +154,7 @@
</ion-item>
<!-- marketplace -->
<ion-item lines="none" [routerLink]="['/services', 'marketplace', appId]">
<ion-icon slot="start" name="cart-outline" color="primary"></ion-icon>
<ion-icon slot="start" name="storefront-outline" color="primary"></ion-icon>
<ion-label><ion-text color="primary">Marketplace Listing</ion-text></ion-label>
</ion-item>

View File

@@ -40,22 +40,11 @@
}
.launch-button {
width: 100%;
padding: 0px 10px;
--background: linear-gradient(200deg, rgb(70 193 255), rgb(70 193 255 / 45%));
width: calc(100% - 32px);
border-radius: 8px;
--border-radius: 8px;
}
.launch-button-off {
--background: #383838;
color: var(--ion-color-medium)
}
.launch-explanation-button {
position: absolute;
z-index: 1;
right: -2px;
--border-radius: 100px;
--background: rgb(70 193 255 / 75%);
--background-hover: rgb(70 193 255);
--background-hover-opacity: 100%;
--border-style: none;
--color: white;
--border-radius: 10px;
margin: 12px 10px;
}

View File

@@ -16,8 +16,6 @@ import { WizardBaker } from 'src/app/components/install-wizard/prebaked-wizards'
import { catchError, concatMap, filter, switchMap, tap } from 'rxjs/operators'
import { Cleanup } from 'src/app/util/cleanup'
import { InformationPopoverComponent } from 'src/app/components/information-popover/information-popover.component'
import { Emver } from 'src/app/services/emver.service'
import { displayEmver } from 'src/app/pipes/emver.pipe'
import { ConfigService } from 'src/app/services/config.service'
@Component({
@@ -34,13 +32,10 @@ export class AppInstalledShowPage extends Cleanup {
appId: string
AppStatus = AppStatus
showInstructions = false
isConsulate: boolean
isTor: boolean
hideLAN: boolean
dependencyDefintion = () => `<span style="font-style: italic">Dependencies</span> are other services which must be installed, configured appropriately, and started in order to start ${this.app.title.getValue()}`
launchDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. If a service does not have this button, you must access it using another interface, such as a mobile app, desktop app, or another service on the Embassy. Please view the instructions for a service for details on how to use it.</p>`
launchOffDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Get your service running in order to launch!</p>`
launchLocalDefinition = `<span style="font-style: italic">Launch A Service</span> <p>This button appears only for services that can be accessed inside the browser. Visit your Embassy at its Tor address to launch this service!</p>`
@ViewChild(IonContent) content: IonContent
@@ -56,12 +51,9 @@ export class AppInstalledShowPage extends Cleanup {
private readonly wizardBaker: WizardBaker,
private readonly appModel: AppModel,
private readonly popoverController: PopoverController,
private readonly emver: Emver,
config: ConfigService,
private readonly config: ConfigService,
) {
super()
this.isConsulate = config.isConsulateIos || config.isConsulateAndroid
this.isTor = config.isTor()
}
async ngOnInit () {
@@ -70,8 +62,12 @@ export class AppInstalledShowPage extends Cleanup {
this.cleanup(
markAsLoadingDuring$(this.$loading$, this.preload.appFull(this.appId))
.pipe(
tap(app => this.app = app),
concatMap(() => this.syncWhenDependencyInstalls()), //must be final in stack
tap(app => {
this.app = app
const appP = peekProperties(this.app)
this.hideLAN = !appP.lanAddress || (appP.id === 'mastodon' && appP.versionInstalled === '3.3.0') // @TODO delete this hack in 0.3.0
}),
concatMap(() => this.syncWhenDependencyInstalls()), // must be final in stack
catchError(e => of(this.setError(e))),
).subscribe(),
)
@@ -103,61 +99,15 @@ export class AppInstalledShowPage extends Cleanup {
}
async launchUiTab () {
let uiAddress = this.app.torAddress.getValue()
uiAddress = uiAddress.startsWith('http') ? uiAddress : `http://${uiAddress}`
let uiAddress: string
if (this.config.isTor()) {
uiAddress = `http://${this.app.torAddress.getValue()}`
} else {
uiAddress = `https://${this.app.lanAddress.getValue()}`
}
return window.open(uiAddress, '_blank')
}
async checkForUpdates () {
const app = peekProperties(this.app)
this.loader.of({
message: `Checking for updates...`,
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync(
async () => {
const { versionLatest } = await this.apiService.getAvailableApp(this.appId)
if (this.emver.compare(versionLatest, app.versionInstalled) === 1) {
this.presentAlertUpdate(app, versionLatest)
} else {
this.presentAlertUpToDate()
}
},
).catch(e => this.setError(e))
}
async presentAlertUpdate (app: AppInstalledFull, versionLatest: string) {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Update Available',
message: `New version ${displayEmver(versionLatest)} found for ${app.title}.`,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'View in Store',
cssClass: 'alert-success',
handler: () => {
this.navCtrl.navigateForward(['/services', 'marketplace', this.appId])
},
},
],
})
await alert.present()
}
async presentAlertUpToDate () {
const alert = await this.alertCtrl.create({
header: 'Up To Date',
message: `You are running the latest version of ${this.app.title.getValue()}!`,
buttons: ['OK'],
})
await alert.present()
}
async copyTor () {
const app = peekProperties(this.app)
let message = ''
@@ -172,6 +122,20 @@ export class AppInstalledShowPage extends Cleanup {
await toast.present()
}
async copyLAN () {
const app = peekProperties(this.app)
let message = ''
await copyToClipboard(app.lanAddress).then(success => { message = success ? 'copied to clipboard!' : 'failed to copy' })
const toast = await this.toastCtrl.create({
header: message,
position: 'bottom',
duration: 1000,
cssClass: 'notification-toast',
})
await toast.present()
}
async stop (): Promise<void> {
const app = peekProperties(this.app)
@@ -200,15 +164,13 @@ export class AppInstalledShowPage extends Cleanup {
}).catch(e => this.setError(e))
}
async start (): Promise<void> {
async tryStart (): Promise<void> {
const app = peekProperties(this.app)
this.loader.of({
message: `Starting ${app.title}...`,
spinner: 'lines',
cssClass: 'loader',
}).displayDuringP(
this.apiService.startApp(this.appId),
).catch(e => this.setError(e))
if (app.startAlert) {
this.presentAlertStart(app)
} else {
this.start(app)
}
}
async presentModalBackup (type: 'create' | 'restore') {
@@ -275,18 +237,6 @@ export class AppInstalledShowPage extends Cleanup {
return this.navCtrl.navigateRoot('/services/installed')
}
async presentLaunchPopover (status: AppStatus, ev: any) {
let desc: string
if (!this.isTor) {
desc = this.launchLocalDefinition
} else if (status !== AppStatus.RUNNING) {
desc = this.launchOffDefinition
} else {
desc = this.launchDefinition
}
return this.presentPopover(desc, ev)
}
async presentPopover (information: string, ev: any) {
const popover = await this.popoverController.create({
component: InformationPopoverComponent,
@@ -301,8 +251,39 @@ export class AppInstalledShowPage extends Cleanup {
return await popover.present()
}
private setError (e: Error) {
private async presentAlertStart (app: AppInstalledFull): Promise<void> {
const alert = await this.alertCtrl.create({
header: 'Warning',
message: app.startAlert,
buttons: [
{
text: 'Cancel',
role: 'cancel',
},
{
text: 'Start',
handler: () => {
this.start(app)
},
},
],
})
await alert.present()
}
private async start (app: AppInstalledFull): Promise<void> {
this.loader.of({
message: `Starting ${app.title}...`,
spinner: 'lines',
cssClass: 'loader',
}).displayDuringP(
this.apiService.startApp(this.appId),
).catch(e => this.setError(e))
}
private setError (e: Error): Observable<void> {
this.$error$.next(e.message)
return of()
}
private clearError () {

View File

@@ -2,10 +2,8 @@ import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppInstructionsPage } from './app-instructions.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
@@ -21,7 +19,6 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
SharingModule,
],
declarations: [AppInstructionsPage],

View File

@@ -4,9 +4,6 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Instructions</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -16,7 +16,6 @@ export class AppInstructionsPage {
error = ''
app: AppInstalledFull = { } as any
appId: string
instructions: any
constructor (
private readonly route: ActivatedRoute,

View File

@@ -1,12 +1,9 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppLogsPage } from './app-logs.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
@@ -21,7 +18,6 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [AppLogsPage],
})

View File

@@ -8,7 +8,6 @@
<ion-button (click)="getLogs()" color="primary">
<ion-icon name="refresh-outline"></ion-icon>
</ion-button>
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -1,12 +1,9 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { AppMetricsPage } from './app-metrics.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { QRComponentModule } from 'src/app/components/qr/qr.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
@@ -23,7 +20,6 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
QRComponentModule,
SharingModule,
],

View File

@@ -4,9 +4,6 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Properties</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -130,7 +130,6 @@ export class AppMetricsPage {
toggleMask (key: string) {
this.unmasked[key] = !this.unmasked[key]
console.log(this.unmasked)
}
asIsOrder (a: any, b: any) {

View File

@@ -43,6 +43,10 @@ const routes: Routes = [
path: 'installed/:appId/metrics',
loadChildren: () => import('./app-metrics/app-metrics.module').then(m => m.AppMetricsPageModule),
},
{
path: 'installed/:appId/actions',
loadChildren: () => import('./app-actions/app-actions.module').then(m => m.AppActionsPageModule),
},
]
@NgModule({

View File

@@ -4,7 +4,6 @@ import { IonicModule } from '@ionic/angular'
import { DevOptionsPage } from './dev-options.page'
import { Routes, RouterModule } from '@angular/router'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
const routes: Routes = [
@@ -21,7 +20,6 @@ const routes: Routes = [
ObjectConfigComponentModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [DevOptionsPage],
})

View File

@@ -4,9 +4,6 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Developer Options</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -4,7 +4,6 @@ import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevSSHKeysPage } from './dev-ssh-keys.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
const routes: Routes = [
{
@@ -19,7 +18,6 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [DevSSHKeysPage],
})

View File

@@ -4,9 +4,6 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>SSH Keys</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -5,7 +5,6 @@ import { ExternalDrivesPage } from './external-drives.page'
import { Routes, RouterModule } from '@angular/router'
import { SharingModule } from 'src/app/modules/sharing.module'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { ObjectConfigComponentModule } from 'src/app/components/object-config/object-config.component.module'
// TODO: EJECT-DISKS
@@ -24,7 +23,6 @@ const routes: Routes = [
ObjectConfigComponentModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
],
declarations: [ExternalDrivesPage],
})

View File

@@ -5,9 +5,6 @@
<pwa-back-button></pwa-back-button>
</ion-buttons>
<ion-title>Backup drives</ion-title>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
</ion-toolbar>
</ion-header>

View File

@@ -4,7 +4,6 @@ import { Routes, RouterModule } from '@angular/router'
import { IonicModule } from '@ionic/angular'
import { LANPage } from './lan.page'
import { PwaBackComponentModule } from 'src/app/components/pwa-back-button/pwa-back.component.module'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
const routes: Routes = [
@@ -20,7 +19,6 @@ const routes: Routes = [
IonicModule,
RouterModule.forChild(routes),
PwaBackComponentModule,
BadgeMenuComponentModule,
SharingModule,
],
declarations: [LANPage],

View File

@@ -1,9 +1,6 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="end">
<badge-menu-button></badge-menu-button>
</ion-buttons>
<ion-title>Secure LAN Setup</ion-title>
<ion-title>LAN Setup</ion-title>
<ion-buttons slot="start">
<pwa-back-button></pwa-back-button>
</ion-buttons>
@@ -11,54 +8,60 @@
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group>
<ion-item lines="none" style="font-size: small; --background: var(--ion-background-color);">
<ion-label size="small" class="ion-text-wrap">
<ion-text color="medium">For a <ion-text style="font-style: italic;">faster</ion-text> experience, you can also securely communicate with your Embassy by visiting its Local Area Network (LAN) address.</ion-text>
<ion-item>
<ion-label class="ion-text-wrap">
If you are having issues connecting to your Embassy or services over LAN, you can try refreshing the network by clicking the button below.
</ion-label>
</ion-item>
<!-- info -->
<ion-item>
<ion-button slot="start" fill="clear" (click)="refreshLAN()">
<ion-icon slot="start" name="refresh-outline"></ion-icon>
Refresh Network
</ion-button>
</ion-item>
<ion-item-divider>About</ion-item-divider>
<ion-item lines="none">
<ion-label class="ion-text-wrap">
<h2><ion-text color="warning">Instructions</ion-text></h2>
<ng-container *ngIf="!lanDisabled">
<ul style="font-size: smaller">
<li>Download your Embassy's SSL Certificate Authority by clicking the download button below.</li>
<li>Install and trust the CA.</li>
<li>Connect this device to the same network as the Embassy. This should be your private home network.</li>
<li>Navigate to your Embassy LAN address, indicated below.</li>
</ul>
</ng-container>
<div *ngIf="lanDisabled" class="ion-padding-top ion-padding-bottom">
<p [innerHtml]="lanDisabledExplanation[lanDisabled]"></p>
</div>
<a *ngIf="!isConsulate" [href]="fullDocumentationLink" target="_blank">full documentation</a>
<ion-button *ngIf="isConsulate" fill="outline" (click)="copyDocumentation()">full documentation</ion-button>
You can connect to your Embassy over your Local Area Network (LAN). This can be useful for achieving a faster experience, as well as a fallback in case the Tor network is experiencing issues.
</ion-label>
</ion-item>
<ion-item-divider style="margin-top: 0px"></ion-item-divider>
<!-- Certificate -->
<ion-item [disabled]="!!lanDisabled">
<ion-item *ngIf="lanDisabled">
<ion-label class="ion-text-wrap">
<h2>SSL Certificate</h2>
<p>Embassy Local CA</p>
<ion-text color="warning" [innerHtml]="lanDisabledExplanation[lanDisabled]"></ion-text>
</ion-label>
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="installCert()">
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
</ion-button>
</ion-item>
<!-- URL -->
<ion-item [disabled]="!!lanDisabled" >
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<a [href]="lanAddress" target="_blank">{{ lanAddress }}</a>
</ion-label>
<ion-button [disabled]="!!lanDisabled" slot="end" fill="clear" (click)="copyLAN()">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
<ion-item>
<ion-button slot="start" fill="clear" color="primary">View Instructions</ion-button>
</ion-item>
<ng-container *ngIf="!lanDisabled">
<ion-item-divider>Certificate and Address</ion-item-divider>
<!-- Certificate -->
<ion-item>
<ion-label class="ion-text-wrap">
<h2>Root Certificate Authority</h2>
<p>Embassy Local CA</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="installCert()">
<ion-icon slot="icon-only" name="download-outline"></ion-icon>
</ion-button>
</ion-item>
<!-- URL -->
<ion-item>
<ion-label class="ion-text-wrap">
<h2>LAN Address</h2>
<p>{{ lanAddress }}</p>
</ion-label>
<ion-button slot="end" fill="clear" (click)="copyLAN()">
<ion-icon slot="icon-only" name="copy-outline"></ion-icon>
</ion-button>
</ion-item>
</ng-container>
</ion-item-group>
<!-- hidden element for downloading cert -->

View File

@@ -0,0 +1,3 @@
.tiny-icon {
font-size: 12px;
}

View File

@@ -3,6 +3,8 @@ import { isPlatform, ToastController } from '@ionic/angular'
import { ServerModel } from 'src/app/models/server-model'
import { copyToClipboard } from 'src/app/util/web.util'
import { ConfigService } from 'src/app/services/config.service'
import { LoaderService } from 'src/app/services/loader.service'
import { ApiService } from 'src/app/services/api/api.service'
@Component({
selector: 'lan',
@@ -10,23 +12,23 @@ import { ConfigService } from 'src/app/services/config.service'
styleUrls: ['./lan.page.scss'],
})
export class LANPage {
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manuals/embassyos/general/secure-lan'
lanDocs = 'docs.start9labs.com/user-manuals/embassyos/general/secure-lan'
torDocs = 'docs.privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion/user-manual/general/secure-lan'
lanDocs = 'docs.start9labs.com/user-manual/general/secure-lan'
lanAddress: string
isTor: boolean
fullDocumentationLink: string
isConsulate: boolean
lanDisabled: LanSetupIssue = undefined
lanDisabled: LanSetupIssue
readonly lanDisabledExplanation: { [k in LanSetupIssue]: string } = {
NotDesktop: `We have detected you are on a mobile device. To setup LAN on a mobile device, use the Start9 Setup App.`,
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection.<br /><br/>Navigate to your Embassy Tor Address and try again.`,
NotTor: `We have detected you are not using a Tor connection. For security reasons, you must setup LAN over a Tor connection. Please navigate to your Embassy Tor Address and try again.`,
}
constructor (
private readonly serverModel: ServerModel,
private readonly toastCtrl: ToastController,
private readonly config: ConfigService,
private readonly loader: LoaderService,
private readonly apiService: ApiService,
) { }
ngOnInit () {
@@ -36,8 +38,6 @@ export class LANPage {
this.lanDisabled = 'NotTor'
}
this.isConsulate = this.config.isConsulateIos || this.config.isConsulateAndroid
if (this.config.isTor()) {
this.fullDocumentationLink = `http://${this.torDocs}`
} else {
@@ -48,7 +48,19 @@ export class LANPage {
this.lanAddress = `https://${server.serverId}.local`
}
async copyLAN (): Promise < void > {
async refreshLAN (): Promise<void> {
this.loader.of({
message: 'Refreshing Network',
spinner: 'lines',
cssClass: 'loader',
}).displayDuringAsync( async () => {
await this.apiService.refreshLAN()
}).catch(e => {
console.error(e)
})
}
async copyLAN (): Promise <void> {
const message = await copyToClipboard(this.lanAddress).then(success => success ? 'copied to clipboard!' : 'failed to copy')
const toast = await this.toastCtrl.create({
@@ -79,4 +91,4 @@ export class LANPage {
}
}
type LanSetupIssue = 'NotTor' | 'NotDesktop'
type LanSetupIssue = 'NotTor' | 'NotDesktop'

Some files were not shown because too many files have changed in this diff Show More