Node.jsJavaScript

Das Secure Copy Protokoll mit Node.js nutzen

Mit dem nativ in Node.js umgesetzten child_process lässt sich das Tool scp aus dem Javascript Kontext heraus starten und steuern.

Die OpenSSH Implementation von Windows bringt unter anderem das Programm scp.exe mit. Das auch Linux und Mac OSX das Kommandozeilentool bieten ist selbstredend. Mit dem nativ in Node.js umgesetzten child_process lässt es sich aus dem Javascript Kontext heraus starten.

Nun gibt es dafür unterschiedliche Herangehensweisen. Mittels exec(cmd [, opt][, cb]) wird der Befehl ausgeführt wobei Windows in einigen Fällen auf eine Shell insistiert und mit execFile(file[, args][, options][, callback]) wird in jedem Fall eine Shell gestartet. Beide Funktionen können optional mit einem Callback aufgerufen werden, dem neben den kumulativen Streams der Ausgabe und der Fehler auch Node.js üblich ein Fehlerobjekt übergeben wird.


Noch interessanter ist allerdings spawn(cmd[, args][, options]). Das instanziierte Rückgabeobjekt enthält die Streams für Ein- und Ausgabe, mit denen man normalerweise über die Kommandozeile interagiert. So kann man zum Beispiel auf Events des Outstreams automatisiert reagieren in dem man Befehle in die Eingabe streamt.

./lib/scp-promises.mjs
import { spawn } from 'child_process';

export class Scp {
    constructor({ host, user, password, port }) {
        if (host === undefined || user === undefined)
            throw new Error(`Host and username are required.`);

        this.port = port || 22;
        this.host = host;
        this.user = user;
        this.password = password;
    }
    

    #proc(source, dest) {
        return new Promise((resolve, reject) => {
            if (source === undefined || dest === undefined)
                reject(new Error(`Source and destination are required.`));

            const _proc = spawn('scp', ['-r', `-P ${this.port}`, source, dest]);

            _proc.on('message', (msg) => resolve(msg.toString()));
            _proc.on('error', (error) => reject(new Error(error.toString())));
            _proc.on('exit', (code) => resolve(code));
            _proc.stderr.on('data', (error) =>reject(new Error(error.toString())));
            _proc.stdout.on('data', (data) => {               
                if (data.toString().includes('password:') && this.password !== undefined) {
                    this.proc.stdin.write(this.password);
                }
                else if (this.password === undefined) {
                    process.stdin.pipe(this.proc.stdin);
                }
            });            
        });
    }
    
    send({ path, file }) {
        return this.#proc(file, this.user !== undefined ? `${this.user}@${this.host}:${path}` : `${this.host}:${path}`);
    }

    get({ path, file }) {
        return this.#proc(this.user !== undefined ? `${this.user}@${this.host}:${path}` : `${this.host}:${path}`, file);
    }
}

export function CreateScpConnection(options) {
    return new Scp(options);
}

Eine nette kleine Klasse stellt zwei Methoden zur Verfügung, die als Wrapper für die #proc Methode dienen. Der Hashtag sagt im Übrigen aus das die Methode private sein soll. Das ist noch ein recht junges Feature von Javascript und wird noch nicht überall unterstützt. Zumindest Firefox, Safari und dem Samsung Mobile Browser fehlen das noch.

Die #proc Methode gibt ein Promise zurück und damit können wir ganz regulär beim Aufruf auf Async / Await zurückgreifen.

Richtig interessant ist aber eigentlich der _proc.stdout Listener. Wenn ein Passwort übergeben wurde und vom Stream ein Passwort erwartet wird dann schreiben wir in den Stream das bei Initialisierung übergebene Passwort und wenn nicht wird von der Eingabe des laufenden Hauptprozesses zur Eingabe des Childs gepiped und so kann man dann das Passwort über die Kommandozeile selbst eingeben.

Wenn man jetzt ein wenig weiter denkt, dann kann man damit herrliche Spielereien mit Electron und der Shell vorstellen.

./index.mjs
import { CreateScpConnection }  from './lib/scp-promises.mjs'

(async () => {
    try {
        const scp = CreateScpConnection({host: 'michm.de', user: 'jibblez'});
        await scp.send({ path: '/home/jibblez/TESTFILE.dat', file: './test.dat'});
        await scp.get({ path: '/home/jibblez/TESTFILE.dat', file: './123.dat'})
    }
    catch(error) {
        console.error(error);
    }
})();

Daraus ergibt sich ein in der Handhabung simples Modul, welches eine solide Grundlage für etwaige Erweiterungen bietet.


Repository
jibbex/scp-promises
Contribute to jibbex/scp-promises development by creating an account on GitHub.
image
npm i scp-promises
scp-promises
Asynchronous scp up- and download
image