在twisted.protocols.ftp.FTP 中实现REST?

发布于 2024-10-04 19:18:06 字数 2059 浏览 3 评论 0原文

有没有人设法在twisted的FTP服务器中实现REST命令?我当前的尝试:

from twisted.protocols import ftp
from twisted.internet import defer

class MyFTP(ftp.FTP):
    def ftp_REST(self, pos):
        try:
            pos = int(pos)
        except ValueError:
            return defer.fail(CmdSyntaxError('Bad argument for REST'))

        def all_ok(result):
            return ftp.REQ_FILE_ACTN_PENDING_FURTHER_INFO # 350

        return self.shell.restart(pos).addCallback(all_ok)

class MyShell(ftp.FTPShell):
    def __init__(self, host, auth):
        self.position = 0
        ...

    def restart(self, pos):
        self.position = pos
        print "Restarting at %s"%pos
        return defer.succeed(pos)

当客户端发送 REST 命令时,我需要几秒钟的时间才能在脚本输出中看到以下内容:

Traceback (most recent call last):
Failure: twisted.protocols.ftp.PortConnectionError: DTPFactory timeout
Restarting at <pos>

我做错了什么?在我看来,REST 命令应该立即做出响应,为什么套接字超时?

更新:

按照 Jean-Paul Calderone 的建议启用日志记录后,在 DTP 连接因缺乏连接而超时(时间戳减少到MM:SS 为简洁起见):

09:53 [TrafficLoggingProtocol,1,127.0.0.1] cleanupDTP
09:53 [TrafficLoggingProtocol,1,127.0.0.1] <<class 'twisted.internet.tcp.Port'> of twisted.protocols.ftp.DTPFactory on 37298>
09:53 [TrafficLoggingProtocol,1,127.0.0.1] dtpFactory.stopFactory
09:53 [-] (Port 37298 Closed)
09:53 [-] Stopping factory <twisted.protocols.ftp.DTPFactory instance at 0x8a792ec>
09:53 [-] dtpFactory.stopFactory
10:31 [-] timed out waiting for DTP connection
10:31 [-] Unexpected FTP error
10:31 [-] Unhandled Error
        Traceback (most recent call last):
        Failure: twisted.protocols.ftp.PortConnectionError: DTPFactory timeout

10:31 [TrafficLoggingProtocol,2,127.0.0.1] Restarting at 1024

ftp_PASV 命令返回 DTPFactory.deferred,它被描述为“连接实例时将触发的延迟”。 RETR 命令执行得很好(否则 ftp.FTP 将毫无价值)。

这让我相信这里存在某种阻塞操作,在建立 DTP 连接之前不会让任何其他事情发生;只有那时我们才能接受进一步的命令。不幸的是,看起来一些(全部?)客户端(具体来说,我正在使用 FileZilla 进行测试)在尝试恢复下载时在连接之前发送 REST 命令。

Has anyone managed to implement the REST command in twisted's FTP server? My current attempt:

from twisted.protocols import ftp
from twisted.internet import defer

class MyFTP(ftp.FTP):
    def ftp_REST(self, pos):
        try:
            pos = int(pos)
        except ValueError:
            return defer.fail(CmdSyntaxError('Bad argument for REST'))

        def all_ok(result):
            return ftp.REQ_FILE_ACTN_PENDING_FURTHER_INFO # 350

        return self.shell.restart(pos).addCallback(all_ok)

class MyShell(ftp.FTPShell):
    def __init__(self, host, auth):
        self.position = 0
        ...

    def restart(self, pos):
        self.position = pos
        print "Restarting at %s"%pos
        return defer.succeed(pos)

When a client sends a REST command, it takes several seconds before I see this at the script output:

Traceback (most recent call last):
Failure: twisted.protocols.ftp.PortConnectionError: DTPFactory timeout
Restarting at <pos>

What am I doing wrong? Seems to me like a response should follow immediately from the REST command, why is the socket timing out?

Update:

After enabling logging as suggested by Jean-Paul Calderone, it looks like the REST command isn't even making it to my FTP class before the DTP connection times out from lack of connection (timestamps reduced to MM:SS for brevity):

09:53 [TrafficLoggingProtocol,1,127.0.0.1] cleanupDTP
09:53 [TrafficLoggingProtocol,1,127.0.0.1] <<class 'twisted.internet.tcp.Port'> of twisted.protocols.ftp.DTPFactory on 37298>
09:53 [TrafficLoggingProtocol,1,127.0.0.1] dtpFactory.stopFactory
09:53 [-] (Port 37298 Closed)
09:53 [-] Stopping factory <twisted.protocols.ftp.DTPFactory instance at 0x8a792ec>
09:53 [-] dtpFactory.stopFactory
10:31 [-] timed out waiting for DTP connection
10:31 [-] Unexpected FTP error
10:31 [-] Unhandled Error
        Traceback (most recent call last):
        Failure: twisted.protocols.ftp.PortConnectionError: DTPFactory timeout

10:31 [TrafficLoggingProtocol,2,127.0.0.1] Restarting at 1024

The ftp_PASV command returns DTPFactory.deferred, which is described as a "deferred [that] will fire when instance is connected". RETR commands come through fine (ftp.FTP would be pretty worthless otherwise).

This leads me to believe that there is some sort of blocking operation in here that won't let anything else happen until that DTP connection is made; then and only then can we accept further commands. Unfortunately, it looks like some (all?) clients (specifically, I'm testing with FileZilla) send the REST command before connecting when trying to resume a download.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

瞎闹 2024-10-11 19:18:06

验证客户端的行为是否符合您的预期。使用 tcpdump 或wireshark 捕获所有相关流量是一个好方法,尽管您也可以通过多种方式在基于 Twisted 的 FTP 服务器中启用日志记录(例如,通过使用工厂包装器 twisted.txt )。 Protocols.policies.TrafficLoggingFactory)。

从超时错误后的“正在重新启动...”日志消息中,我猜测客户端“首先”发送 RETR,然后发送 REST。 RETR 超时是因为客户端在收到对 REST 的响应之前不会尝试连接到数据通道,并且 Twisted 服务器甚至在客户端连接到数据通道(并下载整个文件)。修复此问题可能需要更改 ftp.FTP 处理来自客户端的命令的方式,以便可以正确解释 RETR 之后的 REST(或者您正在使用的 FTP 客户端可能只是有 bug,从协议来看)我可以找到的文档中,RETR 应该遵循 REST,而不是相反)。

不过,这只是一个猜测,您应该查看流量捕获来确认或拒绝它。

Verify that the client is behaving as you expect. Capturing all of the relevant traffic with tcpdump or wireshark is a good way to do this, although you can also enable logging in your Twisted-based FTP server in a number of ways (for example, by using the factory wrapper twisted.protocols.policies.TrafficLoggingFactory).

From the timeout error followed by the "Restarting ..." log message, I would guess that the client is sending a RETR ''first'' and then a REST. The RETR times out because the client doesn't try to connect to the data channel until after it receives a response to the REST, and the Twisted server doesn't even process the REST until after the client connects to the data channel (and downloads the entire file). Fixing this may require changing the way ftp.FTP processes commands from clients, so that a REST which follows a RETR can be interpreted properly (or perhaps the FTP client you are using is merely buggy, from the protocol documentation I can find, RETR is supposed to follow REST, not the other way around).

This is just a guess, though, and you should look at the traffic capture to confirm or reject it.

以歌曲疗慰 2024-10-11 19:18:06

经过深入研究源代码并摆弄想法后,这就是我确定的解决方案:

class MyFTP(ftp.FTP):
  dtpTimeout = 30

  def ftp_PASV(self):
    # FTP.lineReceived calls pauseProducing(), and doesn't allow
    # resuming until the Deferred that the called function returns
    # is called or errored.  If the client sends a REST command
    # after PASV, they will not connect to our DTP connection
    # (and fire our Deferred) until they receive a response.
    # Therefore, we will turn on producing again before returning
    # our DTP's deferred response, allowing the REST to come
    # through, our response to the REST to go out, the client to
    # connect, and everyone to be happy.
    resumer = reactor.callLater(0.25, self.resumeProducing)
    def cancel_resume(_):
      if not resumer.called:
        resumer.cancel()
      return _
    return ftp.FTP.ftp_PASV(self).addBoth(cancel_resume)
  def ftp_REST(self, pos):
    # Of course, allowing a REST command to come in does us no
    # good if we can't handle it.
    try:
      pos = int(pos)
    except ValueError:
      return defer.fail(CmdSyntaxError('Bad argument for REST'))

    def all_ok(result):
      return ftp.REQ_FILE_ACTN_PENDING_FURTHER_INFO

    return self.shell.restart(pos).addCallback(all_ok)

class MyFTPShell(ftp.FTPShell):
  def __init__(self, host, auth):
    self.position = 0

  def restart(self, pos):
    self.position = pos
    return defer.succeed(pos)

callLater 方法有时可能很不稳定,但它在大多数情况下都有效。显然,使用风险由您自己承担。

After much digging into the source and fiddling with ideas, this is the solution I settled on:

class MyFTP(ftp.FTP):
  dtpTimeout = 30

  def ftp_PASV(self):
    # FTP.lineReceived calls pauseProducing(), and doesn't allow
    # resuming until the Deferred that the called function returns
    # is called or errored.  If the client sends a REST command
    # after PASV, they will not connect to our DTP connection
    # (and fire our Deferred) until they receive a response.
    # Therefore, we will turn on producing again before returning
    # our DTP's deferred response, allowing the REST to come
    # through, our response to the REST to go out, the client to
    # connect, and everyone to be happy.
    resumer = reactor.callLater(0.25, self.resumeProducing)
    def cancel_resume(_):
      if not resumer.called:
        resumer.cancel()
      return _
    return ftp.FTP.ftp_PASV(self).addBoth(cancel_resume)
  def ftp_REST(self, pos):
    # Of course, allowing a REST command to come in does us no
    # good if we can't handle it.
    try:
      pos = int(pos)
    except ValueError:
      return defer.fail(CmdSyntaxError('Bad argument for REST'))

    def all_ok(result):
      return ftp.REQ_FILE_ACTN_PENDING_FURTHER_INFO

    return self.shell.restart(pos).addCallback(all_ok)

class MyFTPShell(ftp.FTPShell):
  def __init__(self, host, auth):
    self.position = 0

  def restart(self, pos):
    self.position = pos
    return defer.succeed(pos)

The callLater approach can be flaky at times, but it works the majority of the time. Use at your own risk, obviously.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文