{-# LANGUAGE GeneralizedNewtypeDeriving #-}
module Transit.Internal.App
( Env(..)
, App(..)
, prepareAppEnv
, app
, runApp
, send
, receive
)
where
import Protolude
import qualified Data.Text as Text
import qualified Data.Text.IO as TIO
import qualified MagicWormhole
import qualified System.Console.Haskeline as H
import qualified System.Console.Haskeline.Completion as HC
import qualified Crypto.Spake2 as Spake2
import System.IO.Error (IOError)
import System.Random (randomR, getStdGen)
import Data.String (String)
import Control.Monad.Trans.Except (ExceptT(..))
import Control.Monad.Except (liftEither)
import Data.Text.PgpWordlist.Internal.Words (wordList)
import Data.Text.PgpWordlist.Internal.Types (EvenWord(..), OddWord(..))
import Transit.Internal.Conf (Options(..), Command(..))
import Transit.Internal.Errors (Error(..), CommunicationError(..))
import Transit.Internal.FileTransfer(MessageType(..), sendFile, receiveFile)
import Transit.Internal.Peer (sendOffer, receiveOffer, receiveMessageAck, sendMessageAck, decodeTransitMsg)
data Env
= Env { appID :: MagicWormhole.AppID
, side :: MagicWormhole.Side
, config :: Options
}
prepareAppEnv :: Text -> Options -> IO Env
prepareAppEnv appid options = do
side' <- MagicWormhole.generateSide
let appID' = MagicWormhole.AppID appid
return $ Env appID' side' options
allocateCode :: [(Word8, EvenWord, OddWord)] -> IO Text
allocateCode wordlist = do
g <- getStdGen
let (r1, g') = randomR (0, 255) g
(r2, _) = randomR (0, 255) g'
Just (_, evenW, _) = atMay wordlist r2
Just (_, _, oddW) = atMay wordlist r1
return $ Text.concat [unOddWord oddW, "-", unEvenWord evenW]
printSendHelpText :: Text -> IO ()
printSendHelpText passcode = do
TIO.putStrLn $ "Wormhole code is: " <> passcode
TIO.putStrLn "On the other computer, please run:"
TIO.putStrLn ""
TIO.putStrLn $ "wormhole receive " <> passcode
data CompletionConfig
= CompletionConfig {
nameplates :: [Text]
, oddWords :: [Text]
, evenWords :: [Text]
, numWords :: Int
}
simpleCompletion :: Text -> HC.Completion
simpleCompletion text = (HC.simpleCompletion (toS text)) { HC.isFinished = False }
completeWord :: MonadIO m => CompletionConfig -> HC.CompletionFunc m
completeWord completionConfig = HC.completeWord Nothing "" completionFunc
where
completionFunc :: Monad m => String -> m [HC.Completion]
completionFunc word = do
let (completed, partial) = Text.breakOnEnd "-" (toS word)
hypenCount = Text.count "-" completed
wordlist = if hypenCount == 0
then nameplates completionConfig
else if odd hypenCount
then oddWords completionConfig
else evenWords completionConfig
suffix = if hypenCount < numWords completionConfig
then "-"
else ""
completions = map (\w -> completed `Text.append` (w `Text.append` suffix)) .
filter (Text.isPrefixOf partial) $ wordlist
return $ map simpleCompletion completions
getCode :: MagicWormhole.Session -> [(Word8, EvenWord, OddWord)] -> IO Text
getCode session wordlist = do
nameplates' <- MagicWormhole.list session
let ns = [ n | MagicWormhole.Nameplate n <- nameplates' ]
evens = [ unEvenWord n | (_, n, _) <- wordlist]
odds = [ unOddWord m | (_, _, m) <- wordlist]
completionConfig = CompletionConfig {
nameplates = ns,
oddWords = odds,
evenWords = evens,
numWords = 2
}
putText "Enter the receive wormhole code: "
H.runInputT (settings completionConfig) getInput
where
settings :: MonadIO m => CompletionConfig -> H.Settings m
settings completionConfig = H.Settings
{ H.complete = completeWord completionConfig
, H.historyFile = Nothing
, H.autoAddHistory = False
}
getInput :: H.InputT IO Text
getInput = do
minput <- H.getInputLine ""
case minput of
Nothing -> return ""
Just input -> return (toS input)
newtype App a = App {
getApp :: ReaderT Env (ExceptT Error IO) a
} deriving (Functor, Applicative, Monad, MonadIO, MonadReader Env, MonadError Error)
runApp :: App a -> Env -> IO (Either Error a)
runApp appM env = runExceptT (runReaderT (getApp appM) env)
transitPurpose :: MagicWormhole.AppID -> ByteString
transitPurpose (MagicWormhole.AppID appid) = toS appid <> "/transit-key"
send :: MagicWormhole.Session -> Text -> MessageType -> App ()
send session code tfd = do
env <- ask
let options = config env
let appid = appID env
let transitserver = transitUrl options
nameplate <- liftIO $ MagicWormhole.allocate session
mailbox <- liftIO $ MagicWormhole.claim session nameplate
peer <- liftIO $ MagicWormhole.open session mailbox
let (MagicWormhole.Nameplate n) = nameplate
let passcode = toS n <> "-" <> toS code
liftIO $ printSendHelpText passcode
result <- liftIO $ MagicWormhole.withEncryptedConnection peer (Spake2.makePassword (toS passcode))
(\conn ->
case tfd of
TMsg msg -> do
let offer = MagicWormhole.Message msg
sendOffer conn offer
first NetworkError <$> receiveMessageAck conn
TFile filepath -> do
let transitKey = MagicWormhole.deriveKey conn (transitPurpose appid)
sendFile conn transitserver transitKey filepath
)
liftEither result
receive :: MagicWormhole.Session -> Text -> App ()
receive session code = do
env <- ask
let options = config env
let appid = appID env
let transitserver = transitUrl options
let codeSplit = Text.split (=='-') code
let (Just nameplate) = headMay codeSplit
mailbox <- liftIO $ MagicWormhole.claim session (MagicWormhole.Nameplate nameplate)
peer <- liftIO $ MagicWormhole.open session mailbox
result <- liftIO $ MagicWormhole.withEncryptedConnection peer (Spake2.makePassword (toS (Text.strip code)))
(\conn -> do
someOffer <- receiveOffer conn
case someOffer of
Right (MagicWormhole.Message message) -> do
TIO.putStrLn message
result <- try (sendMessageAck conn "ok") :: IO (Either IOError ())
return $ bimap (const (NetworkError (ConnectionError "sending the ack message failed"))) identity result
Right (MagicWormhole.File _ _) -> do
sendMessageAck conn "not_ok"
return $ Left (NetworkError (ConnectionError "did not expect a file offer"))
Right MagicWormhole.Directory {} ->
return $ Left (NetworkError (UnknownPeerMessage "directory offer is not supported"))
Left received ->
case decodeTransitMsg (toS received) of
Left e -> return $ Left (NetworkError e)
Right transitMsg -> do
let transitKey = MagicWormhole.deriveKey conn (transitPurpose appid)
receiveFile conn transitserver transitKey transitMsg
)
liftEither result
app :: App ()
app = do
env <- ask
let options = config env
endpoint = relayEndpoint options
case cmd options of
Send tfd ->
liftIO (MagicWormhole.runClient endpoint (appID env) (side env) $ \session ->
runApp (sendSession tfd session) env) >>= liftEither
Receive maybeCode ->
liftIO (MagicWormhole.runClient endpoint (appID env) (side env) $ \session ->
runApp (receiveSession maybeCode session) env) >>= liftEither
where
getWormholeCode :: MagicWormhole.Session -> Maybe Text -> IO Text
getWormholeCode session Nothing = getCode session wordList
getWormholeCode _ (Just code) = return code
sendSession offerMsg session = do
code <- liftIO $ allocateCode wordList
send session (toS code) offerMsg
receiveSession maybeCode session = do
code <- liftIO $ getWormholeCode session maybeCode
receive session code