代码之家  ›  专栏  ›  技术社区  ›  eternicode

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

  •  1
  • eternicode  · 技术社区  · 15 年前

    有人在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的建议启用日志记录之后,看起来REST命令甚至没有在DTP连接因缺少连接而超时之前将其发送到我的FTP类(为了简洁起见,时间戳减少到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命令。

    2 回复  |  直到 15 年前
        1
  •  2
  •   Jean-Paul Calderone    15 年前

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

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

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

        2
  •  1
  •   eternicode    15 年前

    在深入挖掘源头和编造想法之后,这是我决定的解决方案:

    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方法有时可能不稳定,但它在大多数情况下都是有效的。很明显,使用的风险由你自己承担。