From 3280a48260f5f7ea509d27ad7a2678662980ea9f Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 10:02:44 +0200 Subject: [PATCH 01/12] Use paramiko for SSH --- README | 3 ++ client.py | 56 ++++++++++++++++------------- clientconfig.example.py | 12 ++++--- clientconfigs/crans/clientconfig.py | 19 +++++----- clientconfigs/tudor/clientconfig.py | 15 ++++---- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/README b/README index 4ef3794..b81b26b 100644 --- a/README +++ b/README @@ -9,6 +9,9 @@ c'est possible. Il faut pour cela changer la variable cmd_name dans le Makefile avant de lancer make install ou make install-server. == Installation et configuration du client == + +Sous Debian il faut avoir `python2.7` et `python-paramiko` d'installés. + * Copiez le dépôt git sur votre machine : $ git clone git@gitlab.crans.org:nounous/cranspasswords.git * Si ce n'est déjà fait, indiquer votre clé publique sur gest_crans diff --git a/client.py b/client.py index 8d00756..76322ed 100755 --- a/client.py +++ b/client.py @@ -25,6 +25,9 @@ import time import datetime import copy +# Import a SSH client +from paramiko.client import SSHClient + # Import de la config envvar = "CRANSPASSWORDS_CLIENT_CONFIG_DIR" try: @@ -265,35 +268,38 @@ class simple_memoize(object): def remote_proc(options, command, arg=None): """ Fabrique un process distant pour communiquer avec le serveur. - Cela consiste à lancer une commande (indiquée dans la config) - qui va potentiellement lancer ssh. + Cela consiste à lancer une commande par ssh (indiquée dans la config). ``command`` désigne l'action à envoyer au serveur ``arg`` est une chaîne (str) accompagnant la commande en paramètre ``options`` contient la liste usuelle d'options """ - full_command = list(options.serverdata['server_cmd']) - full_command.append(command) + host = str(options.serverdata['host']) + remote_cmd = list(options.serverdata['remote_cmd']) + remote_cmd.append(command) if arg: - full_command.append(arg) + remote_cmd.append(arg) if options.verbose and not options.quiet: - print("Running command %s ..." % " ".join(full_command)) + print("Running command %s ..." % " ".join(remote_cmd)) - proc = subprocess.Popen(full_command, - stdin = subprocess.PIPE, - stdout = subprocess.PIPE, - stderr = sys.stderr, - close_fds = True) - return proc + # Crée un client SSH et charge l'agent et les clés du système + client = SSHClient() + client.load_system_host_keys() + client.connect(host) + + # Crée un Channel et lance la commande + chan = client.get_transport().open_session() + chan.exec_command(remote_cmd) + return chan @simple_memoize def get_keep_alive_connection(options): """Fabrique un process parlant avec le serveur suivant la commande 'keep-alive'. On utilise une fonction séparée pour cela afin de memoizer le résultat, et ainsi utiliser une seule connexion""" - proc = remote_proc(options, 'keep-alive', None) - atexit.register(proc.stdin.close) - return proc + chan = remote_proc(options, 'keep-alive', None) + atexit.register(chan.close) + return chan def remote_command(options, command, arg=None, stdin_contents=None): """Exécute la commande distante, et retourne la sortie de cette @@ -302,20 +308,22 @@ def remote_command(options, command, arg=None, stdin_contents=None): keep_alive = options.serverdata.get('keep-alive', False) if keep_alive: - conn = get_keep_alive_connection(options) + chan = get_keep_alive_connection(options) args = filter(None, [arg, stdin_contents]) msg = {u'action': unicode(command), u'args': args } - conn.stdin.write('%s\n' % json.dumps(msg)) - conn.stdin.flush() - raw_out = conn.stdout.readline() + stdin = chan.makefile_stdin() + stdin.write('%s\n' % json.dumps(msg)) + stdin.flush() + raw_out = chan.recv() else: - proc = remote_proc(options, command, arg) + chan = remote_proc(options, command, arg) if stdin_contents is not None: - proc.stdin.write(json.dumps(stdin_contents)) - proc.stdin.flush() + stdin = chan.makefile_stdin() + stdin.write(json.dumps(stdin_contents)) + stdin.flush() - raw_out, raw_err = proc.communicate() - ret = proc.returncode + raw_out = chan.recv() + ret = chan.recv_exit_status() if ret != 0: if not options.quiet: diff --git a/clientconfig.example.py b/clientconfig.example.py index ccdc5a2..13ba000 100755 --- a/clientconfig.example.py +++ b/clientconfig.example.py @@ -21,18 +21,20 @@ distant_cmd = ["sudo", '-n', server_path] #: #: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. #: -#: * ``'server_cmd'`` : La commande exécutée sur le client pour appeler -#: le script sur le serveur distant. +#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. servers = { 'default': { - 'server_cmd': [ssh_path, 'odlyd.crans.org'] + distant_cmd, + 'host': 'odlyd.crans.org', + 'remote_cmd': distant_cmd, }, # Utile pour tester 'localhost': { - 'server_cmd': [ssh_path, 'localhost'] + distant_cmd, + 'host': 'localhost', + 'remote_cmd': distant_cmd, 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { - 'server_cmd': [ssh_path, 'ovh.crans.org'] + distant_cmd, + 'host': 'soyouz.crans.org', + 'remote_cmd': distant_cmd, } } diff --git a/clientconfigs/crans/clientconfig.py b/clientconfigs/crans/clientconfig.py index b3cd623..2ccc131 100644 --- a/clientconfigs/crans/clientconfig.py +++ b/clientconfigs/crans/clientconfig.py @@ -8,9 +8,6 @@ import os #: Pour override le nom si vous voulez renommer la commande cmd_name = 'cranspasswords' -#: Path du binaire ssh sur la machine client -ssh_path = '/usr/bin/ssh' - #: Path du script ``cmd_name``-server sur le serveur server_path = '/usr/local/bin/%s-server' % (cmd_name,) @@ -22,21 +19,25 @@ print distant_cmd #: #: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. #: -#: * ``'server_cmd'`` : La commande exécutée sur le client pour appeler -#: le script sur le serveur distant. +#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. servers = { 'default': { - 'server_cmd': [ssh_path, 'odlyd.crans.org'] + distant_cmd, + 'host': 'odlyd.crans.org', + 'remote_cmd': distant_cmd, }, 'titanic': { - 'server_cmd': [ssh_path, 'freebox.crans.org', ssh_path, 'odlyd.crans.org'] + distant_cmd, + 'host': 'freebox.crans.org', + # manual ssh jump + 'remote_cmd': ['ssh', 'odyld.crans.org'] + distant_cmd, }, # Utile pour tester 'localhost': { - 'server_cmd': [ssh_path, 'localhost'] + distant_cmd, + 'host': 'localhost', + 'remote_cmd': distant_cmd, 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { - 'server_cmd': [ssh_path, 'soyouz.crans.org', 'sudo', '-n', '/usr/local/bin/cpasswords-server'], + 'host': 'soyouz.crans.org', + 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cpasswords-server'], } } diff --git a/clientconfigs/tudor/clientconfig.py b/clientconfigs/tudor/clientconfig.py index eac5e68..9982694 100644 --- a/clientconfigs/tudor/clientconfig.py +++ b/clientconfigs/tudor/clientconfig.py @@ -10,23 +10,26 @@ import os #: #: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. #: -#: * ``'server_cmd'`` : La commande exécutée sur le client pour appeler -#: le script sur le serveur distant. +#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. servers = { 'default': { - 'server_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], + 'host': '', + 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], 'keep-alive': True, }, 'gladys': { - 'server_cmd': ['/usr/bin/ssh', 'home.b2moo.fr', '/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], + 'host': 'home.b2moo.fr', + 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], 'keep-alive': True, }, 'gladys-home': { - 'server_cmd': ['/usr/bin/ssh', 'gladys.home', '/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], + 'host': 'gladys.home', + 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], 'keep-alive': True, }, 'pimeys': { - 'server_cmd': ['/usr/bin/ssh', 'pimeys.fr', 'sudo', '-n', '/usr/local/bin/cranspasswords-server', ], + 'host': 'pimeys.fr', + 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cranspasswords-server', ], 'keep-alive': True, }, } -- GitLab From 91ddc03cadf003786c76bccc311f6124de2a02ca Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 10:45:08 +0200 Subject: [PATCH 02/12] Use Python logging module --- client.py | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/client.py b/client.py index 76322ed..7d7c42a 100755 --- a/client.py +++ b/client.py @@ -24,15 +24,19 @@ import string import time import datetime import copy +import logging # Import a SSH client from paramiko.client import SSHClient +# Logger local +# On n'a pas encore accès à la config donc on devine le nom +bootstrap_cmd_name = os.path.split(sys.argv[0])[1] +log = logging.getLogger(bootstrap_cmd_name) + # Import de la config envvar = "CRANSPASSWORDS_CLIENT_CONFIG_DIR" try: - # Oui, le nom de la commande est dans la config, mais on n'a pas encore accès à la config - bootstrap_cmd_name = os.path.split(sys.argv[0])[1] sys.path.append(os.path.expanduser("~/.config/%s/" % (bootstrap_cmd_name,))) import clientconfig as config except ImportError: @@ -975,16 +979,27 @@ def insult_on_nofilename(options, parser): sys.exit(1) if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Gestion de mots de passe partagés grâce à GPG.") + # Gestion des arguments + parser = argparse.ArgumentParser( + description="Gestion de mots de passe partagés grâce à GPG.", + ) + parser.add_argument( + '--verbose', '-v', + action='count', + default=1, + help="Verbose mode. Multiple -v options increase the verbosity.", + ) + parser.add_argument( + '--quiet', '-q', + action='store_true', + default=False, + help="Mode silencieux. Cache les message d'erreurs (override --verbose et --interactive)." + ) parser.add_argument('-s', '--server', default='default', help="Utilisation d'un serveur alternatif (test, backup, etc)") - parser.add_argument('-v', '--verbose', action='store_true', default=False, - help="Mode verbeux") parser.add_argument('--drop-invalid', action='store_true', default=False, dest='drop_invalid', help="Combiné avec --force, droppe les clés en lesquelles on n'a pas confiance sans demander confirmation.") - parser.add_argument('-q', '--quiet', action='store_true', default=False, - help="Mode silencieux. Cache les message d'erreurs (override --verbose et --interactive).") parser.add_argument('-c', '--clipboard', action='store_true', default=None, help="Stocker le mot de passe dans le presse papier") parser.add_argument('--no-clip', '--noclip', '--noclipboard', action='store_false', default=None, @@ -1048,6 +1063,18 @@ if __name__ == "__main__": # On parse les options fournies en commandline options = parser.parse_args(sys.argv[1:]) + # Active le logger avec des couleurs + # Par défaut on affiche >= WARNING + options.verbose = 40 - (10 * options.verbose) if not options.quiet else 0 + logging.addLevelName(logging.INFO, "\033[1;36mINFO\033[1;0m") + logging.addLevelName(logging.WARNING, "\033[1;33mWARNING\033[1;0m") + logging.addLevelName(logging.ERROR, "\033[1;91mERROR\033[1;0m") + logging.addLevelName(logging.DEBUG, "\033[1;37mDEBUG\033[1;0m") + logging.basicConfig( + level=options.verbose, + format='\033[1;90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' + ) + ## On calcule les options qui dépendent des autres. ## C'est un peu un hack, peut-être que la méthode propre serait de surcharger argparse.ArgumentParser ## et argparse.Namespace, mais j'ai pas réussi à comprendre commenr m'en sortir. @@ -1059,10 +1086,6 @@ if __name__ == "__main__": options.clipboard = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip') # On récupère les données du serveur à partir du nom fourni options.serverdata = config.servers[options.server] - # Attention à l'ordre pour interactive - #  --quiet override --verbose - if options.quiet: - options.verbose = False # On parse les roles fournis, et il doivent exister, ne pas être -w… # parse_roles s'occupe de ça # NB : ça nécessite de se connecter au serveur, or, pour show_servers on n'en a pas besoin -- GitLab From e13d7564e2e447f591463334db1c43cb768939ce Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 12:20:04 +0200 Subject: [PATCH 03/12] Split Paramiko client and channels code --- client.py | 133 ++++++++++++++++++++++-------------------------------- 1 file changed, 55 insertions(+), 78 deletions(-) diff --git a/client.py b/client.py index 7d7c42a..c649236 100755 --- a/client.py +++ b/client.py @@ -25,9 +25,10 @@ import time import datetime import copy import logging +from binascii import hexlify # Import a SSH client -from paramiko.client import SSHClient +from paramiko.client import SSHClient, MissingHostKeyPolicy # Logger local # On n'a pas encore accès à la config donc on devine le nom @@ -269,97 +270,73 @@ class simple_memoize(object): ###### ## Remote commands -def remote_proc(options, command, arg=None): +class MyMissingHostKeyPolicy(MissingHostKeyPolicy): """ - Fabrique un process distant pour communiquer avec le serveur. - Cela consiste à lancer une commande par ssh (indiquée dans la config). - ``command`` désigne l'action à envoyer au serveur - ``arg`` est une chaîne (str) accompagnant la commande en paramètre - ``options`` contient la liste usuelle d'options + Warn when hostkey is unknown and then add """ - host = str(options.serverdata['host']) - remote_cmd = list(options.serverdata['remote_cmd']) - remote_cmd.append(command) - if arg: - remote_cmd.append(arg) - - if options.verbose and not options.quiet: - print("Running command %s ..." % " ".join(remote_cmd)) + def missing_host_key(self, client, hostname, key): + client._host_keys.add(hostname, key.get_name(), key) + if client._host_keys_filename is not None: + client.save_host_keys(client._host_keys_filename) + log.warning('Adding %s host key for %s: %s' % + (key.get_name(), hostname, hexlify(key.get_fingerprint()))) + +def create_ssh_client(): + """ + Create a SSH client with paramiko module + """ + if not "host" in options.serverdata: + log.error("Missing parameter `host` in active server configuration") + exit(1) - # Crée un client SSH et charge l'agent et les clés du système + # Create SSH client with system host keys and agent client = SSHClient() client.load_system_host_keys() - client.connect(host) + client.set_missing_host_key_policy(MyMissingHostKeyPolicy()) + client.connect(str(options.serverdata['host'])) + return client - # Crée un Channel et lance la commande - chan = client.get_transport().open_session() - chan.exec_command(remote_cmd) - return chan +def remote_command(options, command, arg=None, stdin_contents=None): + """ + Execute remote command and return output + """ + # TODO: instantiate client outside to have only one connection + client = create_ssh_client() + + # Build command + if not "remote_cmd" in options.serverdata: + log.error("Missing parameter `remote_cmd` in active server configuration") + exit(1) + remote_cmd = " ".join(options.serverdata['remote_cmd'] + [command]) + if arg: + remote_cmd += " " + arg -@simple_memoize -def get_keep_alive_connection(options): - """Fabrique un process parlant avec le serveur suivant la commande - 'keep-alive'. On utilise une fonction séparée pour cela afin - de memoizer le résultat, et ainsi utiliser une seule connexion""" - chan = remote_proc(options, 'keep-alive', None) - atexit.register(chan.close) - return chan + # Run command + log.info("Running command %s ..." % remote_cmd) + stdin, stdout, stderr = client.exec_command(remote_cmd) -def remote_command(options, command, arg=None, stdin_contents=None): - """Exécute la commande distante, et retourne la sortie de cette - commande""" - detail = options.verbose and not options.quiet - keep_alive = options.serverdata.get('keep-alive', False) - - if keep_alive: - chan = get_keep_alive_connection(options) - args = filter(None, [arg, stdin_contents]) - msg = {u'action': unicode(command), u'args': args } - stdin = chan.makefile_stdin() - stdin.write('%s\n' % json.dumps(msg)) + # Write + if stdin_contents is not None: + stdin.write(json.dumps(stdin_contents)) stdin.flush() - raw_out = chan.recv() - else: - chan = remote_proc(options, command, arg) - if stdin_contents is not None: - stdin = chan.makefile_stdin() - stdin.write(json.dumps(stdin_contents)) - stdin.flush() - raw_out = chan.recv() - ret = chan.recv_exit_status() + # Read + raw_out = stdout.read() + log.debug("Server returned %s" % raw_out) + + # Return code == 0 if success + ret = stdout.channel.recv_exit_status() + if ret != 0: + log.error("Wrong server return code %s, error is %s" % (ret, stderr.read())) + exit(ret) - if ret != 0: - if not options.quiet: - print((u"Mauvais code retour côté serveur, voir erreur " + - u"ci-dessus").encode('utf-8'), - file=sys.stderr) - if detail: - print("raw_output: %s" % raw_out) - sys.exit(ret) try: answer = json.loads(raw_out.strip()) except ValueError: - if not options.quiet: - print(u"Impossible de parser le résultat".encode('utf-8'), - file=sys.stderr) - if detail: - print("raw_output: %s" % raw_out) - sys.exit(42) - if not keep_alive: - return answer - else: - try: - if answer[u'status'] != u'ok': - raise KeyError('Bad answer status') - return answer[u'content'] - except KeyError: - if not options.quiet: - print(u"Réponse erronée du serveur".encode('utf-8'), - file=sys.stderr) - if detail: - print("answer: %s" % repr(answer)) - sys.exit(-1) + log.error("Error while parsing JSON") + exit(42) + + return answer @simple_memoize -- GitLab From 5537150c90aab06848b940a595f9ae8add806429 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:19:48 +0200 Subject: [PATCH 04/12] Check return code before reading --- client.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/client.py b/client.py index c649236..7577d09 100755 --- a/client.py +++ b/client.py @@ -311,31 +311,33 @@ def remote_command(options, command, arg=None, stdin_contents=None): if arg: remote_cmd += " " + arg - # Run command - log.info("Running command %s ..." % remote_cmd) - stdin, stdout, stderr = client.exec_command(remote_cmd) + # Run command and timeout after 10s + log.info("Running command `%s`" % remote_cmd) + stdin, stdout, stderr = client.exec_command(remote_cmd, timeout=10) # Write if stdin_contents is not None: + log.info("Writing to stdin: %s" % stdin_contents) stdin.write(json.dumps(stdin_contents)) stdin.flush() - # Read - raw_out = stdout.read() - log.debug("Server returned %s" % raw_out) - # Return code == 0 if success ret = stdout.channel.recv_exit_status() if ret != 0: - log.error("Wrong server return code %s, error is %s" % (ret, stderr.read())) + err = "" + if sterr.channel.recv_stderr_ready(): + err = stderr.read() + log.error("Wrong server return code %s, error is %s" % (ret, err)) exit(ret) + # Decode directly read buffer try: - answer = json.loads(raw_out.strip()) + answer = json.load(stdout) except ValueError: log.error("Error while parsing JSON") exit(42) + log.debug("Server returned %s" % answer) return answer @@ -961,16 +963,16 @@ if __name__ == "__main__": description="Gestion de mots de passe partagés grâce à GPG.", ) parser.add_argument( - '--verbose', '-v', + '-v', '--verbose', action='count', default=1, - help="Verbose mode. Multiple -v options increase the verbosity.", + help="verbose mode, multiple -v options increase verbosity", ) parser.add_argument( - '--quiet', '-q', + '-q', '--quiet', action='store_true', default=False, - help="Mode silencieux. Cache les message d'erreurs (override --verbose et --interactive)." + help="silent mode: hide errors, overrides verbosity" ) parser.add_argument('-s', '--server', default='default', help="Utilisation d'un serveur alternatif (test, backup, etc)") @@ -1038,7 +1040,7 @@ if __name__ == "__main__": help="Nom du fichier à afficher") # On parse les options fournies en commandline - options = parser.parse_args(sys.argv[1:]) + options = parser.parse_args() # Active le logger avec des couleurs # Par défaut on affiche >= WARNING -- GitLab From ba517c0b04d9ec5b9f549922748c07ce4be9cc6b Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:26:59 +0200 Subject: [PATCH 05/12] Get one file at a time --- client.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/client.py b/client.py index 7577d09..ea69435 100755 --- a/client.py +++ b/client.py @@ -361,10 +361,12 @@ def restore_all_files(options): """Récupère les fichiers du serveur distant""" return remote_command(options, "restorefiles") - -def get_files(options, filenames): - """Récupère le contenu des fichiers distants""" - return remote_command(options, "getfiles", stdin_contents=filenames) +@simple_memoize +def get_file(options, filename): + """ + Get the content of one remote file + """ + return remote_command(options, "getfile", filename) def put_files(options, files): """Dépose les fichiers sur le serveur distant""" @@ -654,7 +656,7 @@ def clipboard(texte): def show_file(options): """Affiche le contenu d'un fichier""" fname = options.fname - gotit, value = get_files(options, [fname])[0] + gotit, value = get_file(options, fname) if not gotit: if not options.quiet: print(value.encode("utf-8")) # value contient le message d'erreur @@ -736,7 +738,7 @@ def show_file(options): def edit_file(options): """Modifie/Crée un fichier""" fname = options.fname - gotit, value = get_files(options, [fname])[0] + gotit, value = get_file(options, fname) nfile = False annotations = u"" @@ -882,7 +884,8 @@ def recrypt_files(options, strict=False): askfiles = [filename for (filename, fileroles) in allfiles.iteritems() if is_wanted(fileroles) ] - files = get_files(options, askfiles) + files = [get_file(options, f) for f in askfiles] + # Au cas où on aurait échoué à récupérer ne serait-ce qu'un de ces fichiers, # on affiche le message d'erreur correspondant et on abandonne. for (success, message) in files: -- GitLab From fa7883e16fd9da22cb8aea9d79a87ff678fe2d67 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:35:04 +0200 Subject: [PATCH 06/12] Memoize SSHClient --- client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client.py b/client.py index ea69435..859a319 100755 --- a/client.py +++ b/client.py @@ -47,7 +47,7 @@ except ImportError: envspecified = os.getenv(envvar, None) if envspecified is None: if ducktape_display_error: - sys.stderr.write(u"Va lire le fichier README.\n".encode("utf-8")) + log.error("Va lire le fichier README.") sys.exit(1) else: # On a spécifié à la main le dossier de conf @@ -281,6 +281,7 @@ class MyMissingHostKeyPolicy(MissingHostKeyPolicy): log.warning('Adding %s host key for %s: %s' % (key.get_name(), hostname, hexlify(key.get_fingerprint()))) +@simple_memoize def create_ssh_client(): """ Create a SSH client with paramiko module -- GitLab From 3aa59583778a4fcb88128894b3c13e0a2387f162 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:37:27 +0200 Subject: [PATCH 07/12] Update default config --- client.py | 4 ++-- clientconfig.example.py | 1 - clientconfigs/crans/clientconfig.py | 1 - clientconfigs/tudor/clientconfig.py | 4 ---- 4 files changed, 2 insertions(+), 8 deletions(-) diff --git a/client.py b/client.py index 859a319..efe3015 100755 --- a/client.py +++ b/client.py @@ -259,8 +259,8 @@ class simple_memoize(object): mais il faudra s'en préoccuper si un jour on veut changer le comportement.""" if self.val == None: self.val = self.f(*args, **kwargs) - # On évite de tout deepcopier. Typiquement, un subprocess.Popen - # ne devrait pas l'être (comme dans get_keep_alive_connection) + # On évite de tout deepcopier. Typiquement, un SSHClient + # ne devrait pas l'être (comme dans create_ssh_client) if type(self.val) in [dict, list]: return copy.deepcopy(self.val) else: diff --git a/clientconfig.example.py b/clientconfig.example.py index 13ba000..78b061f 100755 --- a/clientconfig.example.py +++ b/clientconfig.example.py @@ -31,7 +31,6 @@ servers = { 'localhost': { 'host': 'localhost', 'remote_cmd': distant_cmd, - 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { 'host': 'soyouz.crans.org', diff --git a/clientconfigs/crans/clientconfig.py b/clientconfigs/crans/clientconfig.py index 2ccc131..1e22826 100644 --- a/clientconfigs/crans/clientconfig.py +++ b/clientconfigs/crans/clientconfig.py @@ -34,7 +34,6 @@ servers = { 'localhost': { 'host': 'localhost', 'remote_cmd': distant_cmd, - 'keep-alive': True, # <-- experimental, n'ouvre qu'une connexion }, 'ovh': { 'host': 'soyouz.crans.org', diff --git a/clientconfigs/tudor/clientconfig.py b/clientconfigs/tudor/clientconfig.py index 9982694..3f3e688 100644 --- a/clientconfigs/tudor/clientconfig.py +++ b/clientconfigs/tudor/clientconfig.py @@ -15,21 +15,17 @@ servers = { 'default': { 'host': '', 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - 'keep-alive': True, }, 'gladys': { 'host': 'home.b2moo.fr', 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - 'keep-alive': True, }, 'gladys-home': { 'host': 'gladys.home', 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - 'keep-alive': True, }, 'pimeys': { 'host': 'pimeys.fr', 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cranspasswords-server', ], - 'keep-alive': True, }, } -- GitLab From 0d847ba2a3e63f00661ffcb31b8b85a7a180e386 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 13:46:42 +0200 Subject: [PATCH 08/12] Use logger for all errors --- client.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/client.py b/client.py index efe3015..5578328 100755 --- a/client.py +++ b/client.py @@ -48,7 +48,7 @@ except ImportError: if envspecified is None: if ducktape_display_error: log.error("Va lire le fichier README.") - sys.exit(1) + exit(1) else: # On a spécifié à la main le dossier de conf try: @@ -56,8 +56,8 @@ except ImportError: import clientconfig as config except ImportError: if ducktape_display_error: - sys.stderr.write(u"%s est spécifiée, mais aucune config pour le client ne peut être importée." % (envvar)) - sys.exit(1) + log.error("%s is passed, but no config could be imported" % envvar) + exit(1) #: Pattern utilisé pour détecter la ligne contenant le mot de passe dans les fichiers pass_regexp = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', @@ -899,7 +899,7 @@ def recrypt_files(options, strict=False): filenames = ", ".join(askfiles) message = u"Vous vous apprêtez à rechiffrer les fichiers suivants :\n%s" % filenames if not confirm(options, message + u"\nConfirmer"): - sys.exit(2) + exit(2) # On rechiffre to_put = [{'filename' : f['filename'], 'roles' : f['roles'], @@ -942,24 +942,21 @@ def parse_roles(options, cast=False): ret = set() for role in strroles.split(','): if role not in allroles.keys(): - if not options.quiet: - print((u"Le rôle %s n'existe pas !" % role).encode("utf-8")) - sys.exit(1) + log.warn("role %s do not exists" % role) + exit(1) if role.endswith('-w'): - if not options.quiet: - print((u"Le rôle %s ne devrait pas être utilisé ! (utilisez %s)") - % (role, role[:-2])).encode("utf-8") - sys.exit(1) + log.warn("role %s should not be used, rather use %s" % (role, role[:-2])) + exit(1) ret.add(role) return list(ret) def insult_on_nofilename(options, parser): """Insulte (si non quiet) et quitte si aucun nom de fichier n'a été fourni en commandline.""" if options.fname == None: + log.warn("You need to provide a filename with this command") if not options.quiet: - print(u"Vous devez fournir un nom de fichier avec cette commande".encode("utf-8")) parser.print_help() - sys.exit(1) + exit(1) if __name__ == "__main__": # Gestion des arguments @@ -1055,7 +1052,7 @@ if __name__ == "__main__": logging.addLevelName(logging.DEBUG, "\033[1;37mDEBUG\033[1;0m") logging.basicConfig( level=options.verbose, - format='\033[1;90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' + format='\033[90m%(asctime)s\033[1;0m %(name)s %(levelname)s %(message)s' ) ## On calcule les options qui dépendent des autres. -- GitLab From 2a0cec2ed1becf7b0f5a5d10b95c01e5dd2e0d09 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 14:04:56 +0200 Subject: [PATCH 09/12] Use logger for more warnings --- client.py | 49 +++++++++++++++++++------------------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/client.py b/client.py index 5578328..d58b5ee 100755 --- a/client.py +++ b/client.py @@ -102,8 +102,9 @@ def gpg(options, command, args=None): else: stderr = subprocess.PIPE full_command.extend(['--debug-level=1']) - if options.verbose: - print(" ".join(full_command)) + + # Run gpg + log.info("Running `%s`" % " ".join(full_command)) proc = subprocess.Popen(full_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, @@ -171,10 +172,6 @@ GPG_PARSERS = { u'sub' : _parse_sub, } -def _gpg_printdebug(d): - print("current_pub : %r" % d.get("current_pub", None)) - print("current_sub : %r" % d.get("current_sub", None)) - def parse_keys(gpgout, debug=False): """Parse l'output d'un listing de clés gpg.""" ring = {} @@ -190,19 +187,17 @@ def parse_keys(gpgout, debug=False): line = line.decode("iso8859-1") except UnicodeDecodeError: line = line.decode("iso8859-1", "ignore") - if not options.quiet: - print("gpg raconte de la merde, c'est ni de l'ISO-8859-1 ni de l'UTF-8, je droppe, ça risque de foirer plus tard.", sys.stderr) + log.warning("gpg is telling shit, it is neither ISO-8859-1 nor UTF-8. Dropping!") line = line.split(":") field = line[0] if field in GPG_PARSERS.keys(): - if debug: - print("\nbegin loop. met %s :" % (field)) - _gpg_printdebug(locals()) + log.debug("begin loop, met %s :" % (field)) + log.debug("current_pub : %r" % current_pub) + log.debug("current_sub : %r" % current_sub) try: content = GPG_PARSERS[field](line) except: - print("*** FAILED ***") - print(line) + log.error("*** FAILED *** Line: %s", line) raise if field == u"pub": # Nouvelle clé @@ -235,9 +230,9 @@ def parse_keys(gpgout, debug=False): current_pub[u"subkeys"].append(current_sub) # On place la nouvelle comme sub courant current_sub = content - if debug: - _gpg_printdebug(locals()) - print("parsed object : %r" % content) + log.debug("current_pub : %r" % current_pub) + log.debug("current_sub : %r" % current_sub) + log.debug("parsed object : %r" % content) # À la fin, il faut sauvegarder les derniers (sub, pub) rencontrés, # parce que leur sauvegarde n'a pas encore été déclenchée if current_sub != init_value: @@ -470,8 +465,7 @@ def check_keys(options, recipients=None, quiet=False): if speak: print("") if failed: - if not options.quiet: - print((u"--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)).encode("utf-8")) + log.warn("--> Fail on %s:%s\n--> %s" % (mail, fpr, failed)) if not recipients is None: # On cherche à savoir si on droppe ce recipient message = u"Abandonner le chiffrement pour cette clé ? (Si vous la conservez, il est posible que gpg crashe)" @@ -484,8 +478,7 @@ def check_keys(options, recipients=None, quiet=False): if not drop: trusted_recipients.append(recipient) else: - if not options.quiet: - print(u"Droppe la clé %s:%s" % (fpr, recipient)) + log.warn("Droppe la clé %s:%s" % (fpr, recipient)) else: trusted_recipients.append(recipient) if recipients is None: @@ -536,7 +529,7 @@ def decrypt(options, contents): """Déchiffre le contenu""" stdin, stdout = gpg(options, "decrypt") if type(contents) != unicode: # Kludge (broken db ?) - print("Eau dans le gaz (decrypt)" + repr(contents)) + log.warn("Eau dans le gaz (decrypt)" + repr(contents)) contents = contents[-1] stdin.write(contents.encode("utf-8")) stdin.close() @@ -659,8 +652,7 @@ def show_file(options): fname = options.fname gotit, value = get_file(options, fname) if not gotit: - if not options.quiet: - print(value.encode("utf-8")) # value contient le message d'erreur + log.warn(value) # value contient le message d'erreur return passfile = value (sin, sout) = gpg(options, 'decrypt') @@ -668,7 +660,7 @@ def show_file(options): # Kludge (broken db ?) if type(content) == list: - print("Eau dans le gaz") + log.warn("Eau dans le gaz") content = content[-1] # Déchiffre le contenu @@ -762,8 +754,7 @@ Enregistrez le fichier vide pour annuler.\n""" new_roles = parse_roles(options, cast=True) passfile = {'roles' : new_roles} elif not gotit: - if not options.quiet: - print(value.encode("utf-8")) # value contient le message d'erreur + log.warn(value) return else: passfile = value @@ -782,8 +773,7 @@ Enregistrez le fichier vide pour annuler.\n""" # On vérifie qu'on a le droit actuellement (plutôt que de se faire jeter # plus tard) if not any(r + '-w' in my_roles for r in passfile['roles']): - if not options.quiet: - print(u"Aucun rôle en écriture pour ce fichier ! Abandon.".encode("utf-8")) + log.warn("Aucun rôle en écriture pour ce fichier ! Abandon.") return # On peut vouloir chiffrer un fichier sans avoir la possibilité de le lire @@ -891,8 +881,7 @@ def recrypt_files(options, strict=False): # on affiche le message d'erreur correspondant et on abandonne. for (success, message) in files: if not success: - if not options.quiet: - print(message.encode("utf-8")) + log.warn(message) return # On informe l'utilisateur et on demande confirmation avant de rechiffrer # Si il a précisé --force, on ne lui demandera rien. -- GitLab From ac3b5a98ea0132b56f7f9cbdac63726a6cea2a30 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 14:22:40 +0200 Subject: [PATCH 10/12] Warn on outdated paramiko --- client.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/client.py b/client.py index d58b5ee..b8eba53 100755 --- a/client.py +++ b/client.py @@ -28,7 +28,8 @@ import logging from binascii import hexlify # Import a SSH client -from paramiko.client import SSHClient, MissingHostKeyPolicy +from paramiko.client import SSHClient +from paramiko.ssh_exception import SSHException # Logger local # On n'a pas encore accès à la config donc on devine le nom @@ -265,17 +266,6 @@ class simple_memoize(object): ###### ## Remote commands -class MyMissingHostKeyPolicy(MissingHostKeyPolicy): - """ - Warn when hostkey is unknown and then add - """ - def missing_host_key(self, client, hostname, key): - client._host_keys.add(hostname, key.get_name(), key) - if client._host_keys_filename is not None: - client.save_host_keys(client._host_keys_filename) - log.warning('Adding %s host key for %s: %s' % - (key.get_name(), hostname, hexlify(key.get_fingerprint()))) - @simple_memoize def create_ssh_client(): """ @@ -288,8 +278,12 @@ def create_ssh_client(): # Create SSH client with system host keys and agent client = SSHClient() client.load_system_host_keys() - client.set_missing_host_key_policy(MyMissingHostKeyPolicy()) - client.connect(str(options.serverdata['host'])) + try: + client.connect(str(options.serverdata['host'])) + except SSHException: + log.error("Host key is unknown or you are using a outdated python-paramiko (ssh-ed25519 was implemented in 2017)") + raise + return client def remote_command(options, command, arg=None, stdin_contents=None): @@ -584,11 +578,11 @@ def editor(texte, annotations=u""): def show_files(options): """Affiche la liste des fichiers disponibles sur le serveur distant""" - print(u"Liste des fichiers disponibles :".encode("utf-8")) my_roles, _ = get_my_roles(options) files = all_files(options) keys = files.keys() keys.sort() + print(u"Liste des fichiers disponibles :".encode("utf-8")) for fname in keys: froles = files[fname] access = set(my_roles).intersection(froles) != set([]) -- GitLab From 61dbaacdedcb2b41c3430fd10f168f3a70c33e71 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 16:10:50 +0200 Subject: [PATCH 11/12] Switch to INI configuration format --- client.py | 46 +++++++++++----------------- clientconfig.example.ini | 17 ++++++++++ clientconfig.example.py | 39 ----------------------- clientconfigs/crans/clientconfig.ini | 21 +++++++++++++ clientconfigs/crans/clientconfig.py | 42 ------------------------- clientconfigs/tudor/clientconfig.ini | 21 +++++++++++++ clientconfigs/tudor/clientconfig.py | 31 ------------------- 7 files changed, 77 insertions(+), 140 deletions(-) create mode 100755 clientconfig.example.ini delete mode 100755 clientconfig.example.py create mode 100644 clientconfigs/crans/clientconfig.ini delete mode 100644 clientconfigs/crans/clientconfig.py create mode 100644 clientconfigs/tudor/clientconfig.ini delete mode 100644 clientconfigs/tudor/clientconfig.py diff --git a/client.py b/client.py index b8eba53..354d530 100755 --- a/client.py +++ b/client.py @@ -26,39 +26,30 @@ import datetime import copy import logging from binascii import hexlify +from configparser import ConfigParser # Import a SSH client from paramiko.client import SSHClient from paramiko.ssh_exception import SSHException -# Logger local +# Configuration loading # On n'a pas encore accès à la config donc on devine le nom bootstrap_cmd_name = os.path.split(sys.argv[0])[1] -log = logging.getLogger(bootstrap_cmd_name) - -# Import de la config -envvar = "CRANSPASSWORDS_CLIENT_CONFIG_DIR" -try: - sys.path.append(os.path.expanduser("~/.config/%s/" % (bootstrap_cmd_name,))) - import clientconfig as config -except ImportError: +default_config_path = os.path.expanduser("~/.config/" + bootstrap_cmd_name) +config_path = os.getenv("CRANSPASSWORDS_CLIENT_CONFIG_DIR", default_config_path) +config = ConfigParser() +if not config.read(config_path + "/clientconfig.ini"): + # If config could not be imported, display an error if required ducktape_display_error = sys.stderr.isatty() and \ not any([opt in sys.argv for opt in ["-q", "--quiet"]]) and \ __name__ == '__main__' - envspecified = os.getenv(envvar, None) - if envspecified is None: - if ducktape_display_error: - log.error("Va lire le fichier README.") - exit(1) - else: - # On a spécifié à la main le dossier de conf - try: - sys.path.append(envspecified) - import clientconfig as config - except ImportError: - if ducktape_display_error: - log.error("%s is passed, but no config could be imported" % envvar) - exit(1) + if ducktape_display_error: + # Do not use logger as it has not been initialized yet + print("%s/clientconfig.ini could not be read. Please read README." % config_path) + exit(1) + +# Logger local +log = logging.getLogger(bootstrap_cmd_name) #: Pattern utilisé pour détecter la ligne contenant le mot de passe dans les fichiers pass_regexp = re.compile('[\t ]*pass(?:word)?[\t ]*:[\t ]*(.*)\r?\n?$', @@ -290,14 +281,13 @@ def remote_command(options, command, arg=None, stdin_contents=None): """ Execute remote command and return output """ - # TODO: instantiate client outside to have only one connection client = create_ssh_client() # Build command if not "remote_cmd" in options.serverdata: log.error("Missing parameter `remote_cmd` in active server configuration") exit(1) - remote_cmd = " ".join(options.serverdata['remote_cmd'] + [command]) + remote_cmd = options.serverdata['remote_cmd'] + " " + command if arg: remote_cmd += " " + arg @@ -612,7 +602,7 @@ def show_roles(options): def show_servers(options): """Affiche la liste des serveurs disponibles""" print(u"Liste des serveurs disponibles".encode("utf-8")) - for server in config.servers.keys(): + for server in config.keys(): print((u" * " + server).encode("utf-8")) def saveclipboard(restore=False, old_clipboard=None): @@ -958,7 +948,7 @@ if __name__ == "__main__": default=False, help="silent mode: hide errors, overrides verbosity" ) - parser.add_argument('-s', '--server', default='default', + parser.add_argument('-s', '--server', default='DEFAULT', help="Utilisation d'un serveur alternatif (test, backup, etc)") parser.add_argument('--drop-invalid', action='store_true', default=False, dest='drop_invalid', @@ -1048,7 +1038,7 @@ if __name__ == "__main__": if options.clipboard is None: options.clipboard = bool(os.getenv('DISPLAY')) and os.path.exists('/usr/bin/xclip') # On récupère les données du serveur à partir du nom fourni - options.serverdata = config.servers[options.server] + options.serverdata = config[options.server] # On parse les roles fournis, et il doivent exister, ne pas être -w… # parse_roles s'occupe de ça # NB : ça nécessite de se connecter au serveur, or, pour show_servers on n'en a pas besoin diff --git a/clientconfig.example.ini b/clientconfig.example.ini new file mode 100755 index 0000000..40c5aaa --- /dev/null +++ b/clientconfig.example.ini @@ -0,0 +1,17 @@ +#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. +#: +#: Sans précision du paramètre --server, la clé `DEFAULT` sera utilisée. +#: +#: `host` est l'ip ou le nom de domaine sur lequel se connecter +#: `remote_cmd' est la commande executée sur le serveur. +[DEFAULT] +host=odlyd.crans.org +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[localhost] +host=localhost +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[ovh] +host=soyouz.crans.org +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server diff --git a/clientconfig.example.py b/clientconfig.example.py deleted file mode 100755 index 78b061f..0000000 --- a/clientconfig.example.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- - -""" Configuration du client cranspasswords """ - -import os - -#: Pour override le nom si vous voulez renommer la commande -cmd_name = 'cranspasswords' - -#: Path du binaire ssh sur la machine client -ssh_path = '/usr/bin/ssh' - -#: Path du script ``cmd_name``-server sur le serveur -server_path = '/usr/local/bin/%s-server' % (cmd_name,) - -#: Commande à exécuter sur le serveur après y être entré en ssh -distant_cmd = ["sudo", '-n', server_path] - -#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. -#: -#: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. -#: -#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. -servers = { - 'default': { - 'host': 'odlyd.crans.org', - 'remote_cmd': distant_cmd, - }, - # Utile pour tester - 'localhost': { - 'host': 'localhost', - 'remote_cmd': distant_cmd, - }, - 'ovh': { - 'host': 'soyouz.crans.org', - 'remote_cmd': distant_cmd, - } -} diff --git a/clientconfigs/crans/clientconfig.ini b/clientconfigs/crans/clientconfig.ini new file mode 100644 index 0000000..68d98cf --- /dev/null +++ b/clientconfigs/crans/clientconfig.ini @@ -0,0 +1,21 @@ +#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. +#: +#: Sans précision du paramètre --server, la clé `DEFAULT` sera utilisée. +#: +#: `host` est l'ip ou le nom de domaine sur lequel se connecter +#: `remote_cmd' est la commande executée sur le serveur. +[DEFAULT] +host=odlyd.crans.org +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[titanic] +host=titanic +remote_cmd=ssh odlyd.crans.org sudo -n /usr/local/bin/cranspasswords-server + +[localhost] +host=localhost +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server + +[ovh] +host=soyouz.crans.org +remote_cmd=sudo -n /usr/local/bin/cpasswords-server diff --git a/clientconfigs/crans/clientconfig.py b/clientconfigs/crans/clientconfig.py deleted file mode 100644 index 1e22826..0000000 --- a/clientconfigs/crans/clientconfig.py +++ /dev/null @@ -1,42 +0,0 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- - -""" Configuration du client cranspasswords """ - -import os - -#: Pour override le nom si vous voulez renommer la commande -cmd_name = 'cranspasswords' - -#: Path du script ``cmd_name``-server sur le serveur -server_path = '/usr/local/bin/%s-server' % (cmd_name,) - -#: Commande à exécuter sur le serveur après y être entré en ssh -distant_cmd = ["sudo", '-n', server_path] -print distant_cmd - -#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. -#: -#: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. -#: -#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. -servers = { - 'default': { - 'host': 'odlyd.crans.org', - 'remote_cmd': distant_cmd, - }, - 'titanic': { - 'host': 'freebox.crans.org', - # manual ssh jump - 'remote_cmd': ['ssh', 'odyld.crans.org'] + distant_cmd, - }, - # Utile pour tester - 'localhost': { - 'host': 'localhost', - 'remote_cmd': distant_cmd, - }, - 'ovh': { - 'host': 'soyouz.crans.org', - 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cpasswords-server'], - } -} diff --git a/clientconfigs/tudor/clientconfig.ini b/clientconfigs/tudor/clientconfig.ini new file mode 100644 index 0000000..b553b44 --- /dev/null +++ b/clientconfigs/tudor/clientconfig.ini @@ -0,0 +1,21 @@ +#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. +#: +#: Sans précision du paramètre --server, la clé `DEFAULT` sera utilisée. +#: +#: `host` est l'ip ou le nom de domaine sur lequel se connecter +#: `remote_cmd' est la commande executée sur le serveur. +[DEFAULT] +host= +remote_cmd=/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server + +[gladys] +host=home.b2moo.fr +remote_cmd=/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server + +[gladys-home] +host=gladys.home +remote_cmd=/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server + +[pimeys] +host=pimeys.fr +remote_cmd=sudo -n /usr/local/bin/cranspasswords-server diff --git a/clientconfigs/tudor/clientconfig.py b/clientconfigs/tudor/clientconfig.py deleted file mode 100644 index 3f3e688..0000000 --- a/clientconfigs/tudor/clientconfig.py +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env python2 -# -*- encoding: utf-8 -*- - -""" Configuration du client cranspasswords """ - -import os - - -#: Liste des serveurs sur lesquels ont peut récupérer des mots de passe. -#: -#: Sans précision du paramètre --server, la clé ``'default'`` sera utilisée. -#: -#: * ``'remote_cmd'`` : La commande exécutée sur le serveur. -servers = { - 'default': { - 'host': '', - 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - }, - 'gladys': { - 'host': 'home.b2moo.fr', - 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - }, - 'gladys-home': { - 'host': 'gladys.home', - 'remote_cmd': ['/home/dstan/cranspasswords/serverconfigs/tudor/cpasswords-server', ], - }, - 'pimeys': { - 'host': 'pimeys.fr', - 'remote_cmd': ['sudo', '-n', '/usr/local/bin/cranspasswords-server', ], - }, -} -- GitLab From d06ddf5b9cfaefdde9f84dbc017c026ff43378b5 Mon Sep 17 00:00:00 2001 From: Alexandre Iooss Date: Sun, 12 Apr 2020 16:23:48 +0200 Subject: [PATCH 12/12] Update CHANGELOG --- CHANGELOG | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 366ac4a..5252e24 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -12,6 +12,17 @@ cranspasswords possède plusieurs branches. * 0.1, 0.2,… : anciennes versions (si vieux serveur), ça n'intègre plus de nouvelles fonctionnalités, seulement d'éventuels bugfix. +=== 0.2.0 === + +La configuration du client a changé de format, +il faut donc repartir du `clientconfig.example.ini`. + +Les nouveaux clients utilisent Paramiko pour réaliser la connexion SSH. +Cela permet de garder une unique session SSH ouverte. + +Le module logging de Python est maintenant utilisé. +Vous pouvez augmenter sa verbosité avec `-vv`. + === 0.1.5 === ''Pour voir cette version, git show 0.1.5'' -- GitLab