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
|
module TestSuite.InMemRepo (
InMemRepo(..)
, newInMemRepo
, initRoot
, InMemFile(..)
, inMemFileRender
) where
-- stdlib
import Control.Exception
import Data.Kind (Type)
import Data.Time
import qualified Codec.Archive.Tar as Tar
import qualified Codec.Compression.GZip as GZip
import qualified Data.ByteString.Lazy as BS.L
-- Cabal
import Distribution.Text
-- hackage-security
import Hackage.Security.Client
import Hackage.Security.Client.Formats
import Hackage.Security.Client.Repository
import Hackage.Security.Client.Verify
import Hackage.Security.JSON
import Hackage.Security.Util.Path
import Hackage.Security.Util.Some
-- TestSuite
import TestSuite.PrivateKeys
import TestSuite.Util.StrictMVar
{-------------------------------------------------------------------------------
"Files" from the in-memory repository
-------------------------------------------------------------------------------}
data InMemFile :: Type -> Type where
InMemMetadata :: ToJSON WriteJSON a => RepoLayout -> a -> InMemFile Metadata
InMemBinary :: BS.L.ByteString -> InMemFile Binary
inMemFileRender :: InMemFile typ -> BS.L.ByteString
inMemFileRender (InMemMetadata layout file) = renderJSON layout file
inMemFileRender (InMemBinary bs) = bs
instance DownloadedFile InMemFile where
downloadedRead file =
return $ inMemFileRender file
downloadedVerify file info =
return $ knownFileInfoEqual (fileInfo (inMemFileRender file))
(trusted info)
downloadedCopyTo file dest =
writeLazyByteString dest (inMemFileRender file)
{-------------------------------------------------------------------------------
In-memory repository
-------------------------------------------------------------------------------}
data InMemRepo = InMemRepo {
-- | Get a file from the repository
inMemRepoGet :: forall fs typ.
RemoteFile fs typ
-> Verify (Some (HasFormat fs), InMemFile typ)
-- | Get a file, based on a path (uses hackageRepoLayout)
, inMemRepoGetPath :: RepoPath -> IO (Some InMemFile)
-- | Run the "cron job" on the server
--
-- That is, resign the timestamp and the snapshot
, inMemRepoCron :: UTCTime -> IO ()
-- | Rollover the timestamp and snapshot keys
, inMemRepoKeyRollover :: UTCTime -> IO ()
-- | Set the content of the repo tar index and resign
, inMemRepoSetIndex :: UTCTime -> [Tar.Entry] -> IO ()
}
newInMemRepo :: RepoLayout
-> Signed Root
-> UTCTime
-> PrivateKeys
-> IO InMemRepo
newInMemRepo layout root now keys = do
state <- newMVar $ initRemoteState now layout keys root
return InMemRepo {
inMemRepoGet = get state
, inMemRepoGetPath = getPath state
, inMemRepoCron = cron state
, inMemRepoKeyRollover = keyRollover state
, inMemRepoSetIndex = setIndex state
}
{-------------------------------------------------------------------------------
"Remote" state (as it is "on the server")
-------------------------------------------------------------------------------}
data RemoteState = RemoteState {
remoteKeys :: !PrivateKeys
, remoteLayout :: !RepoLayout
, remoteRoot :: !(Signed Root)
, remoteTimestamp :: !(Signed Timestamp)
, remoteSnapshot :: !(Signed Snapshot)
, remoteMirrors :: !(Signed Mirrors)
, remoteTar :: !BS.L.ByteString
, remoteTarGz :: !BS.L.ByteString
}
initRoot :: UTCTime -> RepoLayout -> PrivateKeys -> Signed Root
initRoot now layout keys = withSignatures layout (privateRoot keys) Root {
rootVersion = FileVersion 1
, rootExpires = expiresInDays now (365 * 10)
, rootKeys = privateKeysEnv keys
, rootRoles = privateKeysRoles keys
}
initRemoteState :: UTCTime
-> RepoLayout
-> PrivateKeys
-> Signed Root
-> RemoteState
initRemoteState now layout keys signedRoot = RemoteState {
remoteKeys = keys
, remoteLayout = layout
, remoteRoot = signedRoot
, remoteTimestamp = signedTimestamp
, remoteSnapshot = signedSnapshot
, remoteMirrors = signedMirrors
, remoteTar = initTar
, remoteTarGz = initTarGz
}
where
signedTimestamp = withSignatures layout [privateTimestamp keys] initTimestamp
signedSnapshot = withSignatures layout [privateSnapshot keys] initSnapshot
signedMirrors = withSignatures layout [privateMirrors keys] initMirrors
initMirrors :: Mirrors
initMirrors = Mirrors {
mirrorsVersion = FileVersion 1
, mirrorsExpires = expiresNever
, mirrorsMirrors = []
}
initSnapshot :: Snapshot
initSnapshot = Snapshot {
snapshotVersion = FileVersion 1
, snapshotExpires = expiresInDays now 3
, snapshotInfoRoot = fileInfo $ renderJSON layout signedRoot
, snapshotInfoMirrors = fileInfo $ renderJSON layout signedMirrors
, snapshotInfoTarGz = fileInfo $ initTarGz
, snapshotInfoTar = Just $ fileInfo initTar
}
initTimestamp :: Timestamp
initTimestamp = Timestamp {
timestampVersion = FileVersion 1
, timestampExpires = expiresInDays now 3
, timestampInfoSnapshot = fileInfo $ renderJSON layout signedSnapshot
}
initTar :: BS.L.ByteString
initTar = Tar.write []
initTarGz :: BS.L.ByteString
initTarGz = GZip.compress initTar
{-------------------------------------------------------------------------------
InMemRepo methods
-------------------------------------------------------------------------------}
-- | Get a file from the server
get :: MVar RemoteState -> RemoteFile fs typ -> Verify (Some (HasFormat fs), InMemFile typ)
get state remoteFile = do
RemoteState{..} <- liftIO $ readMVar state
case remoteFile of
RemoteTimestamp -> return (Some (HFZ FUn), InMemMetadata remoteLayout remoteTimestamp)
RemoteSnapshot _ -> return (Some (HFZ FUn), InMemMetadata remoteLayout remoteSnapshot)
RemoteMirrors _ -> return (Some (HFZ FUn), InMemMetadata remoteLayout remoteMirrors)
RemoteRoot _ -> return (Some (HFZ FUn), InMemMetadata remoteLayout remoteRoot)
RemoteIndex hasGz _ -> return (Some hasGz, InMemBinary remoteTarGz)
RemotePkgTarGz pkgId _ -> error $ "withRemote: RemotePkgTarGz " ++ display pkgId
getPath :: MVar RemoteState -> RepoPath -> IO (Some InMemFile)
getPath state repoPath = do
RemoteState{..} <- readMVar state
case toUnrootedFilePath (unrootPath repoPath) of
"root.json" -> return $ Some (InMemMetadata remoteLayout remoteRoot)
"timestamp.json" -> return $ Some (InMemMetadata remoteLayout remoteTimestamp)
"snapshot.json" -> return $ Some (InMemMetadata remoteLayout remoteSnapshot)
"mirrors.json" -> return $ Some (InMemMetadata remoteLayout remoteMirrors)
"01-index.tar.gz" -> return $ Some (InMemBinary remoteTarGz)
"01-index.tar" -> return $ Some (InMemBinary remoteTar)
otherPath -> throwIO . userError $ "getPath: Unknown path " ++ otherPath
where
cron :: MVar RemoteState -> UTCTime -> IO ()
cron state now = modifyMVar_ state $ \st@RemoteState{..} -> do
let snapshot, snapshot' :: Snapshot
snapshot = signed remoteSnapshot
snapshot' = snapshot {
snapshotVersion = versionIncrement $ snapshotVersion snapshot
, snapshotExpires = expiresInDays now 3
}
timestamp, timestamp' :: Timestamp
timestamp = signed remoteTimestamp
timestamp' = Timestamp {
timestampVersion = versionIncrement $ timestampVersion timestamp
, timestampExpires = expiresInDays now 3
, timestampInfoSnapshot = fileInfo $ renderJSON remoteLayout signedSnapshot
}
signedTimestamp = withSignatures remoteLayout [privateTimestamp remoteKeys] timestamp'
signedSnapshot = withSignatures remoteLayout [privateSnapshot remoteKeys] snapshot'
return st {
remoteTimestamp = signedTimestamp
, remoteSnapshot = signedSnapshot
}
setIndex :: MVar RemoteState -> UTCTime -> [Tar.Entry] -> IO ()
setIndex state now entries = modifyMVar_ state $ \st@RemoteState{..} -> do
let snapshot, snapshot' :: Snapshot
snapshot = signed remoteSnapshot
snapshot' = snapshot {
snapshotVersion = versionIncrement $ snapshotVersion snapshot
, snapshotExpires = expiresInDays now 3
, snapshotInfoTarGz = fileInfo $ newTarGz
, snapshotInfoTar = Just $ fileInfo newTar
}
newTar :: BS.L.ByteString
newTar = Tar.write entries
newTarGz :: BS.L.ByteString
newTarGz = GZip.compress newTar
timestamp, timestamp' :: Timestamp
timestamp = signed remoteTimestamp
timestamp' = Timestamp {
timestampVersion = versionIncrement $ timestampVersion timestamp
, timestampExpires = expiresInDays now 3
, timestampInfoSnapshot = fileInfo $ renderJSON remoteLayout signedSnapshot
}
signedTimestamp = withSignatures remoteLayout [privateTimestamp remoteKeys] timestamp'
signedSnapshot = withSignatures remoteLayout [privateSnapshot remoteKeys] snapshot'
return st {
remoteTimestamp = signedTimestamp
, remoteSnapshot = signedSnapshot
, remoteTar = newTar
, remoteTarGz = newTarGz
}
keyRollover :: MVar RemoteState -> UTCTime -> IO ()
keyRollover state now = modifyMVar_ state $ \st@RemoteState{..} -> do
newKeySnapshot <- createKey' KeyTypeEd25519
newKeyTimestamp <- createKey' KeyTypeEd25519
let remoteKeys' :: PrivateKeys
remoteKeys' = remoteKeys {
privateSnapshot = newKeySnapshot
, privateTimestamp = newKeyTimestamp
}
root, root' :: Root
root = signed remoteRoot
root' = Root {
rootVersion = versionIncrement $ rootVersion root
, rootExpires = expiresInDays now (365 * 10)
, rootKeys = privateKeysEnv remoteKeys'
, rootRoles = privateKeysRoles remoteKeys'
}
snapshot, snapshot' :: Snapshot
snapshot = signed remoteSnapshot
snapshot' = snapshot {
snapshotVersion = versionIncrement $ snapshotVersion snapshot
, snapshotExpires = expiresInDays now 3
, snapshotInfoRoot = fileInfo $ renderJSON remoteLayout signedRoot
}
timestamp, timestamp' :: Timestamp
timestamp = signed remoteTimestamp
timestamp' = Timestamp {
timestampVersion = versionIncrement $ timestampVersion timestamp
, timestampExpires = expiresInDays now 3
, timestampInfoSnapshot = fileInfo $ renderJSON remoteLayout signedSnapshot
}
signedRoot = withSignatures remoteLayout (privateRoot remoteKeys') root'
signedTimestamp = withSignatures remoteLayout [privateTimestamp remoteKeys'] timestamp'
signedSnapshot = withSignatures remoteLayout [privateSnapshot remoteKeys'] snapshot'
return st {
remoteRoot = signedRoot
, remoteTimestamp = signedTimestamp
, remoteSnapshot = signedSnapshot
}
|