summaryrefslogtreecommitdiff
path: root/src/Test.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Test.hs')
-rw-r--r--src/Test.hs550
1 files changed, 0 insertions, 550 deletions
diff --git a/src/Test.hs b/src/Test.hs
deleted file mode 100644
index ab9a78c..0000000
--- a/src/Test.hs
+++ /dev/null
@@ -1,550 +0,0 @@
-module Test (
- runTestTool,
-) where
-
-import Control.Arrow
-import Control.Concurrent
-import Control.Exception
-import Control.Monad.Except
-import Control.Monad.Reader
-import Control.Monad.State
-
-import Crypto.Random
-
-import Data.ByteString qualified as B
-import Data.ByteString.Char8 qualified as BC
-import Data.ByteString.Lazy qualified as BL
-import Data.Foldable
-import Data.Ord
-import Data.Text (Text)
-import Data.Text qualified as T
-import Data.Text.Encoding
-import Data.Text.IO qualified as T
-import Data.Typeable
-
-import Network.Socket
-
-import System.IO
-import System.IO.Error
-
-import Attach
-import Contact
-import Identity
-import Message
-import Network
-import Pairing
-import PubKey
-import Service
-import Set
-import State
-import Storage
-import Storage.Internal (unsafeStoreRawBytes)
-import Storage.Merge
-import Sync
-
-
-data TestState = TestState
- { tsHead :: Maybe (Head LocalState)
- , tsServer :: Maybe RunningServer
- , tsWatchedLocalIdentity :: Maybe WatchedHead
- , tsWatchedSharedIdentity :: Maybe WatchedHead
- }
-
-data RunningServer = RunningServer
- { rsServer :: Server
- , rsPeers :: MVar (Int, [(Int, Peer)])
- , rsPeerThread :: ThreadId
- }
-
-initTestState :: TestState
-initTestState = TestState
- { tsHead = Nothing
- , tsServer = Nothing
- , tsWatchedLocalIdentity = Nothing
- , tsWatchedSharedIdentity = Nothing
- }
-
-data TestInput = TestInput
- { tiOutput :: Output
- , tiStorage :: Storage
- , tiParams :: [Text]
- }
-
-
-runTestTool :: Storage -> IO ()
-runTestTool st = do
- out <- newMVar ()
- let testLoop = getLineMb >>= \case
- Just line -> do
- case T.words line of
- (cname:params)
- | Just (CommandM cmd) <- lookup cname commands -> do
- runReaderT cmd $ TestInput out st params
- | otherwise -> fail $ "Unknown command '" ++ T.unpack cname ++ "'"
- [] -> return ()
- testLoop
-
- Nothing -> return ()
-
- runExceptT (evalStateT testLoop initTestState) >>= \case
- Left x -> hPutStrLn stderr x
- Right () -> return ()
-
-getLineMb :: MonadIO m => m (Maybe Text)
-getLineMb = liftIO $ catchIOError (Just <$> T.getLine) (\e -> if isEOFError e then return Nothing else ioError e)
-
-getLines :: MonadIO m => m [Text]
-getLines = getLineMb >>= \case
- Just line | not (T.null line) -> (line:) <$> getLines
- _ -> return []
-
-getHead :: CommandM (Head LocalState)
-getHead = do
- h <- maybe (fail "failed to reload head") return =<< maybe (fail "no current head") reloadHead =<< gets tsHead
- modify $ \s -> s { tsHead = Just h }
- return h
-
-
-type Output = MVar ()
-
-outLine :: Output -> String -> IO ()
-outLine mvar line = do
- evaluate $ foldl' (flip seq) () line
- withMVar mvar $ \() -> do
- putStrLn line
- hFlush stdout
-
-cmdOut :: String -> Command
-cmdOut line = do
- out <- asks tiOutput
- liftIO $ outLine out line
-
-
-getPeer :: Text -> CommandM Peer
-getPeer spidx = do
- Just RunningServer {..} <- gets tsServer
- Just peer <- lookup (read $ T.unpack spidx) . snd <$> liftIO (readMVar rsPeers)
- return peer
-
-getPeerIndex :: MVar (Int, [(Int, Peer)]) -> ServiceHandler (PairingService a) Int
-getPeerIndex pmvar = do
- peer <- asks svcPeer
- maybe 0 fst . find ((==peer) . snd) . snd <$> liftIO (readMVar pmvar)
-
-pairingAttributes :: PairingResult a => proxy (PairingService a) -> Output -> MVar (Int, [(Int, Peer)]) -> String -> PairingAttributes a
-pairingAttributes _ out peers prefix = PairingAttributes
- { pairingHookRequest = return ()
-
- , pairingHookResponse = \confirm -> do
- index <- show <$> getPeerIndex peers
- afterCommit $ outLine out $ unwords [prefix ++ "-response", index, confirm]
-
- , pairingHookRequestNonce = \confirm -> do
- index <- show <$> getPeerIndex peers
- afterCommit $ outLine out $ unwords [prefix ++ "-request", index, confirm]
-
- , pairingHookRequestNonceFailed = failed "nonce"
-
- , pairingHookConfirmedResponse = return ()
- , pairingHookConfirmedRequest = return ()
-
- , pairingHookAcceptedResponse = do
- index <- show <$> getPeerIndex peers
- afterCommit $ outLine out $ unwords [prefix ++ "-response-done", index]
-
- , pairingHookAcceptedRequest = do
- index <- show <$> getPeerIndex peers
- afterCommit $ outLine out $ unwords [prefix ++ "-request-done", index]
-
- , pairingHookFailed = \case
- PairingUserRejected -> failed "user"
- PairingUnexpectedMessage pstate packet -> failed $ "unexpected " ++ strState pstate ++ " " ++ strPacket packet
- PairingFailedOther str -> failed $ "other " ++ str
- , pairingHookVerifyFailed = failed "verify"
- , pairingHookRejected = failed "rejected"
- }
- where
- failed :: PairingResult a => String -> ServiceHandler (PairingService a) ()
- failed detail = do
- ptype <- svcGet >>= return . \case
- OurRequest {} -> "response"
- OurRequestConfirm {} -> "response"
- OurRequestReady -> "response"
- PeerRequest {} -> "request"
- PeerRequestConfirm -> "request"
- _ -> fail "unexpected pairing state"
-
- index <- show <$> getPeerIndex peers
- afterCommit $ outLine out $ prefix ++ "-" ++ ptype ++ "-failed " ++ index ++ " " ++ detail
-
- strState :: PairingState a -> String
- strState = \case
- NoPairing -> "none"
- OurRequest {} -> "our-request"
- OurRequestConfirm {} -> "our-request-confirm"
- OurRequestReady -> "our-request-ready"
- PeerRequest {} -> "peer-request"
- PeerRequestConfirm -> "peer-request-confirm"
- PairingDone -> "done"
-
- strPacket :: PairingService a -> String
- strPacket = \case
- PairingRequest {} -> "request"
- PairingResponse {} -> "response"
- PairingRequestNonce {} -> "nonce"
- PairingAccept {} -> "accept"
- PairingReject -> "reject"
-
-directMessageAttributes :: Output -> DirectMessageAttributes
-directMessageAttributes out = DirectMessageAttributes
- { dmOwnerMismatch = afterCommit $ outLine out "dm-owner-mismatch"
- }
-
-dmReceivedWatcher :: Output -> Stored DirectMessage -> IO ()
-dmReceivedWatcher out smsg = do
- let msg = fromStored smsg
- outLine out $ unwords
- [ "dm-received"
- , "from", maybe "<unnamed>" T.unpack $ idName $ msgFrom msg
- , "text", T.unpack $ msgText msg
- ]
-
-
-newtype CommandM a = CommandM (ReaderT TestInput (StateT TestState (ExceptT String IO)) a)
- deriving (Functor, Applicative, Monad, MonadIO, MonadReader TestInput, MonadState TestState, MonadError String)
-
-instance MonadFail CommandM where
- fail = throwError
-
-instance MonadRandom CommandM where
- getRandomBytes = liftIO . getRandomBytes
-
-instance MonadStorage CommandM where
- getStorage = asks tiStorage
-
-instance MonadHead LocalState CommandM where
- updateLocalHead f = do
- Just h <- gets tsHead
- (Just h', x) <- maybe (fail "failed to reload head") (flip updateHead f) =<< reloadHead h
- modify $ \s -> s { tsHead = Just h' }
- return x
-
-type Command = CommandM ()
-
-commands :: [(Text, Command)]
-commands = map (T.pack *** id)
- [ ("store", cmdStore)
- , ("stored-generation", cmdStoredGeneration)
- , ("stored-roots", cmdStoredRoots)
- , ("stored-set-add", cmdStoredSetAdd)
- , ("stored-set-list", cmdStoredSetList)
- , ("create-identity", cmdCreateIdentity)
- , ("start-server", cmdStartServer)
- , ("stop-server", cmdStopServer)
- , ("peer-add", cmdPeerAdd)
- , ("shared-state-get", cmdSharedStateGet)
- , ("shared-state-wait", cmdSharedStateWait)
- , ("watch-local-identity", cmdWatchLocalIdentity)
- , ("watch-shared-identity", cmdWatchSharedIdentity)
- , ("update-local-identity", cmdUpdateLocalIdentity)
- , ("update-shared-identity", cmdUpdateSharedIdentity)
- , ("attach-to", cmdAttachTo)
- , ("attach-accept", cmdAttachAccept)
- , ("attach-reject", cmdAttachReject)
- , ("contact-request", cmdContactRequest)
- , ("contact-accept", cmdContactAccept)
- , ("contact-reject", cmdContactReject)
- , ("contact-list", cmdContactList)
- , ("contact-set-name", cmdContactSetName)
- , ("dm-send-peer", cmdDmSendPeer)
- , ("dm-send-contact", cmdDmSendContact)
- , ("dm-list-peer", cmdDmListPeer)
- , ("dm-list-contact", cmdDmListContact)
- ]
-
-cmdStore :: Command
-cmdStore = do
- st <- asks tiStorage
- [otype] <- asks tiParams
- ls <- getLines
-
- let cnt = encodeUtf8 $ T.unlines ls
- ref <- liftIO $ unsafeStoreRawBytes st $ BL.fromChunks [encodeUtf8 otype, BC.singleton ' ', BC.pack (show $ B.length cnt), BC.singleton '\n', cnt]
- cmdOut $ "store-done " ++ show (refDigest ref)
-
-cmdStoredGeneration :: Command
-cmdStoredGeneration = do
- st <- asks tiStorage
- [tref] <- asks tiParams
- Just ref <- liftIO $ readRef st (encodeUtf8 tref)
- cmdOut $ "stored-generation " ++ T.unpack tref ++ " " ++ showGeneration (storedGeneration $ wrappedLoad @Object ref)
-
-cmdStoredRoots :: Command
-cmdStoredRoots = do
- st <- asks tiStorage
- [tref] <- asks tiParams
- Just ref <- liftIO $ readRef st (encodeUtf8 tref)
- cmdOut $ "stored-roots " ++ T.unpack tref ++ concatMap ((' ':) . show . refDigest . storedRef) (storedRoots $ wrappedLoad @Object ref)
-
-cmdStoredSetAdd :: Command
-cmdStoredSetAdd = do
- st <- asks tiStorage
- (item, set) <- asks tiParams >>= liftIO . mapM (readRef st . encodeUtf8) >>= \case
- [Just iref, Just sref] -> return (wrappedLoad iref, loadSet @[Stored Object] sref)
- [Just iref] -> return (wrappedLoad iref, emptySet)
- _ -> fail "unexpected parameters"
- set' <- storeSetAdd st [item] set
- cmdOut $ "stored-set-add" ++ concatMap ((' ':) . show . refDigest . storedRef) (toComponents set')
-
-cmdStoredSetList :: Command
-cmdStoredSetList = do
- st <- asks tiStorage
- [tref] <- asks tiParams
- Just ref <- liftIO $ readRef st (encodeUtf8 tref)
- let items = fromSetBy compare $ loadSet @[Stored Object] ref
- forM_ items $ \item -> do
- cmdOut $ "stored-set-item" ++ concatMap ((' ':) . show . refDigest . storedRef) item
- cmdOut $ "stored-set-done"
-
-cmdCreateIdentity :: Command
-cmdCreateIdentity = do
- st <- asks tiStorage
- names <- asks tiParams
-
- h <- liftIO $ do
- Just identity <- if null names
- then Just <$> createIdentity st Nothing Nothing
- else foldrM (\n o -> Just <$> createIdentity st (Just n) o) Nothing names
-
- shared <- case names of
- _:_:_ -> (:[]) <$> makeSharedStateUpdate st (Just $ finalOwner identity) []
- _ -> return []
-
- storeHead st $ LocalState
- { lsIdentity = idExtData identity
- , lsShared = shared
- }
-
- _ <- liftIO . watchReceivedMessages h . dmReceivedWatcher =<< asks tiOutput
- modify $ \s -> s { tsHead = Just h }
-
-cmdStartServer :: Command
-cmdStartServer = do
- out <- asks tiOutput
-
- Just h <- gets tsHead
- rsPeers <- liftIO $ newMVar (1, [])
- rsServer <- liftIO $ startServer defaultServerOptions h (hPutStrLn stderr)
- [ someServiceAttr $ pairingAttributes (Proxy @AttachService) out rsPeers "attach"
- , someServiceAttr $ pairingAttributes (Proxy @ContactService) out rsPeers "contact"
- , someServiceAttr $ directMessageAttributes out
- , someService @SyncService Proxy
- ]
-
- rsPeerThread <- liftIO $ forkIO $ void $ forever $ do
- peer <- getNextPeerChange rsServer
-
- let printPeer (idx, p) = do
- params <- peerIdentity p >>= return . \case
- PeerIdentityFull pid -> ("id":) $ map (maybe "<unnamed>" T.unpack . idName) (unfoldOwners pid)
- _ -> [ "addr", show (peerAddress p) ]
- outLine out $ unwords $ [ "peer", show idx ] ++ params
-
- update (nid, []) = printPeer (nid, peer) >> return (nid + 1, [(nid, peer)])
- update cur@(nid, p:ps) | snd p == peer = printPeer p >> return cur
- | otherwise = fmap (p:) <$> update (nid, ps)
-
- modifyMVar_ rsPeers update
-
- modify $ \s -> s { tsServer = Just RunningServer {..} }
-
-cmdStopServer :: Command
-cmdStopServer = do
- Just RunningServer {..} <- gets tsServer
- liftIO $ do
- killThread rsPeerThread
- stopServer rsServer
- modify $ \s -> s { tsServer = Nothing }
- cmdOut "stop-server-done"
-
-cmdPeerAdd :: Command
-cmdPeerAdd = do
- Just RunningServer {..} <- gets tsServer
- host:rest <- map T.unpack <$> asks tiParams
-
- let port = case rest of [] -> show discoveryPort
- (p:_) -> p
- addr:_ <- liftIO $ getAddrInfo (Just $ defaultHints { addrSocketType = Datagram }) (Just host) (Just port)
- void $ liftIO $ serverPeer rsServer (addrAddress addr)
-
-cmdSharedStateGet :: Command
-cmdSharedStateGet = do
- h <- getHead
- cmdOut $ unwords $ "shared-state-get" : map (show . refDigest . storedRef) (lsShared $ headObject h)
-
-cmdSharedStateWait :: Command
-cmdSharedStateWait = do
- st <- asks tiStorage
- out <- asks tiOutput
- Just h <- gets tsHead
- trefs <- asks tiParams
-
- liftIO $ do
- mvar <- newEmptyMVar
- w <- watchHeadWith h (lsShared . headObject) $ \cur -> do
- mbobjs <- mapM (readRef st . encodeUtf8) trefs
- case map wrappedLoad <$> sequence mbobjs of
- Just objs | filterAncestors (cur ++ objs) == cur -> do
- outLine out $ unwords $ "shared-state-wait" : map T.unpack trefs
- void $ forkIO $ unwatchHead =<< takeMVar mvar
- _ -> return ()
- putMVar mvar w
-
-cmdWatchLocalIdentity :: Command
-cmdWatchLocalIdentity = do
- Just h <- gets tsHead
- Nothing <- gets tsWatchedLocalIdentity
-
- out <- asks tiOutput
- w <- liftIO $ watchHeadWith h headLocalIdentity $ \idt -> do
- outLine out $ unwords $ "local-identity" : map (maybe "<unnamed>" T.unpack . idName) (unfoldOwners idt)
- modify $ \s -> s { tsWatchedLocalIdentity = Just w }
-
-cmdWatchSharedIdentity :: Command
-cmdWatchSharedIdentity = do
- Just h <- gets tsHead
- Nothing <- gets tsWatchedSharedIdentity
-
- out <- asks tiOutput
- w <- liftIO $ watchHeadWith h (lookupSharedValue . lsShared . headObject) $ \case
- Just (idt :: ComposedIdentity) -> do
- outLine out $ unwords $ "shared-identity" : map (maybe "<unnamed>" T.unpack . idName) (unfoldOwners idt)
- Nothing -> do
- outLine out $ "shared-identity-failed"
- modify $ \s -> s { tsWatchedSharedIdentity = Just w }
-
-cmdUpdateLocalIdentity :: Command
-cmdUpdateLocalIdentity = do
- [name] <- asks tiParams
- updateLocalHead_ $ \ls -> do
- Just identity <- return $ validateExtendedIdentity $ lsIdentity $ fromStored ls
- let public = idKeyIdentity identity
-
- secret <- loadKey public
- nidata <- maybe (error "created invalid identity") (return . idExtData) . validateExtendedIdentity =<<
- mstore =<< sign secret =<< mstore . ExtendedIdentityData =<< return (emptyIdentityExtension $ idData identity)
- { idePrev = toList $ idExtDataF identity
- , ideName = Just name
- }
- mstore (fromStored ls) { lsIdentity = nidata }
-
-cmdUpdateSharedIdentity :: Command
-cmdUpdateSharedIdentity = do
- [name] <- asks tiParams
- updateLocalHead_ $ updateSharedState_ $ \case
- Nothing -> throwError "no existing shared identity"
- Just identity -> do
- let public = idKeyIdentity identity
- secret <- loadKey public
- uidentity <- mergeIdentity identity
- maybe (error "created invalid identity") (return . Just . toComposedIdentity) . validateExtendedIdentity =<<
- mstore =<< sign secret =<< mstore . ExtendedIdentityData =<< return (emptyIdentityExtension $ idData uidentity)
- { idePrev = toList $ idExtDataF identity
- , ideName = Just name
- }
-
-cmdAttachTo :: Command
-cmdAttachTo = do
- [spidx] <- asks tiParams
- attachToOwner =<< getPeer spidx
-
-cmdAttachAccept :: Command
-cmdAttachAccept = do
- [spidx] <- asks tiParams
- attachAccept =<< getPeer spidx
-
-cmdAttachReject :: Command
-cmdAttachReject = do
- [spidx] <- asks tiParams
- attachReject =<< getPeer spidx
-
-cmdContactRequest :: Command
-cmdContactRequest = do
- [spidx] <- asks tiParams
- contactRequest =<< getPeer spidx
-
-cmdContactAccept :: Command
-cmdContactAccept = do
- [spidx] <- asks tiParams
- contactAccept =<< getPeer spidx
-
-cmdContactReject :: Command
-cmdContactReject = do
- [spidx] <- asks tiParams
- contactReject =<< getPeer spidx
-
-cmdContactList :: Command
-cmdContactList = do
- h <- getHead
- let contacts = fromSetBy (comparing contactName) . lookupSharedValue . lsShared . headObject $ h
- forM_ contacts $ \c -> do
- r:_ <- return $ filterAncestors $ concatMap storedRoots $ toComponents c
- cmdOut $ concat
- [ "contact-list-item "
- , show $ refDigest $ storedRef r
- , " "
- , T.unpack $ contactName c
- , case contactIdentity c of Nothing -> ""; Just idt -> " " ++ T.unpack (displayIdentity idt)
- ]
- cmdOut "contact-list-done"
-
-getContact :: Text -> CommandM Contact
-getContact cid = do
- h <- getHead
- let contacts = fromSetBy (comparing contactName) . lookupSharedValue . lsShared . headObject $ h
- [contact] <- flip filterM contacts $ \c -> do
- r:_ <- return $ filterAncestors $ concatMap storedRoots $ toComponents c
- return $ T.pack (show $ refDigest $ storedRef r) == cid
- return contact
-
-cmdContactSetName :: Command
-cmdContactSetName = do
- [cid, name] <- asks tiParams
- contact <- getContact cid
- updateLocalHead_ $ updateSharedState_ $ contactSetName contact name
- cmdOut "contact-set-name-done"
-
-cmdDmSendPeer :: Command
-cmdDmSendPeer = do
- [spidx, msg] <- asks tiParams
- PeerIdentityFull to <- peerIdentity =<< getPeer spidx
- void $ sendDirectMessage to msg
-
-cmdDmSendContact :: Command
-cmdDmSendContact = do
- [cid, msg] <- asks tiParams
- Just to <- contactIdentity <$> getContact cid
- void $ sendDirectMessage to msg
-
-dmList :: Foldable f => Identity f -> Command
-dmList peer = do
- threads <- toThreadList . lookupSharedValue . lsShared . headObject <$> getHead
- case find (sameIdentity peer . msgPeer) threads of
- Just thread -> do
- forM_ (reverse $ threadToList thread) $ \DirectMessage {..} -> cmdOut $ "dm-list-item"
- <> " from " <> (maybe "<unnamed>" T.unpack $ idName msgFrom)
- <> " text " <> (T.unpack msgText)
- Nothing -> return ()
- cmdOut "dm-list-done"
-
-cmdDmListPeer :: Command
-cmdDmListPeer = do
- [spidx] <- asks tiParams
- PeerIdentityFull to <- peerIdentity =<< getPeer spidx
- dmList to
-
-cmdDmListContact :: Command
-cmdDmListContact = do
- [cid] <- asks tiParams
- Just to <- contactIdentity <$> getContact cid
- dmList to