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 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458
|
{- Copyright 2017 Joey Hess <id@joeyh.name>
-
- Licensed under the GNU AGPL version 3 or higher.
-}
{-# LANGUAGE OverloadedStrings #-}
module Role.Developer (
run,
run',
watchSessionReadOnly,
processSessionStart,
getServerMessage,
Output(..),
emitOutput,
DeveloperState,
) where
import Types
import Hash
import Log
import Crypto
import Gpg
import CmdLine
import WebSockets
import SessionID
import Pty
import PrevActivity
import ControlSocket
import ControlWindow
import Control.Concurrent.Async
import Control.Concurrent.STM
import Control.Concurrent.STM.TMChan
import System.IO
import qualified Data.ByteString as B
import qualified Data.Text as T
import Data.List
import Data.Maybe
import Control.Monad
import Data.Time.Clock.POSIX
import Network.URI
import Data.Monoid
import Prelude
run :: DeveloperOpts -> IO ()
run = run' developer . debugUrl
run' :: (TMVar (TVar DeveloperState) -> TMChan (Message Entered) -> TMChan (MissingHashes AnyMessage) -> SessionID -> IO ()) -> URI -> IO ()
run' runner url = do
app <- do
let connect = ConnectMode $ T.pack $ show url
dsv <- newEmptyTMVarIO
return $ clientApp connect Developer Just $ runner dsv
void $ runClientApp url app
developer :: TMVar (TVar DeveloperState) -> TMChan (Message Entered) -> TMChan (MissingHashes AnyMessage) -> SessionID -> IO ()
developer dsv ichan ochan sid = withSessionLogger (Just "remote") sid $ \logger -> do
sk <- genMySessionKey
spk <- myPublicKey sk (GpgSign True)
(controlinput, controloutput) <- openControlWindow
displayInControlWindow controlinput
"Connecting to the user's session ..."
inRawMode $ do
(devstate, startoutput) <- processSessionStart sk ochan logger dsv
displayInControlWindow controlinput
"Connected. You can now see what the user is doing."
displayInControlWindow controlinput
"(But, you can't type anything yet.)"
emitOutput startoutput
displayInControlWindow controlinput
"Waiting for the user to check your GnuPG key and grant write access ..."
authUser spk ichan ochan devstate logger
>>= go controlinput controloutput logger devstate
where
go controlinput controloutput logger devstate Authed = void $ do
displayInControlWindow controlinput
"Write access granted. You can now type into the user's shell."
displayInControlWindow controlinput
"(And, you can type in this window to chat with the user.)"
sendTtyInput ichan devstate logger
`race` sendTtyOutput ochan devstate controlinput logger
`race` sendControlOutput controloutput ichan devstate logger
go controlinput _controloutput logger devstate AuthFailed = do
displayInControlWindow controlinput
"User did not grant write access to their terminal. Watching session in read-only mode."
watchSessionReadOnly ochan logger devstate
go _ _ _ _ SessionEnded =
hPutStrLn stderr "\r\n** This debug-me session has already ended.\r"
watchSessionReadOnly :: TMChan (MissingHashes AnyMessage) -> Logger -> TVar DeveloperState -> IO ()
watchSessionReadOnly ochan logger st = loop
where
loop = do
ts <- getPOSIXTime
v <- atomically $ getServerMessage ochan st ts
case v of
Nothing -> return ()
Just (o, msg) -> do
_ <- logger msg
emitOutput o
loop
data DeveloperState = DeveloperState
{ lastSeen :: Hash
-- ^ Last Seen value received from the user.
, sentSince :: [B.ByteString]
-- ^ Keys pressed since last Seen.
, enteredSince :: [Hash]
-- ^ Messages we've sent since the last Seen.
, lastActivity :: Hash
-- ^ Last Entered or Seen activity
, lastActivityTs :: POSIXTime
, lastEntered :: Maybe Hash
-- ^ Last Entered activity (either from us or another developer).
, fromOtherDevelopersSince :: [Hash]
-- ^ Messages received from other developers since the last Seen.
-- (The next Seen may chain from one of these.)
, developerSessionKey :: MySessionKey
-- ^ Our session key.
, userSigVerifier :: SigVerifier
-- ^ Used to verify signatures on messages from the user.
, developerSigVerifier :: SigVerifier
-- ^ Used to verify signatures on messages from other developers.
}
deriving (Show)
-- | RecentActivity that uses the DeveloperState.
developerStateRecentActivity :: TVar DeveloperState -> RecentActivity
developerStateRecentActivity devstate = do
st <- readTVar devstate
let hs = lastSeen st : fromMaybe (lastSeen st) (lastEntered st)
: enteredSince st ++ fromOtherDevelopersSince st
return (userSigVerifier st <> developerSigVerifier st, hs)
-- | Read things typed by the developer, and forward them to the TMChan.
sendTtyInput :: TMChan (Message Entered) -> TVar DeveloperState -> Logger -> IO ()
sendTtyInput ichan devstate logger = go
where
go = do
b <- B.hGetSome stdin 1024
if b == B.empty
then return ()
else send b
send b = do
ts <- getPOSIXTime
act <- atomically $ do
ds <- readTVar devstate
let ed = if lastActivity ds == lastSeen ds
then B.concat $ sentSince ds
else case reverse (sentSince ds) of
[] -> mempty
(lb:_) -> lb
let entered = Entered
{ enteredData = Val b
, echoData = Val ed
}
let act = mkSigned (developerSessionKey ds) $
Activity entered
(Just $ lastActivity ds)
(lastEntered ds)
(mkElapsedTime (lastActivityTs ds) ts)
writeTMChan ichan (ActivityMessage act)
let acth = hash act
let ds' = ds
{ sentSince = sentSince ds ++ [b]
, enteredSince = enteredSince ds ++ [acth]
, lastActivity = acth
, lastActivityTs = ts
, lastEntered = Just acth
}
writeTVar devstate ds'
return act
logger $ Developer $ ActivityMessage act
go
sendControlOutput :: TMChan ControlOutput -> TMChan (Message Entered) -> TVar DeveloperState -> Logger -> IO ()
sendControlOutput controloutput ichan devstate logger = loop
where
loop = go =<< atomically (readTMChan controloutput)
go Nothing = return ()
go (Just ControlWindowOpened) = loop
go (Just (ControlOutputAction c)) = do
msg <- atomically $ do
ds <- readTVar devstate
let msg = ControlMessage $
mkSigned (developerSessionKey ds) (Control c)
writeTMChan ichan msg
return msg
logger (Developer msg)
loop
go (Just ControlWindowRequestedImmediateQuit) = return ()
-- | Read activity from the TMChan and display it to the developer.
--
-- Control messages are forwarded on to the ControlInput.
sendTtyOutput :: TMChan (MissingHashes AnyMessage) -> TVar DeveloperState -> TMChan ControlInput -> Logger -> IO ()
sendTtyOutput ochan devstate controlinput logger = go
where
go = do
ts <- getPOSIXTime
v <- atomically $ getServerMessage ochan devstate ts
case v of
Nothing -> return ()
Just (o, msg) -> do
logger msg
emitOutput o
forwardcontrol msg
go
forwardcontrol msg = case msg of
User (ControlMessage c) -> fwd c
Developer (ControlMessage c) -> case control c of
EnteredRejected {} -> return ()
SessionKey {} -> return ()
SessionKeyAccepted {} -> return ()
SessionKeyRejected {} -> return ()
ChatMessage {} -> fwd c
_ -> return ()
fwd = atomically . writeTMChan controlinput . ControlInputAction . control
data AuthResult = Authed | AuthFailed | SessionEnded
-- | Present our session key to the user.
-- Wait for them to accept or reject it, while displaying any Seen data
-- in the meantime.
authUser :: PerhapsSigned PublicKey -> TMChan (Message Entered) -> TMChan (MissingHashes AnyMessage) -> TVar DeveloperState -> Logger -> IO AuthResult
authUser spk ichan ochan devstate logger = do
ds <- atomically $ readTVar devstate
let msg = ControlMessage $ mkSigned (developerSessionKey ds)
(Control (SessionKey spk currentProtocolVersion))
atomically $ writeTMChan ichan msg
logger $ Developer msg
waitresp $ case spk of
-- Don't bother verifying the user's gpg public key;
-- normally users send UnSigned.
GpgSigned pk _ _ -> pk
UnSigned pk -> pk
where
waitresp pk = do
ts <- getPOSIXTime
v <- atomically (getServerMessage ochan devstate ts)
case v of
Nothing -> return SessionEnded
Just (o, msg) -> do
logger msg
emitOutput o
case o of
GotControl (SessionKeyAccepted pk')
| pk' == pk -> return Authed
GotControl (SessionKeyRejected pk')
| pk' == pk -> return AuthFailed
_ -> waitresp pk
data Output
= TtyOutput B.ByteString
| Beep
| ProtocolError DeveloperState String
| GotControl ControlAction
| NoOutput
emitOutput :: Output -> IO ()
emitOutput (ProtocolError ds e) =
error ("Protocol error: " ++ e ++ "\nState: " ++ show ds)
emitOutput (TtyOutput b) = do
B.hPut stdout b
hFlush stdout
emitOutput Beep = do
B.hPut stdout "\a"
hFlush stdout
emitOutput (GotControl _) =
return ()
emitOutput NoOutput =
return ()
-- | Get messages from server, check their signature, and make sure that they
-- are properly chained from past messages, before returning.
getServerMessage :: TMChan (MissingHashes AnyMessage) -> TVar DeveloperState -> POSIXTime -> STM (Maybe (Output, AnyMessage))
getServerMessage ochan devstate ts = do
mwiremsg <- readTMChan ochan
case mwiremsg of
Nothing -> return Nothing
Just msg -> process =<< restoreHashes recentactivity msg
where
recentactivity = developerStateRecentActivity devstate
process (User msg) = do
ds <- readTVar devstate
-- Check user's signature before doing anything else.
if verifySigned (userSigVerifier ds) msg
then do
o <- processuser ds msg
return (Just (o, User msg))
else return $ Just (ProtocolError ds $ "Bad signature on message from user: " ++ show msg, User msg)
-- When other developers connect, learn their SessionKeys.
process (Developer msg@(ControlMessage (Control (SessionKey spk _) _))) = do
let sigverifier = mkSigVerifier $ case spk of
GpgSigned pk _ _ -> pk
UnSigned pk -> pk
if verifySigned sigverifier msg
then do
ds <- readTVar devstate
let sv = developerSigVerifier ds
let sv' = sv `mappend` sigverifier
writeTVar devstate $ ds
{ developerSigVerifier = sv'
}
processdeveloper ds msg
return (Just (NoOutput, Developer msg))
else ignore
process (Developer msg) = do
ds <- readTVar devstate
if verifySigned (developerSigVerifier ds) msg
then do
processdeveloper ds msg
return (Just (NoOutput, Developer msg))
else ignore
ignore = getServerMessage ochan devstate ts
processuser ds (ActivityMessage act@(Activity (Seen (Val b)) _ _ _ _)) = do
let (legal, ds') = isLegalSeen act ds ts
if legal
then do
writeTVar devstate ds'
return (TtyOutput b)
else return (ProtocolError ds $ "Illegal Seen value: " ++ show act)
processuser ds (ControlMessage (Control (er@EnteredRejected {}) _)) = do
-- When they rejected a message we sent,
-- anything we sent subsequently will
-- also be rejected, so forget about it.
let ds' = ds
{ sentSince = mempty
, enteredSince = mempty
, lastEntered = enteredLastAccepted er
}
writeTVar devstate ds'
return Beep
processuser _ (ControlMessage (Control c@(SessionKey _ _) _)) =
return (GotControl c)
processuser _ (ControlMessage (Control c@(SessionKeyAccepted _) _)) =
return (GotControl c)
processuser _ (ControlMessage (Control c@(SessionKeyRejected _) _)) =
return (GotControl c)
processuser _ (ControlMessage (Control c@(ChatMessage _ _) _)) =
return (GotControl c)
processdeveloper ds (ActivityMessage a) = do
let msghash = hash a
let ss = msghash : fromOtherDevelopersSince ds
writeTVar devstate (ds { fromOtherDevelopersSince = ss })
processdeveloper _ (ControlMessage _) = return ()
-- | Check if the Seen activity is legal, forming a chain with previous
-- ones, and returns an updated DeveloperState.
--
-- Does not check the signature.
isLegalSeen :: Activity Seen -> DeveloperState -> POSIXTime -> (Bool, DeveloperState)
isLegalSeen (Activity _ Nothing _ _ _) ds _ = (False, ds)
isLegalSeen act@(Activity (Seen (Val b)) (Just hp) _ _ _) ds ts
-- Does it chain to the last Seen activity or to
-- something sent by another developer since the last Seen?
| hp == lastSeen ds || hp `elem` fromOtherDevelopersSince ds =
-- Trim sentSince and enteredSince to
-- values after the Seen value.
let ss = sentSince ds
es = enteredSince ds
n = B.length b
(ss', es') = if b `B.isPrefixOf` mconcat ss
then (drop n ss, drop n es)
else (mempty, mempty)
in yes ds
{ lastSeen = acth
, sentSince = ss'
, enteredSince = es'
, lastActivity = acth
, lastActivityTs = ts
, lastEntered = newlastentered
, fromOtherDevelopersSince = mempty
}
-- Does it chain to something we've entered since the last Seen
-- value? Eg, user sent A, we replied B C, and the user has
-- now replied to B.
-- If so, we can drop B (and anything before it) from
-- enteredSince and sentSince.
| otherwise = case elemIndex hp (enteredSince ds) of
Nothing -> (False, ds)
Just i ->
let ss = sentSince ds
es = enteredSince ds
ss' = drop (i+1) ss
es' = drop (i+1) es
in yes ds
{ lastSeen = acth
, sentSince = ss'
, enteredSince = es'
, lastActivity = acth
, lastActivityTs = ts
, lastEntered = newlastentered
, fromOtherDevelopersSince = mempty
}
where
acth = hash act
yes ds' = (True, ds')
-- If there are multiple developers, need to set our lastEntered
-- to the prevEntered from the Activity Seen, so we can follow on to
-- another developer's activity.
--
-- But, if there's lag, we may have sent some Activity Entered
-- that had not reached the user yet when it constructed the
-- Activity Seen, so check if the prevEntered is one of the
-- things we've enteredSince; if so keep our lastEntered.
newlastentered = case prevEntered act of
Just v | v `notElem` enteredSince ds -> Just v
_ -> lastEntered ds
-- | Start by reading the initial two messages from the user,
-- their session key and the startup message.
processSessionStart :: MySessionKey -> TMChan (MissingHashes AnyMessage) -> Logger -> TMVar (TVar DeveloperState) -> IO (TVar DeveloperState, Output)
processSessionStart sk ochan logger dsv = do
MissingHashes sessionmsg <- fromMaybe (error "Did not get session initialization message")
<$> atomically (readTMChan ochan)
logger sessionmsg
sigverifier <- case sessionmsg of
User (ControlMessage c@(Control (SessionKey spk _) _)) -> do
let pk = case spk of
GpgSigned k _ _ -> k
UnSigned k -> k
let sv = mkSigVerifier pk
if verifySigned sv c
then return sv
else error "Badly signed session initialization message"
_ -> error $ "Unexpected session initialization message: " ++ show sessionmsg
ts <- getPOSIXTime
MissingHashes startmsg <- fromMaybe (error "Did not get session startup message")
<$> atomically (readTMChan ochan)
logger startmsg
let (starthash, output) = case startmsg of
User (ActivityMessage act@(Activity (Seen (Val b)) Nothing Nothing _ _))
| verifySigned sigverifier act ->
(hash act, TtyOutput b)
| otherwise ->
error "Bad signature on startup message"
_ -> error $ "Unexpected startup message: " ++ show startmsg
st <- newTVarIO $ DeveloperState
{ lastSeen = starthash
, sentSince = mempty
, enteredSince = mempty
, lastActivity = starthash
, lastActivityTs = ts
, lastEntered = Nothing
, fromOtherDevelopersSince = mempty
, developerSessionKey = sk
, userSigVerifier = sigverifier
, developerSigVerifier = mempty
}
atomically $ putTMVar dsv st
return (st, output)
|