1 {-# OPTIONS_GHC -cpp -fffi #-}
2 {-# LANGUAGE CPP, ForeignFunctionInterface #-}
4 module Ssh
( grabSSH
, runSSH
, getSSH
, copySSH
, copySSHs
, SSHCmd
(..) ) where
6 import Prelude
hiding ( lookup, catch )
8 import System
.Exit
( ExitCode(..) )
9 import System
.Environment
( getEnv )
11 import System
.Posix
.Process
( getProcessID
)
13 import Darcs
.Utils
( showHexLen
)
14 import Data
.Bits
( (.&.) )
15 import System
.Random
( randomIO )
17 import System
.IO ( Handle, hPutStr, hPutStrLn, hGetLine, hGetContents, hClose, hFlush )
18 import System
.IO.Unsafe
( unsafePerformIO
)
19 import System
.Directory
( doesFileExist, createDirectoryIfMissing
)
20 import Control
.Monad
( when )
21 import System
.Process
( runInteractiveProcess
)
23 import Data
.Map
( Map
, empty, insert, lookup )
24 import Data
.IORef
( IORef
, newIORef
, readIORef
, modifyIORef
)
26 import Darcs
.SignalHandler
( catchNonSignal
)
27 import Darcs
.Utils
( withCurrentDirectory
, breakCommand
, prettyException
, catchall
)
28 import Darcs
.Global
( atexit
, sshControlMasterDisabled
, darcsdir
, withDebugMode
)
29 import Darcs
.Lock
( withTemp
, withOpenTemp
, tempdir_loc
, removeFileMayNotExist
)
30 import Exec
( exec
, Redirects
, Redirect
(..), )
31 import Darcs
.Progress
( withoutProgress
, debugMessage
, debugFail
, progressList
)
33 import qualified Data
.ByteString
as B
(ByteString
, hGet
, writeFile, readFile)
34 import qualified Data
.ByteString
.Char8
as BC
(unpack
)
36 #include
"impossible.h"
38 {-# NOINLINE sshConnections #-}
39 sshConnections
:: IORef
(Map
String (Maybe Connection
))
40 sshConnections
= unsafePerformIO
$ newIORef
empty
42 data Connection
= C
{ inp
:: !Handle, out
:: !Handle, err
:: !Handle, deb
:: String -> IO () }
44 withSSHConnection
:: String -> (Connection
-> IO a
) -> IO a
-> IO a
45 withSSHConnection x withconnection withoutconnection
=
47 do cs
<- readIORef sshConnections
48 let uhost
= takeWhile (/= ':') x
50 case lookup url
(cs
:: Map
String (Maybe Connection
)) of
51 Just Nothing
-> withoutconnection
52 Just
(Just c
) -> withconnection c
54 do mc
<- do (ssh
,sshargs_
) <- getSSHOnly SSH
55 let sshargs
= sshargs_
++ [uhost
,"darcs","transfer-mode","--repodir",cleanrepodir x
]
56 debugMessage
$ "ssh "++unwords sshargs
57 (i
,o
,e
,_
) <- runInteractiveProcess ssh sshargs Nothing Nothing
59 if l
== "Hello user, I am darcs transfer mode"
61 else debugFail
"Couldn't start darcs transfer-mode on server"
62 let c
= C
{ inp
= i
, out
= o
, err
= e
,
63 deb
= \s
-> debugMessage
("with ssh (transfer-mode) "++uhost
++": "++s
) }
64 modifyIORef sshConnections
(insert url
(Just c
))
67 \e
-> do debugMessage
$ "Failed to start ssh connection:\n "++
70 debugMessage
$ unlines $
71 [ "NOTE: the server may be running a version of darcs prior to 2.0.0."
73 , "Installing darcs 2 on the server will speed up ssh-based commands."
76 maybe withoutconnection withconnection mc
78 severSSHConnection
:: String -> IO ()
79 severSSHConnection x
= do debugMessage
$ "Severing ssh failed connection to "++x
80 modifyIORef sshConnections
(insert (cleanrepourl x
) Nothing
)
82 cleanrepourl
:: String -> String
83 cleanrepourl zzz |
take (length dd
) zzz
== dd
= ""
84 where dd
= darcsdir
++"/"
85 cleanrepourl
(z
:zs
) = z
: cleanrepourl zs
88 cleanrepodir
:: String -> String
89 cleanrepodir
= cleanrepourl
. drop 1 . dropWhile (/= ':')
91 grabSSH
:: String -> Connection
-> IO B
.ByteString
93 let dir
= drop 1 $ dropWhile (/= ':') x
95 clean zzz |
take (length dd
) zzz
== dd
= drop (length dd
) zzz
96 clean
(_
:zs
) = clean zs
97 clean
"" = bug
$ "Buggy path in grabSSH: "++x
99 failwith e
= do severSSHConnection x
100 eee
<- hGetContents (err c
) -- ratify hGetContents: it's okay
101 -- here because we're only grabbing
102 -- stderr, and we're also about to
103 -- throw the contents.
104 debugFail
$ e
++ " grabbing ssh file "++x
++"\n"++eee
106 hPutStrLn (inp c
) $ "get " ++ file
108 l2
<- hGetLine (out c
)
109 if l2
== "got "++file
110 then do showlen
<- hGetLine (out c
)
111 case reads showlen
of
112 [(len
,"")] -> B
.hGet
(out c
) len
113 _
-> failwith
"Couldn't get length"
114 else if l2
== "error "++file
115 then do e
<- hGetLine (out c
)
117 (msg
,_
):_
-> debugFail
$ "Error reading file remotely:\n"++msg
118 [] -> failwith
"An error occurred"
119 else failwith
"Error"
121 sshStdErrMode
:: IO Redirect
122 sshStdErrMode
= withDebugMode
$ \amdebugging
->
123 return $ if amdebugging
then AsIs
else Null
125 copySSH
:: String -> FilePath -> IO ()
126 copySSH uRaw f
= withSSHConnection uRaw
(\c
-> grabSSH uRaw c
>>= B
.writeFile f
) $
127 do let u
= escape_dollar uRaw
128 stderr_behavior
<- sshStdErrMode
129 r
<- runSSH SCP u
[] [u
,f
] (AsIs
,AsIs
,stderr_behavior
)
130 when (r
/= ExitSuccess
) $
131 debugFail
$ "(scp) failed to fetch: " ++ u
132 where {- '$' in filenames is troublesome for scp, for some reason.. -}
133 escape_dollar
:: String -> String
134 escape_dollar
= concatMap tr
138 copySSHs
:: String -> [String] -> FilePath -> IO ()
140 withSSHConnection u
(\c
-> withCurrentDirectory d
$
141 mapM_ (\n -> grabSSH
(u
++"/"++n
) c
>>= B
.writeFile n
) $
142 progressList
"Copying via ssh" ns
) $
143 do let path
= drop 1 $ dropWhile (/= ':') u
144 host
= takeWhile (/= ':') u
145 cd
= "cd "++path
++"\n"
146 input
= cd
++(unlines $ map ("get "++) ns
)
147 withCurrentDirectory d
$ withOpenTemp
$ \(th
,tn
) ->
148 withTemp
$ \sftpoutput
->
151 stderr_behavior
<- sshStdErrMode
152 r
<- runSSH SFTP u
[] [host
] (File tn
, File sftpoutput
, stderr_behavior
)
153 let files
= if length ns
> 5
154 then (take 5 ns
) ++ ["and "
155 ++ (show (length ns
- 5)) ++ " more"]
157 hint
= if take 1 path
== "~"
158 then ["sftp doesn't expand ~, use path/ instead of ~/path/"]
160 when (r
/= ExitSuccess
) $ do
161 outputPS
<- B
.readFile sftpoutput
162 debugFail
$ unlines $
163 ["(sftp) failed to fetch files.",
164 "source directory: " ++ path
,
165 "source files:"] ++ files
++
166 ["sftp output:",BC
.unpack outputPS
] ++
169 -- ---------------------------------------------------------------------
170 -- older ssh helper functions
171 -- ---------------------------------------------------------------------
173 data SSHCmd
= SSH | SCP | SFTP
175 instance Show SSHCmd
where
180 runSSH
:: SSHCmd
-> String -> [String] -> [String] -> Redirects
-> IO ExitCode
181 runSSH cmd remoteAddr preArgs postArgs redirs
=
182 do (ssh
, args
) <- getSSH cmd remoteAddr
183 exec ssh
(preArgs
++ args
++ postArgs
) redirs
185 -- | Return the command and arguments needed to run an ssh command
186 -- along with any extra features like use of the control master.
188 getSSH
:: SSHCmd
-> String -- ^ remote path
189 -> IO (String, [String])
190 getSSH cmd remoteAddr
=
191 do (ssh
, ssh_args
) <- getSSHOnly cmd
192 cm_args
<- if sshControlMasterDisabled
194 else do -- control master
195 cmPath
<- controlMasterPath remoteAddr
196 hasLaunchedCm
<- doesFileExist cmPath
197 when (not hasLaunchedCm
) $ launchSSHControlMaster remoteAddr
198 hasCmFeature
<- doesFileExist cmPath
199 return $ if hasCmFeature
then [ "-o ControlPath=" ++ cmPath
] else []
200 let verbosity
= case cmd
of
201 SCP
-> ["-q"] -- (p)scp is the only one that recognises -q
202 -- sftp and (p)sftp do not, and plink neither
205 return (ssh
, verbosity
++ ssh_args
++ cm_args
)
207 -- | Return the command and arguments needed to run an ssh command.
208 -- First try the appropriate darcs environment variable and SSH_PORT
209 -- defaulting to "ssh" and no specified port.
210 getSSHOnly
:: SSHCmd
-> IO (String, [String])
212 do ssh_command
<- getEnv (evar cmd
) `catchall`
return (show cmd
)
214 port
<- (portFlag cmd `
fmap`
getEnv "SSH_PORT") `catchall`
return []
215 let (ssh
, ssh_args
) = breakCommand ssh_command
217 return (ssh
, ssh_args
++ port
)
219 evar SSH
= "DARCS_SSH"
220 evar SCP
= "DARCS_SCP"
221 evar SFTP
= "DARCS_SFTP"
222 portFlag SSH x
= ["-p", x
]
223 portFlag SCP x
= ["-P", x
]
224 portFlag SFTP x
= ["-oPort="++x
]
226 -- | Return True if this version of ssh has a ControlMaster feature
227 -- The ControlMaster functionality allows for ssh multiplexing
228 hasSSHControlMaster
:: Bool
229 hasSSHControlMaster
= unsafePerformIO hasSSHControlMasterIO
231 -- Because of the unsafePerformIO above, this can be called at any
232 -- point. It cannot rely on any state, not even the current directory.
233 hasSSHControlMasterIO
:: IO Bool
234 hasSSHControlMasterIO
= do
235 (ssh
, _
) <- getSSHOnly SSH
236 -- If ssh has the ControlMaster feature, it will recognise the
237 -- the -O flag, but exit with status 255 because of the nonsense
238 -- command. If it does not have the feature, it will simply dump
239 -- a help message on the screen and exit with 1.
240 sx
<- exec ssh
["-O", "an_invalid_command"] (Null
,Null
,Null
)
242 ExitFailure
255 -> return True
245 -- | Launch an SSH control master in the background, if available.
246 -- We don't have to wait for it or anything.
247 -- Note also that this will cleanup after itself when darcs exits
248 launchSSHControlMaster
:: String -> IO ()
249 launchSSHControlMaster rawAddr
=
250 when hasSSHControlMaster
$ do
251 let addr
= takeWhile (/= ':') rawAddr
252 (ssh
, ssh_args
) <- getSSHOnly SSH
253 cmPath
<- controlMasterPath addr
254 removeFileMayNotExist cmPath
255 -- -f : put ssh in the background once it succeeds in logging you in
256 -- -M : launch as the control master for addr
257 -- -N : don't run any commands
258 -- -S : use cmPath as the ControlPath. Equivalent to -oControlPath=
259 exec ssh
(ssh_args
++ [addr
, "-S", cmPath
, "-N", "-f", "-M"]) (Null
,Null
,AsIs
)
260 atexit
$ exitSSHControlMaster addr
263 -- | Tell the SSH control master for a given path to exit.
264 exitSSHControlMaster
:: String -> IO ()
265 exitSSHControlMaster addr
= do
266 (ssh
, ssh_args
) <- getSSHOnly SSH
267 cmPath
<- controlMasterPath addr
268 exec ssh
(ssh_args
++ [addr
, "-S", cmPath
, "-O", "exit"]) (Null
,Null
,Null
)
271 -- | Create the directory ssh control master path for a given address
272 controlMasterPath
:: String -- ^ remote path (foo\@bar.com:file is ok; the file part with be stripped)
274 controlMasterPath rawAddr
= do
275 let addr
= takeWhile (/= ':') rawAddr
276 tmp
<- (fmap (/// ".darcs") $ getEnv "HOME") `catchall` tempdir_loc
279 let suffix
= (showHexLen
6 (r
.&. 0xFFFFFF :: Int))
281 suffix
<- show `
fmap` getProcessID
283 let tmpDarcsSsh
= tmp
/// "darcs-ssh"
284 createDirectoryIfMissing
True tmpDarcsSsh
285 return $ tmpDarcsSsh
/// addr
++ suffix
287 (///) :: FilePath -> FilePath -> FilePath
288 d
/// f
= d
++ "/" ++ f