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 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
|
{- P2P protocol over HTTP,
- data types for servant not including the servant API
-
- Copyright 2024 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE CPP #-}
module P2P.Http.Types where
import Annex.Common
import qualified P2P.Protocol as P2P
import Utility.MonotonicClock
import Servant
import Data.Aeson hiding (Key)
import Text.Read (readMaybe)
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import qualified Data.ByteString as B
import Codec.Binary.Base64Url as B64
import Data.Char
import Control.DeepSeq
import GHC.Generics (Generic)
data V4 = V4 deriving (Show)
data V3 = V3 deriving (Show)
data V2 = V2 deriving (Show)
data V1 = V1 deriving (Show)
data V0 = V0 deriving (Show)
class APIVersion v where
protocolVersion :: v -> P2P.ProtocolVersion
instance APIVersion V4 where protocolVersion _ = P2P.ProtocolVersion 4
instance APIVersion V3 where protocolVersion _ = P2P.ProtocolVersion 3
instance APIVersion V2 where protocolVersion _ = P2P.ProtocolVersion 2
instance APIVersion V1 where protocolVersion _ = P2P.ProtocolVersion 1
instance APIVersion V0 where protocolVersion _ = P2P.ProtocolVersion 0
-- Keys, UUIDs, and filenames can be base64 encoded since Servant uses
-- Text and so needs UTF-8.
newtype B64Key = B64Key Key
deriving (Show)
newtype B64FilePath = B64FilePath OsPath
deriving (Show)
associatedFileToB64FilePath :: AssociatedFile -> Maybe B64FilePath
associatedFileToB64FilePath (AssociatedFile Nothing) = Nothing
associatedFileToB64FilePath (AssociatedFile (Just f)) = Just (B64FilePath f)
b64FilePathToAssociatedFile :: Maybe B64FilePath -> AssociatedFile
b64FilePathToAssociatedFile Nothing = AssociatedFile Nothing
b64FilePathToAssociatedFile (Just (B64FilePath f)) = AssociatedFile (Just f)
newtype B64UUID t = B64UUID { fromB64UUID :: UUID }
deriving (Show, Ord, Eq, Generic, NFData)
encodeB64Text :: B.ByteString -> T.Text
encodeB64Text b = case TE.decodeUtf8' b of
Right t
| (snd <$> B.unsnoc b) == Just closebracket
&& (fst <$> B.uncons b) == Just openbracket ->
b64wrapped
| otherwise -> t
Left _ -> b64wrapped
where
b64wrapped = TE.decodeUtf8 $ "[" <> B64.encode b <> "]"
openbracket = fromIntegral (ord '[')
closebracket = fromIntegral (ord ']')
decodeB64Text :: T.Text -> Either T.Text B.ByteString
decodeB64Text t =
case T.unsnoc t of
Just (t', lastc) | lastc == ']' ->
case T.uncons t' of
Just (firstc, t'') | firstc == '[' ->
case B64.decode (TE.encodeUtf8 t'') of
Right b -> Right b
Left _ -> Left "unable to base64 decode [] wrapped value"
_ -> Right (TE.encodeUtf8 t)
_ -> Right (TE.encodeUtf8 t)
-- Phantom types.
data ClientSide
data ServerSide
data Bypass
data Plus
data Lock
type LockID = B64UUID Lock
newtype DataLength = DataLength Integer
deriving (Show)
newtype CheckPresentResult = CheckPresentResult Bool
deriving (Show)
newtype RemoveResult = RemoveResult Bool
deriving (Show)
data RemoveResultPlus = RemoveResultPlus Bool [B64UUID Plus]
deriving (Show)
newtype GetTimestampResult = GetTimestampResult Timestamp
deriving (Show)
newtype PutResult = PutResult Bool
deriving (Eq, Show)
data PutResultPlus = PutResultPlus Bool [B64UUID Plus]
deriving (Show)
data PutOffsetResult
= PutOffsetResult Offset
| PutOffsetResultAlreadyHave
deriving (Show)
data PutOffsetResultPlus
= PutOffsetResultPlus Offset
| PutOffsetResultAlreadyHavePlus [B64UUID Plus]
deriving (Show, Generic, NFData)
newtype Offset = Offset P2P.Offset
deriving (Show, Generic, NFData)
newtype Timestamp = Timestamp MonotonicTimestamp
deriving (Show)
data LockResult = LockResult Bool (Maybe LockID)
deriving (Show, Generic, NFData)
newtype UnlockRequest = UnlockRequest Bool
deriving (Show, Generic, NFData)
-- Not using servant's built-in basic authentication support,
-- because whether authentication is needed depends on server
-- configuration.
data Auth = Auth B.ByteString B.ByteString
deriving (Show, Generic, NFData, Eq, Ord)
instance ToHttpApiData Auth where
toHeader (Auth u p) = "Basic " <> B64.encode (u <> ":" <> p)
#if MIN_VERSION_text(2,0,0)
toUrlPiece = TE.decodeUtf8Lenient . toHeader
#else
toUrlPiece = TE.decodeUtf8With (\_ _ -> Just '\xfffd') . toHeader
#endif
instance FromHttpApiData Auth where
parseHeader h =
let (b, rest) = B.break (isSpace . chr . fromIntegral) h
in if map toLower (decodeBS b) == "basic"
then case B64.decode (B.dropWhile (isSpace . chr . fromIntegral) rest) of
Right v -> case B.split (fromIntegral (ord ':')) v of
(u:ps) -> Right $
Auth u (B.intercalate ":" ps)
_ -> bad
Left _ -> bad
else bad
where
bad = Left "invalid basic auth header"
parseUrlPiece = parseHeader . encodeBS . T.unpack
newtype ConnectionKeepAlive = ConnectionKeepAlive T.Text
connectionKeepAlive :: ConnectionKeepAlive
connectionKeepAlive = ConnectionKeepAlive "Keep-Alive"
newtype KeepAlive = KeepAlive T.Text
keepAlive :: KeepAlive
keepAlive = KeepAlive "timeout=1200"
instance ToHttpApiData ConnectionKeepAlive where
toUrlPiece (ConnectionKeepAlive t) = t
instance FromHttpApiData ConnectionKeepAlive where
parseUrlPiece = Right . ConnectionKeepAlive
instance ToHttpApiData KeepAlive where
toUrlPiece (KeepAlive t) = t
instance FromHttpApiData KeepAlive where
parseUrlPiece = Right . KeepAlive
instance ToHttpApiData V4 where toUrlPiece _ = "v4"
instance ToHttpApiData V3 where toUrlPiece _ = "v3"
instance ToHttpApiData V2 where toUrlPiece _ = "v2"
instance ToHttpApiData V1 where toUrlPiece _ = "v1"
instance ToHttpApiData V0 where toUrlPiece _ = "v0"
instance FromHttpApiData V4 where parseUrlPiece = parseAPIVersion V4 "v4"
instance FromHttpApiData V3 where parseUrlPiece = parseAPIVersion V3 "v3"
instance FromHttpApiData V2 where parseUrlPiece = parseAPIVersion V2 "v2"
instance FromHttpApiData V1 where parseUrlPiece = parseAPIVersion V1 "v1"
instance FromHttpApiData V0 where parseUrlPiece = parseAPIVersion V0 "v0"
parseAPIVersion :: v -> T.Text -> T.Text -> Either T.Text v
parseAPIVersion v need t
| t == need = Right v
| otherwise = Left "bad version"
instance ToHttpApiData B64Key where
toUrlPiece (B64Key k) = encodeB64Text (serializeKey' k)
instance FromHttpApiData B64Key where
parseUrlPiece t = case decodeB64Text t of
Right b -> maybe (Left "key parse error") (Right . B64Key)
(deserializeKey' b)
Left err -> Left err
instance ToHttpApiData (B64UUID t) where
toUrlPiece (B64UUID u) = encodeB64Text (fromUUID u)
instance FromHttpApiData (B64UUID t) where
parseUrlPiece t = case decodeB64Text t of
Right b -> case toUUID b of
u@(UUID _) -> Right (B64UUID u)
NoUUID -> Left "empty UUID"
Left err -> Left err
instance ToHttpApiData B64FilePath where
toUrlPiece (B64FilePath f) = encodeB64Text (fromOsPath f)
instance FromHttpApiData B64FilePath where
parseUrlPiece t = case decodeB64Text t of
Right b -> Right (B64FilePath (toOsPath b))
Left err -> Left err
instance ToHttpApiData Offset where
toUrlPiece (Offset (P2P.Offset n)) = T.pack (show n)
instance FromHttpApiData Offset where
parseUrlPiece t = case readMaybe (T.unpack t) of
Nothing -> Left "offset parse error"
Just n -> Right (Offset (P2P.Offset n))
instance ToHttpApiData Timestamp where
toUrlPiece (Timestamp (MonotonicTimestamp n)) = T.pack (show n)
instance FromHttpApiData Timestamp where
parseUrlPiece t = case readMaybe (T.unpack t) of
Nothing -> Left "timestamp parse error"
Just n -> Right (Timestamp (MonotonicTimestamp n))
instance ToHttpApiData DataLength where
toUrlPiece (DataLength n) = T.pack (show n)
instance FromHttpApiData DataLength where
parseUrlPiece t = case readMaybe (T.unpack t) of
Nothing -> Left "X-git-annex-data-length parse error"
Just n -> Right (DataLength n)
instance ToJSON PutResult where
toJSON (PutResult b) =
object ["stored" .= b]
instance FromJSON PutResult where
parseJSON = withObject "PutResult" $ \v -> PutResult
<$> v .: "stored"
instance ToJSON PutResultPlus where
toJSON (PutResultPlus b us) = object
[ "stored" .= b
, "plusuuids" .= plusList us
]
instance FromJSON PutResultPlus where
parseJSON = withObject "PutResultPlus" $ \v -> PutResultPlus
<$> v .: "stored"
<*> v .: "plusuuids"
instance ToJSON CheckPresentResult where
toJSON (CheckPresentResult b) = object
["present" .= b]
instance FromJSON CheckPresentResult where
parseJSON = withObject "CheckPresentResult" $ \v -> CheckPresentResult
<$> v .: "present"
instance ToJSON RemoveResult where
toJSON (RemoveResult b) = object
["removed" .= b]
instance FromJSON RemoveResult where
parseJSON = withObject "RemoveResult" $ \v -> RemoveResult
<$> v .: "removed"
instance ToJSON RemoveResultPlus where
toJSON (RemoveResultPlus b us) = object
[ "removed" .= b
, "plusuuids" .= plusList us
]
instance FromJSON RemoveResultPlus where
parseJSON = withObject "RemoveResultPlus" $ \v -> RemoveResultPlus
<$> v .: "removed"
<*> v .: "plusuuids"
instance ToJSON GetTimestampResult where
toJSON (GetTimestampResult (Timestamp (MonotonicTimestamp t))) = object
["timestamp" .= t]
instance FromJSON GetTimestampResult where
parseJSON = withObject "GetTimestampResult" $ \v ->
GetTimestampResult . Timestamp . MonotonicTimestamp
<$> v .: "timestamp"
instance ToJSON PutOffsetResult where
toJSON (PutOffsetResult (Offset (P2P.Offset o))) = object
["offset" .= o]
toJSON PutOffsetResultAlreadyHave = object
["alreadyhave" .= True]
instance FromJSON PutOffsetResult where
parseJSON = withObject "PutOffsetResult" $ \v ->
(PutOffsetResult
<$> (Offset . P2P.Offset <$> v .: "offset"))
<|> (mkalreadyhave
<$> (v .: "alreadyhave"))
where
mkalreadyhave :: Bool -> PutOffsetResult
mkalreadyhave _ = PutOffsetResultAlreadyHave
instance ToJSON PutOffsetResultPlus where
toJSON (PutOffsetResultPlus (Offset (P2P.Offset o))) = object
[ "offset" .= o ]
toJSON (PutOffsetResultAlreadyHavePlus us) = object
[ "alreadyhave" .= True
, "plusuuids" .= plusList us
]
instance FromJSON PutOffsetResultPlus where
parseJSON = withObject "PutOffsetResultPlus" $ \v ->
(PutOffsetResultPlus
<$> (Offset . P2P.Offset <$> v .: "offset"))
<|> (mkalreadyhave
<$> (v .: "alreadyhave")
<*> (v .: "plusuuids"))
where
mkalreadyhave :: Bool -> [B64UUID Plus] -> PutOffsetResultPlus
mkalreadyhave _ us = PutOffsetResultAlreadyHavePlus us
instance FromJSON (B64UUID t) where
parseJSON (String t) = case decodeB64Text t of
Right s -> pure (B64UUID (toUUID s))
Left _ -> mempty
parseJSON _ = mempty
instance ToJSON LockResult where
toJSON (LockResult v (Just (B64UUID lck))) = object
[ "locked" .= v
, "lockid" .= encodeB64Text (fromUUID lck)
]
toJSON (LockResult v Nothing) = object
[ "locked" .= v
]
instance FromJSON LockResult where
parseJSON = withObject "LockResult" $ \v -> LockResult
<$> v .: "locked"
<*> v .:? "lockid"
instance ToJSON UnlockRequest where
toJSON (UnlockRequest v) = object
["unlock" .= v]
instance FromJSON UnlockRequest where
parseJSON = withObject "UnlockRequest" $ \v -> UnlockRequest
<$> v .: "unlock"
plusList :: [B64UUID Plus] -> [String]
plusList = map (\(B64UUID u) -> fromUUID u)
class PlusClass plus unplus where
dePlus :: plus -> unplus
plus :: unplus -> plus
instance PlusClass RemoveResultPlus RemoveResult where
dePlus (RemoveResultPlus b _) = RemoveResult b
plus (RemoveResult b) = RemoveResultPlus b mempty
instance PlusClass PutResultPlus PutResult where
dePlus (PutResultPlus b _) = PutResult b
plus (PutResult b) = PutResultPlus b mempty
instance PlusClass PutOffsetResultPlus PutOffsetResult where
dePlus (PutOffsetResultPlus o) = PutOffsetResult o
dePlus (PutOffsetResultAlreadyHavePlus _) = PutOffsetResultAlreadyHave
plus (PutOffsetResult o) = PutOffsetResultPlus o
plus PutOffsetResultAlreadyHave = PutOffsetResultAlreadyHavePlus []
|