Générateur de mots de passe
Mots de passe aléatoires, passphrases et mots de passe temporaires
diff --git a/.gitignore b/.gitignore
index dce6236..b59570c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,10 @@ cmdb/snapshots/
# Certificats générés (contiennent des clés privées)
cert-generator/LISTS_ALL_CERT/
+# Images et fichiers importés dans les templates email (contenu utilisateur)
+password-generator/images/
+password-generator/files/
+
# === Environnement / build ===
.venv/
__pycache__/
diff --git a/README.md b/README.md
index 4298614..54f279d 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,9 @@ Portail d'outils pour administrateurs systèmes et réseau, servi par un serveur
- **cmdb** — inventaire (machines, réseaux, IP/MAC, salles & baies), recherche Active Directory, import/export CSV, snapshots de la base
- **cert-generator** — génération de certificats SSL (clés RSA, CSR)
- **switch-config** — mapper de VLAN et export de configuration switch
+- **password-generator** — génération de mots de passe aléatoires, passphrases et mots de passe temporaires (préfixe configurable, envoi par mail via l'AD + templates)
-Le tout accessible depuis un seul serveur, avec navigation entre les 3 apps.
+Le tout accessible depuis un seul serveur, avec navigation entre les apps.
## Prérequis
diff --git a/cert-generator/dashboard.html b/cert-generator/dashboard.html
index e64b99d..f527804 100644
--- a/cert-generator/dashboard.html
+++ b/cert-generator/dashboard.html
@@ -184,6 +184,7 @@
Mots de passe aléatoires, passphrases et mots de passe temporaires
par des data
+ URI (lecture du fichier sur disque). Les images data:/http(s): sont laissées
+ telles quelles. Rend le mail HTML autonome au moment de l'envoi."""
+
+ def repl(m):
+ fname = os.path.basename(m.group(2)) # anti-traversal
+ path = os.path.join(PWGEN_IMG, fname)
+ if not os.path.isfile(path):
+ return m.group(0)
+ mime = mimetypes.guess_type(path)[0] or "application/octet-stream"
+ with open(path, "rb") as f:
+ data = base64.b64encode(f.read()).decode("ascii")
+ return 'src="data:%s;base64,%s"' % (mime, data)
+
+ return _LOCAL_IMG_SRC.sub(repl, html)
+
+
+def attach_files(msg, urls):
+ """Joint au mail les fichiers du dossier password-generator/files/ dont l'URL
+ figure dans la liste `urls` (pièces jointes du template). Le nom présenté au
+ destinataire est le nom d'origine (suffixe aléatoire de stockage retiré)."""
+ seen = set()
+ for url in urls or []:
+ fname = os.path.basename(str(url)) # anti-traversal
+ if not fname or fname in seen:
+ continue
+ seen.add(fname)
+ path = os.path.join(PWGEN_FILES, fname)
+ if not os.path.isfile(path):
+ continue
+ ctype = mimetypes.guess_type(path)[0] or "application/octet-stream"
+ maintype, _, subtype = ctype.partition("/")
+ disp = re.sub(r"-[0-9a-f]{8}(\.[^.]+)$", r"\1", fname) # nom d'origine lisible
+ with open(path, "rb") as f:
+ msg.add_attachment(
+ f.read(), maintype=maintype, subtype=subtype or "octet-stream", filename=disp
+ )
+
+
class Handler(SimpleHTTPRequestHandler):
def log_message(self, *a):
pass
@@ -106,6 +286,10 @@ class Handler(SimpleHTTPRequestHandler):
return self._send_db(200, C.db_bytes(), C.db_version())
if self.path == "/db/version":
return self._json({"version": C.db_version()})
+ # VLANs = réseaux CMDB (table networks) → consommés par le mapper switch
+ if self.path == "/api/vlans":
+ with C.DB_LOCK:
+ return self._json(read_vlans())
if self.path == "/db/snapshots":
return self._json({"snapshots": C.list_snapshots()})
if self.path.startswith("/db/snapshot?"):
@@ -177,6 +361,23 @@ class Handler(SimpleHTTPRequestHandler):
)
except Exception as ex:
return self._json({"error": str(ex)}, 500)
+ # envoi de mail (générateur de mots de passe)
+ if self.path == "/mail/send":
+ try:
+ return self.mail_send()
+ except Exception as e:
+ return self._err(e, 500)
+ # upload d'image / de fichier joint (générateur de mots de passe)
+ if self.path == "/pwgen/upload-image":
+ try:
+ return self.pwgen_upload_image()
+ except Exception as e:
+ return self._err(e, 500)
+ if self.path == "/pwgen/upload-file":
+ try:
+ return self.pwgen_upload_file()
+ except Exception as e:
+ return self._err(e, 500)
# API certs (écriture)
api = self._cert_api()
try:
@@ -192,49 +393,113 @@ class Handler(SimpleHTTPRequestHandler):
return self._err(e, 500)
self._err("route inconnue: %s" % self.path, 404)
+ # ---------- mail (générateur de mots de passe) ----------
+ def mail_send(self):
+ """Envoie un mail texte via le serveur SMTP fourni par la page.
+
+ Body JSON : { smtp: {host, port, security, user, pass, from, from_name},
+ to, subject, body }
+ security : none | starttls | ssl
+ """
+ d = self._body()
+ smtp = d.get("smtp") or {}
+ to = (d.get("to") or "").strip()
+ host = (smtp.get("host") or "").strip()
+ if not to or "@" not in to:
+ raise ValueError("destinataire invalide: %r" % to)
+ if not host:
+ raise ValueError("serveur SMTP non configuré")
+ security = (smtp.get("security") or "starttls").lower()
+ try:
+ port = int(smtp.get("port") or 0)
+ except (TypeError, ValueError):
+ port = 0
+ if not port:
+ port = {"ssl": 465, "starttls": 587}.get(security, 25)
+ sender = (smtp.get("from") or smtp.get("user") or "").strip()
+ if not sender:
+ raise ValueError("adresse expéditeur non configurée")
+
+ msg = EmailMessage()
+ from_name = (smtp.get("from_name") or "").strip()
+ msg["From"] = formataddr((from_name, sender)) if from_name else sender
+ msg["To"] = to
+ msg["Subject"] = d.get("subject") or ""
+ msg.set_content(d.get("body") or "")
+ html = (d.get("html") or "").strip()
+ if html:
+ # les images de templates stockées sur disque sont ré-intégrées en
+ # data URI pour que le mail reçu soit autonome (pas de lien vers le
+ # serveur, images visibles hors du réseau).
+ html = inline_local_images(html)
+ msg.add_alternative(html, subtype="html")
+ # pièces jointes du template (ex : .rdp de connexion)
+ attach_files(msg, d.get("attachments"))
+
+ if security == "ssl":
+ server = smtplib.SMTP_SSL(host, port, timeout=15)
+ else:
+ server = smtplib.SMTP(host, port, timeout=15)
+ try:
+ if security == "starttls":
+ server.starttls()
+ user = (smtp.get("user") or "").strip()
+ if user:
+ server.login(user, smtp.get("pass") or "")
+ server.send_message(msg)
+ finally:
+ try:
+ server.quit()
+ except Exception:
+ pass
+ self._json({"ok": True, "to": to})
+
+ # ---------- uploads de templates (disque réel) ----------
+ def _save_upload(self, dest_dir, url_prefix, max_bytes, default_ext, kind):
+ """Stocke un fichier posté ({name, b64}) sur le disque avec un nom sûr
+ (radical assaini + suffixe aléatoire) et renvoie son URL servie."""
+ d = self._body()
+ raw = (d.get("name") or kind).strip()
+ try:
+ data = base64.b64decode(d.get("b64") or "", validate=True)
+ except (ValueError, TypeError):
+ raise ValueError("%s invalide (base64)" % kind)
+ if not data:
+ raise ValueError("%s vide" % kind)
+ if len(data) > max_bytes:
+ raise ValueError("%s trop lourd (max %d Mo)" % (kind, max_bytes // (1024 * 1024)))
+ base, _, ext = raw.rpartition(".")
+ ext = re.sub(r"[^A-Za-z0-9]", "", ext).lower()[:8] or default_ext
+ stem = re.sub(r"[^A-Za-z0-9._-]", "_", base or kind)[:40].strip("._-") or kind
+ fname = "%s-%s.%s" % (stem, os.urandom(4).hex(), ext)
+ os.makedirs(dest_dir, exist_ok=True)
+ with open(os.path.join(dest_dir, fname), "wb") as f:
+ f.write(data)
+ self._json({"ok": True, "url": url_prefix + fname, "name": fname})
+
+ def pwgen_upload_image(self):
+ """Image de template → password-generator/images/ (ré-intégrée en data URI à l'envoi)."""
+ self._save_upload(PWGEN_IMG, PWGEN_IMG_URL, MAX_IMG_BYTES, "png", "image")
+
+ def pwgen_upload_file(self):
+ """Fichier joignable (rdp, pdf…) → password-generator/files/ (joint au mail à l'envoi)."""
+ self._save_upload(PWGEN_FILES, PWGEN_FILES_URL, MAX_FILE_BYTES, "bin", "fichier")
+
# ---------- certificats (disque réel) ----------
def cert_list(self):
ensure_work()
- certs, archives = [], []
- for entry in sorted(os.listdir(WORK)):
- full = os.path.join(WORK, entry)
- if entry == "archives":
- if os.path.isdir(full):
- for a in sorted(os.listdir(full)):
- af = os.path.join(full, a)
- if os.path.isfile(af):
- st = os.stat(af)
- archives.append(
- {
- "name": a,
- "size": st.st_size,
- "mtime": int(st.st_mtime * 1000),
- }
- )
- continue
- if not os.path.isdir(full):
- continue
- files, date = [], 0
- for fn in sorted(os.listdir(full)):
- ff = os.path.join(full, fn)
- if not os.path.isfile(ff):
- continue
- st = os.stat(ff)
- m = int(st.st_mtime * 1000)
- date = max(date, m)
- files.append(
- {
- "name": fn,
- "ext": (fn.rsplit(".", 1)[-1] if "." in fn else "").lower(),
- "size": st.st_size,
- "mtime": m,
- }
- )
- if files:
- certs.append({"dir": entry, "files": files, "date": date})
- archives.sort(key=lambda a: -a["mtime"])
- certs.sort(key=lambda c: -c["date"])
- return {"ok": True, "certs": certs, "archives": archives}
+ sig = _cert_signature()
+ with _CERT_CACHE_LOCK:
+ if (
+ sig is not None
+ and sig == _CERT_CACHE["sig"]
+ and _CERT_CACHE["payload"] is not None
+ ):
+ return _CERT_CACHE["payload"] # inchangé → réponse mémorisée
+ payload = _build_cert_list()
+ _CERT_CACHE["sig"] = sig
+ _CERT_CACHE["payload"] = payload
+ return payload
def cert_write_text(self):
d = self._body()
@@ -249,6 +514,7 @@ class Handler(SimpleHTTPRequestHandler):
newline="",
) as f:
f.write(content)
+ invalidate_cert_cache()
self._json({"ok": True, "path": "LISTS_ALL_CERT/%s/" % dname})
def cert_write_bin(self):
@@ -260,6 +526,7 @@ class Handler(SimpleHTTPRequestHandler):
os.makedirs(os.path.dirname(target), exist_ok=True)
with open(target, "wb") as f:
f.write(base64.b64decode(d["b64"]))
+ invalidate_cert_cache()
self._json({"ok": True, "path": "LISTS_ALL_CERT/%s" % "/".join(parts)})
def cert_delete_dir(self):
@@ -270,10 +537,12 @@ class Handler(SimpleHTTPRequestHandler):
shutil.rmtree(target)
if d.get("archives"):
self._rm_archives(dname)
+ invalidate_cert_cache()
self._json({"ok": True})
def cert_delete_archives(self):
self._rm_archives(safe_name(self._body()["dir"]))
+ invalidate_cert_cache()
self._json({"ok": True})
def _rm_archives(self, dname):