SFTP ("SSH File Transfer Protocol" oder "Secure File Transfer Protocol") ist eine für die Secure Shell (SSH) entworfene Alternative zum File Transfer Protocol (FTP), beispielsweise zur Übertragung von Dateien.
Die im Folgenden gezeigten Beispiele demonstrieren:
FTP ist ein Protokollstandard zur Übertragung von Dateien über TCP/IP-Netzwerke, standardisiert im RFC 959 von 1985. Es verwendet die Ports 20 (Data Channel) und 21 (Command Channel) sowie weitere temporäre Ports. Benutzernamen, Passwörter und Daten werden ohne weitere Maßnahmen unverschlüsselt übertragen.
Java-FTP-Programmierbeispiele finden Sie unter java-ftp.htm.
FTPS erweitert das Netzwerkdateiübertragungsprotokoll FTP um einen Sicherheitslayer per SSL/TLS, standardisiert im RFC 4217. Die Authentifizierung wird verschlüsselt. Die Übertragung der Nutzdaten kann verschlüsselt oder unverschlüsselt erfolgen.
SFTP ist ebenfalls ein Protokollstandard zur Übertragung von Dateien über TCP/IP-Netzwerke, standardisiert im RFC 4253. Aber anders als FTPS erweitert SFTP nicht das FTP-Protokoll, sondern stellt ein eigenständiges Protokoll dar. Es basiert auf SSH (Secure Shell) und verwendet verschlüsselte Authentifizierung und verschlüsselte Datenübertragung. Anders als FTP und FTPS benötigt SFTP nur einen Port, normalerweise Port 22 (wie SSH).
SSH bezeichnet sowohl ein Netzwerkdatenübertragungsprotokoll als auch ein dieses Protokoll verwendendes Programm, mit dem Kommandos über eine TCP/IP-Netzwerkverbindung auf einem entfernten Rechner ausgeführt werden können (Fernwartung). SSH ermöglicht eine sichere, authentifizierte und verschlüsselte Verbindung zwischen zwei Rechnern über ein unsicheres Netzwerk. SSH verwendet den Port 22.
Das folgende Beispiel zeigt die Programmierung des SFTP-Upload und -Download mit Hilfe der ChannelSftp-Klasse aus der Bibliothek JSch (Java Secure Channel). Das Besondere dabei ist der JUnit-Test des SFTP-Zugriffs mit einem temporären embedded SFTP-Server mit Hilfe der SshServer-Klasse aus dem Apache-SSHD-Projekt.
Folgendermaßen führen Sie das Beispiel aus:
Das Java SE JDK und Maven müssen installiert sein.
Öffnen Sie ein Kommandozeilenfenster, wechseln Sie in Ihr Projekte-Verzeichnis (z.B. D:\MeinWorkspace) und erzeugen Sie eine neue Projektstruktur:
cd \MeinWorkspace
md SFtpUploadDownload
cd SFtpUploadDownload
md src\main\java\de\meinefirma\meinprojekt\sftp
md src\test\java\de\meinefirma\meinprojekt\sftp
Erstellen Sie im SFtpUploadDownload-Projektverzeichnis die Projektkonfiguration: pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>de.meinefirma.meinprojekt</groupId> <artifactId>SFtpUploadDownload</artifactId> <version>1.0</version> <packaging>jar</packaging> <name>SFtpUploadDownload</name> <build> <finalName>SFtpUploadDownload</finalName> <plugins> <plugin> <!-- Das maven-assembly-plugin wird nur benoetigt, falls ein Stand-alone-Programm erstellt werden soll: --> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>de.meinefirma.meinprojekt.sftp.DownloadFromSFtp</mainClass> </manifest> </archive> <appendAssemblyId>false</appendAssemblyId> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>com.jcraft</groupId> <artifactId>jsch</artifactId> <version>0.1.53</version> </dependency> <dependency> <groupId>org.apache.sshd</groupId> <artifactId>sshd-core</artifactId> <version>0.14.0</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> </project>
Erstellen Sie im src\main\java\de\meinefirma\meinprojekt\sftp-Verzeichnis die Klasse: DownloadFromSFtp.java
package de.meinefirma.meinprojekt.sftp; import java.io.IOException; /** Stand-alone-Programm zum Download von einem SFTP-Server sowie zum Entpacken von .zip und .gz */ public class DownloadFromSFtp { public static void main( String[] args ) throws IOException { System.out.println( "\nDownload und Unzip von Dateien von einem SFTP-Server.\n" + "8 Parameter werden benoetigt:\n" + " Quellverzeichnis auf dem Remote-Logserver, z.B.: /meinverzeichnis\n" + " Zielerzeichnis im lokalen Dateisystem, z.B.: logs\n" + " Teilstring der Bestandteil des Namens der herunterzuladenden Datei sein muss, z.B.: .log\n" + " maximales Alter der Dateien in Tagen, z.B.: 7\n" + " Benutzername\n" + " Passwort\n" + " Hostname\n" + " Port, z.B.: 22\n" ); if( args == null || args.length < 8 ) { System.out.println( "Fehler: Parameter fehlen." ); System.exit( 1 ); } else { SFtpWrapper.downloadAndUnzip( args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7] ); } } }
Erstellen Sie im src\main\java\de\meinefirma\meinprojekt\sftp-Verzeichnis die Klasse: SFtpWrapper.java
package de.meinefirma.meinprojekt.sftp; import java.io.*; import java.text.SimpleDateFormat; import java.util.*; import java.util.zip.*; import com.jcraft.jsch.*; import com.jcraft.jsch.ChannelSftp.LsEntry; /** Wrapper-Klasse zur Dateiuebertragung von und zu einem SFTP-Server sowie zum Entpacken von .zip und .gz. Die Wrapper-Klasse dient zur Entkopplung von der SFTP-Implementierung. */ public class SFtpWrapper implements AutoCloseable { private Session session; private ChannelSftp channel; public SFtpWrapper( String benutzername, String passwort, String host, int port ) throws IOException { try { session = (new JSch()).getSession( benutzername, host, port ); session.setPassword( passwort ); session.setConfig( "StrictHostKeyChecking", "no" ); session.connect(); } catch( JSchException ex ) { throw new IOException( "Fehler beim SFTP-Connect mit '" + benutzername + "' an '" + host + "': ", ex ); } try { channel = (ChannelSftp) session.openChannel( "sftp" ); if( channel == null ) { close(); throw new IOException( "Fehler beim Oeffnen des SFTP-Channel zur SFTP-Session mit '" + session.getUserName() + "' an '" + session.getHost() + "'. " ); } channel.connect(); } catch( JSchException ex ) { close(); throw new IOException( "Fehler beim Oeffnen des SFTP-Channel zur SFTP-Session mit '" + session.getUserName() + "' an '" + session.getHost() + "': ", ex ); } } @Override public void close() { try { if( channel != null ) { channel.disconnect(); channel = null; } } finally { if( session != null ) { session.disconnect(); session = null; } } } public String getLocalActualDir() { return channel.lpwd(); } public String getRemoteActualDir() throws IOException { try { return channel.pwd(); } catch( SftpException ex ) { throw new IOException( ex ); } } /** Datei-Daten zu einer einzelnen Datei */ public FileData getFileData( String remoteFilePath ) throws IOException { try { @SuppressWarnings("unchecked") List<ChannelSftp.LsEntry> lsEntryLst = channel.ls( remoteFilePath ); if( lsEntryLst == null || lsEntryLst.size() != 1 ) { return null; } LsEntry lsEntry = lsEntryLst.get( 0 ); FileData fd = new FileData(); int i = remoteFilePath.lastIndexOf( '/' ); fd.parentPath = ( i < 0 ) ? "" : remoteFilePath.substring( 0, i ); fd.isDirectory = lsEntry.getAttrs().isDir(); fd.isFile = !lsEntry.getAttrs().isDir() && !lsEntry.getAttrs().isLink(); fd.name = lsEntry.getFilename(); fd.size = lsEntry.getAttrs().getSize(); fd.timestamp = Calendar.getInstance(); fd.timestamp.setTimeInMillis( 1000L * lsEntry.getAttrs().getMTime() ); return fd; } catch( SftpException ex ) { throw new IOException( ex ); } } /** Datei-Daten zu allen Dateien in einem Verzeichnis */ public List<FileData> getFileDataList( String remoteDir ) throws IOException { try { List<FileData> fileDataLst = new ArrayList<FileData>(); @SuppressWarnings("unchecked") List<ChannelSftp.LsEntry> lsEntryLst = channel.ls( remoteDir ); for( LsEntry lsEntry : lsEntryLst ) { FileData fd = new FileData(); fd.parentPath = remoteDir; fd.isDirectory = lsEntry.getAttrs().isDir(); fd.isFile = !lsEntry.getAttrs().isDir() && !lsEntry.getAttrs().isLink(); fd.name = lsEntry.getFilename(); fd.size = lsEntry.getAttrs().getSize(); fd.timestamp = Calendar.getInstance(); fd.timestamp.setTimeInMillis( 1000L * lsEntry.getAttrs().getMTime() ); fileDataLst.add( fd ); } return fileDataLst; } catch( SftpException ex ) { throw new IOException( ex ); } } public void createRemoteFile( InputStream is, String remoteDstFilePath ) throws IOException { try { channel.put( is, remoteDstFilePath ); } catch( SftpException ex ) { throw new IOException( ex ); } } public void uploadFile( String localSrcFilePath, String remoteDstFilePath ) throws IOException { try { channel.put( localSrcFilePath, remoteDstFilePath ); } catch( SftpException ex ) { throw new IOException( ex ); } } public void downloadFile( String remoteSrcFilePath, String localDstFilePath ) throws IOException { try { channel.get( remoteSrcFilePath, localDstFilePath ); } catch( SftpException ex ) { throw new IOException( ex ); } } /** Entpacken von remote .gz */ public void ungzipRemote( String remoteSourceZipFile, String localDestFilePath ) throws IOException { try( InputStream instreamZipped = channel.get( remoteSourceZipFile ) ) { ungzipStream( instreamZipped, localDestFilePath ); } catch( Exception ex ) { throw new IOException( "Fehler beim Unzip von " + remoteSourceZipFile + ",", ex ); } } /** Entpacken von lokalem .gz */ public static void ungzipLocal( String localSourceZipFile, String localDestFilePath ) throws IOException { try( InputStream instreamZipped = new FileInputStream( localSourceZipFile ) ) { ungzipStream( instreamZipped, localDestFilePath ); } catch( Exception ex ) { throw new IOException( "Fehler beim Unzip von " + localSourceZipFile + ",", ex ); } } /** Entpacken von .gz */ public static void ungzipStream( InputStream instreamZipped, String localDestFilePath ) throws IOException { try( GZIPInputStream zin = new GZIPInputStream( new BufferedInputStream( instreamZipped ) ) ) { try( BufferedOutputStream os = new BufferedOutputStream( new FileOutputStream( localDestFilePath ) ) ) { int size; byte[] buffer = new byte[64 * 1024]; while( (size = zin.read( buffer, 0, buffer.length )) > 0 ) { os.write( buffer, 0, size ); } } } } /** Entpacken von remote .zip */ public long unzipRemote( String remoteSourceZipFile, String localDestDir ) throws IOException { try( InputStream instreamZipped = channel.get( remoteSourceZipFile ) ) { return unzipStream( instreamZipped, localDestDir ); } catch( Exception ex ) { throw new IOException( "Fehler beim Unzip von " + remoteSourceZipFile + ",", ex ); } } /** Entpacken von lokalem .zip */ public static long unzipLocal( String localSourceZipFile, String localDestDir ) throws IOException { try( InputStream instreamZipped = new FileInputStream( localSourceZipFile ) ) { return unzipStream( instreamZipped, localDestDir ); } catch( Exception ex ) { throw new IOException( "Fehler beim Unzip von " + localSourceZipFile + ",", ex ); } } /** Entpacken von .zip */ public static long unzipStream( InputStream instreamZipped, String localDestDir ) throws IOException { long anzahlEntries = 0; String remoteResultFilename = null; String destDir = ( localDestDir == null ) ? "" : localDestDir.trim(); destDir = ( destDir.endsWith( "/" ) || destDir.endsWith( "\\" ) ) ? destDir : (destDir + File.separator); try( ZipInputStream zin = new ZipInputStream( new BufferedInputStream( instreamZipped ) ) ) { ZipEntry zipEntry; while( (zipEntry = zin.getNextEntry()) != null ) { remoteResultFilename = zipEntry.getName(); if( remoteResultFilename != null && remoteResultFilename.startsWith( "/" ) && remoteResultFilename.length() > 1 ) { remoteResultFilename = remoteResultFilename.substring( 1 ); } try( BufferedOutputStream os = new BufferedOutputStream( new FileOutputStream( destDir + remoteResultFilename ) ) ) { int size; byte[] buffer = new byte[64 * 1024]; while( (size = zin.read( buffer, 0, buffer.length )) > 0 ) { os.write( buffer, 0, size ); } } zin.closeEntry(); anzahlEntries++; } } catch( Exception ex ) { throw new IOException( "Fehler beim Unzip, letzter Zip-Entry " + remoteResultFilename + ",", ex ); } return anzahlEntries; } /** Downloaden sowie Entpacken von .zip und .gz */ public void downloadAndUnzip( String remoteSrcDir, String localDstDir, String filenameMustContain, int maxAlterInTagen ) throws IOException { Calendar cal = new GregorianCalendar(); cal.add( Calendar.DAY_OF_MONTH, -1 * maxAlterInTagen ); (new File( localDstDir )).mkdirs(); List<FileData> fds = getFileDataList( remoteSrcDir ); for( FileData fd : fds ) { if( fd.isFile && fd.name.contains( filenameMustContain ) && fd.timestamp.after( cal ) ) { String remoteSrcFilePath = fd.parentPath + "/" + fd.name; String localDstFilePath = localDstDir + File.separator + fd.name; if( fd.name.toLowerCase().endsWith( ".zip" ) ) { unzipRemote( remoteSrcFilePath, localDstDir ); } else if( fd.name.toLowerCase().endsWith( ".gz" ) ) { ungzipRemote( remoteSrcFilePath, localDstFilePath.substring( 0, localDstFilePath.length() - 3 ) ); } else { downloadFile( remoteSrcFilePath, localDstFilePath ); } } } } /** Downloaden sowie Entpacken von .zip und .gz */ public static void downloadAndUnzip( String remoteSrcDir, String localDstDir, String filenameMustContain, String maxAlterInTagen, String benutzername, String passwort, String host, String port ) throws IOException { try( SFtpWrapper sftpWrapper = new SFtpWrapper( benutzername, passwort, host, Integer.parseInt( port ) ) ) { SimpleDateFormat df = new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ); System.out.println( "Remote in " + remoteSrcDir + ":" ); List<FileData> fds = sftpWrapper.getFileDataList( remoteSrcDir ); for( FileData fd : fds ) { if( fd.isFile ) { System.out.println( df.format( Long.valueOf( fd.timestamp.getTimeInMillis() ) ) + ", " + fd.size + " Bytes, " + fd.name ); } } sftpWrapper.downloadAndUnzip( remoteSrcDir, localDstDir, filenameMustContain, Integer.parseInt( maxAlterInTagen ) ); System.out.println( "Lokal in " + localDstDir + ":" ); File[] fls = (new File( localDstDir )).listFiles(); for( File fl : fls ) { System.out.println( df.format( Long.valueOf( fl.lastModified() ) ) + ", " + fl.length() + " Bytes, " + fl.getName() ); } } } /** Datei-Daten */ public static class FileData { public boolean isFile; public boolean isDirectory; public String parentPath; public String name; public long size; public Calendar timestamp; } }
Erstellen Sie im src\test\java\de\meinefirma\meinprojekt\sftp-Verzeichnis die Klasse: SFtpTestUtil.java
package de.meinefirma.meinprojekt.sftp; import java.io.IOException; import java.util.Arrays; import org.apache.sshd.SshServer; import org.apache.sshd.common.NamedFactory; import org.apache.sshd.server.Command; import org.apache.sshd.server.PasswordAuthenticator; import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.sftp.SftpSubsystem; public class SFtpTestUtil { public static SshServer startSFtpServer( final String benutzername, final String passwort, int port ) throws IOException { SshServer sftpServer = SshServer.setUpDefaultServer(); sftpServer.setPort( port ); sftpServer.setKeyPairProvider( new SimpleGeneratorHostKeyProvider( "target/hostkey.ser" ) ); sftpServer.setSubsystemFactories( Arrays.<NamedFactory<Command>> asList( new SftpSubsystem.Factory() ) ); sftpServer.setPasswordAuthenticator( new PasswordAuthenticator() { @Override public boolean authenticate( String username, String password, ServerSession session ) { return benutzername.equals( username ) && passwort.equals( password ); } } ); sftpServer.start(); return sftpServer; } }
Erstellen Sie im src\test\java\de\meinefirma\meinprojekt\sftp-Verzeichnis die Klasse: SFtpWrapperTest.java
package de.meinefirma.meinprojekt.sftp; import java.io.*; import java.util.List; import org.apache.sshd.SshServer; import org.junit.*; import de.meinefirma.meinprojekt.sftp.SFtpWrapper.FileData; public class SFtpWrapperTest { private static final String BENUTZERNAME = "usr"; private static final String PASSWORT = "pwd"; private static final String HOST = "localhost"; private static final int PORT = 14022; private static SshServer sftpServer; @BeforeClass public static void startSFtpServer() throws IOException { sftpServer = SFtpTestUtil.startSFtpServer( BENUTZERNAME, PASSWORT, PORT ); } @AfterClass public static void stoppSFtpServer() throws InterruptedException { sftpServer.stop(); } @Test public void testSFtpWrapper() throws IOException { String localSftpSubdir = "target/sftp-local/"; String remoteSftpSubdir = "target/sftp-remote/"; String localFilePathB = localSftpSubdir + "b.txt"; String remoteFilePathA = remoteSftpSubdir + "a.txt"; String remoteFilePathC = remoteSftpSubdir + "c.txt"; String meinText = "äöüß\u20AC"; // \u20AC = Euro-Zeichen String charEncoding = "UTF-8"; (new File( localSftpSubdir )).mkdirs(); (new File( remoteSftpSubdir )).mkdirs(); try( SFtpWrapper sftpWrapper = new SFtpWrapper( BENUTZERNAME, PASSWORT, HOST, PORT ) ) { Assert.assertTrue( sftpWrapper.getLocalActualDir().contains( "Download" ) ); Assert.assertTrue( sftpWrapper.getRemoteActualDir().contains( "Download" ) ); try( ByteArrayInputStream is = new ByteArrayInputStream( meinText.getBytes( charEncoding ) ) ) { sftpWrapper.createRemoteFile( is, remoteFilePathA ); } sftpWrapper.downloadFile( remoteFilePathA, localFilePathB ); sftpWrapper.uploadFile( localFilePathB, remoteFilePathC ); boolean a = false, c = false; List<FileData> fds = sftpWrapper.getFileDataList( remoteSftpSubdir ); for( FileData fd : fds ) { if( "a.txt".equals( fd.name ) ) { a = true; } if( "c.txt".equals( fd.name ) ) { c = true; } } Assert.assertTrue( a && c ); Assert.assertTrue( fds.size() >= 2 ); } SFtpWrapper.downloadAndUnzip( remoteSftpSubdir, "target/sftp-local-dl", ".txt", "7", BENUTZERNAME, PASSWORT, HOST, "" + PORT ); } }
Führen Sie den JUnit-Test in SFtpWrapperTest aus:
cd \MeinWorkspace\SFtpUploadDownload
mvn package
tree /F
Nach der Ausführung des Tests erhalten Sie folgende Verzeichnisse und Dateien:
[\MeinWorkspace\SFtpUploadDownload] |- [src] | |- [main] | | '- [java] | | '- [de] | | '- [meinefirma] | | '- [meinprojekt] | | '- [sftp] | | |- DownloadFromSFtp.java | | '- SFtpWrapper.java | '- [test] | '- [java] | '- [de] | '- [meinefirma] | '- [meinprojekt] | '- [sftp] | |- SFtpTestUtil.java | '- SFtpWrapperTest.java |- [target] | |- ... | |- [sftp-local] | | '- b.txt . . . . . . . . . . . [mit downloadFile() per SFTP heruntergeladene Datei (Download)] | |- [sftp-local-dl] | | |- a.txt . . . . . . . . . . . [mit SFtpWrapper.downloadAndUnzip() per SFTP heruntergeladene Datei] | | '- c.txt . . . . . . . . . . . [mit SFtpWrapper.downloadAndUnzip() per SFTP heruntergeladene Datei] | |- [sftp-remote] | | |- a.txt . . . . . . . . . . . [mit createRemoteFile() per SFTP erzeugte Datei] | | '- c.txt . . . . . . . . . . . [mit uploadFile() per SFTP hochgeladene Datei (Upload)] | |- ... | '- SFtpUploadDownload.jar '- pom.xml
Das Stand-alone-Programm starten Sie so (ersetzen Sie die Parameter durch sinnvolle Werte):
cd \MeinWorkspace\SFtpUploadDownload
mvn package
java -jar target\SFtpUploadDownload.jar /meindir logs .log 7 usr pwd mein.host.de 22