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
|
{- Copyright 2017 Joey Hess <id@joeyh.name>
-
- 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 ()
|