attempt with python ssh tunnelier
This commit is contained in:
@@ -1 +0,0 @@
|
|||||||
../web/ssh-tunnelier
|
|
||||||
1
bin/ssh-tunnelier.sh
Symbolic link
1
bin/ssh-tunnelier.sh
Symbolic link
@@ -0,0 +1 @@
|
|||||||
|
../web/ssh-tunnelier.sh
|
||||||
28
py-packages/sshtunnelier/setup.py
Normal file
28
py-packages/sshtunnelier/setup.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import os
|
||||||
|
from distutils.core import setup
|
||||||
|
|
||||||
|
|
||||||
|
def version_reader(path):
|
||||||
|
for line in open(path, "rt").read(1024).split("\n"):
|
||||||
|
if line.startswith("__version__"):
|
||||||
|
return line.split("=")[1].strip().replace('"', "")
|
||||||
|
|
||||||
|
|
||||||
|
version = version_reader(os.path.join("sshtunnelier", "__init__.py"))
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="sshtunnelier",
|
||||||
|
packages=["sshtunnelier"],
|
||||||
|
version=version,
|
||||||
|
description="SSH tunnel manager (yet another)",
|
||||||
|
author="Ville Rantanen",
|
||||||
|
author_email="q@six9.net",
|
||||||
|
entry_points={
|
||||||
|
"console_scripts": [
|
||||||
|
"ssh-tunnelier = sshtunnelier:main",
|
||||||
|
]
|
||||||
|
},
|
||||||
|
install_requires=[
|
||||||
|
"psutil",
|
||||||
|
],
|
||||||
|
)
|
||||||
236
py-packages/sshtunnelier/sshtunnelier/__init__.py
Normal file
236
py-packages/sshtunnelier/sshtunnelier/__init__.py
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shlex
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from argparse import ArgumentError, ArgumentParser
|
||||||
|
|
||||||
|
import psutil
|
||||||
|
|
||||||
|
__version__ = "2024.01.07"
|
||||||
|
|
||||||
|
CONFDIR = os.path.expanduser("~/.config/ssh-tunnelier")
|
||||||
|
CONF = os.path.join(CONFDIR, "tunnels.json")
|
||||||
|
# Just over a year in minutes
|
||||||
|
MAGIC_TIME = 525601
|
||||||
|
LOCALHOSTSYMBOL = "💻"
|
||||||
|
|
||||||
|
EXAMPLE_CONFIG = """{
|
||||||
|
"test": {
|
||||||
|
"host": "host to connect, or defaults to name",
|
||||||
|
"options": "-4 and other ssh options",
|
||||||
|
"auto-connect": false,
|
||||||
|
"tunnels": [
|
||||||
|
{
|
||||||
|
"local_port": 1111,
|
||||||
|
"remote_port": 8080,
|
||||||
|
"remote_address": "localhost",
|
||||||
|
"reverse": false,
|
||||||
|
"comment": "`comment`, `reverse` and `remote_address` are not required"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def args():
|
||||||
|
"""Create command line options"""
|
||||||
|
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
dest="name",
|
||||||
|
default=None,
|
||||||
|
action="store",
|
||||||
|
type=str,
|
||||||
|
nargs="?",
|
||||||
|
help="Connection name to use",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--kill",
|
||||||
|
dest="kill",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Kill connection",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--list",
|
||||||
|
dest="list",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="List connections. Default if connection name is not given.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--edit",
|
||||||
|
dest="edit",
|
||||||
|
default=None,
|
||||||
|
help="Edit config with user defined editor.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--connect",
|
||||||
|
dest="connect",
|
||||||
|
default=None,
|
||||||
|
action="store",
|
||||||
|
type=str,
|
||||||
|
help="Instant tunnel. Syntax: sshserver:localport:targethost:remoteport",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--auto",
|
||||||
|
dest="auto",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Run all connections with auto-connect: true",
|
||||||
|
)
|
||||||
|
|
||||||
|
options = parser.parse_args()
|
||||||
|
if options.name is None and options.connect is None and not options.auto:
|
||||||
|
options.list = True
|
||||||
|
if options.kill:
|
||||||
|
if options.name is None:
|
||||||
|
parser.error("Connection name required with --kill")
|
||||||
|
return options
|
||||||
|
|
||||||
|
|
||||||
|
def check_type(d, key, expected, error_msg):
|
||||||
|
if not key in d:
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
if not isinstance(d[key], expected):
|
||||||
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
"""Load config, check types, add defaults"""
|
||||||
|
try:
|
||||||
|
os.makedirs(CONFDIR, exist_ok=True)
|
||||||
|
if not os.path.exists(CONF):
|
||||||
|
print("Creating example config: " + CONF)
|
||||||
|
with open(CONF, "w") as fp:
|
||||||
|
fp.write(EXAMPLE_CONFIG)
|
||||||
|
with open(CONF, "r") as fp:
|
||||||
|
config = json.load(fp)
|
||||||
|
|
||||||
|
for name in config:
|
||||||
|
config[name]["host"] = config[name].get("host", name)
|
||||||
|
config[name]["options"] = config[name].get("options", "")
|
||||||
|
check_type(config[name], "host", str, f"Config['{name}']['host'] must be string")
|
||||||
|
check_type(config[name], "options", str, f"Config['{name}']['options'] must be string")
|
||||||
|
|
||||||
|
for i, tunnel in enumerate(config[name]["tunnels"]):
|
||||||
|
tunnel["remote_address"] = tunnel.get("remote_address", "localhost")
|
||||||
|
tunnel["reverse"] = tunnel.get("reverse", False)
|
||||||
|
tunnel["comment"] = tunnel.get("comment", "")
|
||||||
|
check_type(tunnel, "reverse", bool, f"Config['{name}']['tunnel'][{i}]['reverse'] must be Boolean")
|
||||||
|
check_type(
|
||||||
|
tunnel, "remote_address", str, f"Config['{name}']['tunnel'][{i}]['remote_address'] must be string"
|
||||||
|
)
|
||||||
|
check_type(tunnel, "remote_port", int, f"Config['{name}']['tunnel'][{i}]['remote_port'] must be int")
|
||||||
|
check_type(tunnel, "local_port", int, f"Config['{name}']['tunnel'][{i}]['local_port'] must be int")
|
||||||
|
check_type(tunnel, "comment", str, f"Config['{name}']['tunnel'][{i}]['comment'] must be string")
|
||||||
|
|
||||||
|
return config
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print(f"Could not load config. Edit {CONF} with --edit")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def config_edit(editor):
|
||||||
|
subprocess.run([editor, CONF])
|
||||||
|
|
||||||
|
|
||||||
|
def connect(name, config):
|
||||||
|
if name not in config:
|
||||||
|
raise ValueError("No such connection name")
|
||||||
|
|
||||||
|
host = config[name]["host"]
|
||||||
|
options = shlex.split(config[name]["options"])
|
||||||
|
tunnels = []
|
||||||
|
for tunnel in config[name]["tunnels"]:
|
||||||
|
switch = "-R" if tunnel["reverse"] else "-L"
|
||||||
|
tunnels.append(switch)
|
||||||
|
tunnels.append(f"{tunnel['local_port']}:{tunnel['remote_address']}:{tunnel['remote_port']}")
|
||||||
|
conn_id = get_id(name, config)
|
||||||
|
|
||||||
|
remote_cmd = f"nice /bin/bash -c 'for ((i=1;i<{MAGIC_TIME};i++)); do cut -f4 -d \" \" /proc/$PPID/stat | xargs kill -0 || exit ; sleep 60;done'; echo tunnelier {conn_id}"
|
||||||
|
cmd = ["ssh", "-f", "-n", *options, *tunnels, host, remote_cmd]
|
||||||
|
kill_connection(name, config)
|
||||||
|
subprocess.run(cmd)
|
||||||
|
list_connections(config, single=name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_id(name, config):
|
||||||
|
return hashlib.md5((name + config[name]["host"]).encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def get_pid(conn_id):
|
||||||
|
for p in psutil.process_iter():
|
||||||
|
try:
|
||||||
|
if conn_id in " ".join(p.cmdline()):
|
||||||
|
return p.pid
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def kill_connection(name, config):
|
||||||
|
conn_id = get_id(name, config)
|
||||||
|
pid = get_pid(conn_id)
|
||||||
|
if pid:
|
||||||
|
print(f"Killing PID {pid}")
|
||||||
|
p = psutil.Process(pid)
|
||||||
|
p.terminate()
|
||||||
|
p.wait()
|
||||||
|
|
||||||
|
|
||||||
|
def list_connections(config, single=None):
|
||||||
|
for name in config:
|
||||||
|
if single:
|
||||||
|
if name != single:
|
||||||
|
continue
|
||||||
|
conn_id = get_id(name, config)
|
||||||
|
pid = get_pid(conn_id)
|
||||||
|
running = f"True, PID:{pid}" if pid else "False"
|
||||||
|
automatic = "Auto, " if config[name].get("auto-connect", False) else ""
|
||||||
|
print(f"# {name}")
|
||||||
|
print(f" - {automatic}Running: {running}")
|
||||||
|
for i, tunnel in enumerate(config[name]["tunnels"]):
|
||||||
|
url = f" http://localhost:{tunnel['local_port']}" if pid else ""
|
||||||
|
remote = LOCALHOSTSYMBOL if tunnel["remote_address"] == "localhost" else tunnel["remote_address"]
|
||||||
|
cmt = f" '{tunnel['comment']}'" if tunnel["comment"] else ""
|
||||||
|
print(f" - Tunnel {i}: {tunnel['local_port']} → {remote}:{tunnel['remote_port']}{cmt}{url} ")
|
||||||
|
|
||||||
|
|
||||||
|
def auto_connect(config):
|
||||||
|
for name in config:
|
||||||
|
if config[name].get("auto-connect", False):
|
||||||
|
connect(name, config)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
opts = args()
|
||||||
|
config = load_config()
|
||||||
|
if opts.edit:
|
||||||
|
config_edit(opts.edit)
|
||||||
|
sys.exit(0)
|
||||||
|
if opts.list:
|
||||||
|
list_connections(config, opts.name)
|
||||||
|
sys.exit(0)
|
||||||
|
if opts.auto:
|
||||||
|
auto_connect(config)
|
||||||
|
if opts.connect:
|
||||||
|
host, tunnels = parse_connect(opts.connect)
|
||||||
|
connect_tunnel(host, tunnels)
|
||||||
|
sys.exit(0)
|
||||||
|
if opts.kill:
|
||||||
|
kill_connection(opts.name, config)
|
||||||
|
sys.exit(0)
|
||||||
|
if opts.name:
|
||||||
|
connect(opts.name, config)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -9,6 +9,11 @@
|
|||||||
" 1: - ordered list
|
" 1: - ordered list
|
||||||
" c: <code bash file.sh>\n</code>
|
" c: <code bash file.sh>\n</code>
|
||||||
|
|
||||||
|
imapclear
|
||||||
|
set laststatus=2
|
||||||
|
set statusline=%f\ %=\ \[ctrl-wXX\ heads:w1-6\ bold:bb\ it:ii\ undr:uu\ del:dd\ \[\[l\]\]\ {{im}}\ bull"-\ enum:1\ code:c]\ (%v,%l)
|
||||||
|
|
||||||
|
|
||||||
" ====== headings =======
|
" ====== headings =======
|
||||||
imap <C-w>h ====== ======<Esc>6hi
|
imap <C-w>h ====== ======<Esc>6hi
|
||||||
imap <C-w>h1 ====== ======<Esc>6hi
|
imap <C-w>h1 ====== ======<Esc>6hi
|
||||||
|
|||||||
Reference in New Issue
Block a user