I have to establish the SFTP connection within a Spring Boot app that runs from a WAR file. Before posting this I’ve tried the suggestions from the following threads:
- Cannot be resolved to absolute file path because it does not reside in the file system
- File path to resource in our war/WEB-INF folder?
- SFTP Inbound Sync Fails when deployed as war
The application is built as a WAR file, and I used the JSch library in order to establish the SFTP connection. The setup for this library implies usage of its addIdentity
method, where the first argument/parameter should be the path to the Private Key file (the content is not passed directly).
If I run the app within and IDE, then the SFTP connection can be established and it works. But if I build the app as a WAR file, and I run it from that WAR file, then the SFTP connection cannot be established, because the Private Key file cannot be found.
I run the WAR file by executing this command within a CLI, from the location of that WAR file:
java -jar test_sftp_in_war_file-0.0.1-SNAPSHOT.war
The Private Key file was added to the resources
directory from the Spring Boot project.
This image contains the location of the Private Key within the generated WAR file
The passphrase is being read and passed correctly. The issue is the path to the Private Key file.
I use the pom.xml
file, with the following Maven dependencies:
<?xml ...>
<packaging>war</packaging> <!-- to build the project as a WAR file -->
<name>test_sftp_in_war_file</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- dependency for the JSch SFTP library -->
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-sftp</artifactId>
<version>5.5.15</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
This is the code that I've tried:
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import java.io.IOException;
import java.io.InputStream;
import javax.servlet.ServletContext;
import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.io.FileOutputStream;
import java.util.UUID;
import org.apache.commons.io.IOUtils;
import java.net.URL;
import java.net.URLDecoder;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@Slf4j
@RequiredArgsConstructor
public class SftpExample {
private final ServletContext context;
public static void main(String[] args) throws JSchException, IOException {
SpringApplication.run(TestSftpInWarFileApplication.class, args);
}
public void fetchData() throws Exception {
JSch jSch = new JSch();
Session session = null;
Channel channel = null;
ChannelSftp channelSftp = null;
String devServerPath = "/proprietary/path/on/sftp/server";
String filePath;
Path tempFile = null;
InputStream inputStream;
FileOutputStream out;
try {
// version 1
/** => the filePath is retrieved, but the error occurs when it is passed to the addIdentity method
* >>> filePath = C:\projects\test_sftp_in_war_file\target\private-key.ppk
* com.jcraft.jsch.JSchException: java.io.FileNotFoundException: C:\projects\test_sftp_in_war_file\target\private-key.ppk
* (The system cannot find the file specified)
* at com.jcraft.jsch.KeyPair.load(KeyPair.java:543)
* at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:387)
* at com.example.test_sftp_in_war_file.SftpExample.fetchData(SftpExample.java:172)
*/
filePath = Paths.get("private-key.ppk").toAbsolutePath().toString();
// version 2
/** => the filePath is retrieved, but the error occurs when it is passed to the addIdentity method
* >>> filePath = jar:file:/C:/projects/test_sftp_in_war_file/target/test_sftp_in_war_file-0.0.1-SNAPSHOT.war!/WEB-INF/classes!/private-key.ppk
* com.jcraft.jsch.JSchException: java.io.FileNotFoundException: jar:file:\C:\projects\test_sftp_in_war_file\target\test_sftp_in_war_file-0.0.1-SNAPSHOT.war!\WEB-INF\classes!\private-key.ppk
* (The filename, directory name, or volume label syntax is incorrect)
* at com.jcraft.jsch.KeyPair.load(KeyPair.java:543)
* at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:387)
* at com.example.test_sftp_in_war_file.SftpExample.fetchData(SftpExample.java:162)
*/
filePath = this.getClass().getClassLoader().getResource("private-key.ppk").toExternalForm();
// version 3
/** => the filePath is retrieved, but the error occurs when it is passed to the addIdentity method
* >>> filePath = file:/C:/projects/test_sftp_in_war_file/target/classes/private-key.ppk
* java.lang.NullPointerException: null
* at com.jcraft.jsch.Util.checkTilde(Util.java:489) ~[jsch-0.1.55.jar!/:na]
* at com.jcraft.jsch.Util.fromFile(Util.java:506) ~[jsch-0.1.55.jar!/:na]
* at com.jcraft.jsch.KeyPair.load(KeyPair.java:540) ~[jsch-0.1.55.jar!/:na]
* at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40) ~[jsch-0.1.55.jar!/:na]
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:406) ~[jsch-0.1.55.jar!/:na]
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:387) ~[jsch-0.1.55.jar!/:na]
* at com.example.test_sftp_in_war_file.SftpExample.fetchData(SftpExample.java:148)
*/
File privateKeyFile = new ClassPathResource("private-key.ppk").getFile(); // getting the file content
filePath = privateKeyFile.toURI().toString();
// version 4 => read the content and generate a new temporary file, and pass the path to that temp file
/** => this workaround works when running the local WAR, but does not run in the cloud environment
* >>> filePath = C:\Users\USER\AppData\Local\Temp\private-key_3ca449c2-0ed1-459c-a2cd-70f51f4023eb2105597171450888881.ppk
*/
inputStream = new ClassPathResource("private-key.ppk").getInputStream();
tempFile = File.createTempFile("private_key_" + UUID.randomUUID().toString(), ".ppk").toPath();
out = new FileOutputStream(tempFile.toFile());
IOUtils.copy(inputStream, out);
filePath = tempFile.toFile().getAbsolutePath();
// version 5
/** => the filePath is retrieved, but the error occurs when it is passed to the addIdentity method
* >>> filePath = file:\C:\projects\test_sftp_in_war_file\target\test_sftp_in_war_file-0.0.1-SNAPSHOT.war*\WEB-INF\classes\private-key.ppk
* com.jcraft.jsch.JSchException: java.io.FileNotFoundException: file:\C:\projects\test_sftp_in_war_file\target\test_sftp_in_war_file-0.0.1-SNAPSHOT.war*\WEB-INF\classes\private-key.ppk
* (The filename, directory name, or volume label syntax is incorrect)
* at com.jcraft.jsch.KeyPair.load(KeyPair.java:543)
* at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:387)
* at com.example.test_sftp_in_war_file.SftpExample.fetchData(SftpExample.java:95)
*/
URL resourceUrl = context.getResource("/WEB-INF/classes/private-key.ppk");
filePath = resourceUrl.getPath();
// version 6
* >>> filePath = sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@704b2127
* com.jcraft.jsch.JSchException: java.io.FileNotFoundException: sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@704b2127
* (The system cannot find the file specified)
* at com.jcraft.jsch.KeyPair.load(KeyPair.java:543)
* at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:387)
* at com.example.test_sftp_in_war_file.SftpExample.fetchData(SftpExample.java:95)
*/
filePath = getClass().getResourceAsStream("/WEB-INF/classes/private-key.ppk").toString();
// version 7
/** => the filePath is retrieved, but the error occurs when it is passed to the addIdentity method
* >>> filePath = file:\C:\projects\test_sftp_in_war_file\target\test_sftp_in_war_file-0.0.1-SNAPSHOT.war!\WEB-INF\classes!\private-key.ppk
* com.jcraft.jsch.JSchException: java.io.FileNotFoundException: file:\C:\projects\test_sftp_in_war_file\target\test_sftp_in_war_file-0.0.1-SNAPSHOT.war!\WEB-INF\classes!\private-key.ppk
* (The filename, directory name, or volume label syntax is incorrect)
* at com.jcraft.jsch.KeyPair.load(KeyPair.java:543)
* at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
* at com.jcraft.jsch.JSch.addIdentity(JSch.java:387)
* at com.example.test_sftp_in_war_file.SftpExample.fetchData(SftpExample.java:95)
*/
filePath = getClass().getClassLoader().getResource("private-key.ppk").getPath();
// version 8
/**
* java.lang.NullPointerException: null
* at com.jcraft.jsch.Util.checkTilde(Util.java:489)
* at com.jcraft.jsch.Util.fromFile(Util.java:506)
*/
filePath = SftpExample.class.getResource("private-key.ppk").getPath();
// version 9
/**
* java.lang.NullPointerException: null
* at com.jcraft.jsch.Util.checkTilde(Util.java:489)
* at com.jcraft.jsch.Util.fromFile(Util.java:506)
*/
filePath = URLDecoder.decode(SftpExample.class.getResource("private-key.ppk").getPath(), "UTF-8"); // Application run failed: java.lang.NullPointerException: null
// version 10
/**
* java.lang.NullPointerException: null
* at com.jcraft.jsch.Util.checkTilde(Util.java:489)
* at com.jcraft.jsch.Util.fromFile(Util.java:506)
*/
// filePath = context.getRealPath("/WEB-INF/classes/private-key.ppk");
log.info(">>> filePath = " + filePath);
} catch (NullPointerException e) {
// throw new Exception("Could not be retrieved the path for this file: " + "private-key.ppk");
log.error(Arrays.toString(e.getStackTrace()));
}
try {
jSch.addIdentity(filePath, "passphrase_is_correct"); // the error is generated at this line
session = jSch.getSession("username", "host", 22); // this is where the connection should be established
java.util.Properties config = new java.util.Properties();
config.put("StrictHostKeyChecking", "no"); // by default, say that we trust the specified server (to not be required to explicitly say that we trust it for each connection)
session.setConfig(config);
session.connect();
channel = session.openChannel("sftp");
channel.connect();
channelSftp = (ChannelSftp) channel;
// other operations that occur once the SFTP connection is established
log.info(">>> the SFTP connection was established successfully");
} catch (JSchException e) {
e.printStackTrace();
} finally {
if (session != null && channel != null) {
channel.disconnect();
session.disconnect();
}
inputStream.close();
out.close();
}
}
@EventListener(ContextRefreshedEvent.class) // waits to be finished the Spring Context initialization
public void run1() throws Exception {
this.fetchData();
}
}
This is the stack trace that was generated for version 5:
2023-01-03 17:19:32.413 ERROR 25960 --- [ main] o.s.boot.SpringApplication : Application run failed
Bean [com.example.test_sftp_in_war_file.SftpExample]
Method [public void com.example.test_sftp_in_war_file.SftpExample throws java.lang.Exception]
Method [public void com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.run1() throws java.lang.Exception]
Resolved arguments:
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:361) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:229) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:166) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:421) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:378) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:938) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586) ~[spring-context-5.3.24.jar!/:5.3.24]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147) ~[spring-boot-2.7.7.jar!/:2.7.7]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731) ~[spring-boot-2.7.7.jar!/:2.7.7]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408) ~[spring-boot-2.7.7.jar!/:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307) ~[spring-boot-2.7.7.jar!/:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303) ~[spring-boot-2.7.7.jar!/:2.7.7]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292) ~[spring-boot-2.7.7.jar!/:2.7.7]
at com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.main(TestSftpInWarFileApplication.java:42) ~[classes!/:0.0.1-SNAPSHOT]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49) ~[test_sftp_in_war_file-0.0.1-SNAPSHOT.war:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.Launcher.launch(Launcher.java:108) ~[test_sftp_in_war_file-0.0.1-SNAPSHOT.war:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58) ~[test_sftp_in_war_file-0.0.1-SNAPSHOT.war:0.0.1-SNAPSHOT]
at org.springframework.boot.loader.WarLauncher.main(WarLauncher.java:59) ~[test_sftp_in_war_file-0.0.1-SNAPSHOT.war:0.0.1-SNAPSHOT]
Caused by: java.lang.Exception: Could not be retrieved the path for this file: private-key.ppk
at com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.fetchData(TestSftpInWarFileApplication.java:86) ~[classes!/:0.0.1-SNAPSHOT]
at com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.run1(TestSftpInWarFileApplication.java:118) ~[classes!/:0.0.1-SNAPSHOT]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:566) ~[na:na]
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:344) ~[spring-context-5.3.24.jar!/:5.3.24]
... 24 common frames omitted
This is the stack trace generated for version 6 and 7:
2023-01-04 11:09:30.661 INFO 29328 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8090 (http) with context path ''
2023-01-04 11:09:30.670 INFO 29328 --- [ main] c.e.t.TestSftpInWarFileApplication : >>> filePath = sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@7cc9ce8
com.jcraft.jsch.JSchException: java.io.FileNotFoundException: sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@7cc9ce8 (The system cannot find the file specified)
at com.jcraft.jsch.KeyPair.load(KeyPair.java:543)
at com.jcraft.jsch.IdentityFile.newInstance(IdentityFile.java:40)
at com.jcraft.jsch.JSch.addIdentity(JSch.java:406)
at com.jcraft.jsch.JSch.addIdentity(JSch.java:387)
at com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.fetchData(TestSftpInWarFileApplication.java:90)
at com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.run1(TestSftpInWarFileApplication.java:118)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.context.event.ApplicationListenerMethodAdapter.doInvoke(ApplicationListenerMethodAdapter.java:344)
at org.springframework.context.event.ApplicationListenerMethodAdapter.processEvent(ApplicationListenerMethodAdapter.java:229)
at org.springframework.context.event.ApplicationListenerMethodAdapter.onApplicationEvent(ApplicationListenerMethodAdapter.java:166)
at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176)
at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169)
at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:421)
at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:378)
at org.springframework.context.support.AbstractApplicationContext.finishRefresh(AbstractApplicationContext.java:938)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:586)
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:147)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:731)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:307)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1303)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1292)
at com.example.test_sftp_in_war_file.TestSftpInWarFileApplication.main(TestSftpInWarFileApplication.java:42)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:566)
at org.springframework.boot.loader.MainMethodRunner.run(MainMethodRunner.java:49)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:108)
at org.springframework.boot.loader.Launcher.launch(Launcher.java:58)
at org.springframework.boot.loader.WarLauncher.main(WarLauncher.java:59)
Caused by: java.io.FileNotFoundException: sun.net.www.protocol.jar.JarURLConnection$JarURLInputStream@7cc9ce8 (The system cannot find the file specified)
at java.base/java.io.FileInputStream.open0(Native Method)
at java.base/java.io.FileInputStream.open(FileInputStream.java:219)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:157)
at java.base/java.io.FileInputStream.<init>(FileInputStream.java:112)
at com.jcraft.jsch.Util.fromFile(Util.java:508)
at com.jcraft.jsch.KeyPair.load(KeyPair.java:540)
... 34 more