Node-red file repository

De knowledge
Aller à la navigation Aller à la recherche

Objectif

Mettre en place un petit service de partage de fichiers :

  • Upload protégé par login/mot de passe.
  • Téléchargement public via une URL.
  • Noms des fichiers pseudonymisés.
  • Fichiers servis directement par Node-RED.

Architecture :

Internet
    |
    v
Reverse Proxy
    |
    v
Node-RED (main-host)
    |
    +--- Upload via flow Node-RED
    |
    +--- Fichiers statiques (/var/www/static-files)

Installation des modules

Copilot m'avait proposer d'Installer "à ma main" :

node-red-contrib-http-multipart
@alexandrainst/node-red-http-basic-auth

Le premier permet de recevoir des formulaires HTML multipart/form-data.

Le second ajoute une authentification HTTP Basic.

Pour le coup l'interface graphique de node-red le permet tout aussi bien.

Dans le menu en haut a droite on a l'option "Gérer la palette". On a l'impression que c'est juste de la GUI mais non on passe par là pour ajouter des modules.

On clique sur "Gérer la palette" et on a une fenêtre de sélection.

On choisit l'onglet "Installer" et on recherche node-red-contrib-http-multipart.

Dans le screenshot ci-dessus le bouton "Installé" est grisé mais si le module n'était pas installé on aurait un bouton installer. Il suffit de cliquer dessus et c'est fait.

Répertoire de stockage

Créer un répertoire destiné à contenir les fichiers :

sudo mkdir -p /var/www/static-files/
sudo mkdir -p /var/www/static-files/assets
sudo chown nodered:nodered /var/www/static-files/assets

Adapter l'utilisateur si nécessaire.

Activation du serveur statique Node-RED

Modifier :

~/.node-red/settings.js

Décommenter et adapter :

httpStatic: [
    {
        path: "/var/www/static-files/assets",
        root: "/files/"
    }
],

Redémarrer Node-RED :

sudo systemctl restart nodered

Test :

echo hello > /var/www/static-files/test.txt

Puis ouvrir :

https://node-red.mon-domaine.fr/files/test.txt

Si le texte "hello" apparaît, le serveur statique fonctionne.

Page d'upload

URL :

https://node-red.mon-domaine.fr/megaupload/

HTML :

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>MegaUpload</title>

<style>
body {
    font-family: Arial, Helvetica, sans-serif;
    background: #f4f7fb;
}

.container {
    max-width: 700px;
    margin: 80px auto;
    background: white;
    padding: 40px;
    border-radius: 16px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}

h1 {
    text-align: center;
}

.upload-box {
    text-align: center;
}

button {
    background: #2563eb;
    color: white;
    border: 0;
    padding: 12px 30px;
    border-radius: 8px;
    cursor: pointer;
}
</style>
</head>

<body>

<div class="container">

<h1>📤 MegaUpload</h1>

<div class="upload-box">

pload/upload"
      method="post"
      enctype="multipart/form-data">

    <input type="file"
           name="fichier"
           required>

    <button type="submit">
        Envoyer le fichier
    </button>

</form>

</div>

</div>

</body>
</html>

Que l'on rangera dans /var/www/static-files/upload.html

Protection par mot de passe

Ajouter un noeud :

http basic auth

devant :

  • GET /megaupload/
  • POST /megaupload/upload

Ainsi :

[http in]
      |
      v
[http basic auth]
      |
      v
[template]
      |
      v
[http response]

Utilisateur unique

Configurer directement :

Utilisateur : upload
Mot de passe : xxxx

Fichier d'utilisateurs

Je penses que c'est la seule solution acceptable car les mots de passes ne sont pas "en clair"

Format :

jean:motdepasse
alice:motdepasse

Ou mieux, avec mots de passe bcrypt.

Exemple :

jean:$2y$10$...
alice:$2y$10$...

J'ai donc crée la chaine de noeuds:

C'est tres simple :

  • Ou expose l'url /megaupload (noeud index)
  • au authentifie l'utilisateur
  • on lit le fichier /var/www/static-files/upload.html
  • On le renvoie au client

Réception du fichier

Flow :

[httpInMultipart]
        |
        v
[http basic auth]
        |
        v
[function]
        |
        v
[exec]
        |
        v
[template]
        |
        v
[http response]

Configuration HTTP Multipart :

[
  {
    "name": "fichier",
    "maxCount": 1
  }
]

En effet, dans la page d'upload on ne propose qu'un seul fichier. SI on veut en envoyer plusieurs, soit on refait l'opération soit un fait un zip ou un tgz.

Pseudonymisation du nom de fichier

Function :

const f = msg.req.files.fichier[0];

// Extension
const pos = f.originalname.lastIndexOf(".");
const ext = (pos >= 0)
    ? f.originalname.substring(pos).toLowerCase()
    : "";

// Identifiant unique
const id =
    Date.now().toString(36) +
    "-" +
    Math.random().toString(36).substring(2, 10);

// Nom stocké
msg.safeName = id + ext;

// Nom affiché
msg.nomFichier =
    f.originalname.replace(/[<>]/g, "");

// URL publique
msg.urlFichier =
    "https://node-red.mon-domaine.fr/files/" +
    encodeURIComponent(msg.safeName);

// Déplacement du fichier
msg.payload =
    '-c \'mv "' + f.path +
    '" "/var/www/static-files/' +
    msg.safeName + '"\'';

return msg;

Exemple :

PhotoVacances.jpg

devient, par exemple :

mrggesvw-hwdhacrc.jpg

Déplacement du fichier

Noeud Exec :

On lance l'exécution du msg.payload construit au dessus.

Command : bash
Append : msg.payload

Le fichier temporaire créé par Multipart est déplacé :

/tmp/abc123
        ->
/var/www/static-files/mrggesvw-hwdhacrc.jpg

Page de succès

Template :

<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Fichier enregistré</title>

<style>
body {
    font-family: Arial, Helvetica, sans-serif;
    background: #f4f7fb;
}

.container {
    max-width: 800px;
    margin: 80px auto;
    background: white;
    padding: 40px;
    border-radius: 16px;
    box-shadow: 0 10px 30px rgba(0,0,0,0.1);
}

.linkbox {
    padding: 15px;
    background: #f8fafc;
    border-radius: 8px;
}

button {
    background: #2563eb;
    color: white;
    border: 0;
    padding: 10px 20px;
    border-radius: 8px;
}
</style>

<script>
function copyLink() {
    const text =
        document.getElementById("downloadLink").value;

    navigator.clipboard.writeText(text);

    document.getElementById("status").innerText =
        "Lien copié ✓";
}
</script>

</head>
<body>

<div class="container">

<h1>✅ Fichier enregistré</h1>

<p>
Nom du fichier :
<b>{{nomFichier}}</b>
</p>

<div class="linkbox">
    <input id="downloadLink"
           type="text"
           value="{{urlFichier}}"
           readonly
           style="width:100%">
</div>

<br>

<button onclick="copyLink()">
    Copier le lien
</button>

<span id="status"></span>

</div>

</body>
</html>

Récapitulatifs des actions ci-dessus.

Utilisation

Upload

L'utilisateur authentifié ouvre :

https://node-red.mon-domaine.fr/megaupload/

Sélectionne un fichier puis clique :

Envoyer le fichier

Téléchargement

Le destinataire reçoit un lien :

https://node-red.mon-domaine.fr/files/mrggesvw-hwdhacrc.jpg

Aucune authentification n'est nécessaire.

Améliorations possibles

  • Limiter la taille maximale des fichiers.
  • Limiter les extensions autorisées.
  • Suppression automatique après X jours.
  • Historique des uploads dans MariaDB.
  • Génération de liens à durée de vie limitée.
  • Statistiques de téléchargement.
  • QR-Code du lien de téléchargement.

Résultat

On obtient un mini "WeTransfer" personnel :

  • HTTPS via le reverse proxy.
  • Upload privé.
  • Download public.
  • Noms de fichiers non prévisibles.
  • Aucun serveur web supplémentaire requis.
  • Entièrement réalisé avec Node-RED.

Ajouter une base de donnée

Creation

Sur le serveur de base de donnée

create database upload_db;
select PASSWORD('mon mot de passe');
+-------------------------------------------+
| PASSWORD('mon mot de passe')              |
+-------------------------------------------+
| *DA793D3BC059685BB32D33B01DF458DD43CBE127 |
+-------------------------------------------+
CREATE USER 'upload_user'@'main-host.pilan' IDENTIFIED BY PASSWORD '*DA793D3BC059685BB32D33B01DF458DD43CBE127';
GRANT ALL PRIVILEGES ON upload_db.* to 'upload_user'@'main-host.pilan';

Attention ici j'ai le domaine par defaut ".pilan"

Connexion

Sur le serveur database (database.pilan)

admin@main-host:~ $ mariadb -h database -u upload_user -p upload_db
Enter password: 
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 7743891
Server version: 11.8.6-MariaDB-0+deb13u1 from Debian -- Please help get to 10k stars at https://github.com/MariaDB/Server

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [upload_db]>

On crée la table:

USE upload_db;

CREATE TABLE upload_user (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_name VARCHAR(255) NOT NULL UNIQUE
);

CREATE TABLE file (
    id INT AUTO_INCREMENT PRIMARY KEY,
    upload_filename VARCHAR(255) NOT NULL,
    download_filename VARCHAR(255) NOT NULL,
    creation_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    user_id INT NOT NULL,
    INDEX idx_file_user_id (user_id),
    CONSTRAINT fk_file_user
        FOREIGN KEY (user_id)
        REFERENCES upload_user(id)
        ON DELETE CASCADE
);

CREATE TABLE connection_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    connection_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    user_id INT NOT NULL,
    INDEX idx_connection_log_user_id (user_id),
    CONSTRAINT fk_connection_log_user
        FOREIGN KEY (user_id)
        REFERENCES upload_user(id)
        ON DELETE CASCADE
);