
C'était plus pratique que de faire un merge de la branche devel-keep-alive. La feature n'est pas activée automatiquement pour les gens qui ont déjà un fichier de config, mais elle est présente pour ceux qui en crée un.
269 lines
8.8 KiB
Python
Executable file
269 lines
8.8 KiB
Python
Executable file
#!/usr/bin/env python
|
|
# -*- encoding: utf-8 -*-
|
|
|
|
"""Serveur pour cranspasswords"""
|
|
|
|
from __future__ import print_function
|
|
|
|
import glob
|
|
import os
|
|
import pwd
|
|
import sys
|
|
import json
|
|
import smtplib
|
|
import datetime
|
|
from email.mime.text import MIMEText
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
# Même problème que pour le client, il faut bootstraper le nom de la commande
|
|
# Pour accéder à la config
|
|
cmd_name = os.path.split(sys.argv[0])[1].replace("-server", "")
|
|
sys.path.append("/etc/%s/" % (cmd_name,))
|
|
import serverconfig
|
|
|
|
MYUID = pwd.getpwuid(os.getuid())[0]
|
|
if MYUID == 'root':
|
|
MYUID = os.environ['SUDO_USER']
|
|
|
|
## Fonctions internes au serveur
|
|
|
|
def validate(roles, mode='r'):
|
|
"""Vérifie que l'appelant appartient bien aux roles précisés
|
|
Si mode mode='w', recherche un rôle en écriture
|
|
"""
|
|
for role in roles:
|
|
if mode == 'w':
|
|
role += '-w'
|
|
if serverconfig.ROLES.has_key(role) and MYUID in serverconfig.ROLES[role]:
|
|
return True
|
|
return False
|
|
|
|
def getpath(filename, backup=False):
|
|
"""Récupère le chemin du fichier ``filename``"""
|
|
return os.path.join(serverconfig.STORE, '%s.%s' % (filename, 'bak' if backup else 'json'))
|
|
|
|
def writefile(filename, contents):
|
|
"""Écrit le fichier avec les bons droits UNIX"""
|
|
os.umask(0077)
|
|
f = open(filename, 'w')
|
|
f.write(contents.encode("utf-8"))
|
|
f.close()
|
|
|
|
class server_command(object):
|
|
"""
|
|
Une instance est un décorateur pour la fonction servant de commande
|
|
externe du même nom"""
|
|
|
|
#: nom de la commande
|
|
name = None
|
|
|
|
#: fonction wrappée
|
|
decorated = None
|
|
|
|
#: (static) dictionnaire name => fonction
|
|
by_name = {}
|
|
|
|
#: rajoute un argument en fin de fonction à partir de stdin (si standalone)
|
|
stdin_input = False
|
|
|
|
#: Est-ce que ceci a besoin d'écrire ?
|
|
write = False
|
|
|
|
def __init__(self, name, stdin_input = False, write=False):
|
|
"""
|
|
* ``name`` nom de l'action telle qu'appelée par le client
|
|
* ``stdin_input`` si True, stdin sera lu en mode non-keepalive, et
|
|
remplira le dernier argument de la commande.
|
|
* ``write`` s'agit-il d'une commande en écriture ?
|
|
"""
|
|
self.name = name
|
|
self.stdin_input = stdin_input
|
|
self.write = write
|
|
server_command.by_name[name] = self
|
|
|
|
def __call__(self, fun):
|
|
self.decorated = fun
|
|
return fun
|
|
|
|
## Fonction exposées par le serveur
|
|
@server_command('keep-alive')
|
|
def keepalive():
|
|
""" Commande permettant de réaliser un tunnel json (un datagramme par ligne)
|
|
Un message entre le client et le serveur consiste en l'échange de dico
|
|
|
|
Message du client: {'action': "nom_de_l'action",
|
|
'args': liste_arguments_passes_a_la_fonction}
|
|
Réponse du serveur: {'status': 'ok',
|
|
'content': retour_de_la_fonction,
|
|
}
|
|
|
|
"""
|
|
for line in iter(sys.stdin.readline, ''):
|
|
data = json.loads(line.rstrip())
|
|
try:
|
|
# Une action du protocole = de l'ascii
|
|
action = data['action'].encode('ascii')
|
|
content = server_command.by_name[action].decorated(*data['args'])
|
|
status = u'ok'
|
|
except Exception as e:
|
|
status = u'error'
|
|
content = repr(e)
|
|
out = {
|
|
'status': status,
|
|
'content': content,
|
|
}
|
|
print(json.dumps(out, encoding='utf-8'))
|
|
sys.stdout.flush()
|
|
|
|
@server_command('listroles')
|
|
def listroles():
|
|
"""Liste des roles existant et de leurs membres.
|
|
Renvoie également un rôle particulier ``"whoami"``, contenant l'username de l'utilisateur qui s'est connecté."""
|
|
d = serverconfig.ROLES
|
|
if d.has_key("whoami"):
|
|
raise ValueError('La rôle "whoami" ne devrait pas exister')
|
|
d["whoami"] = MYUID
|
|
return d
|
|
|
|
@server_command('listkeys')
|
|
def listkeys():
|
|
"""Liste les usernames et les (mail, fingerprint) correspondants"""
|
|
return serverconfig.KEYS
|
|
|
|
@server_command('listfiles')
|
|
def listfiles():
|
|
"""Liste les fichiers dans l'espace de stockage, et les roles qui peuvent y accéder"""
|
|
os.chdir(serverconfig.STORE)
|
|
|
|
filenames = glob.glob('*.json')
|
|
files = {}
|
|
for filename in filenames:
|
|
file_dict = json.loads(open(filename).read())
|
|
files[filename[:-5]] = file_dict["roles"]
|
|
return files
|
|
|
|
@server_command('getfile')
|
|
def getfile(filename):
|
|
"""Récupère le fichier ``filename``"""
|
|
filepath = getpath(filename)
|
|
try:
|
|
obj = json.loads(open(filepath).read())
|
|
if not validate(obj['roles']):
|
|
return [False, u"Vous n'avez pas les droits de lecture sur le fichier %s." % filename]
|
|
obj["filename"] = filename
|
|
return [True, obj]
|
|
except IOError:
|
|
return [False, u"Le fichier %s n'existe pas." % filename]
|
|
|
|
@server_command('getfiles', stdin_input=True)
|
|
def getfiles(filenames):
|
|
"""Récupère plusieurs fichiers, lit la liste des filenames demandés sur stdin"""
|
|
return [getfile(f) for f in filenames]
|
|
|
|
# TODO ça n'a rien à faire là, à placer plus haut dans le code
|
|
def _putfile(filename, roles, contents):
|
|
"""Écrit ``contents`` avec les roles ``roles`` dans le fichier ``filename``"""
|
|
gotit, old = getfile(filename)
|
|
if not gotit:
|
|
old = u"[Création du fichier]"
|
|
pass
|
|
else:
|
|
oldroles = old['roles']
|
|
if not validate(oldroles, 'w'):
|
|
return [False, u"Vous n'avez pas le droit d'écriture sur %s." % filename]
|
|
|
|
corps = u"Le fichier %s a été modifié par %s." % (filename, MYUID)
|
|
backup(corps, filename, old)
|
|
notification(u"Modification de %s" % filename, corps, filename, old)
|
|
|
|
filepath = getpath(filename)
|
|
writefile(filepath, json.dumps({'roles': roles, 'contents': contents}))
|
|
return [True, u"Modification effectuée."]
|
|
|
|
@server_command('putfile', stdin_input=True, write=True)
|
|
def putfile(filename, parsed_stdin):
|
|
"""Écrit le fichier ``filename`` avec les données reçues sur stdin."""
|
|
try:
|
|
roles = parsed_stdin['roles']
|
|
contents = parsed_stdin['contents']
|
|
except KeyError:
|
|
return [False, u"Entrée invalide"]
|
|
return _putfile(filename, roles, contents)
|
|
|
|
@server_command('putfiles', stdin_input=True, write=True)
|
|
def putfiles(parsed_stdin):
|
|
"""Écrit plusieurs fichiers. Lit les filenames sur l'entrée standard avec le reste."""
|
|
results = []
|
|
for fichier in parsed_stdin:
|
|
try:
|
|
filename = fichier['filename']
|
|
roles = fichier['roles']
|
|
contents = fichier['contents']
|
|
except KeyError:
|
|
results.append([False, u"Entrée invalide"])
|
|
else:
|
|
results.append(_putfile(filename, roles, contents))
|
|
return results
|
|
|
|
@server_command('rmfile', write=True)
|
|
def rmfile(filename):
|
|
"""Supprime le fichier filename après avoir vérifié les droits sur le fichier"""
|
|
gotit, old = getfile(filename)
|
|
if not gotit:
|
|
return old # contient le message d'erreur
|
|
roles = old['roles']
|
|
if validate(roles, 'w'):
|
|
corps = u"Le fichier %s a été supprimé par %s." % (filename, MYUID)
|
|
backup(corps, filename, old)
|
|
notification(u"Suppression de %s" % filename, corps, filename, old)
|
|
os.remove(getpath(filename))
|
|
else:
|
|
return u"Vous n'avez pas les droits d'écriture sur le fichier %s." % filename
|
|
return u"Suppression effectuée"
|
|
|
|
|
|
# TODO monter plus haut
|
|
def backup(corps, fname, old):
|
|
"""Backupe l'ancienne version du fichier"""
|
|
os.umask(0077)
|
|
back = open(getpath(fname, backup=True), 'a')
|
|
back.write(json.dumps(old))
|
|
back.write('\n')
|
|
back.write((u'* %s: %s\n' % (str(datetime.datetime.now()), corps)).encode("utf-8"))
|
|
back.close()
|
|
|
|
# TODO monter plus haut
|
|
def notification(subject, corps, fname, old):
|
|
"""Envoie par mail une notification de changement de fichier"""
|
|
conn = smtplib.SMTP('localhost')
|
|
frommail = serverconfig.CRANSP_MAIL
|
|
tomail = serverconfig.DEST_MAIL
|
|
msg = MIMEMultipart(_charset="utf-8")
|
|
msg['Subject'] = subject
|
|
msg['X-Mailer'] = serverconfig.cmd_name.decode()
|
|
msg['From'] = serverconfig.CRANSP_MAIL
|
|
msg['To'] = serverconfig.DEST_MAIL
|
|
msg.preamble = u"%s report" % (serverconfig.cmd_name.decode(),)
|
|
info = MIMEText(corps +
|
|
u"\nLa version précédente a été sauvegardée." +
|
|
u"\n\n-- \nCranspasswords.py", _charset="utf-8")
|
|
msg.attach(info)
|
|
conn.sendmail(frommail, tomail, msg.as_string())
|
|
conn.quit()
|
|
|
|
if __name__ == "__main__":
|
|
argv = sys.argv[0:]
|
|
command_name = argv[1]
|
|
|
|
command = server_command.by_name[command_name]
|
|
if serverconfig.READONLY and command.write:
|
|
raise IOError("Ce serveur est read-only.")
|
|
|
|
args = argv[2:]
|
|
# On veut des unicode partout
|
|
args = [ s.decode('utf-8') for s in args ]
|
|
if command.stdin_input:
|
|
args.append(json.loads(sys.stdin.read()))
|
|
answer = command.decorated(*args)
|
|
if answer is not None:
|
|
print(json.dumps(answer))
|