df6f9f1ee8
Searched for uses of putStr and hPutStr and changed appropriate ones to filter out control characters and quote filenames. This notably does not make find and findkeys quote filenames in their default output. Because they should only do that when stdout is non a pipe. A few commands like calckey and lookupkey seem too low-level to make sense to filter output, so skipped those. Also when relaying output from other commands that is not progress output, have git-annex filter out control characters. Sponsored-by: k0ld on Patreon
314 lines
10 KiB
Haskell
314 lines
10 KiB
Haskell
{- git-annex command
|
|
-
|
|
- Copyright 2016 Joey Hess <id@joeyh.name>
|
|
-
|
|
- Licensed under the GNU AGPL version 3 or higher.
|
|
-}
|
|
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
|
|
module Command.P2P where
|
|
|
|
import Command
|
|
import P2P.Address
|
|
import P2P.Auth
|
|
import P2P.IO
|
|
import qualified P2P.Protocol as P2P
|
|
import Git.Types
|
|
import qualified Git.Remote
|
|
import qualified Git.Command
|
|
import qualified Annex
|
|
import Annex.UUID
|
|
import Config
|
|
import Utility.AuthToken
|
|
import Utility.Tmp.Dir
|
|
import Utility.FileMode
|
|
import Utility.ThreadScheduler
|
|
import Utility.SafeOutput
|
|
import qualified Utility.RawFilePath as R
|
|
import qualified Utility.MagicWormhole as Wormhole
|
|
|
|
import Control.Concurrent.Async
|
|
import qualified Data.Text as T
|
|
|
|
cmd :: Command
|
|
cmd = command "p2p" SectionSetup
|
|
"configure peer-2-peer links between repositories"
|
|
paramNothing (seek <$$> optParser)
|
|
|
|
data P2POpts
|
|
= GenAddresses
|
|
| LinkRemote
|
|
| Pair
|
|
|
|
optParser :: CmdParamsDesc -> Parser (P2POpts, Maybe RemoteName)
|
|
optParser _ = (,)
|
|
<$> (pair <|> linkremote <|> genaddresses)
|
|
<*> optional name
|
|
where
|
|
genaddresses = flag' GenAddresses
|
|
( long "gen-addresses"
|
|
<> help "generate addresses that allow accessing this repository over P2P networks"
|
|
)
|
|
linkremote = flag' LinkRemote
|
|
( long "link"
|
|
<> help "set up a P2P link to a git remote"
|
|
)
|
|
pair = flag' Pair
|
|
( long "pair"
|
|
<> help "pair with another repository"
|
|
)
|
|
name = Git.Remote.makeLegalName <$> strOption
|
|
( long "name"
|
|
<> metavar paramName
|
|
<> help "name of remote"
|
|
)
|
|
|
|
seek :: (P2POpts, Maybe RemoteName) -> CommandSeek
|
|
seek (GenAddresses, _) = genAddresses =<< loadP2PAddresses
|
|
seek (LinkRemote, Just name) = commandAction $
|
|
linkRemote name
|
|
seek (LinkRemote, Nothing) = commandAction $
|
|
linkRemote =<< unusedPeerRemoteName
|
|
seek (Pair, Just name) = commandAction $
|
|
startPairing name =<< loadP2PAddresses
|
|
seek (Pair, Nothing) = commandAction $ do
|
|
name <- unusedPeerRemoteName
|
|
startPairing name =<< loadP2PAddresses
|
|
|
|
unusedPeerRemoteName :: Annex RemoteName
|
|
unusedPeerRemoteName = go (1 :: Integer) =<< usednames
|
|
where
|
|
usednames = mapMaybe remoteName <$> Annex.getGitRemotes
|
|
go n names = do
|
|
let name = "peer" ++ show n
|
|
if name `elem` names
|
|
then go (n+1) names
|
|
else return name
|
|
|
|
-- Only addresses are output to stdout, to allow scripting.
|
|
genAddresses :: [P2PAddress] -> Annex ()
|
|
genAddresses [] = giveup "No P2P networks are currently available."
|
|
genAddresses addrs = do
|
|
authtoken <- liftIO $ genAuthToken 128
|
|
storeP2PAuthToken authtoken
|
|
earlyWarning "These addresses allow access to this git-annex repository. Only share them with people you trust with that access, using trusted communication channels!"
|
|
liftIO $ putStr $ safeOutput $ unlines $
|
|
map formatP2PAddress $
|
|
map (`P2PAddressAuth` authtoken) addrs
|
|
|
|
-- Address is read from stdin, to avoid leaking it in shell history.
|
|
linkRemote :: RemoteName -> CommandStart
|
|
linkRemote remotename = starting "p2p link" ai si $
|
|
next promptaddr
|
|
where
|
|
ai = ActionItemOther (Just (UnquotedString remotename))
|
|
si = SeekInput []
|
|
promptaddr = do
|
|
liftIO $ putStrLn ""
|
|
liftIO $ putStr "Enter peer address: "
|
|
liftIO $ hFlush stdout
|
|
s <- liftIO getLine
|
|
if null s
|
|
then do
|
|
liftIO $ hPutStrLn stderr "Nothing entered, giving up."
|
|
return False
|
|
else case unformatP2PAddress s of
|
|
Nothing -> do
|
|
liftIO $ hPutStrLn stderr "Unable to parse that address, please check its format and try again."
|
|
promptaddr
|
|
Just addr -> do
|
|
r <- setupLink remotename addr
|
|
case r of
|
|
LinkSuccess -> return True
|
|
ConnectionError e -> giveup e
|
|
AuthenticationError e -> giveup e
|
|
|
|
startPairing :: RemoteName -> [P2PAddress] -> CommandStart
|
|
startPairing _ [] = giveup "No P2P networks are currently available."
|
|
startPairing remotename addrs = ifM (liftIO Wormhole.isInstalled)
|
|
( starting "p2p pair" ai si $
|
|
performPairing remotename addrs
|
|
, giveup "Magic Wormhole is not installed, and is needed for pairing. Install it from your distribution or from https://github.com/warner/magic-wormhole/"
|
|
)
|
|
where
|
|
ai = ActionItemOther (Just (UnquotedString remotename))
|
|
si = SeekInput []
|
|
|
|
performPairing :: RemoteName -> [P2PAddress] -> CommandPerform
|
|
performPairing remotename addrs = do
|
|
-- This note is displayed mainly so when magic wormhole
|
|
-- complains about possible protocol mismatches or other problems,
|
|
-- it's clear what's doing the complaining.
|
|
showNote "using Magic Wormhole"
|
|
next $ do
|
|
showOutput
|
|
r <- wormholePairing remotename addrs ui
|
|
case r of
|
|
PairSuccess -> return True
|
|
SendFailed -> do
|
|
warning "Failed sending data to pair."
|
|
return False
|
|
ReceiveFailed -> do
|
|
warning "Failed receiving data from pair."
|
|
return False
|
|
LinkFailed e -> do
|
|
warning $ UnquotedString $ "Failed linking to pair: " ++ e
|
|
return False
|
|
where
|
|
ui observer producer = do
|
|
ourcode <- Wormhole.waitCode observer
|
|
putStrLn ""
|
|
putStrLn $ "This repository's pairing code is: " ++
|
|
Wormhole.fromCode ourcode
|
|
putStrLn ""
|
|
theircode <- getcode ourcode
|
|
Wormhole.sendCode producer theircode
|
|
|
|
getcode ourcode = do
|
|
putStr "Enter the other repository's pairing code: "
|
|
hFlush stdout
|
|
l <- getLine
|
|
case Wormhole.toCode l of
|
|
Just code
|
|
| code /= ourcode -> do
|
|
putStrLn "Exchanging pairing data..."
|
|
return code
|
|
| otherwise -> do
|
|
putStrLn "Oops -- You entered this repository's pairing code. Enter the pairing code of the *other* repository."
|
|
getcode ourcode
|
|
Nothing -> do
|
|
putStrLn "That does not look like a valiad pairing code. Try again..."
|
|
getcode ourcode
|
|
|
|
-- We generate half of the authtoken; the pair will provide
|
|
-- the other half.
|
|
newtype HalfAuthToken = HalfAuthToken T.Text
|
|
deriving (Show)
|
|
|
|
data PairData = PairData HalfAuthToken [P2PAddress]
|
|
deriving (Show)
|
|
|
|
serializePairData :: PairData -> String
|
|
serializePairData (PairData (HalfAuthToken ha) addrs) = unlines $
|
|
T.unpack ha : map formatP2PAddress addrs
|
|
|
|
deserializePairData :: String -> Maybe PairData
|
|
deserializePairData s = case lines s of
|
|
[] -> Nothing
|
|
(ha:l) -> do
|
|
addrs <- mapM unformatP2PAddress l
|
|
return (PairData (HalfAuthToken (T.pack ha)) addrs)
|
|
|
|
data PairingResult
|
|
= PairSuccess
|
|
| SendFailed
|
|
| ReceiveFailed
|
|
| LinkFailed String
|
|
|
|
wormholePairing
|
|
:: RemoteName
|
|
-> [P2PAddress]
|
|
-> (Wormhole.CodeObserver -> Wormhole.CodeProducer -> IO ())
|
|
-> Annex PairingResult
|
|
wormholePairing remotename ouraddrs ui = do
|
|
ourhalf <- liftIO $ HalfAuthToken . fromAuthToken
|
|
<$> genAuthToken 64
|
|
let ourpairdata = PairData ourhalf ouraddrs
|
|
|
|
-- The magic wormhole interface only supports exchanging
|
|
-- files. Permissions of received files may allow others
|
|
-- to read them. So, set up a temp directory that only
|
|
-- we can read.
|
|
withTmpDir "pair" $ \tmp -> do
|
|
liftIO $ void $ tryIO $ modifyFileMode (toRawFilePath tmp) $
|
|
removeModes otherGroupModes
|
|
let sendf = tmp </> "send"
|
|
let recvf = tmp </> "recv"
|
|
liftIO $ writeFileProtected (toRawFilePath sendf) $
|
|
serializePairData ourpairdata
|
|
|
|
observer <- liftIO Wormhole.mkCodeObserver
|
|
producer <- liftIO Wormhole.mkCodeProducer
|
|
void $ liftIO $ async $ ui observer producer
|
|
-- Provide an appid to magic wormhole, to avoid using
|
|
-- the same channels that other wormhole users use.
|
|
let appid = Wormhole.appId "git-annex.branchable.com/p2p-setup"
|
|
(sendres, recvres) <- liftIO $
|
|
Wormhole.sendFile sendf observer appid
|
|
`concurrently`
|
|
Wormhole.receiveFile recvf producer appid
|
|
liftIO $ removeWhenExistsWith R.removeLink (toRawFilePath sendf)
|
|
if sendres /= True
|
|
then return SendFailed
|
|
else if recvres /= True
|
|
then return ReceiveFailed
|
|
else do
|
|
r <- liftIO $ tryIO $
|
|
readFileStrict recvf
|
|
case r of
|
|
Left _e -> return ReceiveFailed
|
|
Right s -> maybe
|
|
(return ReceiveFailed)
|
|
(finishPairing 100 remotename ourhalf)
|
|
(deserializePairData s)
|
|
|
|
-- | Allow the peer we're pairing with to authenticate to us,
|
|
-- using an authtoken constructed from the two HalfAuthTokens.
|
|
-- Connect to the peer we're pairing with, and try to link to them.
|
|
--
|
|
-- Multiple addresses may have been received for the peer. This only
|
|
-- makes a link to one address.
|
|
--
|
|
-- Since we're racing the peer as they do the same, the first try is likely
|
|
-- to fail to authenticate. Can retry any number of times, to avoid the
|
|
-- users needing to redo the whole process.
|
|
finishPairing :: Int -> RemoteName -> HalfAuthToken -> PairData -> Annex PairingResult
|
|
finishPairing retries remotename (HalfAuthToken ourhalf) (PairData (HalfAuthToken theirhalf) theiraddrs) = do
|
|
case (toAuthToken (ourhalf <> theirhalf), toAuthToken (theirhalf <> ourhalf)) of
|
|
(Just ourauthtoken, Just theirauthtoken) -> do
|
|
liftIO $ putStrLn $ "Successfully exchanged pairing data. Connecting to " ++ remotename ++ "..."
|
|
storeP2PAuthToken ourauthtoken
|
|
go retries theiraddrs theirauthtoken
|
|
_ -> return ReceiveFailed
|
|
where
|
|
go 0 [] _ = return $ LinkFailed $ "Unable to connect to " ++ remotename ++ "."
|
|
go n [] theirauthtoken = do
|
|
liftIO $ threadDelaySeconds (Seconds 2)
|
|
liftIO $ putStrLn $ "Unable to connect to " ++ remotename ++ ". Retrying..."
|
|
go (n-1) theiraddrs theirauthtoken
|
|
go n (addr:rest) theirauthtoken = do
|
|
r <- setupLink remotename (P2PAddressAuth addr theirauthtoken)
|
|
case r of
|
|
LinkSuccess -> return PairSuccess
|
|
_ -> go n rest theirauthtoken
|
|
|
|
data LinkResult
|
|
= LinkSuccess
|
|
| ConnectionError String
|
|
| AuthenticationError String
|
|
|
|
setupLink :: RemoteName -> P2PAddressAuth -> Annex LinkResult
|
|
setupLink remotename (P2PAddressAuth addr authtoken) = do
|
|
g <- Annex.gitRepo
|
|
cv <- liftIO $ tryNonAsync $ connectPeer g addr
|
|
case cv of
|
|
Left e -> return $ ConnectionError $ "Unable to connect with peer. Please check that the peer is connected to the network, and try again. (" ++ show e ++ ")"
|
|
Right conn -> do
|
|
u <- getUUID
|
|
let proto = P2P.auth u authtoken noop
|
|
runst <- liftIO $ mkRunState Client
|
|
go =<< liftIO (runNetProto runst conn proto)
|
|
where
|
|
go (Right (Just theiruuid)) = do
|
|
ok <- inRepo $ Git.Command.runBool
|
|
[ Param "remote", Param "add"
|
|
, Param remotename
|
|
, Param (formatP2PAddress addr)
|
|
]
|
|
when ok $ do
|
|
storeUUIDIn (remoteAnnexConfig remotename "uuid") theiruuid
|
|
storeP2PRemoteAuthToken addr authtoken
|
|
return LinkSuccess
|
|
go (Right Nothing) = return $ AuthenticationError "Unable to authenticate with peer. Please check the address and try again."
|
|
go (Left e) = return $ AuthenticationError $ "Unable to authenticate with peer: " ++ describeProtoFailure e
|