3 - Copyright 2024 Joey Hess <id@joeyh.name>
5 - Licensed under the GNU AGPL version 3 or higher.
8 module Annex.Proxy where
11 import qualified Annex
12 import qualified Remote
13 import qualified Types.Remote as Remote
14 import qualified Remote.Git
18 import Remote.Helper.Ssh (openP2PShellConnection', closeP2PShellConnection)
19 import Annex.Concurrent
27 import Utility.Tmp.Dir
28 import Utility.Metered
30 import qualified Database.Export as Export
32 import Control.Concurrent.STM
33 import Control.Concurrent.Async
34 import qualified Data.ByteString as B
35 import qualified Data.ByteString.Lazy as L
36 import qualified System.FilePath.ByteString as P
37 import qualified Data.Map as M
38 import qualified Data.Set as S
40 proxyRemoteSide :: ProtocolVersion -> Bypass -> Remote -> Annex RemoteSide
41 proxyRemoteSide clientmaxversion bypass r
42 | Remote.remotetype r == Remote.Git.remote =
43 proxyGitRemoteSide clientmaxversion bypass r
45 proxySpecialRemoteSide clientmaxversion r
47 proxyGitRemoteSide :: ProtocolVersion -> Bypass -> Remote -> Annex RemoteSide
48 proxyGitRemoteSide clientmaxversion bypass r = mkRemoteSide r $
49 openP2PShellConnection' r clientmaxversion bypass >>= \case
50 Just conn@(OpenConnection (remoterunst, remoteconn, _)) ->
54 , void $ liftIO $ closeP2PShellConnection conn
58 proxySpecialRemoteSide :: ProtocolVersion -> Remote -> Annex RemoteSide
59 proxySpecialRemoteSide clientmaxversion r = mkRemoteSide r $ do
60 let protoversion = min clientmaxversion maxProtocolVersion
61 remoterunst <- Serving (Remote.uuid r) Nothing <$>
62 liftIO (newTVarIO protoversion)
63 ihdl <- liftIO newEmptyTMVarIO
64 ohdl <- liftIO newEmptyTMVarIO
65 iwaitv <- liftIO newEmptyTMVarIO
66 owaitv <- liftIO newEmptyTMVarIO
67 iclosedv <- liftIO newEmptyTMVarIO
68 oclosedv <- liftIO newEmptyTMVarIO
69 exportdb <- ifM (Remote.isExportSupported r)
70 ( Just <$> Export.openDb (Remote.uuid r)
73 worker <- liftIO . async =<< forkState
74 (proxySpecialRemote protoversion r ihdl ohdl owaitv oclosedv exportdb)
75 let remoteconn = P2PConnection
77 , connCheckAuth = const False
78 , connIhdl = P2PHandleTMVar ihdl (Just iwaitv) iclosedv
79 , connOhdl = P2PHandleTMVar ohdl (Just owaitv) oclosedv
80 , connIdent = ConnIdent (Just (Remote.name r))
82 let closeremoteconn = do
83 liftIO $ atomically $ putTMVar oclosedv ()
84 join $ liftIO (wait worker)
85 maybe noop Export.closeDb exportdb
92 -- Proxy for the special remote, speaking the P2P protocol.
96 -> TMVar (Either L.ByteString Message)
97 -> TMVar (Either L.ByteString Message)
100 -> Maybe Export.ExportHandle
102 proxySpecialRemote protoversion r ihdl ohdl owaitv oclosedv mexportdb = go
105 go = liftIO receivemessage >>= \case
106 Just (CHECKPRESENT k) -> do
107 tryNonAsync (Remote.checkPresent r k) >>= \case
108 Right True -> liftIO $ sendmessage SUCCESS
109 Right False -> liftIO $ sendmessage FAILURE
110 Left err -> liftIO $ propagateerror err
112 Just (LOCKCONTENT _) -> do
113 -- Special remotes do not support locking content.
114 liftIO $ sendmessage FAILURE
116 Just (REMOVE k) -> do
117 tryNonAsync (Remote.removeKey r Nothing k) >>= \case
118 Right () -> liftIO $ sendmessage SUCCESS
119 Left err -> liftIO $ propagateerror err
121 Just (PUT (ProtoAssociatedFile af) k) -> do
124 Just (GET offset (ProtoAssociatedFile af) k) -> do
127 Just (BYPASS _) -> go
129 -- Not supported and the protocol ends here.
130 liftIO $ sendmessage $ CONNECTDONE (ExitFailure 1)
131 Just NOTIFYCHANGE -> do
132 liftIO $ sendmessage $
133 ERROR "NOTIFYCHANGE unsupported for a special remote"
135 Just _ -> giveup "protocol error"
138 receivemessage = liftIO (atomically recv) >>= \case
139 Right (Right m) -> return (Just m)
140 Right (Left _b) -> giveup "unexpected ByteString received from P2P MVar"
141 Left () -> return Nothing
144 (Right <$> takeTMVar ohdl)
146 (Left <$> readTMVar oclosedv)
148 receivebytestring = atomically recv >>= \case
149 Right (Left b) -> return (Just b)
150 Right (Right _m) -> giveup "did not receive ByteString from P2P MVar"
151 Left () -> return Nothing
154 (Right <$> takeTMVar ohdl)
156 (Left <$> readTMVar oclosedv)
158 sendmessage m = atomically $ putTMVar ihdl (Right m)
160 sendbytestring b = atomically $ putTMVar ihdl (Left b)
162 propagateerror err = sendmessage $ ERROR $
163 "proxied special remote reports: " ++ show err
165 -- Not using gitAnnexTmpObjectLocation because there might be
166 -- several concurrent GET and PUTs of the same key being proxied
167 -- from this special remote or others, and each needs to happen
168 -- independently. Also, this key is not getting added into the
169 -- local annex objects.
170 withproxytmpfile k a = withOtherTmp $ \othertmpdir ->
171 withTmpDirIn (fromRawFilePath othertmpdir) "proxy" $ \tmpdir ->
172 a (toRawFilePath tmpdir P.</> keyFile k)
174 -- Verify the content received from the client, to avoid bad content
175 -- being stored in the special remote.
177 liftIO $ sendmessage $ PUT_FROM (Offset 0)
178 withproxytmpfile k $ \tmpfile -> do
179 let store = tryNonAsync (storeput k af (decodeBS tmpfile)) >>= \case
180 Right () -> liftIO $ sendmessage SUCCESS
181 Left err -> liftIO $ propagateerror err
182 liftIO receivemessage >>= \case
183 Just (DATA (Len len)) -> do
184 iv <- startVerifyKeyContentIncrementally Remote.AlwaysVerify k
185 h <- liftIO $ openFile (fromRawFilePath tmpfile) WriteMode
186 gotall <- liftIO $ receivetofile iv h len
188 verified <- if gotall
189 then fst <$> finishVerifyKeyContentIncrementally' True iv
191 if protoversion > ProtocolVersion 1
192 then liftIO receivemessage >>= \case
193 Just (VALIDITY Valid)
195 | otherwise -> liftIO $ sendmessage FAILURE
196 Just (VALIDITY Invalid) ->
197 liftIO $ sendmessage FAILURE
198 _ -> giveup "protocol error"
200 _ -> giveup "protocol error"
201 liftIO $ removeWhenExistsWith removeFile (fromRawFilePath tmpfile)
203 storeput k af tmpfile = case mexportdb of
204 Just exportdb -> liftIO (Export.getExportTree exportdb k) >>= \case
205 [] -> storeputkey k af tmpfile
207 havelocs <- liftIO $ S.fromList
208 <$> Export.getExportedLocation exportdb k
209 let locs' = filter (`S.notMember` havelocs) locs
210 forM_ locs' $ \loc ->
211 storeputexport exportdb k loc tmpfile
212 liftIO $ Export.flushDbQueue exportdb
213 Nothing -> storeputkey k af tmpfile
215 storeputkey k af tmpfile =
216 Remote.storeKey r k af (Just tmpfile) nullMeterUpdate
218 storeputexport exportdb k loc tmpfile = do
219 Remote.storeExport (Remote.exportActions r) tmpfile k loc nullMeterUpdate
220 liftIO $ Export.addExportedLocation exportdb k loc
222 receivetofile iv h n = liftIO receivebytestring >>= \case
224 liftIO $ atomically $
228 n' <- storetofile iv h n (L.toChunks b)
229 -- Normally all the data is sent in a single
230 -- lazy bytestring. However, when the special
231 -- remote is a node in a cluster, a PUT is
232 -- streamed to it in multiple chunks.
235 else receivetofile iv h n'
236 Nothing -> return False
238 storetofile _ _ n [] = pure n
239 storetofile iv h n (b:bs) = do
240 writeVerifyChunk iv h b
241 storetofile iv h (n - fromIntegral (B.length b)) bs
243 proxyget offset af k = withproxytmpfile k $ \tmpfile -> do
244 -- Don't verify the content from the remote,
245 -- because the client will do its own verification.
246 let vc = Remote.NoVerify
247 tryNonAsync (Remote.retrieveKeyFile r k af (fromRawFilePath tmpfile) nullMeterUpdate vc) >>= \case
248 Right _ -> liftIO $ senddata offset tmpfile
249 Left err -> liftIO $ propagateerror err
251 senddata (Offset offset) f = do
252 size <- fromIntegral <$> getFileSize f
253 let n = max 0 (size - offset)
254 sendmessage $ DATA (Len n)
255 withBinaryFile (fromRawFilePath f) ReadMode $ \h -> do
256 hSeek h AbsoluteSeek offset
257 sendbs =<< L.hGetContents h
258 -- Important to keep the handle open until
259 -- the client responds. The bytestring
260 -- could still be lazily streaming out to
266 when (protoversion > ProtocolVersion 0) $
267 sendmessage (VALIDITY Valid)
270 receivemessage >>= \case
271 Just SUCCESS -> return ()
272 Just FAILURE -> return ()
273 Just _ -> giveup "protocol error"
276 {- Check if this repository can proxy for a specified remote uuid,
277 - and if so enable proxying for it. -}
278 checkCanProxy :: UUID -> UUID -> Annex Bool
279 checkCanProxy remoteuuid myuuid = do
280 myproxies <- M.lookup myuuid <$> getProxies
281 checkCanProxy' myproxies remoteuuid >>= \case
283 Annex.changeState $ \st -> st { Annex.proxyremote = Just v }
285 Left Nothing -> return False
286 Left (Just err) -> giveup err
288 checkCanProxy' :: Maybe (S.Set Proxy) -> UUID -> Annex (Either (Maybe String) (Either ClusterUUID Remote))
289 checkCanProxy' Nothing _ = return (Left Nothing)
290 checkCanProxy' (Just proxies) remoteuuid =
291 case filter (\p -> proxyRemoteUUID p == remoteuuid) (S.toList proxies) of
293 ps -> case mkClusterUUID remoteuuid of
294 Just cu -> proxyforcluster cu
295 Nothing -> proxyfor ps
298 rs <- concat . Remote.byCost <$> Remote.remoteList
299 myclusters <- annexClusters <$> Annex.getGitConfig
300 case canProxyForRemote rs ps myclusters remoteuuid of
301 Nothing -> notconfigured
302 Just r -> return (Right (Right r))
304 proxyforcluster cu = do
305 clusters <- getClusters
306 if M.member cu (clusterUUIDs clusters)
307 then return (Right (Left cu))
310 notconfigured = M.lookup remoteuuid <$> uuidDescMap >>= \case
311 Just desc -> return $ Left $ Just $
312 "not configured to proxy for repository " ++ fromUUIDDesc desc
313 Nothing -> return $ Left Nothing
315 {- Remotes that this repository is configured to proxy for.
317 - When there are multiple remotes that access the same repository,
318 - this picks the lowest cost one that is configured to be used as a proxy.
320 proxyForRemotes :: Annex [Remote]
323 (M.lookup myuuid <$> getProxies) >>= \case
326 let myproxies' = S.toList myproxies
327 rs <- concat . Remote.byCost <$> Remote.remoteList
328 myclusters <- annexClusters <$> Annex.getGitConfig
329 return $ mapMaybe (canProxyForRemote rs myproxies' myclusters . Remote.uuid) rs
331 -- Only proxy for a remote when the git configuration allows it.
332 -- This is important to prevent changes to the git-annex branch
333 -- causing unexpected proxying for remotes.
335 :: [Remote] -- ^ must be sorted by cost
337 -> M.Map RemoteName ClusterUUID
340 canProxyForRemote rs myproxies myclusters remoteuuid =
341 headMaybe $ filter canproxy rs
345 proxyisconfigured r &&
346 any (isproxyfor r) myproxies
348 sameuuid r = Remote.uuid r == remoteuuid
351 proxyRemoteUUID p == remoteuuid &&
352 Remote.name r == proxyRemoteName p
355 | remoteAnnexProxy (Remote.gitconfig r) = True
356 -- Proxy for remotes that are configured as cluster nodes.
357 | any (`M.member` myclusters) (fromMaybe [] $ remoteAnnexClusterNode $ Remote.gitconfig r) = True
358 -- Proxy for a remote when it is proxied by another remote
359 -- which is itself configured as a cluster gateway.
360 | otherwise = case remoteAnnexProxiedBy (Remote.gitconfig r) of
361 Just proxyuuid -> not $ null $
362 concatMap (remoteAnnexClusterGateway . Remote.gitconfig) $
363 filter (\p -> Remote.uuid p == proxyuuid) rs
366 mkProxyMethods :: ProxyMethods
367 mkProxyMethods = ProxyMethods
368 { removedContent = \lu u k -> logChange lu k u InfoMissing
369 , addedContent = \lu u k -> logChange lu k u InfoPresent