使用apache commons-net FTPClient传输原始二进制文件?

2022-09-01 15:43:05

更新:已解决

我在登录之前正在呼叫,导致FTP服务器使用默认模式(),无论我将其设置为什么。另一方面,客户端的行为就像文件格式已正确设置一样。 模式现在完全按照预期工作,在所有情况下都逐个字节地传输文件。我所要做的就是在wireshark中嗅探一量,然后使用netcat模仿FTP命令来查看发生了什么。为什么我两天前没有想到这一点!?谢谢大家的帮助!FTPClient.setFileType()ASCIIBINARY

我有一个xml文件,utf-16编码,我正在使用apache的commons-net-2.0 java库的FTPClient从FTP站点下载它。它支持两种传输模式:和 ,不同之处在于,它将用适当的本地行分隔符替换行分隔符(或者只是 - 在十六进制中,或者只是)。我的问题是这样的:我有一个测试文件,utf-16编码,其中包含以下内容:ASCII_FILE_TYPEBINARY_FILE_TYPEASCII'\r\n''\n'0x0d0a0x0a

<?xml version='1.0' encoding='utf-16'?>
<data>
    <blah>blah</blah>
</data>

这是十六进制:
0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.x.m.l. .v.e
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .r.s.i.o.n.=.'.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .e.n.c.o
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .d.i.n.g.=.'.u.t
0000040: 0066 002d 0031 0036 0027 003f 003e 000a .f.-.1.6.'.?.>..
0000050: 003c 0064 0061 0074 0061 003e 000a 0009 .<.d.a.t.a.>....
0000060: 003c 0062 006c 0061 0068 003e 0062 006c .<.b.l.a.h.>.b.l
0000070: 0061 0068 003c 002f 0062 006c 0061 0068 .a.h.<./.b.l.a.h
0000080: 003e 000a 003c 002f 0064 0061 0074 0061 .>...<./.d.a.t.a
0000090: 003e 000a                                                            .>..

当我对此文件使用模式时,它会正确传输,逐字节;结果具有相同的 md5sum。伟大。当我使用传输模式时,除了将字节从 a 洗牌成 a 之外,不应该做任何事情,结果是换行符 () 被转换为回车符 + 换行符对 ()。以下是二进制传输后的十六进制:ASCIIBINARYInputStreamOutputStream0x0a0x0d0a

0000000: 003c 003f 0078 006d 006c 0020 0076 0065 .<.?.x.m.l. .v.e
0000010: 0072 0073 0069 006f 006e 003d 0027 0031 .r.s.i.o.n.=.'.1
0000020: 002e 0030 0027 0020 0065 006e 0063 006f ...0.'. .e.n.c.o
0000030: 0064 0069 006e 0067 003d 0027 0075 0074 .d.i.n.g.=.'.u.t
0000040: 0066 002d 0031 0036 0027 003f 003e 000d .f.-.1.6.'.?.>..
0000050: 0a00 3c00 6400 6100 7400 6100 3e00 0d0a ..<.d.a.t.a.>...
0000060: 0009 003c 0062 006c 0061 0068 003e 0062 ...<.b.l.a.h.>.b
0000070: 006c 0061 0068 003c 002f 0062 006c 0061 .l.a.h.<./.b.l.a
0000080: 0068 003e 000d 0a00 3c00 2f00 6400 6100 .h.>....<./.d.a.
0000090: 7400 6100 3e00 0d0a                                        t.a.>...

它不仅转换换行符(它不应该),而且它不尊重utf-16编码(并不是说我希望它知道它应该,它只是一个愚蠢的FTP管道)。如果不进一步处理以重新对齐字节,则结果不可读。我只使用模式,但我的应用程序也将在同一管道上移动真正的二进制数据(mp3文件和jpeg图像)。对这些二进制文件使用传输模式也会导致它们将随机的s注入到其内容中,由于二进制数据通常包含合法序列,因此无法安全地将其删除。如果我在这些文件上使用模式,那么无论我做什么,“聪明”的FTPClient都会将这些转换为使文件不一致。ASCIIBINARY0x0d0x0d0aASCII0x0d0a0x0a

我想我的问题是:有没有人知道任何好的java FTP库,只是将该死的字节从那里移动到这里,或者我是否将不得不破解apache commons-net-2.0并维护我自己的FTP客户端代码只是为了这个简单的应用程序?有没有人处理过这种奇怪的行为?任何建议将不胜感激。

我查看了commons-net源代码,它看起来并不像是使用模式时奇怪的行为的原因。但是,它在模式下读取的只是一个包裹在套接字上。这些低级Java流是否曾经做过任何奇怪的字节操作?如果他们这样做,我会感到震惊,但我不明白这里还会发生什么。BINARYInputStreamBINARYjava.io.BufferedInptuStreamInputStream

编辑1:

下面是一段最小的代码,它模拟了我为下载文件所做的工作。要编译,只需执行

javac -classpath /path/to/commons-net-2.0.jar Main.java

要运行,您需要目录 /tmp/ascii 和 /tmp/binary 才能将文件下载到其中,以及设置一个 ftp 站点,其中包含该文件。还需要使用适当的 ftp 主机、用户名和密码配置代码。我将文件放在测试ftp站点的test/文件夹下,并调用文件测试.xml。测试文件至少应具有多行,并且采用 utf-16 编码(这可能不是必需的,但有助于重现我的确切情况)。我在打开新文件后使用了vim的命令,并输入了上面引用的xml文本。最后,要运行,只需做:set fileencoding=utf-16

java -cp .:/path/to/commons-net-2.0.jar Main

法典:

(注意:此代码修改为使用自定义 FTPClient 对象,下面在“EDIT 2”下链接)

import java.io.*;
import java.util.zip.CheckedInputStream;
import java.util.zip.CheckedOutputStream;
import java.util.zip.CRC32;
import org.apache.commons.net.ftp.*;

public class Main implements java.io.Serializable
{
    public static void main(String[] args) throws Exception
    {
        Main main = new Main();
        main.doTest();
    }

    private void doTest() throws Exception
    {
        String host = "ftp.host.com";
        String user = "user";
        String pass = "pass";

        String asciiDest = "/tmp/ascii";
        String binaryDest = "/tmp/binary";

        String remotePath = "test/";
        String remoteFilename = "test.xml";

        System.out.println("TEST.XML ASCII");
        MyFTPClient client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE);
        File path = new File("/tmp/ascii");
        downloadFTPFileToPath(client, "test/", "test.xml", path);
        System.out.println("");

        System.out.println("TEST.XML BINARY");
        client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
        path = new File("/tmp/binary");
        downloadFTPFileToPath(client, "test/", "test.xml", path);
        System.out.println("");

        System.out.println("TEST.MP3 ASCII");
        client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.ASCII_FILE_TYPE);
        path = new File("/tmp/ascii");
        downloadFTPFileToPath(client, "test/", "test.mp3", path);
        System.out.println("");

        System.out.println("TEST.MP3 BINARY");
        client = createFTPClient(host, user, pass, org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);
        path = new File("/tmp/binary");
        downloadFTPFileToPath(client, "test/", "test.mp3", path);
    }

    public static File downloadFTPFileToPath(MyFTPClient ftp, String remoteFileLocation, String remoteFileName, File path)
        throws Exception
    {
        // path to remote resource
        String remoteFilePath = remoteFileLocation + "/" + remoteFileName;

        // create local result file object
        File resultFile = new File(path, remoteFileName);

        // local file output stream
        CheckedOutputStream fout = new CheckedOutputStream(new FileOutputStream(resultFile), new CRC32());

        // try to read data from remote server
        if (ftp.retrieveFile(remoteFilePath, fout)) {
            System.out.println("FileOut: " + fout.getChecksum().getValue());
            return resultFile;
        } else {
            throw new Exception("Failed to download file completely: " + remoteFilePath);
        }
    }

    public static MyFTPClient createFTPClient(String url, String user, String pass, int type)
        throws Exception
    {
        MyFTPClient ftp = new MyFTPClient();
        ftp.connect(url);
        if (!ftp.setFileType( type )) {
            throw new Exception("Failed to set ftpClient object to BINARY_FILE_TYPE");
        }

        // check for successful connection
        int reply = ftp.getReplyCode();
        if (!FTPReply.isPositiveCompletion(reply)) {
            ftp.disconnect();
            throw new Exception("Failed to connect properly to FTP");
        }

        // attempt login
        if (!ftp.login(user, pass)) {
            String msg = "Failed to login to FTP";
            ftp.disconnect();
            throw new Exception(msg);
        }

        // success! return connected MyFTPClient.
        return ftp;
    }

}

编辑2:

好吧,我遵循了建议,这是我的结果。我复制了 apache 的 called ,并将 和 和 都 包装在 using 校验和中。此外,我包装了我给出的,以将输出存储在带有校验和的中。MyFTPClient的代码发布在这里,我已经修改了上面的测试代码以使用此版本的FTPClient(尝试将一个gist URL发布到修改后的代码,但我需要10个信誉点才能发布多个URL!),结果是这样的:CheckedXputStreamFTPClientMyFTPClientSocketInputStreamBufferedInputStreamCheckedInputStreamCRC32FileOutputStreamFTPClientCheckOutputStreamCRC32test.xmltest.mp3

14:00:08,644 DEBUG [main,TestMain] TEST.XML ASCII
14:00:08,919 DEBUG [main,MyFTPClient] Socket CRC32: 2739864033
14:00:08,919 DEBUG [main,MyFTPClient] Buffer CRC32: 2739864033
14:00:08,954 DEBUG [main,FTPUtils] FileOut CRC32: 866869773

14:00:08,955 DEBUG [main,TestMain] TEST.XML BINARY
14:00:09,270 DEBUG [main,MyFTPClient] Socket CRC32: 2739864033
14:00:09,270 DEBUG [main,MyFTPClient] Buffer CRC32: 2739864033
14:00:09,310 DEBUG [main,FTPUtils] FileOut CRC32: 2739864033

14:00:09,310 DEBUG [main,TestMain] TEST.MP3 ASCII
14:00:10,635 DEBUG [main,MyFTPClient] Socket CRC32: 60615183
14:00:10,635 DEBUG [main,MyFTPClient] Buffer CRC32: 60615183
14:00:10,636 DEBUG [main,FTPUtils] FileOut CRC32: 2352009735

14:00:10,636 DEBUG [main,TestMain] TEST.MP3 BINARY
14:00:11,482 DEBUG [main,MyFTPClient] Socket CRC32: 60615183
14:00:11,482 DEBUG [main,MyFTPClient] Buffer CRC32: 60615183
14:00:11,483 DEBUG [main,FTPUtils] FileOut CRC32: 60615183

这基本上没有任何意义,因为这里是相关响应文件的md5sums:

bf89673ee7ca819961442062eaaf9c3f  ascii/test.mp3
7bd0e8514f1b9ce5ebab91b8daa52c4b  binary/test.mp3
ee172af5ed0204cf9546d176ae00a509  original/test.mp3

104e14b661f3e5dbde494a54334a6dd0  ascii/test.xml
36f482a709130b01d5cddab20a28a8e8  binary/test.xml
104e14b661f3e5dbde494a54334a6dd0  original/test.xml

我不知所措。我发誓我没有在这个过程中的任何时候排列文件名/路径,并且我已经三重检查了每一步。这一定是一件简单的事情,但我不知道下一步该去哪里看。为了实用性,我将通过调用shell来进行FTP传输,但我打算继续这样做,直到我了解到底发生了什么。我将用我的发现更新这个帖子,我将继续感谢任何人可能做出的任何贡献。希望这在某个时候对某人有用!


答案 1

登录到 ftp 服务器后

ftp.setFileType(FTP.BINARY_FILE_TYPE);

下面的一行不能解决它:

//ftp.setFileTransferMode(org.apache.commons.net.ftp.FTP.BINARY_FILE_TYPE);

答案 2

在我看来,您的应用程序代码可能已经反转了 ASCII 和 BINARY 模式的选择。ASCII是不变的,BINARY执行行尾字符转换与FTP的工作方式完全相反

如果这不是问题所在,请编辑您的问题以添加代码的相关部分。

编辑

其他一些可能(但IMO不太可能)的解释:

  • FTP 服务器已损坏/配置错误。(您可以使用非 Java 命令行 FTP 实用程序以 ASCII/ BINARY 模式成功下载文件吗?
  • 您正在通过损坏或配置错误的代理与FTP服务器通信。
  • 您已经以某种方式设法获得了Apache FTP客户端JAR文件的狡猾(被黑客入侵)副本。(是的,是的,不太可能...)

推荐