1# -*- test-case-name: twisted.conch.test.test_cftp -*-
2# Copyright (c) Twisted Matrix Laboratories.
3# See LICENSE for details.
4
5"""
6Implementation module for the I{cftp} command.
7"""
8import fcntl
9import fnmatch
10import getpass
11import glob
12import os
13import pwd
14import stat
15import struct
16import sys
17import tty
18from typing import List, Optional, Union
19
20from twisted.conch.client import connect, default, options
21from twisted.conch.ssh import channel, common, connection, filetransfer
22from twisted.internet import defer, reactor, stdio, utils
23from twisted.protocols import basic
24from twisted.python import failure, log, usage
25from twisted.python.filepath import FilePath
26
27
28class ClientOptions(options.ConchOptions):
29
30    synopsis = """Usage:   cftp [options] [user@]host
31         cftp [options] [user@]host[:dir[/]]
32         cftp [options] [user@]host[:file [localfile]]
33"""
34    longdesc = (
35        "cftp is a client for logging into a remote machine and "
36        "executing commands to send and receive file information"
37    )
38
39    optParameters: List[List[Optional[Union[str, int]]]] = [
40        ["buffersize", "B", 32768, "Size of the buffer to use for sending/receiving."],
41        ["batchfile", "b", None, "File to read commands from, or '-' for stdin."],
42        ["requests", "R", 5, "Number of requests to make before waiting for a reply."],
43        ["subsystem", "s", "sftp", "Subsystem/server program to connect to."],
44    ]
45
46    compData = usage.Completions(
47        descriptions={"buffersize": "Size of send/receive buffer (default: 32768)"},
48        extraActions=[
49            usage.CompleteUserAtHost(),
50            usage.CompleteFiles(descr="local file"),
51        ],
52    )
53
54    def parseArgs(self, host, localPath=None):
55        self["remotePath"] = ""
56        if ":" in host:
57            host, self["remotePath"] = host.split(":", 1)
58            self["remotePath"].rstrip("/")
59        self["host"] = host
60        self["localPath"] = localPath
61
62
63def run():
64    args = sys.argv[1:]
65    if "-l" in args:  # cvs is an idiot
66        i = args.index("-l")
67        args = args[i : i + 2] + args
68        del args[i + 2 : i + 4]
69    options = ClientOptions()
70    try:
71        options.parseOptions(args)
72    except usage.UsageError as u:
73        print("ERROR: %s" % u)
74        sys.exit(1)
75    if options["log"]:
76        realout = sys.stdout
77        log.startLogging(sys.stderr)
78        sys.stdout = realout
79    else:
80        log.discardLogs()
81    doConnect(options)
82    reactor.run()
83
84
85def handleError():
86    global exitStatus
87    exitStatus = 2
88    try:
89        reactor.stop()
90    except BaseException:
91        pass
92    log.err(failure.Failure())
93    raise
94
95
96def doConnect(options):
97    if "@" in options["host"]:
98        options["user"], options["host"] = options["host"].split("@", 1)
99    host = options["host"]
100    if not options["user"]:
101        options["user"] = getpass.getuser()
102    if not options["port"]:
103        options["port"] = 22
104    else:
105        options["port"] = int(options["port"])
106    host = options["host"]
107    port = options["port"]
108    conn = SSHConnection()
109    conn.options = options
110    vhk = default.verifyHostKey
111    uao = default.SSHUserAuthClient(options["user"], options, conn)
112    connect.connect(host, port, options, vhk, uao).addErrback(_ebExit)
113
114
115def _ebExit(f):
116    if hasattr(f.value, "value"):
117        s = f.value.value
118    else:
119        s = str(f)
120    print(s)
121    try:
122        reactor.stop()
123    except BaseException:
124        pass
125
126
127def _ignore(*args):
128    pass
129
130
131class FileWrapper:
132    def __init__(self, f):
133        self.f = f
134        self.total = 0.0
135        f.seek(0, 2)  # seek to the end
136        self.size = f.tell()
137
138    def __getattr__(self, attr):
139        return getattr(self.f, attr)
140
141
142class StdioClient(basic.LineReceiver):
143
144    _pwd = pwd
145
146    ps = "cftp> "
147    delimiter = b"\n"
148
149    reactor = reactor
150
151    def __init__(self, client, f=None):
152        self.client = client
153        self.currentDirectory = ""
154        self.file = f
155        self.useProgressBar = (not f and 1) or 0
156
157    def connectionMade(self):
158        self.client.realPath("").addCallback(self._cbSetCurDir)
159
160    def _cbSetCurDir(self, path):
161        self.currentDirectory = path
162        self._newLine()
163
164    def _writeToTransport(self, msg):
165        if isinstance(msg, str):
166            msg = msg.encode("utf-8")
167        return self.transport.write(msg)
168
169    def lineReceived(self, line):
170        if self.client.transport.localClosed:
171            return
172        if isinstance(line, bytes):
173            line = line.decode("utf-8")
174        log.msg("got line %s" % line)
175        line = line.lstrip()
176        if not line:
177            self._newLine()
178            return
179        if self.file and line.startswith("-"):
180            self.ignoreErrors = 1
181            line = line[1:]
182        else:
183            self.ignoreErrors = 0
184        d = self._dispatchCommand(line)
185        if d is not None:
186            d.addCallback(self._cbCommand)
187            d.addErrback(self._ebCommand)
188
189    def _dispatchCommand(self, line):
190        if " " in line:
191            command, rest = line.split(" ", 1)
192            rest = rest.lstrip()
193        else:
194            command, rest = line, ""
195        if command.startswith("!"):  # command
196            f = self.cmd_EXEC
197            rest = (command[1:] + " " + rest).strip()
198        else:
199            command = command.upper()
200            log.msg("looking up cmd %s" % command)
201            f = getattr(self, "cmd_%s" % command, None)
202        if f is not None:
203            return defer.maybeDeferred(f, rest)
204        else:
205            errMsg = "No command called `%s'" % (command)
206            self._ebCommand(failure.Failure(NotImplementedError(errMsg)))
207            self._newLine()
208
209    def _printFailure(self, f):
210        log.msg(f)
211        e = f.trap(NotImplementedError, filetransfer.SFTPError, OSError, IOError)
212        if e == NotImplementedError:
213            self._writeToTransport(self.cmd_HELP(""))
214        elif e == filetransfer.SFTPError:
215            errMsg = "remote error %i: %s\n" % (f.value.code, f.value.message)
216            self._writeToTransport(errMsg)
217        elif e in (OSError, IOError):
218            errMsg = "local error %i: %s\n" % (f.value.errno, f.value.strerror)
219            self._writeToTransport(errMsg)
220
221    def _newLine(self):
222        if self.client.transport.localClosed:
223            return
224        self._writeToTransport(self.ps)
225        self.ignoreErrors = 0
226        if self.file:
227            l = self.file.readline()
228            if not l:
229                self.client.transport.loseConnection()
230            else:
231                self._writeToTransport(l)
232                self.lineReceived(l.strip())
233
234    def _cbCommand(self, result):
235        if result is not None:
236            if isinstance(result, str):
237                result = result.encode("utf-8")
238            self._writeToTransport(result)
239            if not result.endswith(b"\n"):
240                self._writeToTransport(b"\n")
241        self._newLine()
242
243    def _ebCommand(self, f):
244        self._printFailure(f)
245        if self.file and not self.ignoreErrors:
246            self.client.transport.loseConnection()
247        self._newLine()
248
249    def cmd_CD(self, path):
250        path, rest = self._getFilename(path)
251        if not path.endswith("/"):
252            path += "/"
253        newPath = path and os.path.join(self.currentDirectory, path) or ""
254        d = self.client.openDirectory(newPath)
255        d.addCallback(self._cbCd)
256        d.addErrback(self._ebCommand)
257        return d
258
259    def _cbCd(self, directory):
260        directory.close()
261        d = self.client.realPath(directory.name)
262        d.addCallback(self._cbCurDir)
263        return d
264
265    def _cbCurDir(self, path):
266        self.currentDirectory = path
267
268    def cmd_CHGRP(self, rest):
269        grp, rest = rest.split(None, 1)
270        path, rest = self._getFilename(rest)
271        grp = int(grp)
272        d = self.client.getAttrs(path)
273        d.addCallback(self._cbSetUsrGrp, path, grp=grp)
274        return d
275
276    def cmd_CHMOD(self, rest):
277        mod, rest = rest.split(None, 1)
278        path, rest = self._getFilename(rest)
279        mod = int(mod, 8)
280        d = self.client.setAttrs(path, {"permissions": mod})
281        d.addCallback(_ignore)
282        return d
283
284    def cmd_CHOWN(self, rest):
285        usr, rest = rest.split(None, 1)
286        path, rest = self._getFilename(rest)
287        usr = int(usr)
288        d = self.client.getAttrs(path)
289        d.addCallback(self._cbSetUsrGrp, path, usr=usr)
290        return d
291
292    def _cbSetUsrGrp(self, attrs, path, usr=None, grp=None):
293        new = {}
294        new["uid"] = (usr is not None) and usr or attrs["uid"]
295        new["gid"] = (grp is not None) and grp or attrs["gid"]
296        d = self.client.setAttrs(path, new)
297        d.addCallback(_ignore)
298        return d
299
300    def cmd_GET(self, rest):
301        remote, rest = self._getFilename(rest)
302        if "*" in remote or "?" in remote:  # wildcard
303            if rest:
304                local, rest = self._getFilename(rest)
305                if not os.path.isdir(local):
306                    return "Wildcard get with non-directory target."
307            else:
308                local = b""
309            d = self._remoteGlob(remote)
310            d.addCallback(self._cbGetMultiple, local)
311            return d
312        if rest:
313            local, rest = self._getFilename(rest)
314        else:
315            local = os.path.split(remote)[1]
316        log.msg((remote, local))
317        lf = open(local, "wb", 0)
318        path = FilePath(self.currentDirectory).child(remote)
319        d = self.client.openFile(path.path, filetransfer.FXF_READ, {})
320        d.addCallback(self._cbGetOpenFile, lf)
321        d.addErrback(self._ebCloseLf, lf)
322        return d
323
324    def _cbGetMultiple(self, files, local):
325        # XXX this can be optimized for times w/o progress bar
326        return self._cbGetMultipleNext(None, files, local)
327
328    def _cbGetMultipleNext(self, res, files, local):
329        if isinstance(res, failure.Failure):
330            self._printFailure(res)
331        elif res:
332            self._writeToTransport(res)
333            if not res.endswith("\n"):
334                self._writeToTransport("\n")
335        if not files:
336            return
337        f = files.pop(0)[0]
338        lf = open(os.path.join(local, os.path.split(f)[1]), "wb", 0)
339        path = FilePath(self.currentDirectory).child(f)
340        d = self.client.openFile(path.path, filetransfer.FXF_READ, {})
341        d.addCallback(self._cbGetOpenFile, lf)
342        d.addErrback(self._ebCloseLf, lf)
343        d.addBoth(self._cbGetMultipleNext, files, local)
344        return d
345
346    def _ebCloseLf(self, f, lf):
347        lf.close()
348        return f
349
350    def _cbGetOpenFile(self, rf, lf):
351        return rf.getAttrs().addCallback(self._cbGetFileSize, rf, lf)
352
353    def _cbGetFileSize(self, attrs, rf, lf):
354        if not stat.S_ISREG(attrs["permissions"]):
355            rf.close()
356            lf.close()
357            return "Can't get non-regular file: %s" % rf.name
358        rf.size = attrs["size"]
359        bufferSize = self.client.transport.conn.options["buffersize"]
360        numRequests = self.client.transport.conn.options["requests"]
361        rf.total = 0.0
362        dList = []
363        chunks = []
364        startTime = self.reactor.seconds()
365        for i in range(numRequests):
366            d = self._cbGetRead("", rf, lf, chunks, 0, bufferSize, startTime)
367            dList.append(d)
368        dl = defer.DeferredList(dList, fireOnOneErrback=1)
369        dl.addCallback(self._cbGetDone, rf, lf)
370        return dl
371
372    def _getNextChunk(self, chunks):
373        end = 0
374        for chunk in chunks:
375            if end == "eof":
376                return  # nothing more to get
377            if end != chunk[0]:
378                i = chunks.index(chunk)
379                chunks.insert(i, (end, chunk[0]))
380                return (end, chunk[0] - end)
381            end = chunk[1]
382        bufSize = int(self.client.transport.conn.options["buffersize"])
383        chunks.append((end, end + bufSize))
384        return (end, bufSize)
385
386    def _cbGetRead(self, data, rf, lf, chunks, start, size, startTime):
387        if data and isinstance(data, failure.Failure):
388            log.msg("get read err: %s" % data)
389            reason = data
390            reason.trap(EOFError)
391            i = chunks.index((start, start + size))
392            del chunks[i]
393            chunks.insert(i, (start, "eof"))
394        elif data:
395            log.msg("get read data: %i" % len(data))
396            lf.seek(start)
397            lf.write(data)
398            if len(data) != size:
399                log.msg("got less than we asked for: %i < %i" % (len(data), size))
400                i = chunks.index((start, start + size))
401                del chunks[i]
402                chunks.insert(i, (start, start + len(data)))
403            rf.total += len(data)
404        if self.useProgressBar:
405            self._printProgressBar(rf, startTime)
406        chunk = self._getNextChunk(chunks)
407        if not chunk:
408            return
409        else:
410            start, length = chunk
411        log.msg("asking for %i -> %i" % (start, start + length))
412        d = rf.readChunk(start, length)
413        d.addBoth(self._cbGetRead, rf, lf, chunks, start, length, startTime)
414        return d
415
416    def _cbGetDone(self, ignored, rf, lf):
417        log.msg("get done")
418        rf.close()
419        lf.close()
420        if self.useProgressBar:
421            self._writeToTransport("\n")
422        return f"Transferred {rf.name} to {lf.name}"
423
424    def cmd_PUT(self, rest):
425        """
426        Do an upload request for a single local file or a globing expression.
427
428        @param rest: Requested command line for the PUT command.
429        @type rest: L{str}
430
431        @return: A deferred which fires with L{None} when transfer is done.
432        @rtype: L{defer.Deferred}
433        """
434        local, rest = self._getFilename(rest)
435
436        # FIXME: https://twistedmatrix.com/trac/ticket/7241
437        # Use a better check for globbing expression.
438        if "*" in local or "?" in local:
439            if rest:
440                remote, rest = self._getFilename(rest)
441                remote = os.path.join(self.currentDirectory, remote)
442            else:
443                remote = ""
444
445            files = glob.glob(local)
446            return self._putMultipleFiles(files, remote)
447
448        else:
449            if rest:
450                remote, rest = self._getFilename(rest)
451            else:
452                remote = os.path.split(local)[1]
453            return self._putSingleFile(local, remote)
454
455    def _putSingleFile(self, local, remote):
456        """
457        Perform an upload for a single file.
458
459        @param local: Path to local file.
460        @type local: L{str}.
461
462        @param remote: Remote path for the request relative to current working
463            directory.
464        @type remote: L{str}
465
466        @return: A deferred which fires when transfer is done.
467        """
468        return self._cbPutMultipleNext(None, [local], remote, single=True)
469
470    def _putMultipleFiles(self, files, remote):
471        """
472        Perform an upload for a list of local files.
473
474        @param files: List of local files.
475        @type files: C{list} of L{str}.
476
477        @param remote: Remote path for the request relative to current working
478            directory.
479        @type remote: L{str}
480
481        @return: A deferred which fires when transfer is done.
482        """
483        return self._cbPutMultipleNext(None, files, remote)
484
485    def _cbPutMultipleNext(self, previousResult, files, remotePath, single=False):
486        """
487        Perform an upload for the next file in the list of local files.
488
489        @param previousResult: Result form previous file form the list.
490        @type previousResult: L{str}
491
492        @param files: List of local files.
493        @type files: C{list} of L{str}
494
495        @param remotePath: Remote path for the request relative to current
496            working directory.
497        @type remotePath: L{str}
498
499        @param single: A flag which signals if this is a transfer for a single
500            file in which case we use the exact remote path
501        @type single: L{bool}
502
503        @return: A deferred which fires when transfer is done.
504        """
505        if isinstance(previousResult, failure.Failure):
506            self._printFailure(previousResult)
507        elif previousResult:
508            if isinstance(previousResult, str):
509                previousResult = previousResult.encode("utf-8")
510            self._writeToTransport(previousResult)
511            if not previousResult.endswith(b"\n"):
512                self._writeToTransport(b"\n")
513
514        currentFile = None
515        while files and not currentFile:
516            try:
517                currentFile = files.pop(0)
518                localStream = open(currentFile, "rb")
519            except BaseException:
520                self._printFailure(failure.Failure())
521                currentFile = None
522
523        # No more files to transfer.
524        if not currentFile:
525            return None
526
527        if single:
528            remote = remotePath
529        else:
530            name = os.path.split(currentFile)[1]
531            remote = os.path.join(remotePath, name)
532            log.msg((name, remote, remotePath))
533
534        d = self._putRemoteFile(localStream, remote)
535        d.addBoth(self._cbPutMultipleNext, files, remotePath)
536        return d
537
538    def _putRemoteFile(self, localStream, remotePath):
539        """
540        Do an upload request.
541
542        @param localStream: Local stream from where data is read.
543        @type localStream: File like object.
544
545        @param remotePath: Remote path for the request relative to current working directory.
546        @type remotePath: L{str}
547
548        @return: A deferred which fires when transfer is done.
549        """
550        remote = os.path.join(self.currentDirectory, remotePath)
551        flags = filetransfer.FXF_WRITE | filetransfer.FXF_CREAT | filetransfer.FXF_TRUNC
552        d = self.client.openFile(remote, flags, {})
553        d.addCallback(self._cbPutOpenFile, localStream)
554        d.addErrback(self._ebCloseLf, localStream)
555        return d
556
557    def _cbPutOpenFile(self, rf, lf):
558        numRequests = self.client.transport.conn.options["requests"]
559        if self.useProgressBar:
560            lf = FileWrapper(lf)
561        dList = []
562        chunks = []
563        startTime = self.reactor.seconds()
564        for i in range(numRequests):
565            d = self._cbPutWrite(None, rf, lf, chunks, startTime)
566            if d:
567                dList.append(d)
568        dl = defer.DeferredList(dList, fireOnOneErrback=1)
569        dl.addCallback(self._cbPutDone, rf, lf)
570        return dl
571
572    def _cbPutWrite(self, ignored, rf, lf, chunks, startTime):
573        chunk = self._getNextChunk(chunks)
574        start, size = chunk
575        lf.seek(start)
576        data = lf.read(size)
577        if self.useProgressBar:
578            lf.total += len(data)
579            self._printProgressBar(lf, startTime)
580        if data:
581            d = rf.writeChunk(start, data)
582            d.addCallback(self._cbPutWrite, rf, lf, chunks, startTime)
583            return d
584        else:
585            return
586
587    def _cbPutDone(self, ignored, rf, lf):
588        lf.close()
589        rf.close()
590        if self.useProgressBar:
591            self._writeToTransport("\n")
592        return f"Transferred {lf.name} to {rf.name}"
593
594    def cmd_LCD(self, path):
595        os.chdir(path)
596
597    def cmd_LN(self, rest):
598        linkpath, rest = self._getFilename(rest)
599        targetpath, rest = self._getFilename(rest)
600        linkpath, targetpath = map(
601            lambda x: os.path.join(self.currentDirectory, x), (linkpath, targetpath)
602        )
603        return self.client.makeLink(linkpath, targetpath).addCallback(_ignore)
604
605    def cmd_LS(self, rest):
606        # possible lines:
607        # ls                    current directory
608        # ls name_of_file       that file
609        # ls name_of_directory  that directory
610        # ls some_glob_string   current directory, globbed for that string
611        options = []
612        rest = rest.split()
613        while rest and rest[0] and rest[0][0] == "-":
614            opts = rest.pop(0)[1:]
615            for o in opts:
616                if o == "l":
617                    options.append("verbose")
618                elif o == "a":
619                    options.append("all")
620        rest = " ".join(rest)
621        path, rest = self._getFilename(rest)
622        if not path:
623            fullPath = self.currentDirectory + "/"
624        else:
625            fullPath = os.path.join(self.currentDirectory, path)
626        d = self._remoteGlob(fullPath)
627        d.addCallback(self._cbDisplayFiles, options)
628        return d
629
630    def _cbDisplayFiles(self, files, options):
631        files.sort()
632        if "all" not in options:
633            files = [f for f in files if not f[0].startswith(b".")]
634        if "verbose" in options:
635            lines = [f[1] for f in files]
636        else:
637            lines = [f[0] for f in files]
638        if not lines:
639            return None
640        else:
641            return b"\n".join(lines)
642
643    def cmd_MKDIR(self, path):
644        path, rest = self._getFilename(path)
645        path = os.path.join(self.currentDirectory, path)
646        return self.client.makeDirectory(path, {}).addCallback(_ignore)
647
648    def cmd_RMDIR(self, path):
649        path, rest = self._getFilename(path)
650        path = os.path.join(self.currentDirectory, path)
651        return self.client.removeDirectory(path).addCallback(_ignore)
652
653    def cmd_LMKDIR(self, path):
654        os.system("mkdir %s" % path)
655
656    def cmd_RM(self, path):
657        path, rest = self._getFilename(path)
658        path = os.path.join(self.currentDirectory, path)
659        return self.client.removeFile(path).addCallback(_ignore)
660
661    def cmd_LLS(self, rest):
662        os.system("ls %s" % rest)
663
664    def cmd_RENAME(self, rest):
665        oldpath, rest = self._getFilename(rest)
666        newpath, rest = self._getFilename(rest)
667        oldpath, newpath = map(
668            lambda x: os.path.join(self.currentDirectory, x), (oldpath, newpath)
669        )
670        return self.client.renameFile(oldpath, newpath).addCallback(_ignore)
671
672    def cmd_EXIT(self, ignored):
673        self.client.transport.loseConnection()
674
675    cmd_QUIT = cmd_EXIT
676
677    def cmd_VERSION(self, ignored):
678        version = "SFTP version %i" % self.client.version
679        if isinstance(version, str):
680            version = version.encode("utf-8")
681        return version
682
683    def cmd_HELP(self, ignored):
684        return """Available commands:
685cd path                         Change remote directory to 'path'.
686chgrp gid path                  Change gid of 'path' to 'gid'.
687chmod mode path                 Change mode of 'path' to 'mode'.
688chown uid path                  Change uid of 'path' to 'uid'.
689exit                            Disconnect from the server.
690get remote-path [local-path]    Get remote file.
691help                            Get a list of available commands.
692lcd path                        Change local directory to 'path'.
693lls [ls-options] [path]         Display local directory listing.
694lmkdir path                     Create local directory.
695ln linkpath targetpath          Symlink remote file.
696lpwd                            Print the local working directory.
697ls [-l] [path]                  Display remote directory listing.
698mkdir path                      Create remote directory.
699progress                        Toggle progress bar.
700put local-path [remote-path]    Put local file.
701pwd                             Print the remote working directory.
702quit                            Disconnect from the server.
703rename oldpath newpath          Rename remote file.
704rmdir path                      Remove remote directory.
705rm path                         Remove remote file.
706version                         Print the SFTP version.
707?                               Synonym for 'help'.
708"""
709
710    def cmd_PWD(self, ignored):
711        return self.currentDirectory
712
713    def cmd_LPWD(self, ignored):
714        return os.getcwd()
715
716    def cmd_PROGRESS(self, ignored):
717        self.useProgressBar = not self.useProgressBar
718        return "%ssing progess bar." % (self.useProgressBar and "U" or "Not u")
719
720    def cmd_EXEC(self, rest):
721        """
722        Run C{rest} using the user's shell (or /bin/sh if they do not have
723        one).
724        """
725        shell = self._pwd.getpwnam(getpass.getuser())[6]
726        if not shell:
727            shell = "/bin/sh"
728        if rest:
729            cmds = ["-c", rest]
730            return utils.getProcessOutput(shell, cmds, errortoo=1)
731        else:
732            os.system(shell)
733
734    # accessory functions
735
736    def _remoteGlob(self, fullPath):
737        log.msg("looking up %s" % fullPath)
738        head, tail = os.path.split(fullPath)
739        if "*" in tail or "?" in tail:
740            glob = 1
741        else:
742            glob = 0
743        if tail and not glob:  # could be file or directory
744            # try directory first
745            d = self.client.openDirectory(fullPath)
746            d.addCallback(self._cbOpenList, "")
747            d.addErrback(self._ebNotADirectory, head, tail)
748        else:
749            d = self.client.openDirectory(head)
750            d.addCallback(self._cbOpenList, tail)
751        return d
752
753    def _cbOpenList(self, directory, glob):
754        files = []
755        d = directory.read()
756        d.addBoth(self._cbReadFile, files, directory, glob)
757        return d
758
759    def _ebNotADirectory(self, reason, path, glob):
760        d = self.client.openDirectory(path)
761        d.addCallback(self._cbOpenList, glob)
762        return d
763
764    def _cbReadFile(self, files, matchedFiles, directory, glob):
765        if not isinstance(files, failure.Failure):
766            if glob:
767                glob = glob.encode("utf-8")
768                matchedFiles.extend([f for f in files if fnmatch.fnmatch(f[0], glob)])
769            else:
770                matchedFiles.extend(files)
771            d = directory.read()
772            d.addBoth(self._cbReadFile, matchedFiles, directory, glob)
773            return d
774        else:
775            reason = files
776            reason.trap(EOFError)
777            directory.close()
778            return matchedFiles
779
780    def _abbrevSize(self, size):
781        # from http://mail.python.org/pipermail/python-list/1999-December/018395.html
782        _abbrevs = [
783            (1 << 50, "PB"),
784            (1 << 40, "TB"),
785            (1 << 30, "GB"),
786            (1 << 20, "MB"),
787            (1 << 10, "kB"),
788            (1, "B"),
789        ]
790
791        for factor, suffix in _abbrevs:
792            if size > factor:
793                break
794        return "%.1f" % (size / factor) + suffix
795
796    def _abbrevTime(self, t):
797        if t > 3600:  # 1 hour
798            hours = int(t / 3600)
799            t -= 3600 * hours
800            mins = int(t / 60)
801            t -= 60 * mins
802            return "%i:%02i:%02i" % (hours, mins, t)
803        else:
804            mins = int(t / 60)
805            t -= 60 * mins
806            return "%02i:%02i" % (mins, t)
807
808    def _printProgressBar(self, f, startTime):
809        """
810        Update a console progress bar on this L{StdioClient}'s transport, based
811        on the difference between the start time of the operation and the
812        current time according to the reactor, and appropriate to the size of
813        the console window.
814
815        @param f: a wrapper around the file which is being written or read
816        @type f: L{FileWrapper}
817
818        @param startTime: The time at which the operation being tracked began.
819        @type startTime: L{float}
820        """
821        diff = self.reactor.seconds() - startTime
822        total = f.total
823        try:
824            winSize = struct.unpack("4H", fcntl.ioctl(0, tty.TIOCGWINSZ, "12345679"))
825        except OSError:
826            winSize = [None, 80]
827        if diff == 0.0:
828            speed = 0.0
829        else:
830            speed = total / diff
831        if speed:
832            timeLeft = (f.size - total) / speed
833        else:
834            timeLeft = 0
835        front = f.name
836        if f.size:
837            percentage = (total / f.size) * 100
838        else:
839            percentage = 100
840        back = "%3i%% %s %sps %s " % (
841            percentage,
842            self._abbrevSize(total),
843            self._abbrevSize(speed),
844            self._abbrevTime(timeLeft),
845        )
846        spaces = (winSize[1] - (len(front) + len(back) + 1)) * " "
847        command = f"\r{front}{spaces}{back}"
848        self._writeToTransport(command)
849
850    def _getFilename(self, line):
851        """
852        Parse line received as command line input and return first filename
853        together with the remaining line.
854
855        @param line: Arguments received from command line input.
856        @type line: L{str}
857
858        @return: Tupple with filename and rest. Return empty values when no path was not found.
859        @rtype: C{tupple}
860        """
861        line = line.strip()
862        if not line:
863            return "", ""
864        if line[0] in "'\"":
865            ret = []
866            line = list(line)
867            try:
868                for i in range(1, len(line)):
869                    c = line[i]
870                    if c == line[0]:
871                        return "".join(ret), "".join(line[i + 1 :]).lstrip()
872                    elif c == "\\":  # quoted character
873                        del line[i]
874                        if line[i] not in "'\"\\":
875                            raise IndexError(f"bad quote: \\{line[i]}")
876                        ret.append(line[i])
877                    else:
878                        ret.append(line[i])
879            except IndexError:
880                raise IndexError("unterminated quote")
881        ret = line.split(None, 1)
882        if len(ret) == 1:
883            return ret[0], ""
884        else:
885            return ret[0], ret[1]
886
887
888setattr(StdioClient, "cmd_?", StdioClient.cmd_HELP)
889
890
891class SSHConnection(connection.SSHConnection):
892    def serviceStarted(self):
893        self.openChannel(SSHSession())
894
895
896class SSHSession(channel.SSHChannel):
897
898    name = b"session"
899
900    def channelOpen(self, foo):
901        log.msg("session %s open" % self.id)
902        if self.conn.options["subsystem"].startswith("/"):
903            request = "exec"
904        else:
905            request = "subsystem"
906        d = self.conn.sendRequest(
907            self, request, common.NS(self.conn.options["subsystem"]), wantReply=1
908        )
909        d.addCallback(self._cbSubsystem)
910        d.addErrback(_ebExit)
911
912    def _cbSubsystem(self, result):
913        self.client = filetransfer.FileTransferClient()
914        self.client.makeConnection(self)
915        self.dataReceived = self.client.dataReceived
916        f = None
917        if self.conn.options["batchfile"]:
918            fn = self.conn.options["batchfile"]
919            if fn != "-":
920                f = open(fn)
921        self.stdio = stdio.StandardIO(StdioClient(self.client, f))
922
923    def extReceived(self, t, data):
924        if t == connection.EXTENDED_DATA_STDERR:
925            log.msg("got %s stderr data" % len(data))
926            sys.stderr.write(data)
927            sys.stderr.flush()
928
929    def eofReceived(self):
930        log.msg("got eof")
931        self.stdio.loseWriteConnection()
932
933    def closeReceived(self):
934        log.msg("remote side closed %s" % self)
935        self.conn.sendClose(self)
936
937    def closed(self):
938        try:
939            reactor.stop()
940        except BaseException:
941            pass
942
943    def stopWriting(self):
944        self.stdio.pauseProducing()
945
946    def startWriting(self):
947        self.stdio.resumeProducing()
948
949
950if __name__ == "__main__":
951    run()
952