Commit 149cf363 authored by Michael Büsch's avatar Michael Büsch

Add basic support for SSH tunnel

Signed-off-by: Michael Büsch's avatarMichael Buesch <[email protected]>
parent 27198876
......@@ -39,20 +39,22 @@ def usage():
print("Usage: awlsim-client [OPTIONS] <ACTIONS>")
print("")
print("Options:")
print(" -c|--connect HOST[:PORT] Connect to the server at HOST:PORT")
print(" Defaults to %s:%d" %\
print(" -C|--connect-to HOST[:PORT] Connect to the server at HOST:PORT")
print(" Defaults to %s:%d" %\
(AwlSimServer.DEFAULT_HOST, AwlSimServer.DEFAULT_PORT))
print(" -t|--timeout 10.0 Set the connection timeout (default 10 s)")
print(" -L|--loglevel LVL Set the client log level:")
print(" 0: Log nothing")
print(" 1: Log errors")
print(" 2: Log errors and warnings (default)")
print(" 3: Log errors, warnings and info messages")
print(" 4: Verbose logging")
print(" 5: Extremely verbose logging")
print(" -c|--connect Connect to default %s:%d" %\
(AwlSimServer.DEFAULT_HOST, AwlSimServer.DEFAULT_PORT))
print(" -t|--timeout 10.0 Set the connection timeout (default 10 s)")
print(" -L|--loglevel LVL Set the client log level:")
print(" 0: Log nothing")
print(" 1: Log errors")
print(" 2: Log errors and warnings (default)")
print(" 3: Log errors, warnings and info messages")
print(" 4: Verbose logging")
print(" 5: Extremely verbose logging")
print("")
print("Actions to be performed on the server:")
print(" -r|--runstate RUN/STOP Set the run state of the CPU.")
print(" -r|--runstate RUN/STOP Set the run state of the CPU.")
def main():
opt_connect = (AwlSimServer.DEFAULT_HOST, AwlSimServer.DEFAULT_PORT)
......@@ -62,8 +64,8 @@ def main():
try:
(opts, args) = getopt.getopt(sys.argv[1:],
"hc:t:L:r:",
[ "help", "connect=", "timeout=", "loglevel=",
"hcC:t:L:r:",
[ "help", "connect", "connect-to=", "timeout=", "loglevel=",
"runstate=", ])
except getopt.GetoptError as e:
printError(str(e))
......@@ -74,6 +76,9 @@ def main():
usage()
return ExitCodes.EXIT_OK
if o in ("-c", "--connect"):
opt_connect = (AwlSimServer.DEFAULT_HOST,
AwlSimServer.DEFAULT_PORT)
if o in ("-C", "--connect-to"):
try:
host, port = parseNetAddress(v)
if port is None:
......
......@@ -11,7 +11,7 @@ [email protected]@
Nice=-15
[email protected]@/bin/awlsim-server -l localhost -4 -w @[email protected]
[email protected]@/bin/awlsim-client -c localhost -r RUN
[email protected]@/bin/awlsim-client -C localhost -r RUN
[email protected][email protected] AWLSIM_CYTHON=1
......
......@@ -37,13 +37,19 @@ class TestAwlSimClient(AwlSimClient):
def handle_CPUDUMP(self, dumpText):
emitCpuDump(dumpText)
class ConsoleSSHTunnel(SSHTunnel):
def sshMessage(self, message, isDebug):
if opt_loglevel > Logging.LOG_INFO:
isDebug = False
super(ConsoleSSHTunnel, self).sshMessage(message, isDebug)
def usage():
print("awlsim version %s" % VERSION_STRING)
print("")
print("Usage: awlsim-test [OPTIONS] <AWL-source or awlsim-project file>")
print("")
print("Options:")
print(" -C|--cycle-limit SEC Cycle time limit, in seconds (default 5.0)")
print(" -Y|--cycle-limit SEC Cycle time limit, in seconds (default 5.0)")
print(" -M|--max-runtime SEC CPU will be stopped after SEC seconds (default: off)")
print(" -2|--twoaccu Force 2-accu mode")
print(" -4|--fouraccu Force 4-accu mode")
......@@ -62,7 +68,8 @@ def usage():
print(" 5: Extremely verbose logging")
print("")
print("Server backend related options:")
print(" -c|--connect IP:PORT Connect to server backend")
print(" -c|--connect Connect to server backend")
print(" -C|--connect-to IP:PORT Connect to server backend")
print(" -b|--spawn-backend Spawn a new backend server and connect to it")
if not isWinStandalone:
print(" -i|--interpreter EXE Set the backend interpreter executable")
......@@ -243,6 +250,7 @@ def run(inputFile):
def runWithServerBackend(inputFile):
client = None
tunnel = None
try:
import awlsim.cython_helper
if awlsim.cython_helper.shouldUseCython():
......@@ -252,13 +260,36 @@ def runWithServerBackend(inputFile):
return ExitCodes.EXIT_ERR_INTERP
project = Project.fromProjectOrRawAwlFile(inputFile)
linkSettings = project.getCoreLinkSettings()
if opt_spawnBackend:
host = AwlSimServer.DEFAULT_HOST
port = AwlSimServer.DEFAULT_PORT
else:
host = linkSettings.getConnectHost()
port = linkSettings.getConnectPort()
if opt_connectTo:
host, port = opt_connectTo
# Establish SSH tunnel, if requested.
if linkSettings.getTunnel() == linkSettings.TUNNEL_SSH:
writeStdout("Establishing SSH tunnel...\n")
localPort = linkSettings.getTunnelLocalPort()
if localPort == linkSettings.TUNNEL_LOCPORT_AUTO:
localPort = None
tunnel = ConsoleSSHTunnel(
remoteHost = host,
remotePort = port,
localPort = localPort,
sshUser = linkSettings.getSSHUser(),
sshPort = linkSettings.getSSHPort(),
sshExecutable = linkSettings.getSSHExecutable(),
)
host, port = tunnel.connect()
# Connect to the server
client = TestAwlSimClient()
if opt_spawnBackend:
host, port = AwlSimServer.DEFAULT_HOST, AwlSimServer.DEFAULT_PORT
if opt_connect:
host, port = opt_connect
if isWinStandalone:
client.spawnServer(serverExecutable = "awlsim-server-module.exe",
listenHost = host,
......@@ -268,11 +299,7 @@ def runWithServerBackend(inputFile):
listenHost = host,
listenPort = port)
writeStdout("Connecting to core server...\n")
if opt_connect:
client.connectToServer(host = opt_connect[0],
port = opt_connect[1])
else:
client.connectToServer()
client.connectToServer(host = host, port = port)
writeStdout("Initializing core...\n")
client.setLoglevel(opt_loglevel)
......@@ -333,6 +360,8 @@ def runWithServerBackend(inputFile):
except KeyboardInterrupt as e:
pass
finally:
if tunnel:
tunnel.shutdown()
if client:
client.shutdown()
return ExitCodes.EXIT_OK
......@@ -357,6 +386,7 @@ def main():
global opt_profile
global opt_loglevel
global opt_connect
global opt_connectTo
global opt_spawnBackend
global opt_interpreter
......@@ -373,18 +403,19 @@ def main():
opt_profile = 0
opt_loglevel = Logging.LOG_INFO
opt_connect = None
opt_connectTo = False
opt_spawnBackend = False
opt_interpreter = None
try:
(opts, args) = getopt.getopt(sys.argv[1:],
"hC:M:24qDxt:T:m:H:I:P:L:c:bi:",
"hY:M:24qDxt:T:m:H:I:P:L:cC:bi:",
[ "help", "cycle-time=", "max-runtime=", "twoaccu", "fouraccu",
"quiet", "no-cpu-dump", "extended-insns",
"obtemp=", "clock-mem=", "mnemonics=",
"hardware=", "hardware-info=", "profile=",
"loglevel=",
"connect=", "spawn-backend", "interpreter=",
"connect", "connect-to=", "spawn-backend", "interpreter=",
"list-sfc", "list-sfc-verbose",
"list-sfb", "list-sfb-verbose", ])
except getopt.GetoptError as e:
......@@ -395,11 +426,11 @@ def main():
if o in ("-h", "--help"):
usage()
return ExitCodes.EXIT_OK
if o in ("-C", "--cycle-time"):
if o in ("-Y", "--cycle-time"):
try:
opt_cycletime = float(v)
except ValueError:
printError("-C|--cycle-time: Invalid time format")
printError("-Y|--cycle-time: Invalid time format")
sys.exit(1)
if o in ("-M", "--max-runtime"):
try:
......@@ -465,11 +496,13 @@ def main():
printError("-L|--loglevel: Invalid log level")
sys.exit(1)
if o in ("-c", "--connect"):
opt_connect = True
if o in ("-C", "--connect-to"):
try:
idx = v.rfind(":")
if idx <= 0:
raise ValueError
opt_connect = (v[:idx], int(v[idx+1:]))
opt_connectTo = (v[:idx], int(v[idx+1:]))
except ValueError:
printError("-c|--connect: Invalid host/port")
sys.exit(1)
......@@ -523,7 +556,7 @@ def main():
"--spawn-backend was requested.")
return ExitCodes.EXIT_ERR_CMDLINE
if opt_spawnBackend or opt_connect:
if opt_spawnBackend or opt_connect or opt_connectTo:
return runWithServerBackend(inputFile)
return run(inputFile)
......
......@@ -2,7 +2,7 @@
#
# AWL simulator - project
#
# Copyright 2014-2015 Michael Buesch <[email protected]>
# Copyright 2014-2016 Michael Buesch <[email protected]>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
......@@ -77,8 +77,13 @@ class GuiSettings(object):
return self.editorFont
class CoreLinkSettings(object):
DEFAULT_INTERPRETERS = "pypy3; pypy; $CURRENT; python3; python2; python; py"
SPAWN_PORT_BASE = 4151 + 32
DEFAULT_INTERPRETERS = "pypy3; pypy; $CURRENT; python3; python2; python; py"
SPAWN_PORT_BASE = 4151 + 32
TUNNEL_NONE = 0
TUNNEL_SSH = 1
TUNNEL_LOCPORT_AUTO = -1
def __init__(self,
spawnLocalEn=True,
......@@ -87,13 +92,23 @@ class CoreLinkSettings(object):
spawnLocalInterpreters="$DEFAULT",
connectHost="localhost",
connectPort=4151,
connectTimeoutMs=3000):
connectTimeoutMs=3000,
tunnel=TUNNEL_NONE,
tunnelLocalPort=TUNNEL_LOCPORT_AUTO,
sshUser="pi",
sshPort=22,
sshExecutable="ssh"):
self.setSpawnLocalEn(spawnLocalEn)
self.setSpawnLocalPortRange(spawnLocalPortRange)
self.setSpawnLocalInterpreters(spawnLocalInterpreters),
self.setConnectHost(connectHost)
self.setConnectPort(connectPort)
self.setConnectTimeoutMs(connectTimeoutMs)
self.setTunnel(tunnel)
self.setTunnelLocalPort(tunnelLocalPort)
self.setSSHUser(sshUser)
self.setSSHPort(sshPort)
self.setSSHExecutable(sshExecutable)
def setSpawnLocalEn(self, spawnLocalEn):
self.spawnLocalEn = spawnLocalEn
......@@ -147,6 +162,36 @@ class CoreLinkSettings(object):
def getConnectTimeoutMs(self):
return self.connectTimeoutMs
def setTunnel(self, tunnel):
self.tunnel = tunnel
def getTunnel(self):
return self.tunnel
def setTunnelLocalPort(self, tunnelLocalPort):
self.tunnelLocalPort = tunnelLocalPort
def getTunnelLocalPort(self):
return self.tunnelLocalPort
def setSSHUser(self, sshUser):
self.sshUser = sshUser
def getSSHUser(self):
return self.sshUser
def setSSHPort(self, sshPort):
self.sshPort = sshPort
def getSSHPort(self):
return self.sshPort
def setSSHExecutable(self, sshExecutable):
self.sshExecutable = sshExecutable
def getSSHExecutable(self):
return self.sshExecutable
class HwmodSettings(object):
def __init__(self,
loadedModules=None):
......@@ -490,6 +535,31 @@ class Project(object):
if p.has_option("CORE_LINK", "connect_timeout_ms"):
timeout = p.getint("CORE_LINK", "connect_timeout_ms")
linkSettings.setConnectTimeoutMs(timeout)
if p.has_option("CORE_LINK", "tunnel"):
tunnel = p.getint("CORE_LINK", "tunnel")
linkSettings.setTunnel(tunnel)
if p.has_option("CORE_LINK", "tunnel_local_port"):
tunnelLocalPort = p.getint("CORE_LINK", "tunnel_local_port")
linkSettings.setTunnelLocalPort(tunnelLocalPort)
if p.has_option("CORE_LINK", "ssh_user"):
sshUser = p.get("CORE_LINK", "ssh_user")
try:
sshUser = base64ToStr(sshUser)
except ValueError as e:
raise AwlSimError("Project file: "
"Invalid ssh_user")
linkSettings.setSSHUser(sshUser)
if p.has_option("CORE_LINK", "ssh_port"):
sshPort = p.getint("CORE_LINK", "ssh_port")
linkSettings.setSSHPort(sshPort)
if p.has_option("CORE_LINK", "ssh_executable"):
sshExecutable = p.get("CORE_LINK", "ssh_executable")
try:
sshExecutable = base64ToStr(sshExecutable)
except ValueError as e:
raise AwlSimError("Project file: "
"Invalid ssh_executable")
linkSettings.setSSHExecutable(sshExecutable)
# HWMODS section
for i in range(0xFFFF):
......@@ -661,6 +731,16 @@ class Project(object):
int(linkSettings.getConnectPort()))
lines.append("connect_timeout_ms=%d" %\
int(linkSettings.getConnectTimeoutMs()))
lines.append("tunnel=%d" % linkSettings.getTunnel())
lines.append("tunnel_local_port=%d" %\
linkSettings.getTunnelLocalPort())
sshUser = linkSettings.getSSHUser()
sshUser = strToBase64(sshUser, ignoreErrors=True)
lines.append("ssh_user=%s" % sshUser)
lines.append("ssh_port=%d" % linkSettings.getSSHPort())
sshExecutable = linkSettings.getSSHExecutable()
sshExecutable = strToBase64(sshExecutable, ignoreErrors=True)
lines.append("ssh_executable=%s" % sshExecutable)
lines.append("")
lines.append("[HWMODS]")
......
......@@ -2,4 +2,5 @@ from __future__ import division, absolute_import, print_function, unicode_litera
from awlsim.common.compat import *
from awlsim.coreclient.client import *
from awlsim.coreclient.sshtunnel import *
from awlsim.coreclient.util import *
# -*- coding: utf-8 -*-
#
# AWL simulator - SSH tunnel helper
#
# Copyright 2016 Michael Buesch <[email protected]>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
from __future__ import division, absolute_import, print_function, unicode_literals
from awlsim.common.compat import *
from awlsim.common.exceptions import *
from awlsim.common.net import *
from awlsim.common.util import *
import pty
import os
import select
import signal
import time
class SSHTunnel(object):
"""SSH tunnel helper.
"""
SSH_DEFAULT_USER = "pi"
SSH_PORT = 22
SSH_LOCAL_PORT_START = 4151 + 10
SSH_LOCAL_PORT_END = SSH_LOCAL_PORT_START + 4096
SSH_DEFAULT_EXECUTABLE = "ssh"
def __init__(self, remoteHost, remotePort,
sshUser=SSH_DEFAULT_USER,
localPort=None,
sshExecutable=SSH_DEFAULT_EXECUTABLE,
sshPort=SSH_PORT):
"""Create an SSH tunnel.
"""
self.remoteHost = remoteHost
self.remotePort = remotePort
self.sshUser = sshUser
self.localPort = localPort
self.sshExecutable = sshExecutable
self.sshPort = sshPort
self.__sshPid = None
def connect(self, timeout=10.0):
"""Establish the SSH tunnel.
"""
localPort = self.localPort
if localPort is None:
localPort = self.SSH_LOCAL_PORT_START
while not netPortIsUnused("localhost", localPort):
localPort += 1
if localPort > self.SSH_LOCAL_PORT_END:
raise AwlSimError("Failed to find an "
"unused local port for the "
"SSH tunnel.")
actualLocalPort = localPort
self.__sshPid = None
try:
# Prepare SSH environment and arguments.
env = envClearLang(os.environ)
argv = [ self.sshExecutable,
"-p", "%d" % self.sshPort,
"-l", self.sshUser,
"-L", "localhost:%d:localhost:%d" % (
localPort, self.remotePort),
"-N",
"-v",
self.remoteHost, ]
# Create a PTY and fork.
childPid, ptyMasterFd = pty.fork()
if childPid == pty.CHILD:
# Run SSH
execargs = argv + [env]
os.execlpe(argv[0], *execargs)
assert(0) # unreachable
self.__sshPid = childPid
self.__handshake(ptyMasterFd, timeout)
except (OSError, ValueError, IOError) as e:
with suppressAllExc:
self.shutdown()
raise AwlSimError("Failed to execute SSH to "
"establish SSH tunnel:\n%s" %\
str(e))
except KeyboardInterrupt as e:
with suppressAllExc:
self.shutdown()
raise AwlSimError("Interrupted by user.")
return "localhost", actualLocalPort
def shutdown(self):
if self.__sshPid is None:
return
try:
with suppressAllExc:
os.kill(self.__sshPid, signal.SIGTERM)
finally:
self.__sshPid = None
@staticmethod
def __read(fd):
data = []
while True:
rfds, wfds, xfds = select.select([fd], [], [], 0)
if fd not in rfds:
break
d = os.read(fd, 1024)
if not d:
break
data.append(d)
return b''.join(data)
@staticmethod
def __write(fd, data):
while data:
count = os.write(fd, data)
data = data[count:]
PROMPT_PW = "'s Password:"
PROMPT_AUTH = "The authenticity of host "
PROMPT_YESNO = " (yes/no)?"
AUTH_FINISH = "Authenticated to "
def __handshake(self, ptyMasterFd, timeout):
timeoutEnd = monotonic_time() + (timeout or 0)
sentPw, authReq, finished = False, False, False
while not finished:
time.sleep(0.1)
if timeout and monotonic_time() >= timeoutEnd:
raise AwlSimError("Timeout establishing SSH tunnel.")
fromSsh = self.__read(ptyMasterFd)
try:
fromSsh = fromSsh.decode("UTF-8", "ignore")
except UnicodeError:
fromSsh = ""
for line in fromSsh.splitlines():
if not line:
continue
lineLow = line.lower()
isDebug = lineLow.strip().startswith("debug")
self.sshMessage(line, isDebug)
if isDebug:
continue
if self.PROMPT_PW.lower() in lineLow:
if sentPw:
# Second try.
raise AwlSimError("SSH tunnel passphrase "
"was not accepted.")
passphrase = self.getPassphrase(line)
if passphrase is None:
raise AwlSimError("SSH tunnel connection "
"requires a passphrase, but "
"no passphrase was given.")
self.__write(ptyMasterFd, passphrase)
if not passphrase.endswith(b"\n"):
self.__write(ptyMasterFd, b"\n")
sentPw = True
timeoutEnd = monotonic_time() + (timeout or 0)
continue
if self.PROMPT_AUTH.lower() in lineLow:
authReq = True
continue
if self.PROMPT_YESNO.lower() in lineLow and authReq:
ok = self.hostAuth(line)
if not ok:
raise AwlSimError("SSH tunnel host "
"authentication failed.")
self.__write(ptyMasterFd, b"yes\n")
authReq = False
timeoutEnd = monotonic_time() + (timeout or 0)
continue
if self.AUTH_FINISH.lower() in lineLow:
# Successfully authenticated.
finished = True
continue
def sshMessage(self, message, isDebug):
"""Print a SSH log message.
"""
if not isDebug:
printInfo("[SSH]: %s" % message)
def getPassphrase(self, prompt):
"""Get a password from the user.
"""
try:
return input(prompt).encode("UTF-8", "ignore")
except UnicodeError:
return ""
def hostAuth(self, prompt):
"""Get the user answer to the host authentication question.
This function returns a boolean.
"""
return str2bool(input(prompt))
......@@ -10,7 +10,7 @@ sh_test()
for testfile in 000-base/shutdown.awl; do
run_test "$interpreter" "$basedir/$testfile" \
--spawn-backend --interpreter "$interpreter" \
--connect localhost:$(get_port)
--connect-to localhost:$(get_port)
done
echo -n "--- Finished coreserver tests "
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment