2012-09-08 04:26:47 +00:00
{- git - annex assistant webapp configurator for pairing
-
2016-12-24 20:54:43 +00:00
- Copyright 2012 , 2016 Joey Hess < id @ joeyh . name >
2012-09-08 04:26:47 +00:00
-
2012-09-24 18:48:47 +00:00
- Licensed under the GNU AGPL version 3 or higher .
2012-09-08 04:26:47 +00:00
- }
2016-12-27 20:36:05 +00:00
{- # LANGUAGE TypeFamilies, QuasiQuotes, TemplateHaskell, OverloadedStrings, FlexibleContexts # -}
2012-09-08 19:07:44 +00:00
{- # LANGUAGE CPP # -}
2012-09-08 04:26:47 +00:00
module Assistant.WebApp.Configurators.Pairing where
import Assistant.Pairing
2012-11-25 04:26:46 +00:00
import Assistant.WebApp.Common
2013-04-21 17:24:56 +00:00
import Annex.UUID
2012-09-08 19:21:34 +00:00
# ifdef WITH_PAIRING
2016-12-27 20:36:05 +00:00
import Assistant.DaemonStatus
2012-09-11 04:23:34 +00:00
import Assistant.Pairing.MakeRemote
2016-12-27 20:36:05 +00:00
import Assistant.Pairing.Network
2012-09-10 19:20:18 +00:00
import Assistant.Ssh
2012-09-08 06:02:39 +00:00
import Utility.Verifiable
2012-09-08 19:21:34 +00:00
# endif
2012-10-25 22:17:32 +00:00
import Utility.UserInfo
2016-12-24 21:08:03 +00:00
import Utility.Tor
2016-12-28 19:55:54 +00:00
import Utility.Su
2016-12-27 20:36:05 +00:00
import Assistant.WebApp.Pairing
2016-12-27 21:13:32 +00:00
import Assistant.Alert
2016-12-24 20:54:43 +00:00
import qualified Utility.MagicWormhole as Wormhole
2016-12-27 20:36:05 +00:00
import Assistant.MakeRemote
import Assistant.RemoteControl
import Assistant.Sync
2016-12-27 21:13:32 +00:00
import Assistant.WebApp.SideBar
2016-12-27 20:36:05 +00:00
import Command.P2P ( unusedPeerRemoteName , PairingResult ( .. ) )
import P2P.Address
2012-11-05 16:21:13 +00:00
import Git
2016-12-27 20:36:05 +00:00
import Config.Files
2017-12-31 20:08:31 +00:00
import Utility.Process.Transcript
2012-09-08 04:26:47 +00:00
2016-12-27 20:36:05 +00:00
import qualified Data.Map as M
2012-09-08 04:26:47 +00:00
import qualified Data.Text as T
2013-04-17 02:46:20 +00:00
# ifdef WITH_PAIRING
2012-09-08 06:02:39 +00:00
import qualified Data.Text.Encoding as T
2015-04-19 15:05:32 +00:00
import qualified Data.ByteString as B
2012-09-08 06:02:39 +00:00
import Data.Char
2012-09-09 20:24:34 +00:00
import qualified Control.Exception as E
import Control.Concurrent
2012-09-08 19:21:34 +00:00
# endif
2016-12-27 20:36:05 +00:00
import Control.Concurrent.STM hiding ( check )
getStartWormholePairFriendR :: Handler Html
getStartWormholePairFriendR = startWormholePairR PairingWithFriend
getStartWormholePairSelfR :: Handler Html
getStartWormholePairSelfR = startWormholePairR PairingWithSelf
2012-11-03 01:13:06 +00:00
2016-12-27 20:36:05 +00:00
startWormholePairR :: PairingWith -> Handler Html
startWormholePairR pairingwith = whenTorInstalled $ whenWormholeInstalled $
2016-12-28 19:55:54 +00:00
pairPage $ do
sucommand <- liftIO $ mkSuCommand " git-annex " [ Param " enable-tor " ]
2016-12-27 20:36:05 +00:00
$ ( widgetFile " configurators/pairing/wormhole/start " )
2016-12-24 20:54:43 +00:00
2016-12-27 20:36:05 +00:00
getPrepareWormholePairR :: PairingWith -> Handler Html
getPrepareWormholePairR pairingwith = do
2016-12-28 15:23:22 +00:00
enableTor
2016-12-27 20:36:05 +00:00
myaddrs <- liftAnnex loadP2PAddresses
remotename <- liftAnnex unusedPeerRemoteName
h <- liftAssistant $
startWormholePairing pairingwith remotename myaddrs
i <- liftIO . addWormholePairingState h
=<< wormholePairingState <$> getYesod
redirect $ RunningWormholePairR i
2016-12-24 20:54:43 +00:00
2016-12-27 20:36:05 +00:00
enableTor :: Handler ()
enableTor = do
gitannex <- liftIO readProgramFile
( transcript , ok ) <- liftIO $ processTranscript gitannex [ " enable-tor " ] Nothing
if ok
-- Reload remotedameon so it's serving the tor hidden
-- service.
then liftAssistant $ sendRemoteControl RELOAD
else giveup $ " Failed to enable tor \ n \ n " ++ transcript
2016-12-24 20:54:43 +00:00
2016-12-27 20:36:05 +00:00
getRunningWormholePairR :: WormholePairingId -> Handler Html
getRunningWormholePairR = runningWormholePairR
2016-12-24 20:54:43 +00:00
2016-12-27 20:36:05 +00:00
postRunningWormholePairR :: WormholePairingId -> Handler Html
postRunningWormholePairR = runningWormholePairR
2016-12-24 20:54:43 +00:00
2016-12-27 20:36:05 +00:00
runningWormholePairR :: WormholePairingId -> Handler Html
runningWormholePairR i = go =<< getWormholePairingHandle i
where
go Nothing = redirect StartWormholePairFriendR
go ( Just h ) = pairPage $ withPairingWith h $ \ pairingwith -> do
ourcode <- liftIO $ getOurWormholeCode h
let codeprompt = case pairingwith of
PairingWithFriend -> " Your friend's pairing code "
PairingWithSelf -> " The other device's pairing code "
2016-12-24 21:08:03 +00:00
( ( result , form ) , enctype ) <- liftH $
runFormPostNoToken $ renderBootstrap3 bootstrapFormLayout $
2016-12-27 20:36:05 +00:00
areq ( checkwormholecode ourcode pairingwith textField ) ( bfs codeprompt ) Nothing
2016-12-24 21:08:03 +00:00
case result of
2016-12-27 20:36:05 +00:00
FormSuccess t -> case Wormhole . toCode ( T . unpack t ) of
Nothing -> giveup invalidcode
Just theircode -> finish h theircode
_ -> showform form enctype ourcode pairingwith
showform form enctype ourcode pairingwith =
$ ( widgetFile " configurators/pairing/wormhole/prompt " )
checkwormholecode ourcode pairingwith = check $ \ t ->
case Wormhole . toCode ( T . unpack t ) of
Nothing -> Left ( T . pack invalidcode )
Just theircode
| theircode == ourcode -> Left $
case pairingwith of
PairingWithSelf -> " Oops -- You entered this repository's pairing code. Enter the pairing code of the *other* repository. "
PairingWithFriend -> " Oops -- You entered your pairing code. Enter your friend's pairing code. "
| otherwise -> Right t
invalidcode = " That does not look like a valid pairing code. Try again... "
finish h theircode = do
void $ liftIO $ sendTheirWormholeCode h theircode
res <- liftAssistant $ finishWormholePairing h
case res of
SendFailed -> giveup " Failed sending data to pair. "
ReceiveFailed -> giveup " Failed receiving data from pair. "
LinkFailed e -> giveup $ " Failed linking to pair: " ++ e
PairSuccess -> withRemoteName h $ \ remotename -> do
r <- liftAnnex $ addRemote ( return remotename )
liftAssistant $ syncRemote r
liftAssistant $ sendRemoteControl RELOAD
redirect DashboardR
getWormholePairingHandle :: WormholePairingId -> Handler ( Maybe WormholePairingHandle )
getWormholePairingHandle i = do
s <- wormholePairingState <$> getYesod
liftIO $ atomically $ M . lookup i <$> readTVar s
2016-12-24 20:54:43 +00:00
2016-12-24 21:08:03 +00:00
whenTorInstalled :: Handler Html -> Handler Html
whenTorInstalled a = ifM ( liftIO torIsInstalled )
( a
, page " Need Tor " ( Just Configuration ) $
$ ( widgetFile " configurators/needtor " )
)
whenWormholeInstalled :: Handler Html -> Handler Html
whenWormholeInstalled a = ifM ( liftIO Wormhole . isInstalled )
( a
, page " Need Magic Wormhole " ( Just Configuration ) $
$ ( widgetFile " configurators/needmagicwormhole " )
)
2012-11-03 01:13:06 +00:00
{- Starts local pairing. -}
2013-06-27 05:15:28 +00:00
getStartLocalPairR :: Handler Html
2013-03-16 22:48:23 +00:00
getStartLocalPairR = postStartLocalPairR
2013-06-27 05:15:28 +00:00
postStartLocalPairR :: Handler Html
2012-09-08 19:07:44 +00:00
# ifdef WITH_PAIRING
2013-03-16 22:48:23 +00:00
postStartLocalPairR = promptSecret Nothing $
2012-11-03 01:13:06 +00:00
startLocalPairing PairReq noop pairingAlert Nothing
2012-09-10 21:53:51 +00:00
# else
2013-03-16 22:48:23 +00:00
postStartLocalPairR = noLocalPairing
2012-11-03 01:13:06 +00:00
2013-06-27 05:15:28 +00:00
noLocalPairing :: Handler Html
2012-11-03 01:13:06 +00:00
noLocalPairing = noPairing " local "
2012-09-10 21:53:51 +00:00
# endif
2012-11-03 01:13:06 +00:00
{- Runs on the system that responds to a local pair request; sets up the ssh
2012-09-11 04:23:34 +00:00
- authorized key first so that the originating host can immediately sync
- with us . - }
2013-06-27 05:15:28 +00:00
getFinishLocalPairR :: PairMsg -> Handler Html
2013-03-16 22:48:23 +00:00
getFinishLocalPairR = postFinishLocalPairR
2013-06-27 05:15:28 +00:00
postFinishLocalPairR :: PairMsg -> Handler Html
2012-09-10 21:53:51 +00:00
# ifdef WITH_PAIRING
2013-03-16 22:48:23 +00:00
postFinishLocalPairR msg = promptSecret ( Just msg ) $ \ _ secret -> do
2019-12-09 17:49:05 +00:00
repodir <- liftH $ fromRawFilePath . repoPath <$> liftAnnex gitRepo
2012-11-05 16:21:13 +00:00
liftIO $ setup repodir
startLocalPairing PairAck ( cleanup repodir ) alert uuid " " secret
2012-10-31 06:34:03 +00:00
where
alert = pairRequestAcknowledgedAlert ( pairRepo msg ) . Just
2012-11-05 16:21:13 +00:00
setup repodir = setupAuthorizedKeys msg repodir
2013-10-01 17:43:35 +00:00
cleanup repodir = removeAuthorizedKeys True repodir $
2012-10-31 06:34:03 +00:00
remoteSshPubKey $ pairMsgData msg
uuid = Just $ pairUUID $ pairMsgData msg
2012-09-10 21:53:51 +00:00
# else
2013-03-16 22:48:23 +00:00
postFinishLocalPairR _ = noLocalPairing
2012-09-10 21:53:51 +00:00
# endif
2013-06-27 05:15:28 +00:00
getRunningLocalPairR :: SecretReminder -> Handler Html
2012-09-10 21:53:51 +00:00
# ifdef WITH_PAIRING
2012-11-03 01:13:06 +00:00
getRunningLocalPairR s = pairPage $ do
2012-09-11 16:26:42 +00:00
let secret = fromSecretReminder s
2012-11-03 01:13:06 +00:00
$ ( widgetFile " configurators/pairing/local/inprogress " )
2012-09-10 21:53:51 +00:00
# else
2012-11-03 01:13:06 +00:00
getRunningLocalPairR _ = noLocalPairing
2012-09-10 21:53:51 +00:00
# endif
# ifdef WITH_PAIRING
2012-11-03 01:13:06 +00:00
{- Starts local pairing, at either the PairReq (initiating host) or
2012-09-10 21:53:51 +00:00
- PairAck ( responding host ) stage .
-
- Displays an alert , and starts a thread sending the pairing message ,
- which will continue running until the other host responds , or until
- canceled by the user . If canceled by the user , runs the oncancel action .
-
- Redirects to the pairing in progress page .
- }
2012-11-03 01:13:06 +00:00
startLocalPairing :: PairStage -> IO () -> ( AlertButton -> Alert ) -> Maybe UUID -> Text -> Secret -> Widget
startLocalPairing stage oncancel alert muuid displaysecret secret = do
2013-06-03 17:51:54 +00:00
urlrender <- liftH getUrlRender
reldir <- fromJust . relDir <$> liftH getYesod
2012-09-11 19:51:27 +00:00
2013-03-16 04:12:28 +00:00
sendrequests <- liftAssistant $ asIO2 $ mksendrequests urlrender
2012-09-11 19:51:27 +00:00
{- Generating a ssh key pair can take a while, so do it in the
- background . - }
2013-03-16 04:12:28 +00:00
thread <- liftAssistant $ asIO $ do
2012-10-30 21:14:26 +00:00
keypair <- liftIO $ genSshKeyPair
2015-07-02 18:50:20 +00:00
let pubkey = either error id $ validateSshPubKey $ sshPubKey keypair
2012-10-30 21:14:26 +00:00
pairdata <- liftIO $ PairData
2012-09-11 19:51:27 +00:00
<$> getHostname
2016-06-08 19:04:15 +00:00
<*> ( either error id <$> myUserName )
2012-09-11 19:51:27 +00:00
<*> pure reldir
2015-07-02 18:50:20 +00:00
<*> pure pubkey
2012-09-11 19:51:27 +00:00
<*> ( maybe genUUID return muuid )
2012-09-11 16:58:00 +00:00
let sender = multicastPairMsg Nothing secret pairdata
let pip = PairingInProgress secret Nothing keypair pairdata stage
2012-10-30 21:14:26 +00:00
startSending pip stage $ sendrequests sender
void $ liftIO $ forkIO thread
2012-09-11 19:51:27 +00:00
2013-06-03 17:51:54 +00:00
liftH $ redirect $ RunningLocalPairR $ toSecretReminder displaysecret
2012-10-31 06:34:03 +00:00
where
{- Sends pairing messages until the thread is killed,
- and shows an activity alert while doing it .
-
2013-03-13 02:18:36 +00:00
- The cancel button returns the user to the DashboardR . This is
2012-10-31 06:34:03 +00:00
- not ideal , but they have to be sent somewhere , and could
- have been on a page specific to the in - process pairing
- that just stopped , so can't go back there .
- }
mksendrequests urlrender sender _stage = do
tid <- liftIO myThreadId
let selfdestruct = AlertButton
{ buttonLabel = " Cancel "
2013-11-23 04:54:08 +00:00
, buttonPrimary = True
2013-03-13 02:18:36 +00:00
, buttonUrl = urlrender DashboardR
2012-10-31 06:34:03 +00:00
, buttonAction = Just $ const $ do
oncancel
killThread tid
}
alertDuring ( alert selfdestruct ) $ liftIO $ do
_ <- E . try ( sender stage ) :: IO ( Either E . SomeException () )
return ()
2012-09-08 04:26:47 +00:00
2012-09-08 06:02:39 +00:00
data InputSecret = InputSecret { secretText :: Maybe Text }
2012-09-10 21:53:51 +00:00
{- If a PairMsg is passed in, ensures that the user enters a secret
- that can validate it . - }
2013-06-27 05:15:28 +00:00
promptSecret :: Maybe PairMsg -> ( Text -> Secret -> Widget ) -> Handler Html
2012-09-10 21:53:51 +00:00
promptSecret msg cont = pairPage $ do
2013-06-03 17:51:54 +00:00
( ( result , form ) , enctype ) <- liftH $
2014-04-18 00:07:09 +00:00
runFormPostNoToken $ renderBootstrap3 bootstrapFormLayout $
InputSecret <$> aopt textField ( bfs " Secret phrase " ) Nothing
2012-12-13 04:45:27 +00:00
case result of
FormSuccess v -> do
2012-09-08 17:04:19 +00:00
let rawsecret = fromMaybe " " $ secretText v
let secret = toSecret rawsecret
2012-09-09 01:06:10 +00:00
case msg of
2012-09-08 06:02:39 +00:00
Nothing -> case secretProblem secret of
2012-09-08 17:04:19 +00:00
Nothing -> cont rawsecret secret
2012-09-08 06:02:39 +00:00
Just problem ->
showform form enctype $ Just problem
2012-09-09 01:06:10 +00:00
Just m ->
2012-09-10 21:53:51 +00:00
if verify ( fromPairMsg m ) secret
2012-09-08 17:04:19 +00:00
then cont rawsecret secret
2012-09-08 06:02:39 +00:00
else showform form enctype $ Just
" That's not the right secret phrase. "
2012-12-13 04:45:27 +00:00
_ -> showform form enctype Nothing
2012-10-31 06:34:03 +00:00
where
showform form enctype mproblem = do
let start = isNothing msg
let badphrase = isJust mproblem
let problem = fromMaybe " " mproblem
let ( username , hostname ) = maybe ( " " , " " )
( \ ( _ , v , a ) -> ( T . pack $ remoteUserName v , T . pack $ fromMaybe ( showAddr a ) ( remoteHostName v ) ) )
( verifiableVal . fromPairMsg <$> msg )
2016-06-08 19:04:15 +00:00
u <- liftIO myUserName
let sameusername = Right username == ( T . pack <$> u )
2012-11-03 01:13:06 +00:00
$ ( widgetFile " configurators/pairing/local/prompt " )
2012-09-08 06:02:39 +00:00
{- This counts unicode characters as more than one character,
- but that's ok ; they * do * provide additional entropy . - }
secretProblem :: Secret -> Maybe Text
secretProblem s
| B . null s = Just " The secret phrase cannot be left empty. (Remember that punctuation and white space is ignored.) "
2013-03-05 20:21:13 +00:00
| B . length s < 6 = Just " Enter a longer secret phrase, at least 6 characters, but really, a phrase is best! This is not a password you'll need to enter every day. "
2013-09-23 17:58:03 +00:00
| s == toSecret sampleQuote = Just " Speaking of foolishness, don't paste in the example I gave. Enter a different phrase, please! "
2012-09-08 06:02:39 +00:00
| otherwise = Nothing
toSecret :: Text -> Secret
2015-04-19 15:05:32 +00:00
toSecret s = T . encodeUtf8 $ T . toLower $ T . filter isAlphaNum s
2012-09-08 06:02:39 +00:00
{- From Dickens -}
sampleQuote :: Text
sampleQuote = T . unwords
[ " It was the best of times, "
, " it was the worst of times, "
, " it was the age of wisdom, "
, " it was the age of foolishness. "
]
2012-09-08 17:04:19 +00:00
2012-09-08 19:07:44 +00:00
# else
# endif
2012-10-26 16:56:19 +00:00
2013-06-27 05:15:28 +00:00
pairPage :: Widget -> Handler Html
2012-12-30 03:10:18 +00:00
pairPage = page " Pairing " ( Just Configuration )
2012-11-12 05:48:15 +00:00
2013-06-27 05:15:28 +00:00
noPairing :: Text -> Handler Html
2012-11-12 05:48:15 +00:00
noPairing pairingtype = pairPage $
$ ( widgetFile " configurators/pairing/disabled " )