Skip to content

Commit

Permalink
feature: add ssh-agent scenario, patch paramiko, add plink.exe in the…
Browse files Browse the repository at this point in the history
… bundle
  • Loading branch information
A. committed May 16, 2024
1 parent e15aeb7 commit 75445dc
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 33 deletions.
3 changes: 1 addition & 2 deletions rcm/client/external/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
turbovnc
step
*
21 changes: 18 additions & 3 deletions rcm/client/logic/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@ def __init__(self):
'preload_command',
fallback=defaults['preload_command']))

# Check if ssh agent is active
try:
paramiko.Agent()
self.allow_agent = True
except paramiko.ssh_exception.SSHException as e:
self.allow_agent = False

def login_setup(self, host, user, password=None, preload=''):
self.proxynode = host
self.preload = preload
Expand Down Expand Up @@ -114,7 +121,8 @@ def prex(self, cmd):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
try:
ssh.connect(host, username=self.user, password=self.password, timeout=10)
ssh.connect(host, username=self.user, password=self.password, timeout=10,
allow_agent=self.allow_agent)
self.auth_method = ssh.get_transport().auth_handler.auth_method
stdin, stdout, stderr = ssh.exec_command(fullcommand)
out = ''.join(stdout)
Expand Down Expand Up @@ -237,7 +245,13 @@ def submit(self, session=None, otp='', gui_cmd=None, configFile=None):
tunnelling_method = json.loads(parser.get('Settings', 'ssh_client'))
except Exception:
tunnelling_method = "internal"
logic_logger.info("Using " + str(tunnelling_method) + " ssh tunnelling")

extra_info = ""
if not self.allow_agent and not self.password:
tunnelling_method = "external"
extra_info = " (forced because ssh-agent is not active)"

logic_logger.info("Using " + str(tunnelling_method) + " ssh tunnelling" + extra_info)

plugin_exe = plugin.TurboVNCExecutable()
plugin_exe.build(session=session, local_portnumber=local_port_number)
Expand All @@ -252,7 +266,8 @@ def submit(self, session=None, otp='', gui_cmd=None, configFile=None):
local_port_number,
compute_node,
port_number,
tunnelling_method)
tunnelling_method,
self.allow_agent)

self.session_threads.append(st)
st.start()
Expand Down
75 changes: 51 additions & 24 deletions rcm/client/logic/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,6 @@ def build(self, session, local_portnumber):
# local_portnumber = rcm_utils.get_unused_portnumber()

tunnel = session.hash['tunnel']
try:
tunnelling_method = json.loads(parser.get('Settings', 'ssh_client'))
except Exception:
tunnelling_method = "internal"
logic_logger.info("Using " + str(tunnelling_method) + " ssh tunnelling")

# Decrypt password
vncpassword = session.hash.get('vncpassword', '')
Expand Down Expand Up @@ -190,18 +185,22 @@ def build(self, session, local_portnumber):


class SSHExecutable(Executable):
def __init__(self):
def __init__(self, ssh_password):

self.set_env()

# ssh executable
if sys.platform == 'win32':
exe = rcm_utils.which('PLINK')
if ssh_password:
self.set_env_plink()
exe = rcm_utils.which('PLINK')
else:
exe = rcm_utils.which('ssh')
else:
exe = rcm_utils.which('ssh')
if not exe:
if sys.platform == 'win32':
logic_logger.error("plink.exe not found! Check the PATH environment variable.")
logic_logger.error("ssh.exe or plink.exe not found! Check the PATH environment variable.")
else:
logic_logger.error("ssh not found!")
return
Expand All @@ -214,6 +213,27 @@ def __init__(self):
def set_env(self):
return

def set_env_plink(self):
# set the environment
if getattr(sys, 'frozen', False):
logic_logger.debug("Running in a bundle")
# if running in a bundle, we hardcode the path
# of the built-in vnc viewer and plink (windows only)
os.environ['PLINK_HOME'] = resource_path('plink')
# on windows 10, administration policies prevent execution of external programs
# located in %TEMP% ... it seems that it cannot be loaded
home_path = os.path.expanduser('~')
desktop_path = os.path.join(home_path, 'Desktop')
exe_dir_path = os.path.dirname(sys.executable)
if os.path.exists(desktop_path):
rcm_unprotected_path = os.path.join(exe_dir_path, '.rcm', 'executables')
os.makedirs(rcm_unprotected_path, exist_ok=True)
dest_dir = os.path.join(rcm_unprotected_path, 'plink')
rcm_utils.copytree(resource_path('plink'), dest_dir)
os.environ['PLINK_HOME'] = dest_dir
os.environ['PATH'] = os.path.join(os.environ['PLINK_HOME'], 'bin') + os.pathsep + os.environ['PATH']
logic_logger.debug("PATH: " + str(os.environ['PATH']))

def build(self,
login_node,
ssh_username,
Expand All @@ -229,14 +249,19 @@ def build(self,

self.add_default_arg("-N")
self.add_arg_value("-L", local_host + ":" + local_port_number + ":" + compute_node + ":" + port_number)

# Only for plink
if sys.platform == 'win32':
self.add_default_arg("-ssh")
if ssh_password:
self.add_arg_value("-pw", str(ssh_password))

default_ssh_pkey = os.path.join(os.path.abspath(os.path.expanduser("~")), '.ssh', 'id_rsa.ppk')
if os.path.exists(default_ssh_pkey):
self.add_arg_value("-i", default_ssh_pkey)
if 'plink.exe' in " ".join(self.exe).lower():
self.add_default_arg("-ssh")
if ssh_password:
self.add_arg_value("-pw", str(ssh_password))

default_ssh_pkey = os.path.join(os.path.abspath(os.path.expanduser("~")), '.ssh', 'id_rsa.ppk')
if os.path.exists(default_ssh_pkey):
self.add_arg_value("-i", default_ssh_pkey)
else:
self.add_arg_value("-o", '"StrictHostKeyChecking no"')

self.add_default_arg(ssh_username + "@" + login_node)

Expand All @@ -254,7 +279,7 @@ def __init__(self,
remote_bind_address,
local_bind_address):

ssh_exe = SSHExecutable()
ssh_exe = SSHExecutable(ssh_password)
ssh_exe.build(login_node=login_node,
ssh_username=ssh_username,
ssh_password=ssh_password,
Expand All @@ -270,15 +295,17 @@ def __init__(self,

def __enter__(self):
if sys.platform == 'win32':
self.tunnel_process = pexpect.popen_spawn.PopenSpawn(self.tunnel_command)

i = self.tunnel_process.expect(['connection',
pexpect.TIMEOUT,
pexpect.EOF],
timeout=2)
if i == 0:
self.tunnel_process.sendline('yes')
if "plink.exe" in self.tunnel_command.lower():
self.tunnel_process = pexpect.popen_spawn.PopenSpawn(self.tunnel_command)

i = self.tunnel_process.expect(['connection',
pexpect.TIMEOUT,
pexpect.EOF],
timeout=2)
if i == 0:
self.tunnel_process.sendline('yes')
else:
self.tunnel_process = pexpect.popen_spawn.PopenSpawn(self.tunnel_command, timeout=None)
else:
self.tunnel_process = pexpect.spawn(self.tunnel_command,
timeout=None)
Expand Down
8 changes: 6 additions & 2 deletions rcm/client/logic/thread.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ def __init__(self,
local_port_number=0,
compute_node='',
port_number=0,
tunnelling_method='internal'
tunnelling_method='internal',
allow_agent=True
):
self.ssh_server = None
self.tunnelling_method = tunnelling_method
Expand Down Expand Up @@ -79,6 +80,8 @@ def __init__(self,
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
self.startupinfo = startupinfo

self.allow_agent = allow_agent

logic_logger.debug('Thread ' + str(self.threadnum) + ' is initialized')

def terminate(self):
Expand Down Expand Up @@ -141,7 +144,8 @@ def execute_service_command_with_internal_ssh_tunnel(self):
ssh_password=self.password,
ssh_pkey=default_ssh_pkey,
remote_bind_address=(self.node, self.portnumber),
local_bind_address=('127.0.0.1', self.local_portnumber)
local_bind_address=('127.0.0.1', self.local_portnumber),
allow_agent=self.allow_agent
) as self.ssh_server:
self.service_process = subprocess.Popen(shlex.split(self.service_command),
bufsize=1,
Expand Down
2 changes: 2 additions & 0 deletions rcm/client/rcm_client_qt.spec
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ if sys.platform != "darwin":
icon = os.path.join(basepath, 'rcm/client/gui/logo/logo.ico')
datas.append((os.path.join(basepath, 'rcm/client/external/turbovnc'), 'turbovnc'))
datas.append((os.path.join(basepath, 'rcm/client/external/step'), 'step'))
if sys.platform == "win32":
datas.append((os.path.join(basepath, 'rcm/client/external/plink'), 'plink'))
else:
icon = os.path.join(basepath, 'rcm/client/gui/logo/logo.icns')
datas.append((os.path.join(basepath, 'rcm/client/gui/logo/logo.icns'), 'gui/logo/'))
Expand Down
6 changes: 5 additions & 1 deletion scripts/ci/03-install-venv.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,9 @@ if (-Not (Get-Command "patch.exe" -ErrorAction SilentlyContinue)) {
}

Invoke-WebRequest -URI "https://github.com/paramiko/paramiko/pull/${env:PARAMIKO_PULL}/commits/${env:PARAMIKO_COMMIT}.patch" -OutFile "${env:RCM_CHECKOUT}\tmp\paramiko.patch"
$env:PARAMIKO_FILE = python -c "import paramiko, os; print(os.path.join(os.path.dirname(paramiko.__file__), 'auth_handler.py'))"
$env:PARAMIKO_DIR = python -c "import paramiko, os; print(os.path.dirname(paramiko.__file__))"
$env:PARAMIKO_FILE = "${env:PARAMIKO_DIR}\auth_handler.py"
patch.exe -N "${env:PARAMIKO_FILE}" -i "${env:RCM_CHECKOUT}\tmp\paramiko.patch"

$env:PARAMIKO_FILE = "${env:PARAMIKO_DIR}\pkey.py"
(Get-Content "${env:PARAMIKO_FILE}").replace("if 0x20 <= padding_length < 0x7F:", "if 0x20 <= padding_length < 0x7F or padding_length == 0:") | Set-Content "${env:PARAMIKO_FILE}"
7 changes: 6 additions & 1 deletion scripts/ci/03-install-venv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,14 @@ pip install -r "${RCM_CHECKOUT:?}/rcm/client/requirements.txt"

mkdir -p "${RCM_CHECKOUT:?}/tmp"
wget "https://github.com/paramiko/paramiko/pull/${PARAMIKO_PULL:?}/commits/${PARAMIKO_COMMIT:?}.patch" -O "${RCM_CHECKOUT:?}/tmp/paramiko.patch"
PARAMIKO_FILE=$(python -c "import paramiko, os; print(os.path.join(os.path.dirname(paramiko.__file__), 'auth_handler.py'))")

PARAMIKO_DIR=$(python -c "import paramiko, os; print(os.path.dirname(paramiko.__file__))")
PARAMIKO_FILE="${PARAMIKO_DIR}"/auth_handler.py
if [ -n "${GITHUB_ENV}" ]; then
patch -N "${PARAMIKO_FILE}" -i "${RCM_CHECKOUT:?}/tmp/paramiko.patch"
else
OUT="$(patch -N "${PARAMIKO_FILE}" -i "${RCM_CHECKOUT:?}/tmp/paramiko.patch")" || grep -q "Skipping patch" <<< "$OUT" || (echo "$OUT" && false)
fi

PARAMIKO_FILE="${PARAMIKO_DIR}"/pkey.py
sed -i "s/if 0x20 <= padding_length < 0x7F:/if 0x20 <= padding_length < 0x7F or padding_length == 0:/" "${PARAMIKO_FILE}"

0 comments on commit 75445dc

Please sign in to comment.