diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index e7cf364b6..70c668b03 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "ftp" -version = "2.14.0" +version = "2.14.1" authors = ["Ballerina"] keywords = ["FTP", "SFTP", "remote file", "file transfer", "client", "service"] repository = "https://github.com/ballerina-platform/module-ballerina-ftp" @@ -45,5 +45,5 @@ path = "./lib/commons-lang3-3.18.0.jar" [[platform.java21.dependency]] groupId = "io.ballerina.stdlib" artifactId = "ftp-native" -version = "2.14.0" -path = "../native/build/libs/ftp-native-2.14.0.jar" +version = "2.14.1" +path = "../native/build/libs/ftp-native-2.14.1-SNAPSHOT.jar" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 5b1c8fbd2..ee7df9870 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,4 +3,4 @@ id = "ftp-compiler-plugin" class = "io.ballerina.stdlib.ftp.plugin.FtpCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/ftp-compiler-plugin-2.14.0.jar" +path = "../compiler-plugin/build/libs/ftp-compiler-plugin-2.14.1-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 61c04c3a9..8cb9d8cc2 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -10,7 +10,7 @@ distribution-version = "2201.12.0" [[package]] org = "ballerina" name = "ftp" -version = "2.14.0" +version = "2.14.1" dependencies = [ {org = "ballerina", name = "io"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina/tests/client_endpoint_test.bal b/ballerina/tests/client_endpoint_test.bal index 9ded6c685..16a4a3615 100644 --- a/ballerina/tests/client_endpoint_test.bal +++ b/ballerina/tests/client_endpoint_test.bal @@ -25,6 +25,9 @@ string nonFittingFilePath = "/home/in/test4.txt"; string newFilePath = "/home/in/test2.txt"; string appendFilePath = "tests/resources/datafiles/file1.txt"; string putFilePath = "tests/resources/datafiles/file2.txt"; +string relativePath = "rel-put.txt"; +string relativePathWithSlash = "/rel-path-slash-put.txt"; +string absPath = "//home/in/double-abs.txt"; // Create the config to access anonymous mock FTP server ClientConfiguration anonConfig = { @@ -65,9 +68,26 @@ ClientConfiguration sftpConfig = { } }; +// Create the config to access mock SFTP server with jailed home +ClientConfiguration sftpConfigUserDirRoot = { + protocol: SFTP, + host: "127.0.0.1", + port: 21213, + auth: { + credentials: {username: "wso2", password: "wso2123"}, + privateKey: { + path: "tests/resources/sftp.private.key", + password: "changeit" + }, + preferredMethods: [GSSAPI_WITH_MIC, PUBLICKEY, KEYBOARD_INTERACTIVE, PASSWORD] + }, + userDirIsRoot: true +}; + Client? anonClientEp = (); Client? clientEp = (); Client? sftpClientEp = (); +Client? sftpClientUserDirRootEp = (); Client? ftpUserHomeRootClientEp = (); Listener? callerListener = (); @@ -81,6 +101,7 @@ function initTestEnvironment() returns error? { anonClientEp = check new (anonConfig); clientEp = check new (config); sftpClientEp = check new (sftpConfig); + sftpClientUserDirRootEp = check new (sftpConfigUserDirRoot); ftpUserHomeRootClientEp = check new (ftpUserHomeRootConfig); callerListener = check new (callerListenerConfig); @@ -237,6 +258,85 @@ function testFtpUserDirIsRootTrue() returns error? { } } +@test:Config { + dependsOn: [testListFiles] +} +public function testPutRelativePath_userDirIsRootTrue() returns error? { + Error? putRes = (ftpUserHomeRootClientEp)->put(relativePath, "hello-jailed-rel"); + if putRes is Error { + test:assertFail("PUT(relative, no slash) failed on userDirIsRoot=true: " + putRes.message()); + } + + stream|Error getRes = (ftpUserHomeRootClientEp)->get(relativePath); + if getRes is stream { + test:assertTrue(check matchStreamContent(getRes, "hello-jailed-rel"), + msg = "Unexpected content from GET(relative) after PUT on userDirIsRoot=true"); + io:Error? closeErr = getRes.close(); + if closeErr is io:Error { + test:assertFail("Error closing relative GET stream: " + closeErr.message()); + } + } else { + test:assertFail("GET(relative) failed on userDirIsRoot=true: " + getRes.message()); + } + + Error? delRes = (ftpUserHomeRootClientEp)->delete(relativePath); + if delRes is Error { + log:printWarn("Cleanup delete failed for " + relativePath + ": " + delRes.message()); + } +} + +@test:Config { + dependsOn: [testListFiles] +} +public function testPutRelativePathWithSlash_userDirIsRootTrue() returns error? { + Error? putRes = (ftpUserHomeRootClientEp)->put(relativePathWithSlash, "hello-jailed-rel-with-slash"); + if putRes is Error { + test:assertFail("PUT(relative, with slash) failed on userDirIsRoot=true: " + putRes.message()); + } + + stream|Error getRes = (ftpUserHomeRootClientEp)->get(relativePathWithSlash); + if getRes is stream { + test:assertTrue(check matchStreamContent(getRes, "hello-jailed-rel-with-slash"), + msg = "Unexpected content from GET(relative) after PUT on userDirIsRoot=true"); + io:Error? closeErr = getRes.close(); + if closeErr is io:Error { + test:assertFail("Error closing relative GET stream: " + closeErr.message()); + } + } else { + test:assertFail("GET(relative) failed on userDirIsRoot=true: " + getRes.message()); + } + + Error? delRes = (ftpUserHomeRootClientEp)->delete(relativePathWithSlash); + if delRes is Error { + log:printWarn("Cleanup delete failed for " + relativePathWithSlash + ": " + delRes.message()); + } +} + +@test:Config { dependsOn: [testListFiles] } +public function testPutAbsoluteDoubleSlash_userDirIsRootFalse() returns error? { + Error? putRes = (clientEp)->put(absPath, "hello-abs-double-slash"); + if putRes is Error { test:assertFail("PUT(//absolute) failed: " + putRes.message()); } + stream|Error getRes = (clientEp)->get(absPath); + if getRes is stream { + test:assertTrue(check matchStreamContent(getRes, "hello-abs-double-slash")); + check getRes.close(); + } else { test:assertFail("GET(//absolute) failed: " + getRes.message()); } + check (clientEp)->delete(absPath); +} + +@test:Config { dependsOn: [testListFiles] } +public function testSftpUserDirIsRootTrue_RelativePutGet() returns error? { + stream bStream = ["hello-sftp-rel".toBytes().cloneReadOnly()].toStream(); + Error? putRes = (sftpClientUserDirRootEp)->put("sftp-rel.txt", bStream); + if putRes is Error { test:assertFail("SFTP relative PUT failed: " + putRes.message()); } + stream|Error getRes = (sftpClientUserDirRootEp)->get("sftp-rel.txt"); + if getRes is stream { + test:assertTrue(check matchStreamContent(getRes, "hello-sftp-rel")); + check getRes.close(); + } else { test:assertFail("SFTP relative GET failed: " + getRes.message()); } + check (sftpClientUserDirRootEp)->delete("sftp-rel.txt"); +} + @test:Config { dependsOn: [testPutCompressedFileContent] } diff --git a/changelog.md b/changelog.md index 828576a78..1952f2d65 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Fixed +- [Fix the issue where the FTP URL is improperly formatted for the jail-break scnenarios](https://github.com/ballerina-platform/ballerina-library/issues/8267) + + ## [2.14.0] - 2025-08-21 ### Added diff --git a/native/src/main/java/io/ballerina/stdlib/ftp/util/FtpUtil.java b/native/src/main/java/io/ballerina/stdlib/ftp/util/FtpUtil.java index e5072c8a3..cd71e2e24 100644 --- a/native/src/main/java/io/ballerina/stdlib/ftp/util/FtpUtil.java +++ b/native/src/main/java/io/ballerina/stdlib/ftp/util/FtpUtil.java @@ -112,14 +112,24 @@ public static String createUrl(BMap config) throws BallerinaFtpException { private static String createUrl(String protocol, String host, int port, String username, String password, String filePath) throws BallerinaFtpException { String userInfo = username + ":" + password; - URI uri; + final String normalizedPath = normalizeFtpPath(filePath); try { - uri = new URI(protocol, userInfo, host, port, filePath, null, null); + URI uri = new URI(protocol, userInfo, host, port, normalizedPath, null, null); + return uri.toString(); } catch (URISyntaxException e) { throw new BallerinaFtpException("Error occurred while constructing a URI from host: " + host + ", port: " + port + ", username: " + username + " and basePath: " + filePath + e.getMessage(), e); } - return uri.toString(); + } + + private static String normalizeFtpPath(String rawPath) { + if (rawPath == null || rawPath.isEmpty()) { + return "/"; + } + if (rawPath.startsWith("/")) { + return rawPath; + } + return "/" + rawPath; } public static Map getAuthMap(BMap config) {