summaryrefslogtreecommitdiff
path: root/Command/P2P.hs
blob: 40a49b49fa11d83f7bf223754fef5e451b3526dc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
{- git-annex command
 -
 - Copyright 2016 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU GPL version 3 or higher.
 -}

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
import Utility.FileMode
import Utility.ThreadScheduler
import qualified Utility.MagicWormhole as Wormhole

import Control.Concurrent.Async
import qualified Data.Text as T
import Data.Time.Clock.POSIX

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 . remotes <$> Annex.gitRepo
	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 currrently 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 $ unlines $
		map formatP2PAddress $
			map (`P2PAddressAuth` authtoken) addrs

-- Address is read from stdin, to avoid leaking it in shell history.
linkRemote :: RemoteName -> CommandStart
linkRemote remotename = do
	showStart' "p2p link" (Just remotename)
	next $ next promptaddr
  where
	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 currrently available."
startPairing remotename addrs = do
	showStart' "p2p pair" (Just remotename)
	ifM (liftIO Wormhole.isInstalled)
		( next $ 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/"
		) 

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 $ "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 tmp $ 
			removeModes otherGroupModes
		let sendf = tmp </> "send"
		let recvf = tmp </> "recv"
		liftIO $ writeFileProtected 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.
		--
		-- Since a version of git-annex that did not provide an
		-- appid is shipping in Debian 9, and having one side
		-- provide an appid while the other does not will make
		-- wormhole fail, this is deferred until 2021-12-31.
		-- After that point, all git-annex's should have been
		-- upgraded to include this code, and they will start
		-- providing an appid.
		--
		-- This assumes reasonably good client clocks. If the clock
		-- is completely wrong, it won't use the appid at that
		-- point, and pairing will fail. On 2021-12-31, minor clock
		-- skew may also cause transient problems.
		--
		-- After 2021-12-31, this can be changed to simply
		-- always provide the appid.
		now <- liftIO getPOSIXTime
		let wormholeparams = if now < 1640950000
			then []
			else Wormhole.appId "git-annex.branchable.com/p2p-setup"
		(sendres, recvres) <- liftIO $
			Wormhole.sendFile sendf observer wormholeparams
				`concurrently`
			Wormhole.receiveFile recvf producer wormholeparams
		liftIO $ nukeFile 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
			go =<< liftIO (runNetProto conn $ P2P.auth u authtoken)
  where
	go (Right (Just theiruuid)) = do
		ok <- inRepo $ Git.Command.runBool
			[ Param "remote", Param "add"
			, Param remotename
			, Param (formatP2PAddress addr)
			]
		when ok $ do
			storeUUIDIn (remoteConfig 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: " ++ e