{- Copyright 2017 Joey Hess - - Licensed under the GNU AGPL version 3 or higher. -} {-# LANGUAGE OverloadedStrings, TupleSections #-} module Role.User where import Types import Pty import Memory import Log import Session import Hash import Crypto import Gpg import CmdLine import WebSockets import SessionID import PrevActivity import ControlSocket import ControlWindow import Control.Concurrent.Async import Control.Concurrent.STM import Control.Concurrent.STM.TMChan import System.Process import System.Exit import qualified Data.Text.IO as T import qualified Data.ByteString as B import Data.List.NonEmpty (NonEmpty(..), toList) import Data.Maybe import Data.Time.Clock.POSIX import System.IO import System.Environment import Data.Monoid import Prelude run :: UserOpts -> IO ExitCode run os = fromMaybe (ExitFailure 101) <$> connect where connect = do putStrLn "A debug-me session lets someone else run commands on your computer" putStrLn "to debug your problem. A log of this session can be emailed to you" putStrLn "at the end, which you can use to prove what they did in this session." putStr "Enter your email address: " hFlush stdout email <- T.getLine (controlinput, controloutput) <- openControlWindow putStr "Connecting to debug-me server..." hFlush stdout usv <- newEmptyTMVarIO let app = clientApp (InitMode email) User developerMessages runClientApp (useServer os) $ app $ \ochan ichan sid -> do let url = sessionIDUrl sid (useServer os) putStrLn "" putStrLn "Others can connect to this session and help you debug by running:" putStrLn $ " debug-me " ++ show url hFlush stdout withSessionLogger Nothing sid $ go ochan ichan usv controlinput controloutput go ochan ichan usv controlinput controloutput logger = do (cmd, cmdparams) <- shellCommand os runWithPty cmd cmdparams $ \(p, ph) -> do us <- startProtocol startSession ochan logger atomically $ putTMVar usv us workers <- mapM async [ sendPtyOutput p ochan us logger , sendControlOutput controloutput ochan us logger ph ] mainworker <- async $ sendPtyInput ichan ochan controlinput p us logger `race` forwardTtyInputToPty p exitstatus <- waitForProcess ph displayOutput ochan us logger $ rawLine "" <> rawLine (endSession exitstatus) atomically $ do closeTMChan ichan closeTMChan controlinput closeTMChan controloutput mapM_ cancel workers _ <- waitCatch mainworker return exitstatus developerMessages :: AnyMessage -> Maybe (Message Entered) developerMessages (Developer m) = Just m developerMessages (User _) = Nothing shellCommand :: UserOpts -> IO (String, [String]) shellCommand os = case cmdToRun os of Just v -> return (v, []) Nothing -> maybe ("bash", ["-l"]) (, []) <$> lookupEnv "SHELL" -- | Log of recent Activity, with the most recent first. type Backlog = NonEmpty Log data UserState = UserState { backLog :: Backlog , userSessionKey :: MySessionKey , sigVerifier :: SigVerifier , lastSeenTs :: POSIXTime , lastAcceptedEntered :: Maybe Hash } -- | RecentActivity that uses the UserState. userStateRecentActivity :: TVar UserState -> RecentActivity userStateRecentActivity us = do st <- readTVar us let hs = catMaybes $ lastAcceptedEntered st : map loggedHash (toList (backLog st)) return (sigVerifier st, hs) -- | Start by establishing our session key, and displaying the starttxt. startProtocol :: B.ByteString -> TMChan (Message Seen) -> Logger -> IO (TVar UserState) startProtocol starttxt ochan logger = do let initialmessage msg = do atomically $ writeTMChan ochan msg logger $ User msg sk <- genMySessionKey pk <- myPublicKey sk (GpgSign False) let c = mkSigned sk $ Control (SessionKey pk currentProtocolVersion) initialmessage $ ControlMessage c let starttxt' = rawLine starttxt let act = mkSigned sk $ Activity (Seen (Val starttxt')) Nothing Nothing mempty let startmsg = ActivityMessage act B.hPut stdout starttxt' hFlush stdout initialmessage startmsg now <- getPOSIXTime let l = mkLog (User startmsg) now newTVarIO $ UserState { backLog = l :| [] , userSessionKey = sk , sigVerifier = mempty , lastSeenTs = now , lastAcceptedEntered = Nothing } -- | Forward things the user types to the Pty. forwardTtyInputToPty :: Pty -> IO () forwardTtyInputToPty p = do b <- B.hGetSome stdin 1024 if B.null b then return () else do writePty p b forwardTtyInputToPty p -- | Forward things written to the Pty out the TMChan, and also display -- it on their Tty. sendPtyOutput :: Pty -> TMChan (Message Seen) -> TVar UserState -> Logger -> IO () sendPtyOutput p ochan us logger = go where go = do displayOutput ochan us logger =<< readPty p go -- | Display to Tty and send out the TMChan. displayOutput :: TMChan (Message Seen) -> TVar UserState -> Logger -> B.ByteString -> IO () displayOutput ochan us logger b = do B.hPut stdout b hFlush stdout now <- getPOSIXTime l <- atomically $ do let seen = Seen (Val b) sendDeveloper ochan us seen now logger $ User l -- | Since the Tty is in raw mode, need \r before \n rawLine :: B.ByteString -> B.ByteString rawLine b = b <> "\r\n" class SendableToDeveloper t where sendDeveloper :: TMChan (Message Seen) -> TVar UserState -> t -> POSIXTime -> STM (Message Seen) instance SendableToDeveloper Seen where sendDeveloper ochan us seen now = do st <- readTVar us let bl@(prev :| _) = backLog st let msg = ActivityMessage $ mkSigned (userSessionKey st) $ Activity seen (loggedHash prev) (lastAcceptedEntered st) (mkElapsedTime (lastSeenTs st) now) let l = mkLog (User msg) now writeTMChan ochan msg writeTVar us $ st { backLog = l :| toList bl , lastSeenTs = now } return msg instance SendableToDeveloper ControlAction where sendDeveloper ochan us c _now = do st <- readTVar us let msg = ControlMessage $ mkSigned (userSessionKey st) (Control c) -- Control messages are not kept in the backlog. writeTMChan ochan msg return msg -- | Read things to be entered from the TMChan, verify if they're legal, -- and send them to the Pty. Also handles control messages from the -- developer. sendPtyInput :: TMChan (MissingHashes (Message Entered)) -> TMChan (Message Seen) -> TMChan ControlInput -> Pty -> TVar UserState -> Logger -> IO () sendPtyInput ichan ochan controlinput p us logger = go where go = do now <- getPOSIXTime v <- atomically $ getDeveloperMessage ichan ochan us now case v of Nothing -> return () Just (InputMessage msg@(ActivityMessage entered)) -> do logger $ Developer msg writePty p $ val $ enteredData $ activity entered go Just (InputMessage msg@(ControlMessage (Control c _))) -> do logger $ Developer msg atomically $ writeTMChan controlinput (ControlInputAction c) go Just (RejectedMessage ent rej) -> do logger $ Developer ent logger $ User rej go Just (BadlySignedMessage _) -> go data Input = InputMessage (Message Entered) | RejectedMessage (Message Entered) (Message Seen) | BadlySignedMessage (Message Entered) -- Get message from developer, verify its signature is from a developer we -- have allowed (unless it's a SessionKey control message, then the -- signature of the message is only verified against the key in it), and -- make sure it's legal before returning it. If it's not legal, sends a -- Reject message. getDeveloperMessage :: TMChan (MissingHashes (Message Entered)) -> TMChan (Message Seen) -> TVar UserState -> POSIXTime -> STM (Maybe Input) getDeveloperMessage ichan ochan us now = maybe (return Nothing) (\msg -> Just <$> getDeveloperMessage' msg ochan us now) =<< readTMChan ichan getDeveloperMessage' :: MissingHashes (Message Entered) -> TMChan (Message Seen) -> TVar UserState -> POSIXTime -> STM Input getDeveloperMessage' (MissingHashes wiremsg) ochan us now = do st <- readTVar us Developer msg <- restoreHashes (userStateRecentActivity us) (MissingHashes (Developer wiremsg)) case msg of ControlMessage (Control (SessionKey spk _) _) -> do let sigverifier = mkSigVerifier $ case spk of GpgSigned pk _ _ -> pk UnSigned pk -> pk if verifySigned sigverifier msg then return (InputMessage msg) else return (BadlySignedMessage msg) _ -> if verifySigned (sigVerifier st) msg then case msg of ActivityMessage entered -> do -- Don't need to retain backlog -- before the Activity that entered -- references. let bl' = reduceBacklog $ truncateBacklog (backLog st) entered if isLegalEntered entered (st { backLog = bl' }) then do let l = mkLog (Developer msg) now writeTVar us $ st { backLog = l :| toList bl' , lastAcceptedEntered = Just (hash entered) } return (InputMessage msg) else do let reject = EnteredRejected { enteredRejected = hash entered , enteredLastAccepted = lastAcceptedEntered st } RejectedMessage msg <$> sendDeveloper ochan us reject now ControlMessage (Control _ _) -> return (InputMessage msg) else return (BadlySignedMessage msg) -- | Truncate the Backlog to remove entries older than the one -- that the Activity Entered refers to, but only if the referred -- to Activity is an Activity Seen. -- -- Once the developer has referred to a given Activity Seen in -- their Activity Entered, they cannot refer backwards to anything -- that came before it. -- -- If the Activity refers to an item not in the backlog, no truncation is -- done. truncateBacklog :: Backlog -> Activity Entered -> Backlog truncateBacklog (b :| l) (Activity _ (Just hp) _ _ _) | truncationpoint b = b :| [] | otherwise = b :| go [] l where go c [] = reverse c go c (x:xs) | truncationpoint x = reverse (x:c) | otherwise = go (x:c) xs truncationpoint x@(Log { loggedMessage = User {}}) = loggedHash x == Just hp truncationpoint _ = False truncateBacklog bl (Activity _ Nothing _ _ _) = bl -- | To avoid DOS attacks that try to fill up the backlog and so use all -- memory, don't let the backlog contain more than 1000 items, or -- more than 16 megabytes of total data. (Excluding the most recent -- item). reduceBacklog :: Backlog -> Backlog reduceBacklog (b :| l) = b :| go 0 (take 1000 l) where go _ [] = [] go n (x:xs) | n > 16777216 = [] | otherwise = x : go (n + dataSize x) xs -- | Entered activity is legal when its prevActivity points to the last -- logged activity, because this guarantees that the person who entered -- it saw the current state of the system before manipulating it. -- -- To support typeahead on slow links, some echoData may be provided -- in the Entered activity. If the Entered activity points -- to an older prevActivity, then the echoData must match the -- concatenation of all Seen activities after that one, up to the -- last logged activity. -- -- Also, the prevEntered field must point to the last accepted -- Entered activity. -- -- Does not check the signature. isLegalEntered :: Activity Entered -> UserState -> Bool isLegalEntered (Activity _ Nothing _ _ _) _ = False isLegalEntered (Activity a (Just hp) lastentered _ _) us | lastentered /= lastAcceptedEntered us = False | loggedHash lastact == Just hp = True | B.null (val (echoData a)) = False -- optimisation | any (== Just hp) (map loggedHash bl) = let sincehp = reverse (lastact : takeWhile (\l -> loggedHash l /= Just hp) bl) in echoData a == mconcat (map (getseen . loggedMessage) sincehp) | otherwise = False where (lastact :| bl) = backLog us getseen (User (ActivityMessage as)) = seenData $ activity as getseen _ = mempty -- | Forward messages from the control window to the developer. -- -- When the control window sends a SessionKeyAccepted, add it to the -- sigVerifier. sendControlOutput :: TMChan ControlOutput -> TMChan (Message Seen) -> TVar UserState -> Logger -> ProcessHandle -> IO () sendControlOutput controloutput ochan us logger ph = loop where loop = go =<< atomically (readTMChan controloutput) go Nothing = return () go (Just ControlWindowOpened) = loop go (Just (ControlOutputAction c)) = do case c of SessionKeyAccepted pk -> atomically $ do st <- readTVar us let sv = sigVerifier st let sv' = sv `mappend` mkSigVerifier pk let st' = st { sigVerifier = sv' } writeTVar us st' _ -> return () now <- getPOSIXTime l <- atomically $ sendDeveloper ochan us c now logger (User l) loop go (Just ControlWindowRequestedImmediateQuit) = do terminateProcess ph return ()