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
=== modified file 'src/txpkgupload/twistedftp.py'
--- src/txpkgupload/twistedftp.py 2018-07-04 10:04:49 +0000
+++ src/txpkgupload/twistedftp.py 2019-05-31 16:40:35 +0000
@@ -10,6 +10,7 @@
10 ]10 ]
1111
12import os12import os
13import re
13import tempfile14import tempfile
1415
15from twisted.application import (16from twisted.application import (
@@ -24,7 +25,11 @@
24 IRealm,25 IRealm,
25 Portal,26 Portal,
26 )27 )
27from twisted.internet import defer28from twisted.internet import (
29 defer,
30 error,
31 reactor,
32 )
28from twisted.protocols import ftp33from twisted.protocols import ftp
29from twisted.python import filepath34from twisted.python import filepath
30from zope.interface import implementer35from zope.interface import implementer
@@ -132,6 +137,182 @@
132 "Only IFTPShell interface is supported by this realm")137 "Only IFTPShell interface is supported by this realm")
133138
134139
140_AFNUM_IP = 1
141_AFNUM_IP6 = 2
142
143
144class UnsupportedNetworkProtocolError(Exception):
145 """Raised when the client requests an unsupported network protocol."""
146
147
148def decodeExtendedAddress(address):
149 """
150 Decode an FTP protocol/address/port combination, using the syntax
151 defined in RFC 2428 section 2.
152
153 @return: a 3-tuple of (protocol, host, port).
154 """
155 delim = address[0]
156 protocol, host, port, _ = address[1:].split(delim)
157 return protocol, host, int(port)
158
159
160def decodeExtendedAddressLine(line):
161 """
162 Decode an FTP response specifying a protocol/address/port combination,
163 using the syntax defined in RFC 2428 sections 2 and 3.
164
165 @return: a 3-tuple of (protocol, host, port).
166 """
167 match = re.search(r'\((.*)\)', line)
168 if match:
169 return decodeExtendedAddress(match.group(1))
170 else:
171 raise ValueError('No extended address found in "%s"' % line)
172
173
174class FTPWithEPSV(ftp.FTP):
175
176 epsvAll = False
177 supportedNetworkProtocols = (_AFNUM_IP, _AFNUM_IP6)
178
179 def connectionLost(self, reason):
180 ftp.FTP.connectionLost(self, reason)
181 self.epsvAll = False
182
183 def getDTPPort(self, factory, interface=''):
184 """
185 Return a port for passive access, using C{self.passivePortRange}
186 attribute.
187 """
188 for portn in self.passivePortRange:
189 try:
190 dtpPort = self.listenFactory(portn, factory,
191 interface=interface)
192 except error.CannotListenError:
193 continue
194 else:
195 return dtpPort
196 raise error.CannotListenError('', portn,
197 "No port available in range %s" %
198 (self.passivePortRange,))
199
200 def ftp_PASV(self):
201 if self.epsvAll:
202 return defer.fail(ftp.BadCmdSequenceError(
203 'may not send PASV after EPSV ALL'))
204 return ftp.FTP.ftp_PASV(self)
205
206 def _validateNetworkProtocol(self, protocol):
207 """
208 Validate the network protocol requested in an EPRT or EPSV command.
209
210 For now we just hardcode the protocols we support, since this layer
211 doesn't have a good way to discover that.
212
213 @param protocol: An address family number. See RFC 2428 section 2.
214 @type protocol: L{str}
215
216 @raise FTPCmdError: If validation fails.
217 """
218 # We can't actually honour an explicit network protocol request
219 # (violating a SHOULD in RFC 2428 section 3), but let's at least
220 # validate it.
221 try:
222 protocol = int(protocol)
223 except ValueError:
224 raise ftp.CmdArgSyntaxError(protocol)
225 if protocol not in self.supportedNetworkProtocols:
226 raise UnsupportedNetworkProtocolError(
227 ','.join(str(p) for p in self.supportedNetworkProtocols))
228
229 def ftp_EPSV(self, protocol=''):
230 if protocol == 'ALL':
231 self.epsvAll = True
232 self.sendLine('200 EPSV ALL OK')
233 return defer.succeed(None)
234 elif protocol:
235 try:
236 self._validateNetworkProtocol(protocol)
237 except ftp.FTPCmdError:
238 return defer.fail()
239 except UnsupportedNetworkProtocolError as e:
240 self.sendLine(
241 '522 Network protocol not supported, use (%s)' % e.args)
242 return defer.succeed(None)
243
244 # if we have a DTP port set up, lose it.
245 if self.dtpFactory is not None:
246 # cleanupDTP sets dtpFactory to none. Later we'll do
247 # cleanup here or something.
248 self.cleanupDTP()
249 self.dtpFactory = ftp.DTPFactory(pi=self)
250 self.dtpFactory.setTimeout(self.dtpTimeout)
251 if not protocol or protocol == _AFNUM_IP6:
252 interface = '::'
253 else:
254 interface = ''
255 self.dtpPort = self.getDTPPort(self.dtpFactory, interface=interface)
256
257 port = self.dtpPort.getHost().port
258 self.reply(ftp.ENTERING_EPSV_MODE, port)
259 return self.dtpFactory.deferred.addCallback(lambda ign: None)
260
261 def ftp_PORT(self):
262 if self.epsvAll:
263 return defer.fail(ftp.BadCmdSequenceError(
264 'may not send PORT after EPSV ALL'))
265 return ftp.FTP.ftp_PORT(self)
266
267 def ftp_EPRT(self, extendedAddress):
268 """
269 Extended request for a data connection.
270
271 As described by U{RFC 2428 section
272 2<https://tools.ietf.org/html/rfc2428#section-2>}::
273
274 The EPRT command allows for the specification of an extended
275 address for the data connection. The extended address MUST
276 consist of the network protocol as well as the network and
277 transport addresses.
278 """
279 if self.epsvAll:
280 return defer.fail(ftp.BadCmdSequenceError(
281 'may not send EPRT after EPSV ALL'))
282
283 try:
284 protocol, ip, port = decodeExtendedAddress(extendedAddress)
285 except ValueError:
286 return defer.fail(ftp.CmdArgSyntaxError(extendedAddress))
287 if protocol:
288 try:
289 self._validateNetworkProtocol(protocol)
290 except ftp.FTPCmdError:
291 return defer.fail()
292 except UnsupportedNetworkProtocolError as e:
293 self.sendLine(
294 '522 Network protocol not supported, use (%s)' % e.args)
295 return defer.succeed(None)
296
297 # if we have a DTP port set up, lose it.
298 if self.dtpFactory is not None:
299 self.cleanupDTP()
300
301 self.dtpFactory = ftp.DTPFactory(
302 pi=self, peerHost=self.transport.getPeer().host)
303 self.dtpFactory.setTimeout(self.dtpTimeout)
304 self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
305
306 def connected(ignored):
307 return ftp.ENTERING_PORT_MODE
308
309 def connFailed(err):
310 err.trap(ftp.PortConnectionError)
311 return ftp.CANT_OPEN_DATA_CNX
312
313 return self.dtpFactory.deferred.addCallbacks(connected, connFailed)
314
315
135class FTPServiceFactory(service.Service):316class FTPServiceFactory(service.Service):
136 """A factory that makes an `FTPService`"""317 """A factory that makes an `FTPService`"""
137318
@@ -142,7 +323,7 @@
142 factory = ftp.FTPFactory(portal)323 factory = ftp.FTPFactory(portal)
143324
144 factory.tld = root325 factory.tld = root
145 factory.protocol = ftp.FTP326 factory.protocol = FTPWithEPSV
146 factory.welcomeMessage = "Launchpad upload server"327 factory.welcomeMessage = "Launchpad upload server"
147 factory.timeOut = idle_timeout328 factory.timeOut = idle_timeout
148329
@@ -151,6 +332,6 @@
151332
152 @staticmethod333 @staticmethod
153 def makeFTPService(port, root, temp_dir, idle_timeout):334 def makeFTPService(port, root, temp_dir, idle_timeout):
154 strport = "tcp:%s" % port335 strport = "tcp6:%s" % port
155 factory = FTPServiceFactory(port, root, temp_dir, idle_timeout)336 factory = FTPServiceFactory(port, root, temp_dir, idle_timeout)
156 return strports.service(strport, factory.ftpfactory)337 return strports.service(strport, factory.ftpfactory)

Subscribers

People subscribed via source and target branches

to all changes: