{-# LANGUAGE OverloadedStrings, TupleSections #-} module Role.User where import Types import Pty import Memory import Log import Session import Crypto import CmdLine import WebSockets import SessionID import Control.Concurrent.Async import Control.Concurrent.STM import Control.Concurrent.STM.TMChan import System.Process import System.Exit import qualified Data.ByteString as B import Data.List.NonEmpty (NonEmpty(..), toList) import Data.Monoid import Data.Maybe import Data.Time.Clock.POSIX import System.IO import System.Environment run :: UserOpts -> IO ExitCode run os = fromMaybe (ExitFailure 101) <$> connect where connect = do putStr "Connecting to debug-me server..." hFlush stdout runClientApp $ clientApp (InitMode mempty) User developerMessages $ \ochan ichan sid -> do let url = sessionIDUrl sid "localhost" 8081 putStrLn "" putStrLn "Others can connect to this session and help you debug by running:" putStrLn $ " debug-me --debug " ++ url hFlush stdout withLogger "debug-me.log" $ go ochan ichan go ochan ichan logger = do (cmd, cmdparams) <- shellCommand os runWithPty cmd cmdparams $ \(p, ph) -> do us <- startProtocol startSession ochan logger p1 <- async $ sendPtyOutput p ochan us logger p2 <- async $ sendPtyInput ichan ochan p us logger `race` forwardTtyInputToPty p exitstatus <- waitForProcess ph displayOutput ochan us logger $ rawLine "" <> rawLine (endSession exitstatus) atomically $ closeTMChan ichan cancel p1 _ <- waitCatch p2 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 } -- | 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) initialmessage $ ControlMessage c let starttxt' = rawLine starttxt let act = mkSigned sk $ Activity (Seen (Val starttxt')) Nothing Nothing 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 } -- | 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) (Just $ 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. sendPtyInput :: TMChan (Message Entered) -> TMChan (Message Seen) -> Pty -> TVar UserState -> Logger -> IO () sendPtyInput ichan ochan 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 case c of SessionKey pk -> do checkDeveloperPublicKey ochan us logger pk go Rejected r -> error $ "User side received a Rejected: " ++ show r SessionKeyAccepted _ -> error "User side received a SessionKeyAccepted" SessionKeyRejected _ -> error "User side received a SessionKeyRejected" Just (RejectedMessage rej) -> do logger $ User rej go Just (BadlySignedMessage _) -> go data Input = InputMessage (Message Entered) | RejectedMessage (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 (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' :: Message Entered -> TMChan (Message Seen) -> TVar UserState -> POSIXTime -> STM Input getDeveloperMessage' msg ochan us now = do st <- readTVar us 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' }) return (InputMessage msg) else do let reject = Rejected entered RejectedMessage <$> sendDeveloper ochan us reject now ControlMessage (Control _ _) -> return (InputMessage msg) else return (BadlySignedMessage msg) -- | Check if the public key a developer presented is one we want to use, -- and if so, add it to the sigVerifier. checkDeveloperPublicKey :: TMChan (Message Seen) -> TVar UserState -> Logger -> PerhapsSigned PublicKey -> IO () checkDeveloperPublicKey ochan us logger spk = do now <- getPOSIXTime -- TODO check gpg sig.. msg <- atomically $ do st <- readTVar us let sv = sigVerifier st let sv' = sv `mappend` mkSigVerifier pk let st' = st { sigVerifier = sv' } writeTVar us st' sendDeveloper ochan us (SessionKeyAccepted pk) now logger $ User msg where pk = case spk of GpgSigned k _ -> k UnSigned k -> k -- | 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 it 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 activity, then the echoData must match the -- concatenation of all Seen activities after that one, up to the -- last logged activity. -- -- Activities that do not enter data point to the first message -- sent in the debug-me session. -- -- Does not check the signature. isLegalEntered :: Activity Entered -> UserState -> Bool isLegalEntered (Activity _ Nothing _ _) _ = False isLegalEntered (Activity _ _ Nothing _) _ = False isLegalEntered (Activity a (Just hp) _ _) us | 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