Creare un client ssh e sftp con il Java

« Older   Newer »
  Share  
view post Posted on 7/5/2010, 22:06

Group:
,,,..--::|| AMMINISTRATORE ||::--..,,,
Posts:
1,602
Location:
Scheda di rete

Status:


In questo articolo vedremo come implementare un client ssh e un client sftp usando jsch, che è una pura implementazione Java di SSH2.
Jsch è, a mio modo di vedere, semplice da utilizzare, oltre che essere usato da Eclipse, Ant e JIRA.

Per avviare la connessione ssh bisogna innanzitutto creare una session con l'host remoto:

/**
* Inizializzo il framework e creo la session
*/
JSch jsch = new JSch();

String user = "mrwebmaster";
String host = "localhost";
int port = 22;

Session session = jsch.getSession(user, host, port);

Dopo aver creato la session dobbiamo creare un channel:

/**
* Setto la classe UserInfo per recupare la password dell'utente
*/
UserInfo ui = new MrWebmasterUserInfo();
session.setUserInfo(ui);

/**
* Eseguo la connessione della sessione
*/
session.connect();

/**
* Creo il channel shell
*/
ChannelShell channel = (ChannelShell) session.openChannel("shell");

Ci sono molti tipi di channel, ma in questo articolo verranno analizzati solo il channel shell e il channel sftp, si lascia al lettore un eventuale approfondimento.

Per connettersi attraverso il channel bisogna settare nella session un oggetto che implementi l'interfaccia UserInfo.
Questa interfaccia ha i seguenti metodi:

* public String getPassphrase()
* public String getPassword()
* public boolean promptPassphrase(String message)
* public boolean promptPassword(String message)
* public boolean promptYesNo(String message)
* public void showMessage(String message)

I metodi getPassphrase e getPassword vengono invocati rispettivamente per recuperare la password o la passphrase subito dopo aver invocato promptPassphrase o promptPassword che restituiscono true se e solo se l'utente ha inserito la password o la passphrase.
Il metodo promptYesNo viene invocato quando non è possibile verificare l'autenticità dell'host remoto, e restituisce true solo se l'utente accetta la connessione ad un host non verificato. Infine il metodo showMessage viene invocato quando deve essere mostrato un messaggio all'utente.

Una volta creato il channel non resta che legare lo stdin e stdout di java al channel ed effettuare la connessione:

/**
* Associo stdin e stdout
*/
channel.setInputStream(System.in);

/**
* per Windows
*
* channel.setInputStream(new FilterInputStream(System.in) { public
* int read(byte[] b, int off, int len) throws IOException { return
* in.read(b, off, (len > 1024 ? 1024 : len)); } });
*/
channel.setOutputStream(System.out);

/**
* Eseguo la connessione con un timeout di 3 secondi
*/
channel.connect(3000);


Ora che sappiamo come creare una shell interattiva, non ci resta che creare una piccola applicazione, che data una stringa di connessione ssh del tipo user@host:port, restituisca la shell remota.
La classe main sarà del tipo:

package it.mrwebmaster;

import com.jcraft.jsch.ChannelShell;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;

public class MrWebmasterSshClient {

public static void main(String[] args) {
try {

/**
* Verifico che la stringa di connessione sia corretta
*/
if (args.length == 0) {
System.out.println("Usage: user@host:port");
return;
}

if (!args[0].contains("@")) {
System.out.println("Usage: user@host:port");
}

/**
* Recupero utente, host e porta
*/
String user = args[0].substring(0, args[0].indexOf('@'));

String host = null;

/**
* La porta di default dell'ssh è 22
*/
int port = 22;

if (args[0].contains(":")) {
host = args[0].substring(args[0].indexOf('@') + 1, args[0].indexOf(":"));
port = Integer.parseInt(args[0].substring(args[0].indexOf(':') + 1));
} else {
host = args[0].substring(args[0].indexOf('@') + 1);
}

/**
* Inizializzo il framework e creo la session
*/
JSch jsch = new JSch();

Session session = jsch.getSession(user, host, port);

/**
* Setto la classe UserInfo per recupare la password dell'utente
*/
UserInfo ui = new MrWebmasterUserInfo();
session.setUserInfo(ui);

/**
* Eseguo la connessione della sessione
*/
session.connect();

/**
* Creo il channel shell
*/
ChannelShell channel = (ChannelShell) session.openChannel("shell");

/**
* Associo stdin e stdout
*/
channel.setInputStream(System.in);

/**
* per Windows
*
* channel.setInputStream(new FilterInputStream(System.in) { public
* int read(byte[] b, int off, int len) throws IOException { return
* in.read(b, off, (len > 1024 ? 1024 : len)); } });
*/
channel.setOutputStream(System.out);

/**
* Eseguo la connessione con un timeout di 3 secondi
*/
channel.connect(3000);

/**
* Controllo ogni secondo se il canale è stato chiuso
*/
while (true) {
if (channel.isClosed()) {
System.exit(channel.getExitStatus());
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
} catch (JSchException e) {
e.printStackTrace();
}
}
}

La classe UserInfo

package it.mrwebmaster;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import com.jcraft.jsch.UserInfo;

public class MrWebmasterUserInfo implements UserInfo {

private String password;
private String passphrase;

/**
* Restituisco la passphrase
* salvata in precedenza
*/
@Override
public String getPassphrase() {
return passphrase;
}

/**
* Restituisco la password
* salvata in precedenza
*/
@Override
public String getPassword() {
return password;
}

/**
* Chiedo all'utente di inserire la passphrase
*/
@Override
public boolean promptPassphrase(String message) {
/**
* Resetto la passphrase precedente
*/
passphrase = null;

/**
* Stampo il messaggio
*/
System.out.println(message);

/**
* Leggo la passphrase
*/
passphrase = readOneLine();

/**
* Controllo l'input
*/
if (passphrase != null && passphrase.length() > 0) {
return true;
} else {
return false;
}
}

/**
* Chiedo all'utente di inserire la password
*/
@Override
public boolean promptPassword(String message) {
/**
* Resetto la passphrase password
*/
password = null;

/**
* Stampo il messaggio
*/
System.out.println(message);

/**
* Leggo la password
*/
password = readOneLine();

/**
* Controllo l'input
*/
if (password != null && password.length() > 0) {
return true;
} else {
return false;
}
}

/**
* Chiedo all'utente di dare l'autorizzazione
* per la connessione ad un host non verificato
*/
@Override
public boolean promptYesNo(String message) {
/**
* Stampo il messaggio
*/
System.out.println(message);

/**
* Verifico se l'utente ha inserito yes
*/
String s = readOneLine();
if ("yes".equalsIgnoreCase(s)) {
return true;
}

return false;
}

/**
* Stampo un messaggio del framework
*/
@Override
public void showMessage(String message) {
System.out.println(message);
}

/**
* Metodo di utility per leggere l'input dell'utente
*/
private String readOneLine() {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
return in.readLine();
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}

Nell'immagine seguente un esempio di shell ssh da una macchina virtuale xp verso una macchina ubuntu:
image
Esempio Shell
Ora che abbiamo visto come realizzare una shell ssh possiamo passare alla creazione di un client sftp.
La creazione della session rimane invariata, quello che cambia è solo l'apertura del channel che non sarà del tipo shell ma sftp:

/**
* Creo il channel sftp
*/
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");

/**
* Eseguo la connessione con un timeout di 3 secondi
*/
channel.connect(3000);

Al contrario, della shell ssh non possiamo semplicemente legare lo stdin e lo stdout al channel appena creato, ma ci dobbiamo preoccupare di interpretare i comandi dell'utente per poi eseguire le operazioni sul channel.

In questo articolo implementeremo solo i comandi che permettono un uso minimale dell'sftp:

* exit | quit
* pwd | lpwd
* cd | lcd
* ls | dir
* lls | ldir
* put
* get
* help | ?

Viene lasciata al lettore l'implementazione degli altri comandi.

Ora prima di tutto dobbiamo implementare la parte che legge e interpreta i comandi dell'utente. Un modo potrebbe essere quello di creare una java.util.List di String dove contenere il comando digitato e i suoi eventuali parametri.

Sotto il codice che legge i comandi dell'utente e l'implementazione dei comandi quit ed exit:

/**
* Creo una lista di stringhe dove salvare i comandi con i relativi parametri
*/
List<string> commandList = new ArrayList<string>();

/**
* Creo un BufferedReader per leggere i comandi dallo stdin e un PrintStream per stampare su stdout
*/
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintStream out = System.out;

while (true) {
try {
out.print("sftp >");

/**
* Ad ogni ciclo cancello la lista comandi precendente e leggo il nuovo comando
*/
commandList.clear();

String command = in.readLine();

/**
* Separo il comando dai parametri
*/
String[] split = command.split(" ");
commandList.addAll(Arrays.asList(split));

if (commandList.size() > 0) {
String cmd = commandList.get(0);
/**
* Comando quit
*/
if (cmd.equals("quit")) {
channel.quit();
break;
}

/**
* Comando exit
*/
if (cmd.equals("exit")) {
channel.exit();
break;
}
}
} catch (Exception e) {
out.println(e.getMessage());
}
}


Per implementare i comandi pwd, lpwd, cd, lcd basta interpretare il comando e invocare il metodo opportuno del channel:

/**
* Comando pwd e lpwd
*/
if (cmd.equals("pwd") || cmd.equals("lpwd")) {
String output = (cmd.equals("pwd") ? "Remote" : "Local");
output += " working directory: ";

if (cmd.equals("pwd")) {
output += channel.pwd();
} else {
output += channel.lpwd();
}

out.println(output);
continue;
}

/**
* Comando cd e lcd
*/
if (cmd.equals("cd") || cmd.equals("lcd")) {
/**
* Controllo se è stato passato il path
*/
if (commandList.size() < 2) {
out.println("No path specified");
continue;
}

/**
* Recupero il path e lancio il comando
*/
String path = commandList.get(1);

if (cmd.equals("cd")) {
channel.cd(path);
} else {
channel.lcd(path);
}
continue;
}

Per i comandi ls e dir l'implementazione è un pò più complicata, poichè oltre recuperare la lista dei file dobbiamo anche ordinarli, dato che il channel sftp non lo fa per noi.
Il metodto ls() restituisce un Vector di LsEntry, che implementa Comparable, quindi basta usare un TreeSet per ordinare le entry (Vedi Java Collection Framework):

/**
* Comando ls e dir
*/
if (cmd.equals("ls") || cmd.equals("dir")) {
/**
* Di default ls si riferisce alla directory corrente, se invece è stato
* inserito un path uso quello
*/
String path = ".";

if (commandList.size() == 2) {
path = commandList.get(1);
}

/**
* Recupero e poi ordino la lista dei file presenti
*/
Vector lsList = channel.ls(path);
SortedSet<lsentry> sortedList = new TreeSet>LsEntry<(lsList);

/**
* Stampo la lista dei file
*/
for (LsEntry entry : sortedList) {
out.println(entry.getLongname());
}
continue;
}

I comandi lls e ldir non sono implementati dal channel sftp e di conseguenza dobbiamo implementarli noi. Prima di tutto verifichiamo che il path si riferisca ad un file o ad una directory esistente, e nel caso in cui il path si riferisca ad una directory stampiamo la lista dei file:

/**
* Comando lls e ldir
*/
if (cmd.equals("lls") || cmd.equals("ldir")) {
/**
* Di default lls si riferisce alla directory corrente, se invece è stato
* inserito un path uso quello
*/
String path = ".";

if (commandList.size() == 2) {
path = commandList.get(1);
}

/**
* Verifico se esiste il file
*/
File file = new File(path);
if (!file.exists()) {
out.println(path + ": No such file or directory");
continue;
}

/**
* Se il file è una directory devo stampare la lista dei file contenuti
*/
if (file.isDirectory()) {
/**
* Recupero la lista e la ordino
*/
String[] list = file.list();
SortedSet<string> sortedList = new TreeSet<string>(Arrays.asList(list));

/**
* Stampo la lista dei file
*/
for (String entry : sortedList) {
out.println(entry);
}
continue;
}

/**
* Se il file non è una directory mi limito a stampare il nome dei file
*/
out.println(path);
continue;
}


Una volta implementata la parte che ci permette di muoverci tra le directory e di verificarne il contenuto, vediamo come fare un upload o un download utilizzando i comandi put e get. L'implementazione base di questi due comandi è molto semplice, basta soltanto leggere i path dei file locale e remoto per poi passarli al relativo metodo del channel:

/**
* Comando put
*/
if (cmd.equalsIgnoreCase("put")) {
/**
* Il nome del file remoto di default corrisponde a quello locale
*/
String localFile;
String remoteFile = ".";

/**
* Controllo se è stato passato almeno il nome del file locale
*/
if (commandList.size() < 2) {
out.println("No arguments specified for command " + cmd);
continue;
}

/**
* Recupero il nome del file locale
*/
localFile = commandList.get(1);

/**
* Controllo se è stato inserito un eventuale nome per il file remoto
*/
if (commandList.size() == 3) {
remoteFile = commandList.get(2);
}

/**
* Eseguo l'upload
*/
channel.put(localFile, remoteFile);
continue;
}

/**
* Comando get
*/
if (cmd.equalsIgnoreCase("get")) {
/**
* Il nome del file locale di default corrisponde a quello remoto
*/
String localFile = ".";
String remoteFile;

/**
* Controllo se è stato passato almeno il nome del file remoto
*/
if (commandList.size() < 2) {
out.println("No arguments specified for command " + cmd);
continue;
}

/**
* Recupero il nome del file remoto
*/
remoteFile = commandList.get(1);

/**
* Controllo se è stato inserito un eventuale nome per il file locale
*/
if (commandList.size() == 3) {
localFile = commandList.get(2);
}

/**
* Eseguo il download
*/
channel.get(remoteFile, localFile);
continue;
}

Oltre a questa semplice implementazione le API di jsch offrono anche la possibilità di specificare il modo di scrittura del file: OVERWRITE, APPEND, RESUME, e di monitorare l'upload o il download, implementando l'interfaccia com.jcraft.jsch.SftpProgressMonitor. Viene lasciato al lettore l'implementazione di tale classe.

Infine non ci resta che implementare i comandi help e ?, che altro non fanno che stampare la lista di tutti i comandi disponibili con una breve descrizione.
Di seguito un'immagine di esempio dove viene mostrata una sessione di sftp dove viene fatto il download del file download.txt:

image

Esempio Sftp
Il codice completo dell'applicazione:

package it.mrwebmaster;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;

import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.UserInfo;
import com.jcraft.jsch.ChannelSftp.LsEntry;

public class MrWebmasterSftpClient {

@SuppressWarnings("unchecked")
public static void main(String[] args) {
try {

/**
* Verifico che la stringa di connessione sia corretta
*/
if (args.length == 0) {
System.out.println("Usage: user@host:port");
return;
}

if (!args[0].contains("@")) {
System.out.println("Usage: user@host:port");
}

/**
* Recupero utente, host e porta
*/
String user = args[0].substring(0, args[0].indexOf('@'));

String host = null;

/**
* La porta di default dell'ssh è 22
*/
int port = 22;

if (args[0].contains(":")) {
host = args[0].substring(args[0].indexOf('@') + 1, args[0].indexOf(":"));
port = Integer.parseInt(args[0].substring(args[0].indexOf(':') + 1));
} else {
host = args[0].substring(args[0].indexOf('@') + 1);
}

/**
* Inizializzo il framework e creo la session
*/
JSch jsch = new JSch();

Session session = jsch.getSession(user, host, port);

/**
* Setto la classe UserInfo per recupare la password dell'utente
*/
UserInfo ui = new MrWebmasterUserInfo();
session.setUserInfo(ui);

/**
* Eseguo la connessione della sessione
*/
session.connect();

/**
* Creo il channel sftp
*/
ChannelSftp channel = (ChannelSftp) session.openChannel("sftp");

/**
* Eseguo la connessione con un timeout di 3 secondi
*/
channel.connect(3000);

/**
* Creo una lista di stringhe dove salvare i comandi con i relativi parametri
*/
List<string> commandList = new ArrayList<string>();

/**
* Creo un BufferedReader per leggere i comandi dallo stdin e pu PrintStream per stampare su stdout
*/
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
PrintStream out = System.out;

while (true) {
try {
out.print("sftp >");

/**
* Ad ogni ciclo cancello la lista comandi precendente e leggo il nuovo comando
*/
commandList.clear();

String command = in.readLine();

/**
* Separo il comando dai parametri
*/
String[] split = command.split(" ");
commandList.addAll(Arrays.asList(split));

if (commandList.size() > 0) {
String cmd = commandList.get(0);

/**
* Comando quit
*/
if (cmd.equals("quit")) {
channel.quit();
break;
}

/**
* Comando exit
*/
if (cmd.equals("exit")) {
channel.exit();
break;
}

/**
* Comando pwd e lpwd
*/
if (cmd.equals("pwd") || cmd.equals("lpwd")) {
String output = (cmd.equals("pwd") ? "Remote" : "Local");
output += " working directory: ";

if (cmd.equals("pwd")) {
output += channel.pwd();
} else {
output += channel.lpwd();
}

out.println(output);
continue;
}

/**
* Comando cd e lcd
*/
if (cmd.equals("cd") || cmd.equals("lcd")) {
/**
* Controllo se è stato passato il path
*/
if (commandList.size() < 2) {
out.println("No path specified");
continue;
}

/**
* Recupero il path e lancio il comando
*/
String path = commandList.get(1);

if (cmd.equals("cd")) {
channel.cd(path);
} else {
channel.lcd(path);
}
continue;
}

/**
* Comando ls e dir
*/
if (cmd.equals("ls") || cmd.equals("dir")) {
/**
* Di default ls si riferisce alla directory corrente, se invece è stato
* inserito un path uso quello
*/
String path = ".";

if (commandList.size() == 2) {
path = commandList.get(1);
}

/**
* Recupero e poi ordino la lista dei file presenti
*/
Vector lsList = channel.ls(path);
SortedSet<lsentry> sortedList = new TreeSet>LsEntry<(lsList);

/**
* Stampo la lista dei file
*/
for (LsEntry entry : sortedList) {
out.println(entry.getLongname());
}
continue;
}

/**
* Comando lls e ldir
*/
if (cmd.equals("lls") || cmd.equals("ldir")) {
/**
* Di default lls si riferisce alla directory corrente, se invece è stato
* inserito un path uso quello
*/
String path = ".";

if (commandList.size() == 2) {
path = commandList.get(1);
}

/**
* Verifico se esiste il file
*/
File file = new File(path);
if (!file.exists()) {
out.println(path + ": No such file or directory");
continue;
}

/**
* Se il file è una directory devo stampare la lista dei file contenuti
*/
if (file.isDirectory()) {
/**
* Recupero la lista e la ordino
*/
String[] list = file.list();
SortedSet<string> sortedList = new TreeSet<string>(Arrays.asList(list));

/**
* Stampo la lista dei file
*/
for (String entry : sortedList) {
out.println(entry);
}
continue;
}

/**
* Se il file non è una directory mi limito a stampare il nome dei file
*/
out.println(path);
continue;
}

/**
* Comando put
*/
if (cmd.equalsIgnoreCase("put")) {
/**
* Il nome del file remoto di default corrisponde a quello locale
*/
String localFile;
String remoteFile = ".";

/**
* Controllo se è stato passato almeno il nome del file locale
*/
if (commandList.size() < 2) {
out.println("No arguments specified for command " + cmd);
continue;
}

/**
* Recupero il nome del file locale
*/
localFile = commandList.get(1);

/**
* Controllo se è stato inserito un eventuale nome per il file remoto
*/
if (commandList.size() == 3) {
remoteFile = commandList.get(2);
}

/**
* Eseguo l'upload
*/
channel.put(localFile, remoteFile);
continue;
}

/**
* Comando get
*/
if (cmd.equalsIgnoreCase("get")) {
/**
* Il nome del file locale di default corrisponde a quello remoto
*/
String localFile = ".";
String remoteFile;

/**
* Controllo se è stato passato almeno il nome del file remoto
*/
if (commandList.size() < 2) {
out.println("No arguments specified for command " + cmd);
continue;
}

/**
* Recupero il nome del file remoto
*/
remoteFile = commandList.get(1);

/**
* Controllo se è stato inserito un eventuale nome per il file locale
*/
if (commandList.size() == 3) {
localFile = commandList.get(2);
}

/**
* Eseguo il download
*/
channel.get(remoteFile, localFile);
continue;
}

/**
* Comando help
*/
if (cmd.equalsIgnoreCase("help") || cmd.equalsIgnoreCase("?")) {
out.println(help);
continue;
}

/**
* Nel caso in cui il comando non sia stato riconosciuto
*/
out.println("Unsupported command");
}
} catch (SftpException e) {
out.println(e.getMessage());
}
}

/**
* Se sono qui sono uscito dal while
* quindi chiudo la sessione ed esco
*/
session.disconnect();
System.exit(0);
} catch (JSchException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}

}

/**
* Construisco l'help string
*/
private static final String help = " Available commands:\n"
+ "dir [path] Display remote directory listing\n"
+ "cd path Change remote directory to 'path'\n"
+ "help Display this help text\n"
+ "exit Quit sftp\n"
+ "get remote-path [local-path] Download file\n"
+ "lcd path Change local directory to 'path'\n"
+ "ldir [path] Display local directory listing\n"
+ "lls [path] Display local directory listing\n"
+ "lpwd Print local working directory\n"
+ "ls [path] Display remote directory listing\n"
+ "put local-path [remote-path] Upload file\n"
+ "pwd Display remote working directory\n"
+ "quit Quit sftp\n"
+ "? Synonym for help";


p.s. Scusate la lunghezza del post ma era necessario implementare tutto il discorso ed il codice sorgente
 
Web     Top
0 replies since 7/5/2010, 22:05   994 views
  Share