pairing probably works now (untested)

This commit is contained in:
Joey Hess 2012-09-10 21:55:59 -04:00
parent a41255723c
commit d19bbd29d8
11 changed files with 323 additions and 229 deletions

View file

@ -181,7 +181,7 @@ startAssistant assistant daemonize webappwaiter = do
#ifdef WITH_WEBAPP
, assist $ webAppThread (Just st) dstatus scanremotes transferqueue transferslots urlrenderer Nothing webappwaiter
#ifdef WITH_PAIRING
, assist $ pairListenerThread st dstatus urlrenderer
, assist $ pairListenerThread st dstatus scanremotes urlrenderer
#endif
#endif
, assist $ pushThread st dstatus commitchan pushmap

107
Assistant/MakeRemote.hs Normal file
View file

@ -0,0 +1,107 @@
{- git-annex assistant remote creation utilities
-
- Copyright 2012 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU GPL version 3 or higher.
-}
module Assistant.MakeRemote where
import Assistant.Common
import Assistant.ThreadedMonad
import Assistant.DaemonStatus
import Assistant.ScanRemotes
import Assistant.Ssh
import Assistant.Sync
import qualified Types.Remote as R
import qualified Remote
import Remote.List
import qualified Remote.Rsync as Rsync
import qualified Git
import qualified Git.Command
import qualified Command.InitRemote
import Logs.UUID
import Logs.Remote
import qualified Data.Text as T
import qualified Data.Map as M
{- Sets up and begins syncing with a new ssh or rsync remote. -}
makeSshRemote :: ThreadState -> DaemonStatusHandle -> ScanRemoteMap -> Bool -> SshData -> IO ()
makeSshRemote st dstatus scanremotes forcersync sshdata = do
r <- runThreadState st $
addRemote $ maker (sshRepoName sshdata) sshurl
syncNewRemote st dstatus scanremotes r
where
rsync = forcersync || rsyncOnly sshdata
maker
| rsync = makeRsyncRemote
| otherwise = makeGitRemote
sshurl = T.unpack $ T.concat $
if rsync
then [u, h, T.pack ":", sshDirectory sshdata, T.pack "/"]
else [T.pack "ssh://", u, h, d, T.pack "/"]
where
u = maybe (T.pack "") (\v -> T.concat [v, T.pack "@"]) $ sshUserName sshdata
h = sshHostName sshdata
d
| T.pack "/" `T.isPrefixOf` sshDirectory sshdata = d
| otherwise = T.concat [T.pack "/~/", sshDirectory sshdata]
{- Runs an action that returns a name of the remote, and finishes adding it. -}
addRemote :: Annex String -> Annex Remote
addRemote a = do
name <- a
void $ remoteListRefresh
maybe (error "failed to add remote") return =<< Remote.byName (Just name)
{- Inits a rsync special remote, and returns the name of the remote. -}
makeRsyncRemote :: String -> String -> Annex String
makeRsyncRemote name location = makeRemote name location $ const $ do
(u, c) <- Command.InitRemote.findByName name
c' <- R.setup Rsync.remote u $ M.union config c
describeUUID u name
configSet u c'
where
config = M.fromList
[ ("encryption", "shared")
, ("rsyncurl", location)
, ("type", "rsync")
]
{- Returns the name of the git remote it created. If there's already a
- remote at the location, returns its name. -}
makeGitRemote :: String -> String -> Annex String
makeGitRemote basename location = makeRemote basename location $ \name ->
void $ inRepo $
Git.Command.runBool "remote"
[Param "add", Param name, Param location]
{- If there's not already a remote at the location, adds it using the
- action, which is passed the name of the remote to make.
-
- Returns the name of the remote. -}
makeRemote :: String -> String -> (String -> Annex ()) -> Annex String
makeRemote basename location a = do
r <- fromRepo id
if (null $ filter samelocation $ Git.remotes r)
then do
let name = uniqueRemoteName r basename 0
a name
return name
else return basename
where
samelocation x = Git.repoLocation x == location
{- Generate an unused name for a remote, adding a number if
- necessary. -}
uniqueRemoteName :: Git.Repo -> String -> Int -> String
uniqueRemoteName r basename n
| null namecollision = name
| otherwise = uniqueRemoteName r basename (succ n)
where
namecollision = filter samename (Git.remotes r)
samename x = Git.remoteName x == Just name
name
| n == 0 = basename
| otherwise = basename ++ show n

View file

@ -25,23 +25,24 @@ data PairStage
| PairDone
deriving (Eq, Read, Show)
newtype PairMsg = PairMsg (Verifiable (PairStage, PairData))
newtype PairMsg = PairMsg (Verifiable (PairStage, PairData, SomeAddr))
deriving (Eq, Read, Show)
fromPairMsg :: PairMsg -> (Verifiable (PairStage, PairData))
fromPairMsg :: PairMsg -> (Verifiable (PairStage, PairData, SomeAddr))
fromPairMsg (PairMsg m) = m
pairMsgStage :: PairMsg -> PairStage
pairMsgStage (PairMsg (Verifiable (s, _) _)) = s
pairMsgStage (PairMsg (Verifiable (s, _, _) _)) = s
pairMsgData :: PairMsg -> PairData
pairMsgData (PairMsg (Verifiable (_, d) _)) = d
pairMsgData (PairMsg (Verifiable (_, d, _) _)) = d
pairMsgAddr :: PairMsg -> SomeAddr
pairMsgAddr (PairMsg (Verifiable (_, _, a) _)) = a
data PairData = PairData
-- uname -n output, not a full domain name
{ remoteHostName :: Maybe HostName
-- the address is included so that it can be verified, avoiding spoofing
, remoteAddress :: SomeAddr
, remoteUserName :: UserName
, remoteDirectory :: FilePath
, remoteSshPubKey :: SshPubKey
@ -55,8 +56,9 @@ type UserName = String
- set up on disk. -}
data PairingInProgress = PairingInProgress
{ inProgressSecret :: Secret
, inProgressThreadId :: ThreadId
, inProgressThreadId :: Maybe ThreadId
, inProgressSshKeyPair :: SshKeyPair
, inProgressPairData :: PairData
}
data SomeAddr = IPv4Addr HostAddress | IPv6Addr HostAddress6

View file

@ -0,0 +1,81 @@
{- git-annex assistant pairing remote creation
-
- Copyright 2012 Joey Hess <joey@kitenet.net>
-
- Licensed under the GNU GPL version 3 or higher.
-}
module Assistant.Pairing.MakeRemote where
import Assistant.Common
import Assistant.ThreadedMonad
import Assistant.DaemonStatus
import Assistant.ScanRemotes
import Assistant.Ssh
import Assistant.Pairing
import Assistant.Pairing.Network
import Assistant.MakeRemote
import Network.Socket
import qualified Data.Text as T
{- When pairing is complete, this is used to set up the remote for the host
- we paired with. -}
finishedPairing :: ThreadState -> DaemonStatusHandle -> ScanRemoteMap -> PairMsg -> SshKeyPair -> IO ()
finishedPairing st dstatus scanremotes msg keypair = do
sshdata <- setupSshKeyPair keypair =<< pairMsgToSshData msg
{- Ensure that we know
- the ssh host key for the host we paired with.
- If we don't, ssh over to get it. -}
unlessM (knownHost $ sshHostName sshdata) $ do
void $ sshTranscript
[ sshOpt "StrictHostKeyChecking" "no"
, sshOpt "NumberOfPasswordPrompts" "0"
, "-n"
, genSshHost (sshHostName sshdata) (sshUserName sshdata)
, "git-annex-shell -c configlist " ++ T.unpack (sshDirectory sshdata)
]
""
makeSshRemote st dstatus scanremotes False sshdata
{- Mostly a straightforward conversion. Except:
- * Determine the best hostname to use to contact the host.
- * Strip leading ~/ from the directory name.
-}
pairMsgToSshData :: PairMsg -> IO SshData
pairMsgToSshData msg = do
let d = pairMsgData msg
hostname <- liftIO $ bestHostName msg
let dir = case remoteDirectory d of
('~':'/':v) -> v
v -> v
return $ SshData
{ sshHostName = T.pack hostname
, sshUserName = Just (T.pack $ remoteUserName d)
, sshDirectory = T.pack dir
, sshRepoName = genSshRepoName hostname dir
, needsPubKey = True
, rsyncOnly = False
}
{- Finds the best hostname to use for the host that sent the PairMsg.
-
- If remoteHostName is set, tries to use a .local address based on it.
- That's the most robust, if this system supports .local.
- Otherwise, looks up the hostname in the DNS for the remoteAddress,
- if any. May fall back to remoteAddress if there's no DNS. Ugh. -}
bestHostName :: PairMsg -> IO HostName
bestHostName msg = case (remoteHostName $ pairMsgData msg) of
Just h -> do
let localname = h ++ ".local"
addrs <- catchDefaultIO (getAddrInfo Nothing (Just localname) Nothing) []
maybe fallback (const $ return localname) (headMaybe addrs)
Nothing -> fallback
where
fallback = do
let a = pairMsgAddr msg
let sockaddr = case a of
IPv4Addr addr -> SockAddrInet (PortNum 0) addr
IPv6Addr addr -> SockAddrInet6 (PortNum 0) 0 addr 0
fromMaybe (showAddr a)
<$> catchDefaultIO (fst <$> getNameInfo [] True False sockaddr) Nothing

View file

@ -1,4 +1,8 @@
{- git-annex assistant pairing network code
-
- All network traffic is sent over multicast UDP. For reliability,
- each message is repeated until acknowledged. This is done using a
- thread, that gets stopped before the next message is sent.
-
- Copyright 2012 Joey Hess <joey@kitenet.net>
-
@ -7,15 +11,18 @@
module Assistant.Pairing.Network where
import Common
import Assistant.Common
import Assistant.Pairing
import Assistant.DaemonStatus
import Utility.ThreadScheduler
import Utility.Verifiable
import Network.Multicast
import Network.Info
import Network.Socket
import Control.Exception (bracket)
import qualified Data.Map as M
import Control.Concurrent
{- This is an arbitrary port in the dynamic port range, that could
- conceivably be used for some other broadcast messages.
@ -30,8 +37,9 @@ multicastAddress :: SomeAddr -> HostName
multicastAddress (IPv4Addr _) = "224.0.0.1"
multicastAddress (IPv6Addr _) = "ff02::1"
{- Multicasts a message repeatedly on all interfaces forever, until killed
- with a 2 second delay between each transmission.
{- Multicasts a message repeatedly on all interfaces, with a 2 second
- delay between each transmission. The message is repeated forever
- unless a number of repeats is specified.
-
- The remoteHostAddress is set to the interface's IP address.
-
@ -39,15 +47,16 @@ multicastAddress (IPv6Addr _) = "ff02::1"
- but it allows new network interfaces to be used as they come up.
- On the other hand, the expensive DNS lookups are cached.
-}
multicastPairMsg :: (SomeAddr -> PairMsg) -> IO ()
multicastPairMsg mkmsg = go M.empty
multicastPairMsg :: Maybe Int -> Secret -> PairStage -> PairData -> IO ()
multicastPairMsg repeats secret stage pairdata = go M.empty repeats
where
go cache = do
go _ (Just 0) = noop
go cache n = do
addrs <- activeNetworkAddresses
let cache' = updatecache cache addrs
mapM_ (sendinterface cache') addrs
threadDelaySeconds (Seconds 2)
go cache'
go cache' $ pred <$> n
sendinterface cache i = void $ catchMaybeIO $
withSocketsDo $ bracket
(multicastSender (multicastAddress i) pairingPort)
@ -61,27 +70,23 @@ multicastPairMsg mkmsg = go M.empty
updatecache cache (i:is)
| M.member i cache = updatecache cache is
| otherwise = updatecache (M.insert i (show $ mkmsg i) cache) is
mkmsg addr = PairMsg $
mkVerifiable (stage, pairdata, addr) secret
{- Finds the best hostname to use for the host that sent the PairData.
-
- If remoteHostName is set, tries to use a .local address based on it.
- That's the most robust, if this system supports .local.
- Otherwise, looks up the hostname in the DNS for the remoteAddress,
- if any. May fall back to remoteAddress if there's no DNS. Ugh. -}
bestHostName :: PairData -> IO HostName
bestHostName d = case remoteHostName d of
Just h -> do
let localname = h ++ ".local"
addrs <- catchDefaultIO (getAddrInfo Nothing (Just localname) Nothing) []
maybe fallback (const $ return localname) (headMaybe addrs)
Nothing -> fallback
startSending :: DaemonStatusHandle -> PairingInProgress -> IO () -> IO ()
startSending dstatus pip sender = do
tid <- forkIO sender
let pip' = pip { inProgressThreadId = Just tid }
oldpip <- modifyDaemonStatus dstatus $
\s -> (s { pairingInProgress = Just pip' }, pairingInProgress s)
maybe noop stopold oldpip
where
fallback = do
let sockaddr = case remoteAddress d of
IPv4Addr a -> SockAddrInet (PortNum 0) a
IPv6Addr a -> SockAddrInet6 (PortNum 0) 0 a 0
fromMaybe (show $ remoteAddress d)
<$> catchDefaultIO (fst <$> getNameInfo [] True False sockaddr) Nothing
stopold = maybe noop killThread . inProgressThreadId
stopSending :: DaemonStatusHandle -> PairingInProgress -> IO ()
stopSending dstatus pip = do
maybe noop killThread $ inProgressThreadId pip
modifyDaemonStatus_ dstatus $ \s -> s { pairingInProgress = Nothing }
class ToSomeAddr a where
toSomeAddr :: a -> SomeAddr

View file

@ -7,7 +7,7 @@
module Assistant.Ssh where
import Common
import Common.Annex
import Utility.TempFile
import Data.Text (Text)
@ -43,6 +43,10 @@ sshDir = do
home <- myHomeDir
return $ home </> ".ssh"
{- user@host or host -}
genSshHost :: Text -> Maybe Text -> String
genSshHost host user = maybe "" (\v -> T.unpack v ++ "@") user ++ T.unpack host
{- host_dir, with all / in dir replaced by _, and bad characters removed -}
genSshRepoName :: String -> FilePath -> String
genSshRepoName host dir
@ -171,3 +175,12 @@ setupSshKeyPair sshkeypair sshdata = do
sshpubkeyfile = sshprivkeyfile ++ ".pub"
mangledhost = "git-annex-" ++ T.unpack (sshHostName sshdata) ++ user
user = maybe "" (\u -> "-" ++ T.unpack u) (sshUserName sshdata)
{- Does ssh have known_hosts data for a hostname? -}
knownHost :: Text -> IO Bool
knownHost hostname = do
sshdir <- sshDir
ifM (doesFileExist $ sshdir </> "known_hosts")
( not . null <$> readProcess "ssh-keygen" ["-F", T.unpack hostname]
, return False
)

View file

@ -24,6 +24,7 @@ import qualified Annex.Branch
import Data.Time.Clock
import qualified Data.Map as M
import Control.Concurrent
{- Syncs with remotes that may have been disconnected for a while.
-
@ -108,3 +109,9 @@ manualPull st currentbranch remotes = do
forM_ remotes $ \r ->
runThreadState st $ Command.Sync.mergeRemote r currentbranch
return haddiverged
{- Start syncing a newly added remote, using a background thread. -}
syncNewRemote :: ThreadState -> DaemonStatusHandle -> ScanRemoteMap -> Remote -> IO ()
syncNewRemote st dstatus scanremotes remote = do
runThreadState st $ updateKnownRemotes dstatus
void $ forkIO $ do reconnectRemotes "SyncRemote" st dstatus scanremotes [remote]

View file

@ -10,7 +10,9 @@ module Assistant.Threads.PairListener where
import Assistant.Common
import Assistant.Pairing
import Assistant.Pairing.Network
import Assistant.Pairing.MakeRemote
import Assistant.ThreadedMonad
import Assistant.ScanRemotes
import Assistant.DaemonStatus
import Assistant.WebApp
import Assistant.WebApp.Types
@ -25,8 +27,8 @@ import qualified Data.Text as T
thisThread :: ThreadName
thisThread = "PairListener"
pairListenerThread :: ThreadState -> DaemonStatusHandle -> UrlRenderer -> NamedThread
pairListenerThread st dstatus urlrenderer = thread $ withSocketsDo $ do
pairListenerThread :: ThreadState -> DaemonStatusHandle -> ScanRemoteMap -> UrlRenderer -> NamedThread
pairListenerThread st dstatus scanremotes urlrenderer = thread $ withSocketsDo $ do
sock <- multicastReceiver (multicastAddress $ IPv4Addr undefined) pairingPort
go sock
where
@ -47,15 +49,16 @@ pairListenerThread st dstatus urlrenderer = thread $ withSocketsDo $ do
dispatch Nothing = noop
dispatch (Just m@(PairMsg v)) = do
verified <- maybe False (verify v . inProgressSecret)
. pairingInProgress
<$> getDaemonStatus dstatus
pip <- pairingInProgress <$> getDaemonStatus dstatus
let verified = maybe False (verify v . inProgressSecret) pip
case pairMsgStage m of
PairReq -> pairReqReceived verified dstatus urlrenderer m
PairAck -> pairAckReceived verified dstatus m
PairDone -> pairDoneReceived verified dstatus m
PairAck -> pairAckReceived verified pip st dstatus scanremotes m
PairDone -> pairDoneReceived verified pip st dstatus scanremotes m
{- Pair request alerts from the same host combine,
{- Show an alert when a PairReq is seen.
-
- Pair request alerts from the same host combine,
- so repeated requests do not add additional alerts. -}
pairReqReceived :: Bool -> DaemonStatusHandle -> UrlRenderer -> PairMsg -> IO ()
pairReqReceived True _ _ _ = noop -- ignore out own PairReq
@ -69,12 +72,11 @@ pairReqReceived False dstatus urlrenderer msg = do
, buttonAction = Just onclick
}
where
v = fromPairMsg msg
(_, pairdata) = verifiableVal v
pairdata = pairMsgData msg
repo = concat
[ remoteUserName pairdata
, "@"
, fromMaybe (showAddr $ remoteAddress pairdata)
, fromMaybe (showAddr $ pairMsgAddr msg)
(remoteHostName pairdata)
, ":"
, (remoteDirectory pairdata)
@ -90,27 +92,34 @@ pairReqReceived False dstatus urlrenderer msg = do
, alertData = [UnTensed $ T.pack $ "pair request with " ++ repo ++ " in progress"]
}
{- When a valid PairAck is seen, a host has successfully paired with
- us, and we should finish pairing with them. Then send a single PairDone.
{- When a verified PairAck is seen, a host is ready to pair with us, and has
- already configured our ssh key. Stop sending PairReqs, finish the pairing,
- and send a few PairDones.
-
- A stale PairAck might also be seen, after we've finished pairing.
- TODO: A stale PairAck might also be seen, after we've finished pairing.
- Perhaps our PairDone was not received. To handle this, we keep
- a list of recently finished pairings, and re-send PairDone in
- response to stale PairAcks for them.
-}
pairAckReceived :: Bool -> DaemonStatusHandle -> PairMsg -> IO ()
pairAckReceived False _ _ = noop -- not verified
pairAckReceived True dstatus msg = error "TODO"
pairAckReceived :: Bool -> Maybe PairingInProgress -> ThreadState -> DaemonStatusHandle -> ScanRemoteMap -> PairMsg -> IO ()
pairAckReceived False _ _ _ _ _ = noop -- not verified
pairAckReceived True Nothing _ _ _ _ = noop -- not in progress
pairAckReceived True (Just pip) st dstatus scanremotes msg = do
stopSending dstatus pip
finishedPairing st dstatus scanremotes msg (inProgressSshKeyPair pip)
startSending dstatus pip $ multicastPairMsg
(Just 10) (inProgressSecret pip) PairDone (inProgressPairData pip)
{- If we get a valid PairDone, and are sending PairAcks, we can stop
- sending them, as the message has been received.
{- If we get a verified PairDone, the host has accepted our PairAck, and
- has paired with us. Stop sending PairAcks, and finish pairing with them.
-
- Also, now is the time to remove the pair request alert, as pairing is
- over. Do that even if the PairDone cannot be validated, as we might
- be a third host that did not participate in the pairing.
- Note: This does allow a bad actor to squelch pairing on a network
- by sending bogus PairDones.
- TODO: Should third-party hosts remove their pair request alert when they
- see a PairDone? How to tell if a PairDone matches with the PairReq
- that brought up the alert? Cannot verify it without the secret..
-}
pairDoneReceived :: Bool -> DaemonStatusHandle -> PairMsg -> IO ()
pairDoneReceived False _ _ = noop -- not verified
pairDoneReceived True dstatus msg = error "TODO"
pairDoneReceived :: Bool -> Maybe PairingInProgress -> ThreadState -> DaemonStatusHandle -> ScanRemoteMap -> PairMsg -> IO ()
pairDoneReceived False _ _ _ _ _ = noop -- not verified
pairDoneReceived True Nothing _ _ _ _ = noop -- not in progress
pairDoneReceived True (Just pip) st dstatus scanremotes msg = do
stopSending dstatus pip
finishedPairing st dstatus scanremotes msg (inProgressSshKeyPair pip)

View file

@ -14,15 +14,12 @@ import Assistant.WebApp
import Assistant.WebApp.Types
import Assistant.WebApp.SideBar
import Assistant.Sync
import Assistant.DaemonStatus
import Assistant.MakeRemote
import Utility.Yesod
import Remote.List
import qualified Remote
import Init
import qualified Git
import qualified Git.Construct
import qualified Git.Config
import qualified Git.Command
import qualified Annex
import Locations.UserConfig
import Utility.FreeDesktop
@ -37,7 +34,6 @@ import qualified Data.Text as T
import Data.Char
import System.Posix.Directory
import qualified Control.Exception as E
import Control.Concurrent
data RepositoryPath = RepositoryPath Text
deriving Show
@ -198,61 +194,15 @@ getAddDriveR = bootstrap (Just Config) $ do
void $ makeGitRemote hostname hostlocation
addRemote $ makeGitRemote name dir
{- Runs an action that returns a name of the remote, and finishes adding it. -}
addRemote :: Annex String -> Annex Remote
addRemote a = do
name <- a
void $ remoteListRefresh
maybe (error "failed to add remote") return =<< Remote.byName (Just name)
{- Returns the name of the git remote it created. If there's already a
- remote at the location, returns its name. -}
makeGitRemote :: String -> String -> Annex String
makeGitRemote basename location = makeRemote basename location $ \name ->
void $ inRepo $
Git.Command.runBool "remote"
[Param "add", Param name, Param location]
{- If there's not already a remote at the location, adds it using the
- action, which is passed the name of the remote to make.
-
- Returns the name of the remote. -}
makeRemote :: String -> String -> (String -> Annex ()) -> Annex String
makeRemote basename location a = do
r <- fromRepo id
if (null $ filter samelocation $ Git.remotes r)
then do
let name = uniqueRemoteName r basename 0
a name
return name
else return basename
where
samelocation x = Git.repoLocation x == location
{- Generate an unused name for a remote, adding a number if
- necessary. -}
uniqueRemoteName :: Git.Repo -> String -> Int -> String
uniqueRemoteName r basename n
| null namecollision = name
| otherwise = uniqueRemoteName r basename (succ n)
where
namecollision = filter samename (Git.remotes r)
samename x = Git.remoteName x == Just name
name
| n == 0 = basename
| otherwise = basename ++ show n
{- Start syncing a newly added remote, using a background thread. -}
syncRemote :: Remote -> Handler ()
syncRemote remote = do
webapp <- getYesod
runAnnex () $ updateKnownRemotes (daemonStatus webapp)
void $ liftIO $ forkIO $ do
reconnectRemotes "WebApp"
(fromJust $ threadState webapp)
(daemonStatus webapp)
(scanRemotes webapp)
[remote]
liftIO $ syncNewRemote
(fromJust $ threadState webapp)
(daemonStatus webapp)
(scanRemotes webapp)
remote
{- List of removable drives. -}
driveList :: IO [RemovableDrive]

View file

@ -39,7 +39,6 @@ import Utility.Yesod
import Assistant.Common
import Assistant.Pairing.Network
import Assistant.Ssh
import qualified Assistant.WebApp.Configurators.Ssh as Ssh
import Assistant.Alert
import Assistant.DaemonStatus
import Utility.Verifiable
@ -60,9 +59,7 @@ import Control.Concurrent
getStartPairR :: Handler RepHtml
#ifdef WITH_PAIRING
getStartPairR = do
keypair <- liftIO genSshKeyPair
promptSecret Nothing $ startPairing PairReq keypair noop
getStartPairR = promptSecret Nothing $ startPairing PairReq noop
#else
getStartPairR = noPairing
#endif
@ -70,44 +67,19 @@ getStartPairR = noPairing
getFinishPairR :: PairMsg -> Handler RepHtml
#ifdef WITH_PAIRING
getFinishPairR msg = promptSecret (Just msg) $ \_ secret -> do
keypair <- setup
startPairing PairAck keypair cleanup "" secret
setup
startPairing PairAck cleanup "" secret
where
pubkey = remoteSshPubKey $ pairMsgData msg
setup = do
validateSshPubKey pubKey
liftIO $ validateSshPubKey pubkey
unlessM (liftIO $ makeAuthorizedKeys False pubkey) $
error "failed setting up ssh authorized keys"
keypair <- liftIO genSshKeyPair
sshdata <- liftIO $ pairMsgToSshData msg
sshdata' <- liftIO $ setupSshKeyPair keypair sshdata
void $ lift $ Ssh.makeSshWithKeyPair False sshdata' (Just keypair)
return keypair
cleanup = error "TODO clean up authorized keys and generated ssh key and remove git remote"
#else
getFinishPairR _ = noPairing
#endif
{- Mostly a straightforward conversion. Except:
- * Determine the best hostname to use to contact the host.
- * Strip leading ~/ from the directory name.
-}
pairMsgToSshData :: PairMsg -> IO SshData
pairMsgToSshData msg = do
let d = pairMsgData msg
hostname <- liftIO $ bestHostName d
let dir = case remoteDirectory d of
('~':'/':v) -> v
v -> v
return $ SshData
{ sshHostName = T.pack hostname
, sshUserName = Just (T.pack $ remoteUserName d)
, sshDirectory = T.pack dir
, sshRepoName = genSshRepoName besthostname dir
, needsPubKey = True
, rsyncOnly = False
}
getInprogressPairR :: Text -> Handler RepHtml
#ifdef WITH_PAIRING
getInprogressPairR secret = pairPage $ do
@ -127,27 +99,23 @@ getInprogressPairR _ = noPairing
-
- Redirects to the pairing in progress page.
-}
startPairing :: PairStage -> SshKeyPair -> IO () -> Text -> Secret -> Widget
startPairing stage keypair oncancel displaysecret secret = do
startPairing :: PairStage -> IO () -> Text -> Secret -> Widget
startPairing stage oncancel displaysecret secret = do
keypair <- liftIO $ genSshKeyPair
dstatus <- daemonStatus <$> lift getYesod
urlrender <- lift getUrlRender
let homeurl = urlrender HomeR
sender <- mksender
pairdata <- PairData
<$> liftIO getHostname
<*> liftIO getUserName
<*> (fromJust . relDir <$> lift getYesod)
<*> pure (sshPubKey keypair)
liftIO $ do
pip <- PairingInProgress secret
<$> sendrequests sender dstatus homeurl
<*> pure keypair
oldpip <- modifyDaemonStatus dstatus $
\s -> (s { pairingInProgress = Just pip }, pairingInProgress s)
maybe noop stopold oldpip
let sender = multicastPairMsg Nothing secret stage pairdata
let pip = PairingInProgress secret Nothing keypair pairdata
startSending dstatus pip $ sendrequests sender dstatus homeurl
lift $ redirect $ InprogressPairR displaysecret
where
mksender = do
hostname <- liftIO getHostname
username <- liftIO getUserName
reldir <- fromJust . relDir <$> lift getYesod
return $ multicastPairMsg $ \addr -> PairMsg $ mkVerifiable
(stage, PairData hostname addr username reldir (sshPubKey keypair)) secret
{- Sends pairing messages until the thread is killed,
- and shows an activity alert while doing it.
-
@ -156,7 +124,7 @@ startPairing stage keypair oncancel displaysecret secret = do
- have been on a page specific to the in-process pairing
- that just stopped, so can't go back there.
-}
sendrequests sender dstatus homeurl = forkIO $ do
sendrequests sender dstatus homeurl = do
tid <- myThreadId
let selfdestruct = AlertButton
{ buttonLabel = "Cancel"
@ -168,7 +136,6 @@ startPairing stage keypair oncancel displaysecret secret = do
alertDuring dstatus (pairingAlert selfdestruct) $ do
_ <- E.try sender :: IO (Either E.SomeException ())
return ()
stopold = killThread . inProgressThreadId
data InputSecret = InputSecret { secretText :: Maybe Text }
@ -200,7 +167,7 @@ promptSecret msg cont = pairPage $ do
let badphrase = isJust mproblem
let problem = fromMaybe "" mproblem
let (username, hostname) = maybe ("", "")
(\(_, v) -> (T.pack $ remoteUserName v, T.pack $ fromMaybe (showAddr $ remoteAddress v) (remoteHostName v)))
(\(_, v, a) -> (T.pack $ remoteUserName v, T.pack $ fromMaybe (showAddr a) (remoteHostName v)))
(verifiableVal . fromPairMsg <$> msg)
u <- T.pack <$> liftIO getUserName
let sameusername = username == u

View file

@ -11,21 +11,15 @@ module Assistant.WebApp.Configurators.Ssh where
import Assistant.Common
import Assistant.Ssh
import Assistant.MakeRemote
import Assistant.WebApp
import Assistant.WebApp.Types
import Assistant.WebApp.SideBar
import Utility.Yesod
import Assistant.WebApp.Configurators.Local
import qualified Types.Remote as R
import qualified Remote.Rsync as Rsync
import qualified Command.InitRemote
import Logs.UUID
import Logs.Remote
import Yesod
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Map as M
import Network.BSD
import System.Posix.User
@ -127,7 +121,7 @@ getAddSshR = sshConfigurator $ do
testServer :: SshServer -> IO (ServerStatus, Bool)
testServer (SshServer { hostname = Nothing }) = return
(UnusableServer "Please enter a host name.", False)
testServer sshserver = do
testServer sshserver@(SshServer { hostname = Just hn }) = do
status <- probe [sshOpt "NumberOfPasswordPrompts" "0"]
if usable status
then return (status, False)
@ -141,7 +135,7 @@ testServer sshserver = do
, checkcommand "git-annex-shell"
, checkcommand "rsync"
]
knownhost <- knownHost sshserver
knownhost <- knownHost hn
let sshopts = filter (not . null) $ extraopts ++
{- If this is an already known host, let
- ssh check it as usual.
@ -165,10 +159,6 @@ testServer sshserver = do
token r = "git-annex-probe " ++ r
report r = "echo " ++ token r
{- user@host or host -}
genSshHost :: Text -> Maybe Text -> String
genSshHost host user = maybe "" (\v -> T.unpack v ++ "@") user ++ T.unpack host
{- Runs a ssh command; if it fails shows the user the transcript,
- and if it succeeds, runs an action. -}
sshSetup :: [String] -> String -> Handler RepHtml -> Handler RepHtml
@ -182,16 +172,6 @@ showSshErr :: String -> Handler RepHtml
showSshErr msg = sshConfigurator $
$(widgetFile "configurators/ssh/error")
{- Does ssh have known_hosts data for a hostname? -}
knownHost :: SshServer -> IO Bool
knownHost (SshServer { hostname = Nothing }) = return False
knownHost (SshServer { hostname = Just h }) = do
sshdir <- sshDir
ifM (doesFileExist $ sshdir </> "known_hosts")
( not . null <$> readProcess "ssh-keygen" ["-F", T.unpack h]
, return False
)
getConfirmSshR :: SshData -> Handler RepHtml
getConfirmSshR sshdata = sshConfigurator $ do
let authtoken = webAppFormAuthToken
@ -208,11 +188,11 @@ makeSsh rsync sshdata
| needsPubKey sshdata = do
keypair <- liftIO $ genSshKeyPair
sshdata' <- liftIO $ setupSshKeyPair keypair sshdata
makeSshWithKeyPair rsync sshdata' (Just keypair)
| otherwise = makeSshWithKeyPair rsync sshdata Nothing
makeSsh' rsync sshdata' (Just keypair)
| otherwise = makeSsh' rsync sshdata Nothing
makeSshWithKeyPair :: Bool -> SshData -> Maybe SshKeyPair -> Handler RepHtml
makeSshWithKeyPair rsync sshdata keypair =
makeSsh' :: Bool -> SshData -> Maybe SshKeyPair -> Handler RepHtml
makeSsh' rsync sshdata keypair =
sshSetup [sshhost, remoteCommand] "" $
makeSshRepo rsync sshdata
where
@ -230,40 +210,13 @@ makeSshWithKeyPair rsync sshdata keypair =
makeSshRepo :: Bool -> SshData -> Handler RepHtml
makeSshRepo forcersync sshdata = do
r <- runAnnex undefined $
addRemote $ maker (sshRepoName sshdata) sshurl
syncRemote r
webapp <- getYesod
liftIO $ makeSshRemote
(fromJust $ threadState webapp)
(daemonStatus webapp)
(scanRemotes webapp)
forcersync sshdata
redirect RepositoriesR
where
rsync = forcersync || rsyncOnly sshdata
maker
| rsync = makeRsyncRemote
| otherwise = makeGitRemote
sshurl = T.unpack $ T.concat $
if rsync
then [u, h, ":", sshDirectory sshdata, "/"]
else ["ssh://", u, h, d, "/"]
where
u = maybe "" (\v -> T.concat [v, "@"]) $ sshUserName sshdata
h = sshHostName sshdata
d
| "/" `T.isPrefixOf` sshDirectory sshdata = d
| otherwise = T.concat ["/~/", sshDirectory sshdata]
{- Inits a rsync special remote, and returns the name of the remote. -}
makeRsyncRemote :: String -> String -> Annex String
makeRsyncRemote name location = makeRemote name location $ const $ do
(u, c) <- Command.InitRemote.findByName name
c' <- R.setup Rsync.remote u $ M.union config c
describeUUID u name
configSet u c'
where
config = M.fromList
[ ("encryption", "shared")
, ("rsyncurl", location)
, ("type", "rsync")
]
getAddRsyncNetR :: Handler RepHtml
getAddRsyncNetR = do
@ -276,7 +229,7 @@ getAddRsyncNetR = do
$(widgetFile "configurators/addrsync.net")
case result of
FormSuccess sshserver -> do
knownhost <- liftIO $ knownHost sshserver
knownhost <- liftIO $ maybe (return False) knownHost (hostname sshserver)
keypair <- liftIO $ genSshKeyPair
sshdata <- liftIO $ setupSshKeyPair keypair
(mkSshData sshserver)