packages feed

scp-streams-0.1.0: Network/SCP/Protocol.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Network.SCP.Protocol where

import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as C
import qualified Data.ByteString.Lazy as L
import Data.Word (Word8)
import System.Exit (ExitCode(..))
import System.IO (hClose, hFlush, hPutStrLn, stderr)
import System.IO.Streams (InputStream, OutputStream)
import qualified System.IO.Streams as S
import qualified System.IO.Streams.Attoparsec as S
import System.Process (ProcessHandle)
import System.Process (CreateProcess(..), StdStream(..))
import qualified System.Process as P

import Network.SCP.Types

data SCP = SCP
  { scpIn :: OutputStream ByteString
  , scpOut :: InputStream ByteString
  , scpProcess :: Maybe ProcessHandle
  }

----------------------------------------------------------------------
-- "Mid-level" interface
----------------------------------------------------------------------

sendSsh :: String -> String -> FilePath -> IO SCP
sendSsh user host target = sendProcess "ssh"
  [ "-q"
  -- TODO actually use known_hosts and key check.
  , "-o", "UserKnownHostsFile=/dev/null"
  , "-o", "StrictHostKeyChecking=no"
  , "-i", "/home/scp/.ssh/insecure_id_rsa"
  , user ++ "@" ++ host
  , "scp"
  , "-r" -- TODO Only when pushing directories.
         -- TODO Error out when receiving directories without -r.
  , "-t"
  , target
  ]

sendStd :: IO SCP
sendStd = do
  let scp = SCP S.stdout S.stdin Nothing
  _ <- startSending scp
  return scp

sendSelf :: FilePath -> IO SCP
sendSelf target = sendProcess "dist/build/scp-streams/scp-streams" ["-t", target]

sendDirect :: FilePath -> IO SCP
sendDirect target = sendProcess "scp" ["-t", target]

stop :: SCP -> IO ExitCode
stop scp@SCP{..} = do
  stopSending scp
  maybe (return ExitSuccess) P.waitForProcess scpProcess

copy :: SCP -> Word8 -> Word8 -> Word8 -> Word8 -> Int -> ByteString
  -> InputStream ByteString -> IO Bool
copy scp a b c d len filename content = do
  _ <- sendCommand scp $ Copy a b c d len filename
  sendContent scp content

push :: SCP -> Word8 -> Word8 -> Word8 -> Word8 -> ByteString -> IO Bool
push scp a b c d directory = sendCommand scp $ Push a b c d directory

pop :: SCP -> IO Bool
pop scp = sendCommand scp Pop

receiveSsh :: String -> String -> String -> IO SCP
receiveSsh user host path = receiveProcess "ssh"
  [ "-q"
  -- TODO actually use known_hosts and key check.
  , "-o", "UserKnownHostsFile=/dev/null"
  , "-o", "StrictHostKeyChecking=no"
  , "-i", "/home/scp/.ssh/insecure_id_rsa"
  , user ++ "@" ++ host
  , "scp"
  , "-f"
  , path
  ]

receiveStd :: IO SCP
receiveStd = do
  let scp = SCP S.stdout S.stdin Nothing
  startReceiving scp
  return scp

receiveDirect :: String -> IO SCP
receiveDirect path = receiveProcess "scp" ["-f", path]

readCommand :: SCP -> IO Command
readCommand SCP{..} = do
  command <- S.parseFromStream commandParser scpOut
  confirm scpIn
  return command

contentAsInputStream :: SCP -> Int -> IO (InputStream ByteString)
contentAsInputStream SCP{..} len = do
  is <- S.takeBytes (fromIntegral len) scpOut
  is' <- S.atEndOfInput (do
    _ <- getFeedback scpOut
    confirm scpIn) is
  return is'

doneReceiving :: SCP -> IO Bool
doneReceiving SCP{..} = S.atEOF scpOut

----------------------------------------------------------------------
-- "Low-level" interface
----------------------------------------------------------------------

sendProcess :: FilePath -> [String] -> IO SCP
sendProcess cmd args = do
  (inp, out, h) <- runInteractiveProcess cmd args
  let scp = SCP inp out $ Just h
  _ <- startSending scp
  return scp

startSending :: SCP -> IO Bool
startSending SCP{..} = do
  getFeedback scpOut

sendCommand :: SCP -> Command -> IO Bool
sendCommand SCP{..} command = do
  let c = unparse command
  hPutStrLn stderr ("Sending command " ++ C.unpack c) >> hFlush stderr
  S.writeLazyByteString (L.fromChunks [c, "\n"]) scpIn
  S.write (Just "") scpIn -- flush
  getFeedback scpOut

sendContent :: SCP -> InputStream ByteString -> IO Bool
sendContent SCP{..} content = do
  hPutStrLn stderr "Sending content..." >> hFlush stderr
  S.supply content scpIn
  confirm scpIn
  b <- getFeedback scpOut
  return b

stopSending :: SCP -> IO ()
stopSending SCP{..} = S.write Nothing scpIn

receiveProcess :: String -> [String] -> IO SCP
receiveProcess cmd args = do
  (inp, out, h) <- runInteractiveProcess cmd args
  let scp = SCP inp out $ Just h
  startReceiving scp
  return scp

startReceiving :: SCP -> IO ()
startReceiving SCP{..} = do
  confirm scpIn

getFeedback :: InputStream ByteString -> IO Bool
getFeedback feedback = do
  i' <- S.readExactly 1 feedback
  case i' of
    "\0" -> return True
    _ -> do
      msg <- sGetLine feedback
      hPutStrLn stderr ("Bad feedback: " ++ C.unpack msg) >> hFlush stderr
      return False

confirm :: OutputStream ByteString -> IO ()
confirm os = do
  S.writeLazyByteString "\0" os
  S.write (Just "") os -- flush

whine :: OutputStream ByteString -> ByteString -> IO ()
whine os msg = do
  S.writeLazyByteString (L.fromChunks ["\x01", msg, "\n"]) os
  S.write (Just "") os -- flush

sGetLine :: InputStream ByteString -> IO ByteString
sGetLine is = do
  mline <- S.takeBytesWhile (/= '\n') is
  return $ maybe "" B.init mline

-- | Similar to S.runInteractiveProcess but without the stderr pipe.
runInteractiveProcess :: String -> [String]
  -> IO (OutputStream ByteString, InputStream ByteString, ProcessHandle)
runInteractiveProcess cmd args = do
  (Just hin, Just hout , _, ph) <- P.createProcess (P.proc cmd args)
    { std_in = CreatePipe, std_out = CreatePipe }
  sIn  <- S.handleToOutputStream hin >>=
          S.atEndOfOutput (hClose hin) >>=
          S.lockingOutputStream
  sOut <- S.handleToInputStream hout >>=
          S.atEndOfInput (hClose hout) >>=
          S.lockingInputStream
  return (sIn, sOut, ph)