2012-09-08 04:26:47 +00:00
{- git - annex assistant webapp configurator for pairing
-
- Copyright 2012 Joey Hess < joey @ kitenet . net >
-
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
- }
{- # LANGUAGE TypeFamilies, QuasiQuotes, MultiParamTypeClasses, TemplateHaskell, OverloadedStrings, RankNTypes # -}
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-09-10 19:20:18 +00:00
import Assistant.WebApp
import Assistant.WebApp.Types
import Assistant.WebApp.SideBar
import Utility.Yesod
2012-09-08 19:21:34 +00:00
# ifdef WITH_PAIRING
2012-09-10 21:53:51 +00:00
import Assistant.Common
2012-09-08 19:21:34 +00:00
import Assistant.Pairing.Network
2012-09-11 04:23:34 +00:00
import Assistant.Pairing.MakeRemote
2012-09-10 19:20:18 +00:00
import Assistant.Ssh
2012-09-09 20:24:34 +00:00
import Assistant.Alert
2012-09-08 17:04:19 +00:00
import Assistant.DaemonStatus
2012-09-08 06:02:39 +00:00
import Utility.Verifiable
2012-09-08 19:21:34 +00:00
import Utility.Network
2012-09-11 07:16:00 +00:00
import Annex.UUID
2012-09-08 19:21:34 +00:00
# endif
2012-09-08 04:26:47 +00:00
import Yesod
import Data.Text ( Text )
2012-09-08 19:21:34 +00:00
# ifdef WITH_PAIRING
2012-09-08 04:26:47 +00:00
import qualified Data.Text as T
2012-09-08 06:02:39 +00:00
import qualified Data.Text.Encoding as T
import qualified Data.ByteString.Lazy as B
import Data.Char
import System.Posix.User
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
2012-09-08 04:26:47 +00:00
2012-09-11 04:23:34 +00:00
{- Starts sending out pair requests. -}
2012-09-08 04:26:47 +00:00
getStartPairR :: Handler RepHtml
2012-09-08 19:07:44 +00:00
# ifdef WITH_PAIRING
2012-09-11 19:06:29 +00:00
getStartPairR = promptSecret Nothing $ startPairing PairReq noop pairingAlert Nothing
2012-09-10 21:53:51 +00:00
# else
getStartPairR = noPairing
# endif
2012-09-11 04:23:34 +00:00
{- Runs on the system that responds to a pair request; sets up the ssh
- authorized key first so that the originating host can immediately sync
- with us . - }
2012-09-10 21:53:51 +00:00
getFinishPairR :: PairMsg -> Handler RepHtml
# ifdef WITH_PAIRING
getFinishPairR msg = promptSecret ( Just msg ) $ \ _ secret -> do
2012-09-11 04:23:34 +00:00
liftIO $ setup
2012-09-11 19:06:29 +00:00
startPairing PairAck cleanup alert uuid " " secret
2012-09-10 21:53:51 +00:00
where
2012-09-11 19:43:33 +00:00
alert = pairRequestAcknowledgedAlert ( pairRepo msg ) . Just
2012-09-11 04:23:34 +00:00
setup = setupAuthorizedKeys msg
cleanup = removeAuthorizedKeys False $
remoteSshPubKey $ pairMsgData msg
2012-09-11 19:06:29 +00:00
uuid = Just $ pairUUID $ pairMsgData msg
2012-09-10 21:53:51 +00:00
# else
getFinishPairR _ = noPairing
# endif
2012-09-11 16:26:42 +00:00
getInprogressPairR :: SecretReminder -> Handler RepHtml
2012-09-10 21:53:51 +00:00
# ifdef WITH_PAIRING
2012-09-11 16:26:42 +00:00
getInprogressPairR s = pairPage $ do
let secret = fromSecretReminder s
2012-09-10 21:53:51 +00:00
$ ( widgetFile " configurators/pairing/inprogress " )
# else
getInprogressPairR _ = noPairing
# endif
# ifdef WITH_PAIRING
{- Starts pairing, at either the PairReq (initiating host) or
- 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-09-11 19:06:29 +00:00
startPairing :: PairStage -> IO () -> ( AlertButton -> Alert ) -> Maybe UUID -> Text -> Secret -> Widget
startPairing stage oncancel alert muuid displaysecret secret = do
2012-09-09 20:24:34 +00:00
dstatus <- daemonStatus <$> lift getYesod
urlrender <- lift getUrlRender
2012-09-11 19:51:27 +00:00
reldir <- fromJust . relDir <$> lift getYesod
{- Generating a ssh key pair can take a while, so do it in the
- background . - }
void $ liftIO $ forkIO $ do
keypair <- genSshKeyPair
pairdata <- PairData
<$> getHostname
<*> getUserName
<*> pure reldir
<*> pure ( sshPubKey keypair )
<*> ( 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
startSending dstatus pip stage $ sendrequests sender dstatus urlrender
2012-09-11 19:51:27 +00:00
2012-09-11 16:26:42 +00:00
lift $ redirect $ InprogressPairR $ toSecretReminder displaysecret
2012-09-09 20:24:34 +00:00
where
{- Sends pairing messages until the thread is killed,
- and shows an activity alert while doing it .
-
2012-09-10 21:53:51 +00:00
- The cancel button returns the user to the HomeR . This is
2012-09-09 20:24:34 +00:00
- not ideal , but they have to be sent somewhere , and could
- have been on a page specific to the in - process pairing
2012-09-10 21:53:51 +00:00
- that just stopped , so can't go back there .
2012-09-09 20:24:34 +00:00
- }
2012-09-11 16:58:00 +00:00
sendrequests sender dstatus urlrender _stage = do
2012-09-09 20:24:34 +00:00
tid <- myThreadId
let selfdestruct = AlertButton
{ buttonLabel = " Cancel "
2012-09-11 04:23:34 +00:00
, buttonUrl = urlrender HomeR
2012-09-10 21:53:51 +00:00
, buttonAction = Just $ const $ do
oncancel
killThread tid
2012-09-09 20:24:34 +00:00
}
2012-09-11 19:06:29 +00:00
alertDuring dstatus ( alert selfdestruct ) $ do
2012-09-11 16:58:00 +00:00
_ <- E . try ( sender stage ) :: IO ( Either E . SomeException () )
2012-09-09 20:24:34 +00:00
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 . - }
2012-09-09 01:06:10 +00:00
promptSecret :: Maybe PairMsg -> ( Text -> Secret -> Widget ) -> Handler RepHtml
2012-09-10 21:53:51 +00:00
promptSecret msg cont = pairPage $ do
2012-09-08 06:02:39 +00:00
( ( result , form ) , enctype ) <- lift $
runFormGet $ renderBootstrap $
InputSecret <$> aopt textField " Secret phrase " Nothing
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. "
_ -> showform form enctype Nothing
where
showform form enctype mproblem = do
2012-09-09 01:06:10 +00:00
let start = isNothing msg
2012-09-08 06:02:39 +00:00
let badphrase = isJust mproblem
2012-09-09 01:06:10 +00:00
let problem = fromMaybe " " mproblem
2012-09-08 06:02:39 +00:00
let ( username , hostname ) = maybe ( " " , " " )
2012-09-11 01:55:59 +00:00
( \ ( _ , v , a ) -> ( T . pack $ remoteUserName v , T . pack $ fromMaybe ( showAddr a ) ( remoteHostName v ) ) )
2012-09-09 01:06:10 +00:00
( verifiableVal . fromPairMsg <$> msg )
2012-09-08 17:04:19 +00:00
u <- T . pack <$> liftIO getUserName
2012-09-08 06:02:39 +00:00
let sameusername = username == u
let authtoken = webAppFormAuthToken
2012-09-09 03:32:08 +00:00
$ ( widgetFile " configurators/pairing/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.) "
| B . length s < 7 = 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. "
| s == toSecret sampleQuote = Just " Speaking of foolishness, don't paste in the example I gave. Enter a different phrase, please! "
| otherwise = Nothing
toSecret :: Text -> Secret
toSecret s = B . fromChunks [ T . encodeUtf8 $ T . toLower $ T . filter isAlphaNum s ]
2012-09-10 21:53:51 +00:00
getUserName :: IO String
getUserName = userName <$> ( getUserEntryForID =<< getEffectiveUserID )
pairPage :: Widget -> Handler RepHtml
pairPage w = bootstrap ( Just Config ) $ do
sideBarDisplay
setTitle " Pairing "
w
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
noPairing :: Handler RepHtml
2012-09-10 21:53:51 +00:00
noPairing = pairPage $
2012-09-09 03:32:08 +00:00
$ ( widgetFile " configurators/pairing/disabled " )
2012-09-08 19:07:44 +00:00
# endif