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 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378
|
{- git-annex external backend
-
- Copyright 2020 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE OverloadedStrings #-}
module Backend.External (makeBackend) where
import Annex.Common
import Annex.ExternalAddonProcess
import Backend.Utilities
import Types.Key
import Types.Backend
import Types.KeySource
import Utility.Metered
import qualified Utility.SimpleProtocol as Proto
import qualified Data.ByteString as S
import qualified Data.ByteString.Short as S (toShort, fromShort)
import qualified Data.Map.Strict as M
import Data.Char
import Control.Concurrent
import System.IO.Unsafe (unsafePerformIO)
newtype ExternalBackendName = ExternalBackendName S.ByteString
deriving (Show, Eq, Ord)
-- Makes Backend representing an external backend of any type.
-- If the program is not available or doesn't work, makes a Backend
-- that cannot generate or verify keys, but that still lets the keys be
-- basically used.
makeBackend :: S.ByteString -> HasExt -> Annex Backend
makeBackend bname hasext =
withExternalState ebname hasext (return . externalBackend)
where
ebname = ExternalBackendName bname
makeBackend' :: ExternalBackendName -> HasExt -> Either ExternalAddonStartError ExternalAddonProcess -> Annex Backend
makeBackend' ebname@(ExternalBackendName bname) hasext (Right p) = do
let st = ExternalState
{ externalAddonProcess = Right p
, externalBackend = unavailBackend ebname hasext
}
canverify <- handleRequest st CANVERIFY (pure False) $ \case
CANVERIFY_YES -> result True
CANVERIFY_NO -> result False
_ -> Nothing
isstable <- handleRequest st ISSTABLE (pure False) $ \case
ISSTABLE_YES -> result True
ISSTABLE_NO -> result False
_ -> Nothing
iscryptographicallysecure <- handleRequest st ISCRYPTOGRAPHICALLYSECURE (pure False) $ \case
ISCRYPTOGRAPHICALLYSECURE_YES -> result True
ISCRYPTOGRAPHICALLYSECURE_NO -> result False
_ -> Nothing
return $ Backend
{ backendVariety = ExternalKey bname hasext
, genKey = Just $ genKeyExternal ebname hasext
, verifyKeyContent = if canverify
then Just $ verifyKeyContentExternal ebname hasext
-- The protocol supports PROGRESS here,
-- but it's not actually used. It was put
-- in to avoid needing a protocol version
-- bump if progress handling is later added.
nullMeterUpdate
else Nothing
, verifyKeyContentIncrementally = Nothing
, canUpgradeKey = Nothing
, fastMigrate = Nothing
, isStableKey = const isstable
, isCryptographicallySecure = iscryptographicallysecure
, isCryptographicallySecureKey = const (pure iscryptographicallysecure)
}
makeBackend' ebname hasext (Left _) = return $ unavailBackend ebname hasext
unavailBackend :: ExternalBackendName -> HasExt -> Backend
unavailBackend (ExternalBackendName bname) hasext =
Backend
{ backendVariety = ExternalKey bname hasext
, genKey = Nothing
, verifyKeyContent = Nothing
, verifyKeyContentIncrementally = Nothing
, canUpgradeKey = Nothing
, fastMigrate = Nothing
, isStableKey = const False
, isCryptographicallySecure = False
, isCryptographicallySecureKey = const (pure False)
}
genKeyExternal :: ExternalBackendName -> HasExt -> KeySource -> MeterUpdate -> Annex Key
genKeyExternal ebname hasext ks meterupdate =
withExternalState ebname hasext $ \st ->
handleRequest st req notavail go
where
req = GENKEY (fromOsPath (contentLocation ks))
notavail = giveup $ "Cannot generate a key, since " ++ externalBackendProgram ebname ++ " is not available."
go (GENKEY_SUCCESS pk) = Just $ Result <$> fromProtoKey pk hasext ks
go (GENKEY_FAILURE msg) = Just $ giveup $
"External backend program failed to generate a key: " ++ msg
go (PROGRESS bytesprocessed) = Just $ do
liftIO $ meterupdate bytesprocessed
return $ GetNextMessage go
go _ = Nothing
verifyKeyContentExternal :: ExternalBackendName -> HasExt -> MeterUpdate -> Key -> OsPath -> Annex Bool
verifyKeyContentExternal ebname hasext meterupdate k f =
withExternalState ebname hasext $ \st ->
handleRequest st req notavail go
where
req = VERIFYKEYCONTENT (toProtoKey k) (fromOsPath f)
-- This should not be able to happen, because CANVERIFY is checked
-- before this function is enable, and so the external program
-- is available. But if it does, fail the verification.
notavail = return False
go VERIFYKEYCONTENT_SUCCESS = result True
go VERIFYKEYCONTENT_FAILURE = result False
go (PROGRESS bytesprocessed) = Just $ do
liftIO $ meterupdate bytesprocessed
return $ GetNextMessage go
go _ = Nothing
-- State about a running external backend program.
data ExternalState = ExternalState
{ externalAddonProcess :: Either ExternalAddonStartError ExternalAddonProcess
, externalBackend :: Backend
}
handleRequest :: ExternalState -> Request -> Annex a -> ResponseHandler a -> Annex a
handleRequest st req whenunavail responsehandler =
withExternalAddon st whenunavail $ \p -> do
sendMessage p req
let loop = receiveResponse p responsehandler
(Just . handleExceptionalMessage loop)
loop
where
handleExceptionalMessage _ (ERROR err) = do
warning $ UnquotedString $
"external special remote error: " ++ err
whenunavail
handleExceptionalMessage loop (DEBUG msg) = do
fastDebug "Backend.External" msg
loop
withExternalAddon :: ExternalState -> a -> (ExternalAddonProcess -> a) -> a
withExternalAddon st whenunavail a = case externalAddonProcess st of
Right addon -> a addon
Left _ -> whenunavail
sendMessage :: Proto.Sendable m => ExternalAddonProcess -> m -> Annex ()
sendMessage p m = liftIO $ do
protocolDebug p True line
hPutStrLn (externalSend p) line
hFlush (externalSend p)
where
line = unwords $ Proto.formatMessage m
{- A response handler can yield a result, or it can request that another
- message be consumed from the external. -}
data ResponseHandlerResult a
= Result a
| GetNextMessage (ResponseHandler a)
type ResponseHandler a = Response -> Maybe (Annex (ResponseHandlerResult a))
result :: a -> Maybe (Annex (ResponseHandlerResult a))
result = Just . return . Result
{- Waits for a message from the external backend, and passes it to the
- appropriate handler.
-
- If the handler returns Nothing, this is a protocol error.
-}
receiveResponse
:: ExternalAddonProcess
-> ResponseHandler a
-> (ExceptionalMessage -> Maybe (Annex a))
-> Annex a
receiveResponse p handleresponse handleexceptional =
go =<< liftIO (catchMaybeIO $ hGetLine $ externalReceive p)
where
go Nothing = protocolError False ""
go (Just s) = do
liftIO $ protocolDebug p False s
case Proto.parseMessage s :: Maybe Response of
Just resp -> case handleresponse resp of
Nothing -> protocolError True s
Just callback -> callback >>= \case
Result a -> return a
GetNextMessage handleresponse' ->
receiveResponse p handleresponse' handleexceptional
Nothing -> case Proto.parseMessage s :: Maybe ExceptionalMessage of
Just msg -> maybe (protocolError True s) id (handleexceptional msg)
Nothing -> protocolError False s
protocolError parsed s = giveup $ "external backend protocol error, unexpectedly received \"" ++ s ++ "\" " ++
if parsed
then "(message not allowed at this time)"
else "(unable to parse message)"
-- Information about pools of of running external backends that are
-- available to use is stored in this global.
{-# NOINLINE poolVar #-}
poolVar :: MVar (M.Map ExternalBackendName (ExternalAddonPID, [ExternalState]))
poolVar = unsafePerformIO $ newMVar M.empty
-- Starts a new instance of an external backend.
-- Does not add it to the poolVar; caller should add it once it's done
-- using it.
newExternalState :: ExternalBackendName -> HasExt -> ExternalAddonPID -> Annex ExternalState
newExternalState ebname hasext pid = do
st <- startExternalAddonProcess basecmd [] pid
st' <- case st of
Left (ProgramNotInstalled msg) -> warnonce msg >> return st
Left (ProgramFailure msg) -> warnonce msg >> return st
Right p -> do
sendMessage p GETVERSION
v <- receiveResponse p
(\resp -> case resp of
VERSION v -> result v
_ -> Nothing
)
(const Nothing)
if v `notElem` supportedProtocolVersions
then do
warnonce (basecmd ++ " uses an unsupported version of the external backend protocol")
return $ Left (ProgramFailure "bad protocol version")
else return (Right p)
backend <- makeBackend' ebname hasext st'
return $ ExternalState
{ externalAddonProcess = st'
, externalBackend = backend
}
where
basecmd = externalBackendProgram ebname
warnonce msg = when (pid == 1) $
warning (UnquotedString msg)
externalBackendProgram :: ExternalBackendName -> String
externalBackendProgram (ExternalBackendName bname) = "git-annex-backend-X" ++ decodeBS bname
-- Runs an action with an ExternalState, starting a new external backend
-- process if necessary. It is returned to the pool once the action
-- finishes successfully. On exception, it's shut down.
withExternalState :: ExternalBackendName -> HasExt -> (ExternalState -> Annex a) -> Annex a
withExternalState bname hasext a = do
st <- get
r <- a st `onException` shutdown st
put st -- only when no exception is thrown
return r
where
get = do
m <- liftIO $ takeMVar poolVar
case fromMaybe (1, []) (M.lookup bname m) of
(pid, []) -> do
let m' = M.insert bname (succ pid, []) m
liftIO $ putMVar poolVar m'
newExternalState bname hasext pid
(pid, (st:rest)) -> do
let m' = M.insert bname (pid, rest) m
liftIO $ putMVar poolVar m'
return st
put st = liftIO $ modifyMVar_ poolVar $
pure . M.adjust (\(pid, l) -> (pid, st:l)) bname
shutdown st = liftIO $
withExternalAddon st noop (flip externalShutdown False)
-- This is a key as seen by the protocol consumer. When the "E" variant
-- of the external backend is in use, it does not include an extension.
-- And it's assumed not to contain spaces or newlines, or anything besides
-- ascii alphanumerics, because the protocol does not allow keys containing
-- such things.
newtype ProtoKey = ProtoKey Key
deriving (Show)
fromProtoKey :: ProtoKey -> HasExt -> KeySource -> Annex Key
fromProtoKey (ProtoKey k) (HasExt False) _ = pure k
fromProtoKey (ProtoKey k) hasext@(HasExt True) source =
addE source (setHasExt hasext) k
toProtoKey :: Key -> ProtoKey
toProtoKey k = ProtoKey $ alterKey k $ \d -> d
-- The extension can be easily removed, because the protocol
-- documentation does not allow '.' to be used in the keyName,
-- so the first one is the extension.
{ keyName = S.toShort (S.takeWhile (/= dot) (S.fromShort (keyName d)))
, keyVariety = setHasExt (HasExt False) (keyVariety d)
}
where
dot = fromIntegral (ord '.')
setHasExt :: HasExt -> KeyVariety -> KeyVariety
setHasExt hasext (ExternalKey name _) = ExternalKey name hasext
setHasExt _ v = v
instance Proto.Serializable ProtoKey where
serialize (ProtoKey k) = Proto.serialize k
deserialize = fmap ProtoKey . Proto.deserialize
data Request
= GETVERSION
| CANVERIFY
| ISSTABLE
| ISCRYPTOGRAPHICALLYSECURE
| GENKEY FilePath
| VERIFYKEYCONTENT ProtoKey FilePath
deriving (Show)
data Response
= VERSION ProtocolVersion
| CANVERIFY_YES
| CANVERIFY_NO
| ISSTABLE_YES
| ISSTABLE_NO
| ISCRYPTOGRAPHICALLYSECURE_YES
| ISCRYPTOGRAPHICALLYSECURE_NO
| GENKEY_SUCCESS ProtoKey
| GENKEY_FAILURE ErrorMsg
| VERIFYKEYCONTENT_SUCCESS
| VERIFYKEYCONTENT_FAILURE
| PROGRESS BytesProcessed
deriving (Show)
data ExceptionalMessage
= ERROR ErrorMsg
| DEBUG String
deriving (Show)
type ErrorMsg = String
newtype ProtocolVersion = ProtocolVersion Int
deriving (Show, Eq)
supportedProtocolVersions :: [ProtocolVersion]
supportedProtocolVersions = [ProtocolVersion 1]
instance Proto.Serializable ProtocolVersion where
serialize (ProtocolVersion n) = show n
deserialize = ProtocolVersion <$$> readish
instance Proto.Sendable ExceptionalMessage where
formatMessage (ERROR err) = ["ERROR", Proto.serialize err]
formatMessage (DEBUG msg) = ["DEBUG", Proto.serialize msg]
instance Proto.Receivable ExceptionalMessage where
parseCommand "ERROR" = Proto.parse1 ERROR
parseCommand "DEBUG" = Proto.parse1 DEBUG
parseCommand _ = Proto.parseFail
instance Proto.Sendable Request where
formatMessage GETVERSION = ["GETVERSION"]
formatMessage CANVERIFY = ["CANVERIFY"]
formatMessage ISSTABLE = ["ISSTABLE"]
formatMessage ISCRYPTOGRAPHICALLYSECURE = ["ISCRYPTOGRAPHICALLYSECURE"]
formatMessage (GENKEY file) = ["GENKEY", Proto.serialize file]
formatMessage (VERIFYKEYCONTENT key file) =
["VERIFYKEYCONTENT", Proto.serialize key, Proto.serialize file]
instance Proto.Receivable Response where
parseCommand "VERSION" = Proto.parse1 VERSION
parseCommand "CANVERIFY-YES" = Proto.parse0 CANVERIFY_YES
parseCommand "CANVERIFY-NO" = Proto.parse0 CANVERIFY_NO
parseCommand "ISSTABLE-YES" = Proto.parse0 ISSTABLE_YES
parseCommand "ISSTABLE-NO" = Proto.parse0 ISSTABLE_NO
parseCommand "ISCRYPTOGRAPHICALLYSECURE-YES" = Proto.parse0 ISCRYPTOGRAPHICALLYSECURE_YES
parseCommand "ISCRYPTOGRAPHICALLYSECURE-NO" = Proto.parse0 ISCRYPTOGRAPHICALLYSECURE_NO
parseCommand "GENKEY-SUCCESS" = Proto.parse1 GENKEY_SUCCESS
parseCommand "GENKEY-FAILURE" = Proto.parse1 GENKEY_FAILURE
parseCommand "VERIFYKEYCONTENT-SUCCESS" = Proto.parse0 VERIFYKEYCONTENT_SUCCESS
parseCommand "VERIFYKEYCONTENT-FAILURE" = Proto.parse0 VERIFYKEYCONTENT_FAILURE
parseCommand "PROGRESS" = Proto.parse1 PROGRESS
parseCommand _ = Proto.parseFail
|