SERVEI DE SFTP










Introducció

Propòsit

El servei de SFTP de Canigó permet enviar i rebre arxius entre el servidor on s'executa l'aplicació a altres servidors de forma segura mitjançant l'intercanvi de claus.

El servei està basat en les llibreries JSCH i Commons-VFS, la primera es tracta d'un projecte open source que permet la connexió via SSH a qualsevol màquina. La segona llibrería és un projecte també open_source de la Apache Foundation que permet treballar amb més facilitat amb la JSCH, donant eines per crear connexions SFTP (entre d'altres) contra un servidor.

Context i Escenaris d'Ús

El servei de SFTP es troba dins dels serveis d'integració de Canigó.

El seu ús és necessari en cas de voler intercanviar fitxers entre servidors utilitzant el protocol SFTP, és a dir, protocol FTP de forma segura.

Versions i Dependències

Les dependències descrites a la següent url son requerides per tal de compilar i fer funcionar el projecte:
Dependències Servei de SFTP

A qui va dirigit

Aquest document va dirigit als següents perfils:

  1. Programador. Per conèixer l'ús del servei.
  2. Arquitecte. Per conèixer quins són els components i la configuració del servei.
  3. Administrador. Per conèixer com configurar el servei en cadascun dels entorns en cas de necessitat.

Documents i Fonts de Referència

[1] JSCH http://www.jcraft.com/jsch/
[2] Commons VFS http://commons.apache.org/vfs/

Glossari

SFTP
Secure File Transfer Protocol (Protocol de Transferència Segura d'Arxius), es tracta d'un protocol de transferència de fitxers entre computadors de forma segura.

Download
Rebre arxius des d'un servidor.

Upload
Enviar arxius a un servidor.

Socket
Port del servidor habilitat per realitzar la transferència de fitxers/streams mitjançant el protocol SFTP.

Descripció Detallada

Arquitectura i Components

Canigó ofereix una arquitectura d'ús del protocol SFTP totalment deslligada de qualsevol implementació.

Els components podem classificar-los en:

  1. Interfícies i Components Genèrics. Interfícies del servei i components d'ús general amb independència de la implementació escollida.
  2. Implementació de les interfícies.
    Es pot trobar tota la documentació JavaDoc i el codi font referent aquests components a les següents urls:

JavaDoc: http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_3/canigo-services-sftp/apidocs/index.html
Codi Font:  http://canigo.ctti.gencat.net/confluence/canigodocs/site/canigo2_3/canigo-services-sftp/xref/index.html

Instal.lació i Configuració

Instal.lació

La instal.lació del servei requereix de la utilització de la llibreria 'canigo-services-sftp' i les dependències indicades a l'apartat 'Introducció - Versions i Dependències'.

Per la instal.lació dels components Commons VFS i JSCH, no es requereix res a part dels JAR indicats en les dependències "Commons VFS" i "JSCH".

Configuració

La configuració del Servei de SFTP implica:

  • Definir el servei.

La definició del servei requereix configurar un bean amb un identificador (es recomana usar 'sftpService') i els següents atributs:

Atributs:

Atribut Requerit Descripció
class Implementació concreta del servei a utilitzar.

Opcions:
# net.gencat.ctti.canigo.services.sftp.impl.SftpServiceImpl
init-method Mètode d'inicialització del servei.

Opcions:
# init

També es poden configurar les següents propietats:

Propietat Requerit Descripció
logService No Referència a la definició del servei de traces.
ftpUrl Adreça del servidor.
ftpPort No Socket de connexió al servidor.
ftpUsername No Usuari per la connexió al servidor.
ftpPassword No Contrasenya per la connexió al servidor.
identityPath
No
Path del fitxer de claus privades. El fitxer ha de tenir format OpenSSH (RSA o DSA).

Aquesta propietat està disponible a partir de la versió 2.3.7 del Framework.
strictHostKeyChecking No Indica si volem intercanviar claus amb el servidor o no.
knownHosts No Directori on es troba el fitxer de claus SSH.
proxyType No Tipus de proxy.
proxyHost No Adreça del proxy.
proxyPort No Port del proxy.
userDirIsRoot
No
Possibilitat d'indicar els paths relatius al directori home de l'usuari.
true --> Paths relatius a partir del directori home
false --> Paths absoluts
Aquesta propietat està disponible a partir de la versió 2.3.10 del Framework.
uploadChecksum
No
Ofereix la possibilitat de generar i pujar un fitxer de checksum.
default --> Generarà i farà l'upload d'un fitxer .md5 a més del fitxer indicat.
all --> Generarà i farà l'upload d'un fitxer .md5, .sha1 a més del fitxer indicat.
md5 --> Generarà i farà l'upload d'un fitxer .md5 a més del fitxer indicat.
sha1 --> Generarà i farà l'upload d'un fitxer .sha1 a més del fitxer indicat.
none --> No generarà cap fitxer de checksum. El mateix que si no s'informa aquesta propietat.
Aquesta propietat està disponible a partir de la versió 2.3.10 del Framework.
uploadChecksumException
No
Serveix per a indicar si un error de checksum és bloquejant o no.
true --> Si es produeix un error al generar el fitxer de checksum, es llançarà una excepció i no es pujarà el fitxer al servidor.
false --> Si es produeix un error al generar el fitxer de checksum, es mostrarà un log d'error, però continuarà la pujada del fitxer indicat al servidor.
Aquesta propietat està disponible a partir de la versió 2.3.10 del Framework.
downloadChecksum
No
Ofereix la possibilitat de generar i pujar un fitxer de checksum.
default --> Validarà que el fitxer descarregat és correcte mitjançant el fitxer .md5.
md5 --> Validarà que el fitxer descarregat és correcte mitjançant el fitxer .md5.
sha1 --> Validarà que el fitxer descarregat és correcte mitjançant el fitxer .sha1.
none --> No realitzarà cap tipus de validació sobre el fitxer descarregat.
Aquesta propietat està disponible a partir de la versió 2.3.10 del Framework.
downloadChecksumValidation
No
Serveix per a indicar el nivell de bloqueig del checksum.
exception --> Si no coincideix el checksum generat del fitxer descarregat amb el que hi ha al servidor es llançarà una excepció.
log --> Si no coincideix el checksum generat del fitxer descarregat amb el que hi ha al servidor mostrarà un log d'error.
none --> Si no coincideix el checksum generat del fitxer descarregat amb el que hi ha al servidor no farà res.
Aquesta propietat està disponible a partir de la versió 2.3.10 del Framework.

Exemple:

...
<!- SFTP service ->
<bean id="sftpService" class="net.gencat.ctti.canigo.services.sftp.impl.SftpServiceImpl" init-method="init">
	<property name="logService">
		<ref bean="loggingService" />
	</property>
	<property name="ftpUrl" value="192.168.1.100"/>
	<property name="ftpPort" value="22"/>
	<property name="ftpUsername" value="username"/>
	<property name="ftpPassword" value="password"/>
	<property name="knownHosts" value="C:/ssh/known_hosts"/>
	<property name="strictHostKeyChecking" value="yes"/>
</bean>

...
<bean id="loggingService" class="net.gencat.ctti.canigo.services.logging.log4j.Log4JServiceImpl" init-method="init">
...
</bean>
...

Exemple identificació fitxer de claus privades:
...
<!- SFTP service ->
<bean id="sftpService" class="net.gencat.ctti.canigo.services.sftp.impl.SftpServiceImpl" init-method="init">
	<property name="logService">
		<ref bean="loggingService" />
	</property>
	<property name="ftpUrl" value="192.168.1.100"/>
	<property name="ftpPort" value="22"/>
	<property name="ftpUsername" value="username"/>
	<property name="identityPath" value="c:/ssh/privateKeyRSA.cfg"/>
	<property name="knownHosts" value="C:/ssh/known_hosts"/>
	<property name="strictHostKeyChecking" value="yes"/>
</bean>

...
<bean id="loggingService" class="net.gencat.ctti.canigo.services.logging.log4j.Log4JServiceImpl" init-method="init">
...
</bean>
...







Utilització del Servei

Realitzar connexió

Tal i com s'ha comentat a l'apartat 'Arquitectura i Components' les classes principals per realitzar transferències de fitxers es troben al package 'net.gencat.ctti.canigo.services.sftp'.

Per realitzar una connexió segura només cal inicialitzar el bean tal i com s'ha indicat a l'apartat 'Configuració'.
Tenim dos formes de fer la connexió al servidor segons haguem indicat o no l'usuari i el password a la configuració, en el cas de que s'hagi indicat, farem la crida de la següent manera:

if (this.sftpService!= null ) {
    sftpService.login();
}




En canvi, si no hem indicat a través de la configuració l'usuari i la contrasenya per realitzar la connexió, es farà la següent crida:
if (this.sftpService!= null ) {
    sftpService.login(usuari,contrasenya);
}



A més a més, l'API del servei permet fer la connexió a un servidor de forma dinàmica, és a dir, es pot indicar l'adreça i port del servidor al que ens volem connectar en temps d'execució, per fer-ho el servei ofereix un tercer mètode de login:
if (this.sftpService!= null ) {
    sftpService.login(usuari,contrasenya,ftpUrl,ftpPort);
}



Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() o login(usuari,contrasenya) per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.

El mètode retorna un valor booleà (TRUE/FALSE) indicant si la connexió s'ha realitzat correctament.

NOTA: Per realitzar una connexió al servidor a través d'un proxy cal informar els següents camps:

  • proxyType: Tipus de proxy, pot tenir els valors http o socks.
  • proxyHost: Adreça del host.
  • proxyPort: Port del host.
    <bean id="sftpService" class="net.gencat.ctti.canigo.services.sftp.impl.SftpServiceImpl" init-method="init">
    ...
        <property name="proxyType" value="http"/>
        <property name="proxyHost" value="192.168.1.101"/>
        <property name="proxyPort" value="22"/>
    ...
    </bean>




Realitzar desconnexió del servidor SFTP

Per realitzar la desconnexió del servidor FTP seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    if (sftpService.isLogged()) {
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Realitzar la desconnexió del servidor SFTP cridant al mètode logout().

Realitzar download d'un fitxer

Per realitzar un download d'un fitxer seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    sftpService.login();
    if (sftpService.isLogged()) {
        sftpService.downloadFile(fileName, localPath, remotePath);
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.
  3. En cas de que la connexió al servidor remot s'hagi realitzat correctament es crida al mètode downloadFile() per realitzar el download del fitxer del servidor remot.
  4. Realitzar la desconnexió del servidor SFTP.

Es crida el mètode downloadFile() que amb els següents paràmetres:

ordre Requerit Tipus Descripció
1 Si String Nom del fitxer del servidor remot que es vol realitzar el download.
2 Si String Directori local on es vol guardar el fitxer.
3 Si String Directori remot on es troba el fitxer a baixar.

I retorna:

ordre Requerit Tipus Descripció
1 Si boolean TRUE o FALSE segons s'ha realitzat correcta o incorrectament el procés.

Realitzar upload d'un fitxer

Per realitzar un upload d'un fitxer seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    sftpService.login();
    if (sftpService.isLogged()) {
        sftpService.uploadFile(fileName, localPath, remotePath);
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.
  3. En cas de que la connexió al servidor remot s'hagi realitzat correctament es crida al mètode uploadFile() per realitzar el upload del fitxer del servidor local al servidor remot.
  4. Realitzar la desconnexió del servidor SFTP.

Es crida el mètode uploadFile() que amb els següents paràmetres:

ordre Requerit Tipus Descripció
1 Si String Nom del fitxer del servidor remot que es vol realitzar el upload.
2 Si String Directori local on es troba el fitxer a pujar.
3 Si String Directori remot on es vol guardar el fitxer.

Obtenir la llista de noms dels fitxers del servidor remot

Per obtenir la llista de fitxers en una carpeta d'un servidor remot seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    sftpService.login();
    if (sftpService.isLogged()) {
        FileObject[] listFiles = sftpService.listFiles(remotePath);
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.
  3. En cas de que la connexió al servidor remot s'hagi realitzat correctament es crida al mètode listFiles() per obtenir una llista de fitxers en una carpeta d'un servidor remot.

Es crida el mètode listFiles() que amb els següents paràmetres:

ordre Requerit Tipus Descripció
1 String Nom de la carpeta del servidor remot d'on es vol obtenir la llista de fitxers.

El mètode retorna un array de FileObject, interfícies de la llibrería Commons VFS que ens donen una sèrie de mètodes per a treballar amb els fitxers remots.

Crear un fitxer al servidor remot

Per crear un fitxer al servidor remot seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    sftpService.login();
    if (sftpService.isLogged()) {
        createFile(fileName, remotePath);
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.
  3. En cas de que la connexió al servidor remot s'hagi realitzat correctament es crida al mètode createFile() per crear el fitxer en el servidor remot.

Es crida el mètode createFile() que amb els següents paràmetres:

ordre Requerit Tipus *Descripció*
1 Si String Nom del fitxer a crear en el servidor remot.
2 Si String Directori remot on es vol crear el fitxer.

El mètode retorna un boolean TRUE o FALSE segons s'ha realitzat correcta o incorrectament el procés.

Crear un directori al servidor remot

Per crear un directori al servidor remot seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    sftpService.login();
    if (sftpService.isLogged()) {
        createFolder(folderName, remotePath);
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.
  3. En cas de que la connexió al servidor remot s'hagi realitzat correctament es crida al mètode createFolder() per crear el fitxer en el servidor remot.

Es crida el mètode createFolder() que amb els següents paràmetres:

ordre Requerit Tipus *Descripció*
1 Si String Nom del directori a crear en el servidor remot.
2 Si String Directori remot on es vol crear el directori.

El mètode retorna un boolean TRUE o FALSE segons s'ha realitzat correcta o incorrectament el procés.

Esborrar un fitxer al servidor remot

Per esborrar un fitxer al servidor remot seguirem un patró com el mostrat en el següent exemple:

if (this.sftpService!= null ) {
    sftpService.login();
    if (sftpService.isLogged()) {
        deleteFile(fileName, remotePath);
        sftpService.logout();
    }
}




Realitzarem doncs els següents passos:


  1. En primer lloc comprovarem que s'ha realitzat correctament la injecció a la nostra classe del servei de SFTP.
  2. Seguidament cridem al mètode login() per realitzar la connexió i l'autentificació al servidor remot mitjançant el protocol SFTP.
  3. En cas de que la connexió al servidor remot s'hagi realitzat correctament es crida al mètode deleteFile() per crear el fitxer en el servidor remot.

Es crida el mètode deleteFile() que amb els següents paràmetres:

ordre Requerit Tipus *Descripció*
1 Si String Nom del fitxer a esborrar en el servidor remot.
2 Si String Directori remot on es troba el fitxer.

El mètode retorna un boolean TRUE o FALSE segons s'ha realitzat correcta o incorrectament el procés.

Exemples

Tests Unitaris

Un exemple d'utilització del servei de SFTP són els tests unitaris, a on s'obté el bean del servei a partir del fitxer de definició (applicationContext.xml).

S'ha de tenir en compte que s'ha de disposar d'accés a un servidor remot mitjançant el protocol SFTP per poder realitzar els tests.

package net.gencat.ctti.canigo.services.sftp.test;

...

La utilització del servei es independent de la configuració del mateix. Així, un possible fitxer de configuració del servei seria:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="sftpService" class="net.gencat.ctti.canigo.services.sftp.impl.SftpServiceImpl" init-method="init">
        <property name="logService">
            <ref bean="loggingService" />
        </property>
        <property name="ftpUsername" value="user"/>
        <property name="ftpPassword" value="pass"/>
        <property name="ftpUrl" value="192.168.131.75"/>
        <property name="ftpPort" value="22"/>
        <property name="knownHosts" value="C:/ssh/known_hosts"/>
        <property name="strictHostKeyChecking" value="yes"/>
    </bean>

    <!-- LOGGING service  -->
    <bean id="loggingService" class="net.gencat.ctti.canigo.services.logging.log4j.Log4JServiceImpl">
        <property name="configurator">
            <ref local="loggingConfigurator"/>
        </property>
    </bean>

    <!-- configurator bean -->
    <bean id="loggingConfigurator" class="net.gencat.ctti.canigo.services.logging.log4j.xml.HostDOMConfigurator" init-method="init">
        <property name="configFileName">
            <value>classpath:log4j-test.xml</value>
        </property>
    </bean>
</beans>




...

    public void testLogin() throws Exception {
        try {
            SftpService sftpService = getSftpService();

            assertTrue(sftpService.login());

            if (sftpService.isLogged()) {
                sftpService.logout();
            }
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }


    public void testLoginFailed() throws Exception {
        try {
            SftpService sftpService = getSftpService();

            assertFalse(sftpService.login("username", "incorrectPassword"));

            if (sftpService.isLogged()) {
                sftpService.logout();
            }
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }

    public void testUploadFile() throws Exception {
        String file = "file_"+System.currentTimeMillis()+".txt";
        try {
            SftpService sftpService = getSftpService();
            sftpService.login();

            sftpService.uploadFile(file, localPath, remotePath);
            assertTrue(sftpService.existsFile(remotePath, file));
            if(sftpService.isLogged()) {
                sftpService.logout();
            }
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }


    public void testDownloadFile() throws Exception {
        try {
            SftpService sftpService = getSftpService();
            sftpService.login();

            if(!sftpService.existsFile(remotePath, fileName)) {
                sftpService.uploadFile(fileName, localPath, remotePath);
            }
            sftpService.downloadFile(fileName, localPath, remotePath);
            if(sftpService.isLogged()) {
                sftpService.logout();
            }

            File file = new File(localPath+"/"+fileName);
            assertTrue(file.exists());
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }


    public void testCreateFolder() throws Exception {
        String folder = "folder_"+System.currentTimeMillis();
        try {
            SftpService sftpService = getSftpService();
            sftpService.login();

            sftpService.createFolder(remotePath,folder);
            assertTrue(sftpService.existsFolder(remotePath,folder));
            if(sftpService.isLogged()) {
                sftpService.logout();
            }
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }


    public void testDeleteFile() throws Exception {
        String file = "file_"+System.currentTimeMillis()+".txt";
        try {
            SftpService sftpService = getSftpService();
            sftpService.login();

            sftpService.createFile(remotePath,file);
            assertTrue(sftpService.deleteFile(remotePath,file));
            if(sftpService.isLogged()) {
                sftpService.logout();
            }
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }

    public void testListFiles() throws Exception {
        try {
            SftpService sftpService = getSftpService();
            sftpService.login();

            assertNotNull(sftpService.listFiles(remotePath));
            if(sftpService.isLogged()) {
                sftpService.logout();
            }
        } catch (SftpServiceException e) {
            log.error(e.getCause());
        }
    }


    private SftpService getSftpService() {
        BeanFactory beanFactory = new ClassPathXmlApplicationContext("applicationContext.xml");
        SftpService sftpService = (SftpService)beanFactory.getBean("sftpService");
        logService = sftpService.getLogService();
        log = logService.getLog(SftpServiceTest.class);
        assertNotNull(sftpService);

        return sftpService;
    }
...