• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Red-M / RedSSH / 52350

pending completion
52350

push

coveralls-python

Red_M
Fix bug with pty argument to open a single use exec command channel. Add test for `redssh.RedSSH().execute_command()`. Version bump.

9 of 9 new or added lines in 4 files covered. (100.0%)

1479 of 1605 relevant lines covered (92.15%)

0.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.0
/redssh/clients/libssh/redsshlibssh.py
1
# RedSSH
2
# Copyright (C) 2018 - 2022 Red_M ( http://bitbucket.com/Red_M )
3

4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
8

9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU General Public License for more details.
13

14
# You should have received a copy of the GNU General Public License along
15
# with this program; if not, write to the Free Software Foundation, Inc.,
16
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
17

18

19
import os
1✔
20
import re
1✔
21
import time
1✔
22
import hashlib
1✔
23
import threading
1✔
24
import multiprocessing
1✔
25
import socket
1✔
26
import select
1✔
27

28
from redssh.clients.base_client import BaseClient, BaseClientModules
1✔
29
from redssh.clients.libssh import libssh
1✔
30
from redssh.clients.libssh import enums as client_enums
1✔
31
from redssh import exceptions
1✔
32
from redssh import enums
1✔
33
from redssh import utils
1✔
34
from redssh.clients.libssh import sftp
1✔
35
from redssh.clients.libssh import tunneling
1✔
36
from redssh.clients.libssh import x11
1✔
37

38
class LibSSHModules(BaseClientModules):
1✔
39
    client_enums = client_enums
1✔
40
    scp = sftp
1✔
41
    sftp = sftp
1✔
42
    tunneling = tunneling
1✔
43
    x11 = x11
1✔
44

45
class LibSSH(BaseClient):
1✔
46
    '''
47
    Instances the start of an SSH connection.
48
    Extra options are available after :func:`redssh.RedSSH.connect` is called.
49
    Older versions of Pageant will not authenticate correctly with this client, please update your agent!
50

51
    :param encoding: Set the encoding to something other than the default of ``'utf8'`` when your target SSH server doesn't return UTF-8.
52
    :type encoding: ``str``
53
    :param terminal: Set the terminal sent to the remote server to something other than the default of ``'vt100'``.
54
    :type terminal: ``str``
55
    :param ssh_host_key_verification: Change the behaviour of remote host key verification. Can be set to one of the following values, ``strict``, ``warn``, ``auto_add`` or ``none``.
56
    :type ssh_host_key_verification: :class:`redssh.enums.SSHHostKeyVerify`
57
    :param ssh_keepalive_interval: Enable or disable SSH keepalive packets, value is interval in seconds, ``0`` is off.
58
    :type ssh_keepalive_interval: ``float``
59
    :param set_flags: Not supported in ssh2-python 0.18.0
60
    :type set_flags: ``dict``
61
    :param method_preferences: Not supported in ssh2-python 0.18.0
62
    :type method_preferences: ``dict``
63
    :param callbacks: Not supported yet
64
    :type callbacks: ``dict``
65
    :param auto_terminate_tunnels: Automatically terminate tunnels when errors are detected
66
    :type auto_terminate_tunnels: ``bool``
67
    :param tcp_nodelay: Set `TCP_NODELAY` for the underlying :func:`socket.socket`, by default this is off via `False`.
68
    :type tcp_nodelay: ``bool``
69
    '''
70
    def __init__(self,*args,set_flags={},method_preferences={},callbacks={},**kwargs):
1✔
71
        super().__init__(*args,**kwargs)
1✔
72

73
        set_flags.update(method_preferences)
1✔
74
        self.set_options = set_flags
1✔
75
        self.callbacks = callbacks
1✔
76
        self._ssh_keepalive_thread = None
1✔
77
        self._ssh_keepalive_event = None
1✔
78
        self._modules = LibSSHModules
1✔
79
        self.enums = self._modules.client_enums
1✔
80

81
    def ssh_keepalive(self):
1✔
82
        timeout = 0.01
×
83
        while self.__check_for_attr__('channel')==False:
×
84
            time.sleep(timeout)
×
85
        while self._ssh_keepalive_event.is_set()==False and self.__check_for_attr__('channel')==True:
×
86
            timeout = self._block(self.session.keepalive_send,_select_timeout=self._select_timeout)
×
87
            self._ssh_keepalive_event.wait(timeout=timeout)
×
88

89
    def _block(self,func,*args,**kwargs):
1✔
90
        if self.__shutdown_all__.is_set()==False:
1✔
91
            default_str = 'sdkljfhklsdjf'
1✔
92
            _select_timeout = kwargs.get('_select_timeout',default_str)
1✔
93
            if _select_timeout==default_str:
1✔
94
                _select_timeout = None
1✔
95
            else:
96
                _select_timeout = float(_select_timeout)
1✔
97
                del kwargs['_select_timeout']
1✔
98
            out = libssh.error_codes.SSH_AGAIN
1✔
99
            while out==libssh.error_codes.SSH_AGAIN:
1✔
100
                self._block_select(_select_timeout)
1✔
101
                with self.session._block_lock:
1✔
102
                    out = func(*args,**kwargs)
1✔
103
            return(out)
1✔
104

105
    def _block_write(self,func,data,_select_timeout=None):
1✔
106
        data_len = len(data)
1✔
107
        total_written = 0
1✔
108
        while total_written<data_len:
1✔
109
            if self.__shutdown_all__.is_set()==False:
1✔
110
                self._block_select(_select_timeout)
1✔
111
                with self.session._block_lock:
1✔
112
                    (rc,bytes_written) = func(data[total_written:])
1✔
113
                total_written+=bytes_written
1✔
114
        return(total_written)
1✔
115

116
    def _read_iter(self,func,block=False,max_read=-1,_select_timeout=None):
1✔
117
        pos = 0
1✔
118
        remainder_len = 0
1✔
119
        remainder = b''
1✔
120
        if self.__shutdown_all__.is_set()==False:
1✔
121
            self._block_select(_select_timeout)
1✔
122
            with self.session._block_lock:
1✔
123
                try:
1✔
124
                    (size,data) = func()
1✔
125
                except libssh.exceptions.EOF:
1✔
126
                    return(b'')
1✔
127
            while size==libssh.error_codes.SSH_AGAIN or size>0:
1✔
128
                if size==libssh.error_codes.SSH_AGAIN:
1✔
129
                    if self.__shutdown_all__.is_set()==False:
×
130
                        self._block_select(_select_timeout)
×
131
                        with self.session._block_lock:
×
132
                            try:
×
133
                                (size,data) = func()
×
134
                            except libssh.exceptions.EOF:
×
135
                                return(b'')
×
136
                # if timeout is not None and size==libssh.error_codes.SSH_AGAIN:
137
                if size==libssh.error_codes.SSH_AGAIN and (block==False or (max_read>size and max_read!=-1)):
1✔
138
                    return(b'')
×
139
                while size>0:
1✔
140
                    while pos<size:
1✔
141
                        if remainder_len>0:
1✔
142
                            yield(remainder+data[pos:size])
×
143
                            remainder = b''
×
144
                            remainder_len = 0
×
145
                        else:
146
                            yield(data[pos:size])
1✔
147
                        pos = size
1✔
148
                    self._block_select(_select_timeout)
1✔
149
                    with self.session._block_lock:
1✔
150
                        try:
1✔
151
                            (size,data) = func()
1✔
152
                        except libssh.exceptions.EOF:
1✔
153
                            return(b'')
1✔
154
                    pos = 0
1✔
155
            if remainder_len>0:
1✔
156
                yield(remainder)
×
157

158
    def _auth_get_supported(self):
1✔
159
        try:
1✔
160
            self.session.userauth_none()
1✔
161
        except Exception as e:
1✔
162
            pass
1✔
163
        server_auth_supported = self.session.userauth_list()
1✔
164
        auth_supported = []
1✔
165
        for auth_meth in libssh.enums.Auth_Method:
1✔
166
            if (server_auth_supported & auth_meth)>0:
1✔
167
                auth_supported.append(auth_meth)
1✔
168
        return(auth_supported)
1✔
169

170
    def _auth_attempt(self,func,*args,**kwargs):
1✔
171
        try:
1✔
172
            return(func(*args,**kwargs))
1✔
173
        except Exception as e:
1✔
174
            pass
1✔
175

176
    def _auth(self,username,password,allow_agent,host_based,key_filepath,passphrase,look_for_keys):
1✔
177
        auth_supported = self._auth_get_supported()
1✔
178
        auth_types_tried = []
1✔
179
        if isinstance(auth_supported,type([])):
1✔
180

181
            if libssh.enums.Auth_Method.PUBLICKEY in auth_supported:
1✔
182
                if allow_agent==True:
1✔
183
                    auth_types_tried.append('publickey')
1✔
184
                    if self._auth_attempt(self.session.userauth_agent)==libssh.SSH_AUTH_SUCCESS:
1✔
185
                        return()
1✔
186
                elif key_filepath!=None:
1✔
187
                    auth_types_tried.append('publickey')
1✔
188
                    pkey = libssh.key.import_privkey_file(key_filepath,passphrase)
1✔
189
                    if self._auth_attempt(self.session.userauth_publickey,pkey)==libssh.SSH_AUTH_SUCCESS:
1✔
190
                        return()
1✔
191

192
                # elif host_based==True:
193
                    # auth_types_tried.append('hostbased')
194
                    # if res==self._auth_attempt(self.session.userauth_hostbased_fromfile,username,private_key,hostname,passphrase=passphrase):
195
                        # return()
196

197
            if libssh.enums.Auth_Method.PASSWORD in auth_supported:
1✔
198
                if not password==None:
1✔
199
                    auth_types_tried.append('password')
1✔
200
                    if self._auth_attempt(self.session.userauth_password,None,password)==libssh.SSH_AUTH_SUCCESS:
1✔
201
                        return()
×
202
            # if libssh.enums.Auth_Method.INTERACTIVE in auth_supported:
203
                # auth_types_tried.append('keyboard-interactive')
204
                # if self._auth_attempt(self.session.userauth_keyboardinteractive,None,password)==libssh.SSH_AUTH_SUCCESS:
205
                    # return()
206

207
        raise(exceptions.AuthenticationFailedException(list(set(auth_types_tried))))
1✔
208

209
    def eof(self):
1✔
210
        '''
211
        Returns ``True`` or ``False`` when the main channel has recieved an ``EOF``.
212
        '''
213
        if self.__check_for_attr__('channel')==True:
1✔
214
            return(self._block(self.channel.is_eof))
1✔
215

216
    def methods(self, method):
1✔
217
        pass
×
218
        # '''
219
        # Returns what value was settled on during session negotiation.
220
        # '''
221
        # if self.__check_for_attr__('session')==True:
222
            # return(self._block(self.session.options_get,method)) # broken in redlibssh, refer to https://api.libssh.org/stable/group__libssh__session.html#gaaa9d400920cad4d6e4a0fb09ff8c7b01
223

224
    def setenv(self, varname, value):
1✔
225
        '''
226
        Set an environment variable on the mainchannel.
227

228
        :param varname: Name of environment variable to set on the remote channel.
229
        :type varname: ``str``
230
        :param value: Value to set ``varname`` to.
231
        :type value: ``str``
232
        :return: ``None``
233
        '''
234
        if self.past_login==True:
1✔
235
            self._block(self.channel.request_env,varname,value)
1✔
236

237
    def open_channel(self,shell=True,pty=False):
1✔
238
        channel = self._block(self.session.channel_new)
1✔
239
        self._block(channel.set_blocking,False)
1✔
240
        if shell==True:
1✔
241
            self._block(channel.open_session)
1✔
242
        if self.request_pty==True and pty==True:
1✔
243
            self._block(channel.request_pty_size,self.terminal,0,0)
1✔
244
        return(channel)
1✔
245

246
    def check_host_key(self): # TODO, properly get this working
1✔
247
        if self.ssh_host_key_verification==enums.SSHHostKeyVerify.none:
1✔
248
            return(None)
1✔
249
        server_known = self.session.is_server_known()
1✔
250
        if server_known==False:
1✔
251
            pass
1✔
252

253
    def connect(self,hostname,port=22,username='',password=None,
1✔
254
        allow_agent=False,host_based=None,key_filepath=None,passphrase=None,
255
        look_for_keys=False,sock=None,timeout=None):
256
        '''
257
        .. warning::
258
            Some authentication methods are not yet supported!
259

260
        :param hostname: Hostname to connect to.
261
        :type hostname: ``str``
262
        :param port: SSH port to connect to.
263
        :type port: ``int``
264
        :param username: Username to connect as to the remote server.
265
        :type username: ``str``
266
        :param password: Password to offer to the remote server for authentication.
267
        :type password: ``str``
268
        :param allow_agent: Allow the local SSH key agent to offer the keys held in it for authentication.
269
        :type allow_agent: ``bool``
270
        :param host_based: Allow the local SSH host keys to be used for authentication. NOT IMPLEMENTED!
271
        :type host_based: ``bool``
272
        :param key_filepath: Array of filenames to offer to the remote server. Can be a string for a single key.
273
        :type key_filepath: ``array``/``str``
274
        :param passphrase: Passphrase to decrypt any keys offered to the remote server.
275
        :type passphrase: ``str``
276
        :param look_for_keys: Enable offering keys in ``~/.ssh`` automatically. NOT IMPLEMENTED!
277
        :type look_for_keys: ``bool``
278
        :param sock: A pre-connected socket to the remote server. Useful if you have strange network requirements.
279
        :type sock: :func:`socket.socket`
280
        :param timeout: Timeout for the socket connection to the remote server.
281
        :type timeout: ``float``
282
        '''
283
        if password==None and allow_agent==False and host_based==None and key_filepath==None and look_for_keys==False:
1✔
284
            raise(exceptions.NoAuthenticationOfferedException())
1✔
285
        if self.past_login==False:
1✔
286
            if sock==None:
1✔
287
                self.sock = socket.create_connection((hostname,port),timeout)
1✔
288
                self.sock.setsockopt(socket.SOL_SOCKET,socket.SO_KEEPALIVE,1)
1✔
289
                self.sock.setsockopt(socket.IPPROTO_TCP,socket.TCP_NODELAY,self.tcp_nodelay)
1✔
290
            else:
291
                self.sock = sock
1✔
292

293
            self.session = libssh.Session()
1✔
294
            # self.session.publickey_init()
295

296
            # if not self.set_options=={}:
297
                # for opt in self.set_options:
298
                    # self.session.options_set(opt, self.set_options[opt]) # broken in redlibssh, need to refer to https://api.libssh.org/stable/group__libssh__session.html#ga7a801b85800baa3f4e16f5b47db0a73d
299

300
            # if 'callback_set' in dir(self.session):
301
                # if not self.callbacks=={}:
302
                    # for cbtype in self.callbacks:
303
                        # self.session.callback_set(cbtype, self.callbacks[cbtype])
304

305
            self.session.options_set(libssh.options.HOST, hostname)
1✔
306
            self.session.options_set(libssh.options.USER, username)
1✔
307
            self.session.options_set_port(self.sock.getsockname()[1])
1✔
308
            self.session.set_socket(self.sock)
1✔
309
            self.session.connect()
1✔
310

311
            # __initial = time.time()
312
            # self.session.keepalive_send()
313
            # new_select_timeout = float(time.time()-__initial)
314
            # if new_select_timeout>self._select_timeout and self._auto_select_timeout_enabled==True:
315
                # self._select_timeout = new_select_timeout
316

317
            self.check_host_key()
1✔
318

319
            self._auth(username,password,allow_agent,host_based,key_filepath,passphrase,look_for_keys)
1✔
320

321
            # if self.ssh_keepalive_interval>0:
322
                # self.session.keepalive_config(True, self.ssh_keepalive_interval)
323
                # self._ssh_keepalive_thread = threading.Thread(target=self.ssh_keepalive)
324
                # self._ssh_keepalive_event = threading.Event()
325
                # self._ssh_keepalive_thread.start()
326
            self.session.set_blocking(False)
1✔
327
            self.channel = self.open_channel(True,self.request_pty)
1✔
328

329
            # if 'callback_set' in dir(self.session):
330
                # self._forward_x11()
331

332
            self._block(self.channel.request_shell)
1✔
333
            self.past_login = True
1✔
334

335
    def read(self,block=False):
1✔
336
        '''
337
        Recieve data from the remote session.
338
        Only works if the current session has made it past the login process.
339

340
        :param block: Block until data is received from the remote server. ``True``
341
            will block until data is recieved and ``False`` may return ``b''`` if no data is available from the remote server.
342
        :type block: ``bool``
343
        :return: ``generator`` - A generator of byte strings that has been recieved in the time given.
344
        '''
345
        if self.past_login==True:
1✔
346
            return(self._read_iter(self.channel.read_nonblocking,block))
1✔
347
        return([])
×
348

349
    def send(self,string):
1✔
350
        '''
351
        Send data to the remote session.
352
        Only works if the current session has made it past the login process.
353

354
        :param string: String to send to the remote session.
355
        :type string: ``str``
356
        :return: ``int`` - Amount of bytes sent to remote machine.
357
        '''
358
        if self.past_login==True:
1✔
359
            return(self._block_write(self.channel.write,string))
1✔
360
        return(0)
×
361

362
    def flush(self):
1✔
363
        '''
364
        Flush all data on the primary channel's stdin to the remote connection.
365
        Only works if connected, otherwise returns ``0``.
366

367
        :return: ``int`` - Amount of bytes sent to remote machine.
368
        '''
369
        if self.past_login==True:
×
370
            return(self._block(self.channel.flush))
×
371
        return(0)
×
372

373
    def last_error(self):
1✔
374
        '''
375
        Get the last error from the current session.
376

377
        :return: ``str``
378
        '''
379
        return(self._block(self.session.get_error))
1✔
380

381
    def execute_command(self,command,env=None,channel=None,pty=False):
1✔
382
        '''
383
        Run a command. This will block as the command executes.
384

385
        :param command: Command to execute.
386
        :type command: ``str``
387
        :param env: Environment variables to set during ``command``.
388
        :type env: ``dict``
389
        :param channel: Use an existing SSH channel instead of spawning a new one.
390
        :type channel: ``redssh.RedSSH.channel``
391
        :param pty: Request a pty for the command to be executed via.
392
        :type pty: ``bool``
393
        :return: ``tuple (int, str)`` - of ``(return_code, command_output)``
394
        '''
395
        if env==None:
1✔
396
            env = {}
1✔
397
        if len(env)>0:
1✔
398
            for key in env:
×
399
                self.setenv(key,env[key])
×
400
        out = b''
1✔
401
        if channel==None:
1✔
402
            channel = self.open_channel(True,pty)
1✔
403
        self._block(channel.request_exec,command)
1✔
404
        ret = self._block(channel.get_exit_status)
1✔
405
        while self._block(channel.is_eof)==False and ret==-1:
1✔
406
            ret = self._block(channel.get_exit_status)
1✔
407
        iter = self._read_iter(channel.read_nonblocking,True)
1✔
408
        for data in iter:
1✔
409
            out+=data
1✔
410
        self._block(channel.send_eof)
1✔
411
        self._block(channel.close)
1✔
412
        del channel
1✔
413
        return(ret,out)
1✔
414

415
    def start_sftp(self):
1✔
416
        '''
417
        Start the SFTP client.
418
        The client will be available at `self.sftp` and will be an instance of `redssh.sftp.RedSFTP`
419

420
        :return: ``None``
421
        '''
422
        if self.past_login and self.__check_for_attr__('sftp')==False:
1✔
423
            self.sftp = sftp.RedSFTP(self)
1✔
424
            self.scp = utils.ObjectProxy(self,'sftp')
1✔
425

426
    def start_scp(self):
1✔
427
        '''
428
        Start the SCP client.
429

430
        Note that libssh has deprecated SCP, if this fails to start the SCP client it will transparently start the SFTP client as an alternative.
431

432
        :return: ``None``
433
        '''
434
        if self.past_login and self.__check_for_attr__('scp')==False:
×
435
            self.start_sftp()
×
436

437

438
    # def forward_x11(self): # This is also horribly broken in nonblocking mode for libssh
439
        # self.x11_channels = []
440
        # display_number = 10
441
        # thread_terminate = threading.Event()
442
        # wait_for_chan = threading.Event()
443
        # forward_thread = threading.Thread(target=x11.forward,args=(self,display_number,thread_terminate))
444
        # forward_thread.daemon = True
445
        # forward_thread.name = enums.TunnelType.x11+':'+str(display_number)
446
        # forward_thread.start()
447
        # self.tunnels[enums.TunnelType.x11][display_number] = (forward_thread,thread_terminate,None,None)
448
        # wait_for_chan.wait()
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc