Merge lp:~cjwatson/txpkgupload/ipv6-ftp into lp:~lazr-developers/txpkgupload/trunk

Proposed by Colin Watson
Status: Merged
Merged at revision: 45
Proposed branch: lp:~cjwatson/txpkgupload/ipv6-ftp
Merge into: lp:~lazr-developers/txpkgupload/trunk
Diff against target: 224 lines (+184/-3)
1 file modified
src/txpkgupload/twistedftp.py (+184/-3)
To merge this branch: bzr merge lp:~cjwatson/txpkgupload/ipv6-ftp
Reviewer Review Type Date Requested Status
William Grant code Approve
Review via email: mp+368202@code.launchpad.net

Commit message

Implement FTP extensions for IPv6.

Description of the change

This is a backport from https://github.com/twisted/twisted/pull/1149; if and when it's merged into a released version of Twisted then we should drop this rather hacky backport and use that instead.

Portions of this are by William Grant, but I did some more work to get things like EPSV ALL working. Getting the tests backported here as well was unfortunately unreasonably difficult, but I've at least tested it manually and hopefully the fact that the Twisted PR has reasonable test coverage will be good enough.

To post a comment you must log in.
lp:~cjwatson/txpkgupload/ipv6-ftp updated
44. By Colin Watson

Implement FTP extensions for IPv6.

This is a backport from https://github.com/twisted/twisted/pull/1149; if
and when it's merged into a released version of Twisted then we should
drop this rather hacky backport and use that instead.

Portions of this are by William Grant, but I did some more work to get
things like EPSV ALL working. Getting the tests backported here as well
was unfortunately unreasonably difficult, but I've at least tested it
manually and hopefully the fact that the Twisted PR has reasonable test
coverage will be good enough.

Revision history for this message
William Grant (wgrant) :
review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/txpkgupload/twistedftp.py'
2--- src/txpkgupload/twistedftp.py 2018-07-04 10:04:49 +0000
3+++ src/txpkgupload/twistedftp.py 2019-05-31 16:40:35 +0000
4@@ -10,6 +10,7 @@
5 ]
6
7 import os
8+import re
9 import tempfile
10
11 from twisted.application import (
12@@ -24,7 +25,11 @@
13 IRealm,
14 Portal,
15 )
16-from twisted.internet import defer
17+from twisted.internet import (
18+ defer,
19+ error,
20+ reactor,
21+ )
22 from twisted.protocols import ftp
23 from twisted.python import filepath
24 from zope.interface import implementer
25@@ -132,6 +137,182 @@
26 "Only IFTPShell interface is supported by this realm")
27
28
29+_AFNUM_IP = 1
30+_AFNUM_IP6 = 2
31+
32+
33+class UnsupportedNetworkProtocolError(Exception):
34+ """Raised when the client requests an unsupported network protocol."""
35+
36+
37+def decodeExtendedAddress(address):
38+ """
39+ Decode an FTP protocol/address/port combination, using the syntax
40+ defined in RFC 2428 section 2.
41+
42+ @return: a 3-tuple of (protocol, host, port).
43+ """
44+ delim = address[0]
45+ protocol, host, port, _ = address[1:].split(delim)
46+ return protocol, host, int(port)
47+
48+
49+def decodeExtendedAddressLine(line):
50+ """
51+ Decode an FTP response specifying a protocol/address/port combination,
52+ using the syntax defined in RFC 2428 sections 2 and 3.
53+
54+ @return: a 3-tuple of (protocol, host, port).
55+ """
56+ match = re.search(r'\((.*)\)', line)
57+ if match:
58+ return decodeExtendedAddress(match.group(1))
59+ else:
60+ raise ValueError('No extended address found in "%s"' % line)
61+
62+
63+class FTPWithEPSV(ftp.FTP):
64+
65+ epsvAll = False
66+ supportedNetworkProtocols = (_AFNUM_IP, _AFNUM_IP6)
67+
68+ def connectionLost(self, reason):
69+ ftp.FTP.connectionLost(self, reason)
70+ self.epsvAll = False
71+
72+ def getDTPPort(self, factory, interface=''):
73+ """
74+ Return a port for passive access, using C{self.passivePortRange}
75+ attribute.
76+ """
77+ for portn in self.passivePortRange:
78+ try:
79+ dtpPort = self.listenFactory(portn, factory,
80+ interface=interface)
81+ except error.CannotListenError:
82+ continue
83+ else:
84+ return dtpPort
85+ raise error.CannotListenError('', portn,
86+ "No port available in range %s" %
87+ (self.passivePortRange,))
88+
89+ def ftp_PASV(self):
90+ if self.epsvAll:
91+ return defer.fail(ftp.BadCmdSequenceError(
92+ 'may not send PASV after EPSV ALL'))
93+ return ftp.FTP.ftp_PASV(self)
94+
95+ def _validateNetworkProtocol(self, protocol):
96+ """
97+ Validate the network protocol requested in an EPRT or EPSV command.
98+
99+ For now we just hardcode the protocols we support, since this layer
100+ doesn't have a good way to discover that.
101+
102+ @param protocol: An address family number. See RFC 2428 section 2.
103+ @type protocol: L{str}
104+
105+ @raise FTPCmdError: If validation fails.
106+ """
107+ # We can't actually honour an explicit network protocol request
108+ # (violating a SHOULD in RFC 2428 section 3), but let's at least
109+ # validate it.
110+ try:
111+ protocol = int(protocol)
112+ except ValueError:
113+ raise ftp.CmdArgSyntaxError(protocol)
114+ if protocol not in self.supportedNetworkProtocols:
115+ raise UnsupportedNetworkProtocolError(
116+ ','.join(str(p) for p in self.supportedNetworkProtocols))
117+
118+ def ftp_EPSV(self, protocol=''):
119+ if protocol == 'ALL':
120+ self.epsvAll = True
121+ self.sendLine('200 EPSV ALL OK')
122+ return defer.succeed(None)
123+ elif protocol:
124+ try:
125+ self._validateNetworkProtocol(protocol)
126+ except ftp.FTPCmdError:
127+ return defer.fail()
128+ except UnsupportedNetworkProtocolError as e:
129+ self.sendLine(
130+ '522 Network protocol not supported, use (%s)' % e.args)
131+ return defer.succeed(None)
132+
133+ # if we have a DTP port set up, lose it.
134+ if self.dtpFactory is not None:
135+ # cleanupDTP sets dtpFactory to none. Later we'll do
136+ # cleanup here or something.
137+ self.cleanupDTP()
138+ self.dtpFactory = ftp.DTPFactory(pi=self)
139+ self.dtpFactory.setTimeout(self.dtpTimeout)
140+ if not protocol or protocol == _AFNUM_IP6:
141+ interface = '::'
142+ else:
143+ interface = ''
144+ self.dtpPort = self.getDTPPort(self.dtpFactory, interface=interface)
145+
146+ port = self.dtpPort.getHost().port
147+ self.reply(ftp.ENTERING_EPSV_MODE, port)
148+ return self.dtpFactory.deferred.addCallback(lambda ign: None)
149+
150+ def ftp_PORT(self):
151+ if self.epsvAll:
152+ return defer.fail(ftp.BadCmdSequenceError(
153+ 'may not send PORT after EPSV ALL'))
154+ return ftp.FTP.ftp_PORT(self)
155+
156+ def ftp_EPRT(self, extendedAddress):
157+ """
158+ Extended request for a data connection.
159+
160+ As described by U{RFC 2428 section
161+ 2<https://tools.ietf.org/html/rfc2428#section-2>}::
162+
163+ The EPRT command allows for the specification of an extended
164+ address for the data connection. The extended address MUST
165+ consist of the network protocol as well as the network and
166+ transport addresses.
167+ """
168+ if self.epsvAll:
169+ return defer.fail(ftp.BadCmdSequenceError(
170+ 'may not send EPRT after EPSV ALL'))
171+
172+ try:
173+ protocol, ip, port = decodeExtendedAddress(extendedAddress)
174+ except ValueError:
175+ return defer.fail(ftp.CmdArgSyntaxError(extendedAddress))
176+ if protocol:
177+ try:
178+ self._validateNetworkProtocol(protocol)
179+ except ftp.FTPCmdError:
180+ return defer.fail()
181+ except UnsupportedNetworkProtocolError as e:
182+ self.sendLine(
183+ '522 Network protocol not supported, use (%s)' % e.args)
184+ return defer.succeed(None)
185+
186+ # if we have a DTP port set up, lose it.
187+ if self.dtpFactory is not None:
188+ self.cleanupDTP()
189+
190+ self.dtpFactory = ftp.DTPFactory(
191+ pi=self, peerHost=self.transport.getPeer().host)
192+ self.dtpFactory.setTimeout(self.dtpTimeout)
193+ self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
194+
195+ def connected(ignored):
196+ return ftp.ENTERING_PORT_MODE
197+
198+ def connFailed(err):
199+ err.trap(ftp.PortConnectionError)
200+ return ftp.CANT_OPEN_DATA_CNX
201+
202+ return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
203+
204+
205 class FTPServiceFactory(service.Service):
206 """A factory that makes an `FTPService`"""
207
208@@ -142,7 +323,7 @@
209 factory = ftp.FTPFactory(portal)
210
211 factory.tld = root
212- factory.protocol = ftp.FTP
213+ factory.protocol = FTPWithEPSV
214 factory.welcomeMessage = "Launchpad upload server"
215 factory.timeOut = idle_timeout
216
217@@ -151,6 +332,6 @@
218
219 @staticmethod
220 def makeFTPService(port, root, temp_dir, idle_timeout):
221- strport = "tcp:%s" % port
222+ strport = "tcp6:%s" % port
223 factory = FTPServiceFactory(port, root, temp_dir, idle_timeout)
224 return strports.service(strport, factory.ftpfactory)

Subscribers

People subscribed via source and target branches

to all changes: