Merge lp:~jelmer/brz/no-more-ftp into lp:brz

Proposed by Jelmer Vernooij
Status: Merged
Approved by: Jelmer Vernooij
Approved revision: no longer in the source branch.
Merge reported by: The Breezy Bot
Merged at revision: not available
Proposed branch: lp:~jelmer/brz/no-more-ftp
Merge into: lp:brz
Diff against target: 1510 lines (+17/-1308)
16 files modified
breezy/help_topics/en/authentication.txt (+3/-3)
breezy/lockdir.py (+0/-3)
breezy/plugins/git/tests/test_dir.py (+1/-1)
breezy/plugins/upload/__init__.py (+1/-1)
breezy/plugins/upload/tests/test_upload.py (+1/-25)
breezy/tests/__init__.py (+0/-6)
breezy/tests/blackbox/test_help.py (+0/-2)
breezy/tests/ftp_server/__init__.py (+0/-82)
breezy/tests/ftp_server/pyftpdlib_based.py (+0/-223)
breezy/tests/test_ftp_transport.py (+0/-151)
breezy/tests/test_http.py (+2/-2)
breezy/tests/test_smart_transport.py (+1/-1)
breezy/tests/transport_util.py (+8/-19)
breezy/transport/__init__.py (+0/-23)
breezy/transport/ftp/__init__.py (+0/-638)
breezy/transport/ftp/_gssapi.py (+0/-128)
To merge this branch: bzr merge lp:~jelmer/brz/no-more-ftp
Reviewer Review Type Date Requested Status
Vincent Ladeuil Approve
Martin Packman Approve
Review via email: mp+342526@code.launchpad.net

Commit message

Drop support for FTP.

Description of the change

Drop support for FTP.

* Our testing situation for FTP isn't great
* FTP is not as commonly used anymore as when Bazaar was started
* Breezy operations over FTP are slower than over other transports, so we
  should steer users away from it
* FTP isn't well defined in some areas, resulting in lots of annoying corner case bugs
* FTP connections are almost always insecure

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

I'm a little sad about this (my first patch to bzr was fixing some dumb ftp thing), but I agree with your reasoning. Along with ftp being discouraged as a whole (see also https://lwn.net/Articles/720856/ for instance), removing support altogether seems correct.

review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
https://ci.breezy-vcs.org/job/brz-dev/152/

Revision history for this message
Vincent Ladeuil (vila) wrote :

Die ftp die, like Martin, a bit sad, but meh, ftp needs to die anyway.
Plus that will make fullermd happy.

review: Approve
Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
https://ci.breezy-vcs.org/job/brz-dev/153/

Revision history for this message
The Breezy Bot (the-breezy-bot) wrote :

Running landing tests failed
https://ci.breezy-vcs.org/job/brz-dev/154/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'breezy/help_topics/en/authentication.txt'
2--- breezy/help_topics/en/authentication.txt 2017-05-21 14:47:52 +0000
3+++ breezy/help_topics/en/authentication.txt 2018-05-11 10:19:44 +0000
4@@ -24,16 +24,16 @@
5
6 Instead of using::
7
8- brz branch ftp://joe:secret@host.com/path/to/my/branch
9+ brz branch sftp://joe:secret@host.com/path/to/my/branch
10
11 you simply use::
12
13- brz branch ftp://host.com/path/to/my/branch
14+ brz branch sftp://host.com/path/to/my/branch
15
16 provided you have created the following ``authentication.conf`` file::
17
18 [myprojects]
19- scheme=ftp
20+ scheme=sftp
21 host=host.com
22 user=joe
23 password=secret
24
25=== modified file 'breezy/lockdir.py'
26--- breezy/lockdir.py 2017-10-28 12:56:32 +0000
27+++ breezy/lockdir.py 2018-05-11 10:19:44 +0000
28@@ -374,9 +374,6 @@
29 # another locker within the 'held' directory. do a slower
30 # deletion where we list the directory and remove everything
31 # within it.
32- #
33- # Maybe this should be broader to allow for ftp servers with
34- # non-specific error messages?
35 self._trace("doing recursive deletion of non-empty directory "
36 "%s", tmpname)
37 self.transport.delete_tree(tmpname)
38
39=== modified file 'breezy/plugins/git/tests/test_dir.py'
40--- breezy/plugins/git/tests/test_dir.py 2018-03-28 01:51:03 +0000
41+++ breezy/plugins/git/tests/test_dir.py 2018-05-11 10:19:44 +0000
42@@ -91,5 +91,5 @@
43 self.assertEquals(self.format, format2)
44 self.assertEquals(self.format, self.format)
45 bzr_format = controldir.format_registry.make_controldir("default")
46- self.assertNotEquals(self.format, bzr_format)
47+ self.assertNotEqual(self.format, bzr_format)
48
49
50=== modified file 'breezy/plugins/upload/__init__.py'
51--- breezy/plugins/upload/__init__.py 2017-07-30 16:59:50 +0000
52+++ breezy/plugins/upload/__init__.py 2018-05-11 10:19:44 +0000
53@@ -107,7 +107,7 @@
54 It is possible to upload to a remote location from another remote location by
55 specifying it with the --directory option:
56
57- brz upload ftp://public.example.com --directory sftp://private.example.com
58+ brz upload sftp://public.example.com --directory sftp://private.example.com
59
60 This, together with --auto, can be used to upload when you push to your
61 central branch, rather than when you commit to your local branch.
62
63=== modified file 'breezy/plugins/upload/tests/test_upload.py'
64--- breezy/plugins/upload/tests/test_upload.py 2018-02-18 15:21:06 +0000
65+++ breezy/plugins/upload/tests/test_upload.py 2018-05-11 10:19:44 +0000
66@@ -34,9 +34,6 @@
67 per_branch,
68 per_transport,
69 )
70-from ....transport import (
71- ftp,
72- )
73 from .. import (
74 cmds,
75 )
76@@ -46,7 +43,7 @@
77 result = []
78 basis = per_transport.transport_test_permutations()
79 # Keep only the interesting ones for upload
80- usable_classes = {ftp.FtpTransport}
81+ usable_classes = set()
82 if features.paramiko.available():
83 from ....transport import sftp
84 usable_classes.add(sftp.SFTPTransport)
85@@ -54,27 +51,6 @@
86 t_class = d['transport_class']
87 if t_class in usable_classes:
88 result.append((name, d))
89- try:
90- import breezy.plugins.local_test_server
91- from breezy.plugins.local_test_server import test_server
92- if False:
93- # XXX: Disable since we can't get chmod working for anonymous
94- # user
95- scenario = ('vsftpd',
96- {'transport_class': test_server.FtpTransport,
97- 'transport_server': test_server.Vsftpd,
98- })
99- result.append(scenario)
100- from test_server import ProftpdFeature
101- if ProftpdFeature().available():
102- scenario = ('proftpd',
103- {'transport_class': test_server.FtpTransport,
104- 'transport_server': test_server.Proftpd,
105- })
106- result.append(scenario)
107- # XXX: add support for pyftpdlib
108- except ImportError:
109- pass
110 return result
111
112
113
114=== modified file 'breezy/tests/__init__.py'
115--- breezy/tests/__init__.py 2018-05-07 17:17:22 +0000
116+++ breezy/tests/__init__.py 2018-05-11 10:19:44 +0000
117@@ -179,11 +179,6 @@
118 'NO_PROXY': None,
119 'all_proxy': None,
120 'ALL_PROXY': None,
121- # Nobody cares about ftp_proxy, FTP_PROXY AFAIK. So far at
122- # least. If you do (care), please update this comment
123- # -- vila 20080401
124- 'ftp_proxy': None,
125- 'FTP_PROXY': None,
126 'BZR_REMOTE_PATH': None,
127 # Generally speaking, we don't want apport reporting on crashes in
128 # the test envirnoment unless we're specifically testing apport,
129@@ -3948,7 +3943,6 @@
130 'breezy.tests.test_fifo_cache',
131 'breezy.tests.test_filters',
132 'breezy.tests.test_filter_tree',
133- 'breezy.tests.test_ftp_transport',
134 'breezy.tests.test_foreign',
135 'breezy.tests.test_generate_docs',
136 'breezy.tests.test_generate_ids',
137
138=== modified file 'breezy/tests/blackbox/test_help.py'
139--- breezy/tests/blackbox/test_help.py 2017-05-21 18:10:28 +0000
140+++ breezy/tests/blackbox/test_help.py 2018-05-11 10:19:44 +0000
141@@ -66,11 +66,9 @@
142 def test_help_urlspec(self):
143 """Smoke test for 'brz help urlspec'"""
144 out, err = self.run_bzr('help urlspec')
145- self.assertContainsRe(out, 'aftp://')
146 self.assertContainsRe(out, 'bzr://')
147 self.assertContainsRe(out, 'bzr\+ssh://')
148 self.assertContainsRe(out, 'file://')
149- self.assertContainsRe(out, 'ftp://')
150 self.assertContainsRe(out, 'http://')
151 self.assertContainsRe(out, 'https://')
152 self.assertContainsRe(out, 'sftp://')
153
154=== removed directory 'breezy/tests/ftp_server'
155=== removed file 'breezy/tests/ftp_server/__init__.py'
156--- breezy/tests/ftp_server/__init__.py 2017-06-11 13:22:31 +0000
157+++ breezy/tests/ftp_server/__init__.py 1970-01-01 00:00:00 +0000
158@@ -1,82 +0,0 @@
159-# Copyright (C) 2009, 2010 Canonical Ltd
160-#
161-# This program is free software; you can redistribute it and/or modify
162-# it under the terms of the GNU General Public License as published by
163-# the Free Software Foundation; either version 2 of the License, or
164-# (at your option) any later version.
165-#
166-# This program is distributed in the hope that it will be useful,
167-# but WITHOUT ANY WARRANTY; without even the implied warranty of
168-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
169-# GNU General Public License for more details.
170-#
171-# You should have received a copy of the GNU General Public License
172-# along with this program; if not, write to the Free Software
173-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
174-
175-"""
176-Facilities to use ftp test servers.
177-"""
178-
179-import sys
180-
181-from breezy import tests
182-from breezy.tests import (
183- features,
184- )
185-
186-
187-try:
188- import pyftpdlib
189-except ImportError:
190- pyftpdlib_available = False
191-else:
192- pyftpdlib_available = True
193-
194-
195-class _FTPServerFeature(features.Feature):
196- """Some tests want an FTP Server, check if one is available.
197-
198- Right now, the only way this is available is if one of the following is
199- installed:
200-
201- - 'pyftpdlib': http://code.google.com/p/pyftpdlib/
202- """
203-
204- def _probe(self):
205- return pyftpdlib_available
206-
207- def feature_name(self):
208- return 'FTPServer'
209-
210-
211-FTPServerFeature = _FTPServerFeature()
212-
213-
214-class UnavailableFTPTestServer(object):
215- """Dummy ftp test server.
216-
217- This allows the test suite report the number of tests needing that
218- feature. We raise UnavailableFeature from methods before the test server is
219- being used. Doing so in the setUp method has bad side-effects (tearDown is
220- never called).
221- """
222-
223- def start_server(self, vfs_server=None):
224- pass
225-
226- def stop_server(self):
227- pass
228-
229- def get_url(self):
230- raise tests.UnavailableFeature(FTPServerFeature)
231-
232- def get_bogus_url(self):
233- raise tests.UnavailableFeature(FTPServerFeature)
234-
235-
236-if pyftpdlib_available:
237- from . import pyftpdlib_based
238- FTPTestServer = pyftpdlib_based.FTPTestServer
239-else:
240- FTPTestServer = UnavailableFTPTestServer
241
242=== removed file 'breezy/tests/ftp_server/pyftpdlib_based.py'
243--- breezy/tests/ftp_server/pyftpdlib_based.py 2017-06-11 13:36:02 +0000
244+++ breezy/tests/ftp_server/pyftpdlib_based.py 1970-01-01 00:00:00 +0000
245@@ -1,223 +0,0 @@
246-# Copyright (C) 2009, 2010 Canonical Ltd
247-#
248-# This program is free software; you can redistribute it and/or modify
249-# it under the terms of the GNU General Public License as published by
250-# the Free Software Foundation; either version 2 of the License, or
251-# (at your option) any later version.
252-#
253-# This program is distributed in the hope that it will be useful,
254-# but WITHOUT ANY WARRANTY; without even the implied warranty of
255-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
256-# GNU General Public License for more details.
257-#
258-# You should have received a copy of the GNU General Public License
259-# along with this program; if not, write to the Free Software
260-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
261-"""
262-FTP test server.
263-
264-Based on pyftpdlib: http://code.google.com/p/pyftpdlib/
265-"""
266-
267-import errno
268-import logging
269-import os
270-import pyftpdlib
271-import sys
272-from pyftpdlib.authorizers import (
273- AuthorizerError,
274- DummyAuthorizer,
275- )
276-from pyftpdlib.filesystems import AbstractedFS
277-from pyftpdlib.handlers import (
278- FTPHandler,
279- proto_cmds,
280- )
281-from pyftpdlib.servers import FTPServer
282-import select
283-import threading
284-
285-
286-from breezy import (
287- osutils,
288- tests,
289- trace,
290- )
291-from breezy.tests import test_server
292-
293-
294-class NullHandler(logging.Handler):
295-
296- def emit(self, record):
297- pass
298-
299-# Shut up very verbose pyftpdlib
300-logging.getLogger('pyftpdlib').addHandler(NullHandler())
301-
302-
303-# Convert the pyftplib string version into a tuple to avoid traps in string
304-# comparison.
305-pyftplib_version = tuple(map(int, pyftpdlib.__ver__.split('.')))
306-
307-
308-class AnonymousWithWriteAccessAuthorizer(DummyAuthorizer):
309-
310- def _check_permissions(self, username, perm):
311- # Like base implementation but don't warn about write permissions
312- # assigned to anonymous, since that's exactly our purpose.
313- for p in perm:
314- if p not in self.read_perms + self.write_perms:
315- raise AuthorizerError('No such permission "%s"' %p)
316-
317-
318-class BzrConformingFS(AbstractedFS):
319-
320- def chmod(self, path, mode):
321- return os.chmod(path, mode)
322-
323- def listdir(self, path):
324- """List the content of a directory."""
325- return [osutils.safe_utf8(s) for s in os.listdir(path)]
326-
327- def fs2ftp(self, fspath):
328- p = AbstractedFS.fs2ftp(self, osutils.safe_unicode(fspath))
329- return osutils.safe_utf8(p)
330-
331- def ftp2fs(self, ftppath):
332- p = osutils.safe_unicode(ftppath)
333- return AbstractedFS.ftp2fs(self, p)
334-
335-
336-class BzrConformingFTPHandler(FTPHandler):
337-
338- abstracted_fs = BzrConformingFS
339-
340- def __init__(self, conn, server, ioloop=None):
341- FTPHandler.__init__(self, conn, server)
342- self.authorizer = server.authorizer
343-
344- def ftp_SIZE(self, path):
345- # bzr is overly picky here, but we want to make the test suite pass
346- # first. This may need to be revisited -- vila 20090226
347- line = self.fs.fs2ftp(path)
348- if self.fs.isdir(self.fs.realpath(path)):
349- why = "%s is a directory" % line
350- self.log('FAIL SIZE "%s". %s.' % (line, why))
351- self.respond("550 %s." %why)
352- else:
353- FTPHandler.ftp_SIZE(self, path)
354-
355- def ftp_NLST(self, path):
356- # bzr is overly picky here, but we want to make the test suite pass
357- # first. This may need to be revisited -- vila 20090226
358- line = self.fs.fs2ftp(path)
359- if self.fs.isfile(self.fs.realpath(path)):
360- why = "Not a directory: %s" % line
361- self.log('FAIL NLST "%s". %s.' % (line, why))
362- self.respond("550 %s." %why)
363- else:
364- FTPHandler.ftp_NLST(self, path)
365-
366- def log_cmd(self, cmd, arg, respcode, respstr):
367- # base class version choke on unicode, the alternative is to just
368- # provide an empty implementation and relies on the client to do
369- # the logging for debugging purposes. Not worth the trouble so far
370- # -- vila 20110607
371- if cmd in ("DELE", "RMD", "RNFR", "RNTO", "MKD"):
372- line = '"%s" %s' % (' '.join([cmd, unicode(arg)]).strip(), respcode)
373- self.log(line)
374-
375-
376-# An empty password is valid, hence the arg is neither mandatory nor forbidden
377-proto_cmds['PASS']['arg'] = None
378-
379-class ftp_server(FTPServer):
380-
381- def __init__(self, address, handler, authorizer):
382- FTPServer.__init__(self, address, handler)
383- self.authorizer = authorizer
384- # Worth backporting upstream ?
385- self.addr = self.socket.getsockname()
386-
387-
388-class FTPTestServer(test_server.TestServer):
389- """Common code for FTP server facilities."""
390-
391- def __init__(self):
392- self._root = None
393- self._ftp_server = None
394- self._port = None
395- self._async_thread = None
396- # ftp server logs
397- self.logs = []
398- self._ftpd_running = False
399-
400- def get_url(self):
401- """Calculate an ftp url to this server."""
402- return 'ftp://anonymous@localhost:%d/' % (self._port)
403-
404- def get_bogus_url(self):
405- """Return a URL which cannot be connected to."""
406- return 'ftp://127.0.0.1:1/'
407-
408- def log(self, message):
409- """This is used by ftp_server to log connections, etc."""
410- self.logs.append(message)
411-
412- def start_server(self, vfs_server=None):
413- if not (vfs_server is None or isinstance(vfs_server,
414- test_server.LocalURLServer)):
415- raise AssertionError(
416- "FTPServer currently assumes local transport, got %s"
417- % vfs_server)
418- self._root = osutils.getcwd()
419-
420- address = ('localhost', 0) # bind to a random port
421- authorizer = AnonymousWithWriteAccessAuthorizer()
422- authorizer.add_anonymous(self._root, perm='elradfmwM')
423- self._ftp_server = ftp_server(address, BzrConformingFTPHandler,
424- authorizer)
425-
426- self._port = self._ftp_server.socket.getsockname()[1]
427- self._ftpd_starting = threading.Lock()
428- self._ftpd_starting.acquire() # So it can be released by the server
429- self._ftpd_thread = threading.Thread(target=self._run_server,)
430- self._ftpd_thread.start()
431- if 'threads' in tests.selftest_debug_flags:
432- sys.stderr.write('Thread started: %s\n'
433- % (self._ftpd_thread.ident,))
434- # Wait for the server thread to start (i.e release the lock)
435- self._ftpd_starting.acquire()
436- self._ftpd_starting.release()
437-
438- def stop_server(self):
439- """See breezy.transport.Server.stop_server."""
440- # Tell the server to stop, but also close the server socket for tests
441- # that start the server but never initiate a connection. Closing the
442- # socket should be done first though, to avoid further connections.
443- self._ftp_server.close()
444- self._ftpd_running = False
445- self._ftpd_thread.join()
446- if 'threads' in tests.selftest_debug_flags:
447- sys.stderr.write('Thread joined: %s\n'
448- % (self._ftpd_thread.ident,))
449-
450- def _run_server(self):
451- """Run the server until stop_server is called.
452-
453- Shut it down properly then.
454- """
455- self._ftpd_running = True
456- self._ftpd_starting.release()
457- while self._ftpd_running:
458- try:
459- self._ftp_server.serve_forever(timeout=0.1)
460- except select.error as e:
461- if e.args[0] != errno.EBADF:
462- raise
463- self._ftp_server.close_all()
464-
465- def add_user(self, user, password):
466- """Add a user with write access."""
467- self._ftp_server.authorizer.add_user(user, password, self._root,
468- perm='elradfmwM')
469
470=== removed file 'breezy/tests/test_ftp_transport.py'
471--- breezy/tests/test_ftp_transport.py 2017-05-22 00:56:52 +0000
472+++ breezy/tests/test_ftp_transport.py 1970-01-01 00:00:00 +0000
473@@ -1,151 +0,0 @@
474-# Copyright (C) 2006, 2007, 2009, 2010, 2011 Canonical Ltd
475-#
476-# This program is free software; you can redistribute it and/or modify
477-# it under the terms of the GNU General Public License as published by
478-# the Free Software Foundation; either version 2 of the License, or
479-# (at your option) any later version.
480-#
481-# This program is distributed in the hope that it will be useful,
482-# but WITHOUT ANY WARRANTY; without even the implied warranty of
483-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
484-# GNU General Public License for more details.
485-#
486-# You should have received a copy of the GNU General Public License
487-# along with this program; if not, write to the Free Software
488-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
489-
490-import ftplib
491-import getpass
492-
493-from .. import (
494- config,
495- errors,
496- tests,
497- transport,
498- ui,
499- urlutils,
500- )
501-
502-from ..transport import ftp
503-
504-from . import ftp_server
505-
506-
507-class TestCaseWithFTPServer(tests.TestCaseWithTransport):
508-
509- _test_needs_features = [ftp_server.FTPServerFeature]
510-
511- def setUp(self):
512- self.transport_server = ftp_server.FTPTestServer
513- super(TestCaseWithFTPServer, self).setUp()
514-
515-
516-class TestCaseAFTP(tests.TestCaseWithTransport):
517- """Test aftp transport."""
518-
519- def test_aftp_degrade(self):
520- t = transport.get_transport_from_url('aftp://host/path')
521- self.assertTrue(t.is_active)
522- parent = t.clone('..')
523- self.assertTrue(parent.is_active)
524-
525- self.assertEqual('aftp://host/path', t.abspath(''))
526-
527-
528-class TestFTPTestServer(TestCaseWithFTPServer):
529-
530- def test_basic_exists(self):
531- url = self.get_url()
532- self.assertStartsWith(url, 'ftp://')
533-
534- t = self.get_transport()
535- t.put_bytes('foo', 'test bytes\n')
536- self.assertEqual('test bytes\n', t.get_bytes('foo'))
537-
538- def test__reconnect(self):
539- t = self.get_transport()
540- t.put_bytes('foo', 'test bytes\n')
541- self.assertEqual('test bytes\n', t.get_bytes('foo'))
542- t._reconnect()
543- t.put_bytes('foo', 'test more bytes\n')
544- self.assertEqual('test more bytes\n', t.get_bytes('foo'))
545-
546-
547-class TestFTPTestServerUI(TestCaseWithFTPServer):
548-
549- def setUp(self):
550- super(TestFTPTestServerUI, self).setUp()
551- self.user = 'joe'
552- self.password = 'secret'
553- self.get_server().add_user(self.user, self.password)
554-
555- def get_url(self, relpath=None):
556- """Overrides get_url to inject our user."""
557- base = super(TestFTPTestServerUI, self).get_url(relpath)
558- parsed_url = transport.ConnectedTransport._split_url(base)
559- new_url = parsed_url.clone()
560- new_url.user = self.user
561- new_url.quoted_user = urlutils.quote(self.user)
562- new_url.password = self.password
563- new_url.quoted_password = urlutils.quote(self.password)
564- return str(new_url)
565-
566- def test_no_prompt_for_username(self):
567- """ensure getpass.getuser() is used if there's no username in the
568- configuration.""",
569- self.get_server().add_user(getpass.getuser(), self.password)
570- t = self.get_transport()
571- ui.ui_factory = ui.CannedInputUIFactory([self.password])
572- # Issue a request to the server to connect
573- t.put_bytes('foo', 'test bytes\n')
574- self.assertEqual('test bytes\n', t.get_bytes('foo'))
575- # Only the password should've been read
576- ui.ui_factory.assert_all_input_consumed()
577-
578- def test_prompt_for_password(self):
579- t = self.get_transport()
580- ui.ui_factory = ui.CannedInputUIFactory([self.password])
581- # Issue a request to the server to connect
582- t.has('whatever/not/existing')
583- # stdin should be empty (the provided password have been consumed)
584- ui.ui_factory.assert_all_input_consumed()
585-
586- def test_no_prompt_for_password_when_using_auth_config(self):
587- t = self.get_transport()
588- ui.ui_factory = ui.CannedInputUIFactory([])
589- # Create a config file with the right password
590- conf = config.AuthenticationConfig()
591- conf._get_config().update({'ftptest': {'scheme': 'ftp',
592- 'user': self.user,
593- 'password': self.password}})
594- conf._save()
595- # Issue a request to the server to connect
596- t.put_bytes('foo', 'test bytes\n')
597- self.assertEqual('test bytes\n', t.get_bytes('foo'))
598-
599- def test_empty_password(self):
600- # Override the default user/password from setUp
601- self.user = 'jim'
602- self.password = ''
603- self.get_server().add_user(self.user, self.password)
604- t = self.get_transport()
605- ui.ui_factory = ui.CannedInputUIFactory([self.password])
606- # Issue a request to the server to connect
607- t.has('whatever/not/existing')
608- # stdin should be empty (the provided password have been consumed),
609- # even if the password is empty, it's followed by a newline.
610- ui.ui_factory.assert_all_input_consumed()
611-
612-
613-class TestFTPErrorTranslation(tests.TestCase):
614-
615- def test_translate_directory_not_empty(self):
616- # https://bugs.launchpad.net/bugs/528722
617-
618- t = ftp.FtpTransport("ftp://none/")
619-
620- try:
621- raise ftplib.error_temp("Rename/move failure: Directory not empty")
622- except Exception as e:
623- e = self.assertRaises(errors.DirectoryNotEmpty,
624- t._translate_ftp_error, e, "/path")
625
626=== modified file 'breezy/tests/test_http.py'
627--- breezy/tests/test_http.py 2018-04-01 02:54:41 +0000
628+++ breezy/tests/test_http.py 2018-05-11 10:19:44 +0000
629@@ -1895,9 +1895,9 @@
630 def test_redirected_to_same_host_different_protocol(self):
631 t = self._transport('http://www.example.com/foo')
632 r = t._redirected_to('http://www.example.com/foo',
633- 'ftp://www.example.com/foo')
634+ 'bzr://www.example.com/foo')
635 self.assertNotEqual(type(r), type(t))
636- self.assertEqual('ftp://www.example.com/foo/', r.external_url())
637+ self.assertEqual('bzr://www.example.com/foo/', r.external_url())
638
639 def test_redirected_to_same_host_specific_implementation(self):
640 t = self._transport('http://www.example.com/foo')
641
642=== modified file 'breezy/tests/test_smart_transport.py'
643--- breezy/tests/test_smart_transport.py 2018-03-25 11:34:51 +0000
644+++ breezy/tests/test_smart_transport.py 2018-05-11 10:19:44 +0000
645@@ -4310,7 +4310,7 @@
646 def test_redirected_to_same_host_different_protocol(self):
647 t = remote.RemoteHTTPTransport('bzr+http://joe@www.example.com/foo')
648 r = t._redirected_to('http://www.example.com/foo',
649- 'ftp://www.example.com/foo')
650+ 'bzr://www.example.com/foo')
651 self.assertNotEqual(type(r), type(t))
652
653
654
655=== modified file 'breezy/tests/transport_util.py'
656--- breezy/tests/transport_util.py 2017-05-22 00:56:52 +0000
657+++ breezy/tests/transport_util.py 2018-05-11 10:19:44 +0000
658@@ -15,24 +15,14 @@
659 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
660
661 from . import features
662-
663-# SFTPTransport offers better performances but relies on paramiko, if paramiko
664-# is not available, we fallback to FtpTransport
665-if features.paramiko.available():
666- from . import test_sftp_transport
667- from ..transport import sftp, Transport
668- _backing_scheme = 'sftp'
669- _backing_transport_class = sftp.SFTPTransport
670- _backing_test_class = test_sftp_transport.TestCaseWithSFTPServer
671-else:
672- from ..transport import ftp, Transport
673- from . import test_ftp_transport
674- _backing_scheme = 'ftp'
675- _backing_transport_class = ftp.FtpTransport
676- _backing_test_class = test_ftp_transport.TestCaseWithFTPServer
677-
678-
679-class TestCaseWithConnectionHookedTransport(_backing_test_class):
680+from . import TestCaseWithTransport
681+from ..transport import Transport
682+
683+# SFTPTransport is the only bundled transport that properly counts connections
684+# at the moment.
685+from . import test_sftp_transport
686+
687+class TestCaseWithConnectionHookedTransport(test_sftp_transport.TestCaseWithSFTPServer):
688
689 def setUp(self):
690 super(TestCaseWithConnectionHookedTransport, self).setUp()
691@@ -44,4 +34,3 @@
692
693 def reset_connections(self):
694 self.connections = []
695-
696
697=== modified file 'breezy/transport/__init__.py'
698--- breezy/transport/__init__.py 2018-05-05 20:39:37 +0000
699+++ breezy/transport/__init__.py 2018-05-11 10:19:44 +0000
700@@ -1686,33 +1686,10 @@
701 register_lazy_transport('https://', 'breezy.transport.http',
702 'HttpTransport')
703
704-register_transport_proto('ftp://', help="Access using passive FTP.")
705-register_lazy_transport('ftp://', 'breezy.transport.ftp', 'FtpTransport')
706-register_transport_proto('aftp://', help="Access using active FTP.")
707-register_lazy_transport('aftp://', 'breezy.transport.ftp', 'FtpTransport')
708 register_transport_proto('gio+', help="Access using any GIO supported protocols.")
709 register_lazy_transport('gio+', 'breezy.transport.gio_transport', 'GioTransport')
710
711
712-# Default to trying GSSAPI authentication (if the kerberos module is
713-# available)
714-register_transport_proto('ftp+gssapi://', register_netloc=True)
715-register_transport_proto('aftp+gssapi://', register_netloc=True)
716-register_transport_proto('ftp+nogssapi://', register_netloc=True)
717-register_transport_proto('aftp+nogssapi://', register_netloc=True)
718-register_lazy_transport('ftp+gssapi://', 'breezy.transport.ftp._gssapi',
719- 'GSSAPIFtpTransport')
720-register_lazy_transport('aftp+gssapi://', 'breezy.transport.ftp._gssapi',
721- 'GSSAPIFtpTransport')
722-register_lazy_transport('ftp://', 'breezy.transport.ftp._gssapi',
723- 'GSSAPIFtpTransport')
724-register_lazy_transport('aftp://', 'breezy.transport.ftp._gssapi',
725- 'GSSAPIFtpTransport')
726-register_lazy_transport('ftp+nogssapi://', 'breezy.transport.ftp',
727- 'FtpTransport')
728-register_lazy_transport('aftp+nogssapi://', 'breezy.transport.ftp',
729- 'FtpTransport')
730-
731 register_transport_proto('memory://')
732 register_lazy_transport('memory://', 'breezy.transport.memory',
733 'MemoryTransport')
734
735=== removed directory 'breezy/transport/ftp'
736=== removed file 'breezy/transport/ftp/__init__.py'
737--- breezy/transport/ftp/__init__.py 2017-11-12 13:53:51 +0000
738+++ breezy/transport/ftp/__init__.py 1970-01-01 00:00:00 +0000
739@@ -1,638 +0,0 @@
740-# Copyright (C) 2005-2010 Canonical Ltd
741-#
742-# This program is free software; you can redistribute it and/or modify
743-# it under the terms of the GNU General Public License as published by
744-# the Free Software Foundation; either version 2 of the License, or
745-# (at your option) any later version.
746-#
747-# This program is distributed in the hope that it will be useful,
748-# but WITHOUT ANY WARRANTY; without even the implied warranty of
749-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
750-# GNU General Public License for more details.
751-#
752-# You should have received a copy of the GNU General Public License
753-# along with this program; if not, write to the Free Software
754-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
755-
756-"""Implementation of Transport over ftp.
757-
758-Written by Daniel Silverstone <dsilvers@digital-scurf.org> with serious
759-cargo-culting from the sftp transport and the http transport.
760-
761-It provides the ftp:// and aftp:// protocols where ftp:// is passive ftp
762-and aftp:// is active ftp. Most people will want passive ftp for traversing
763-NAT and other firewalls, so it's best to use it unless you explicitly want
764-active, in which case aftp:// will be your friend.
765-"""
766-
767-from __future__ import absolute_import
768-
769-import ftplib
770-import getpass
771-import os
772-import random
773-import socket
774-import stat
775-import time
776-
777-from ... import (
778- config,
779- errors,
780- osutils,
781- urlutils,
782- )
783-from ...sixish import (
784- BytesIO,
785- )
786-from ...trace import mutter, warning
787-from ...transport import (
788- AppendBasedFileStream,
789- ConnectedTransport,
790- _file_streams,
791- register_urlparse_netloc_protocol,
792- Server,
793- )
794-
795-
796-register_urlparse_netloc_protocol('aftp')
797-
798-
799-class FtpPathError(errors.PathError):
800- """FTP failed for path: %(path)s%(extra)s"""
801-
802-
803-class FtpStatResult(object):
804-
805- def __init__(self, f, abspath):
806- try:
807- self.st_size = f.size(abspath)
808- self.st_mode = stat.S_IFREG
809- except ftplib.error_perm:
810- pwd = f.pwd()
811- try:
812- f.cwd(abspath)
813- self.st_mode = stat.S_IFDIR
814- finally:
815- f.cwd(pwd)
816-
817-
818-_number_of_retries = 2
819-_sleep_between_retries = 5
820-
821-# FIXME: there are inconsistencies in the way temporary errors are
822-# handled. Sometimes we reconnect, sometimes we raise an exception. Care should
823-# be taken to analyze the implications for write operations (read operations
824-# are safe to retry). Overall even some read operations are never
825-# retried. --vila 20070720 (Bug #127164)
826-class FtpTransport(ConnectedTransport):
827- """This is the transport agent for ftp:// access."""
828-
829- def __init__(self, base, _from_transport=None):
830- """Set the base path where files will be stored."""
831- if not (base.startswith('ftp://') or base.startswith('aftp://')):
832- raise ValueError(base)
833- super(FtpTransport, self).__init__(base,
834- _from_transport=_from_transport)
835- self._unqualified_scheme = 'ftp'
836- if self._parsed_url.scheme == 'aftp':
837- self.is_active = True
838- else:
839- self.is_active = False
840-
841- # Most modern FTP servers support the APPE command. If ours doesn't, we
842- # (re)set this flag accordingly later.
843- self._has_append = True
844-
845- def _get_FTP(self):
846- """Return the ftplib.FTP instance for this object."""
847- # Ensures that a connection is established
848- connection = self._get_connection()
849- if connection is None:
850- # First connection ever
851- connection, credentials = self._create_connection()
852- self._set_connection(connection, credentials)
853- return connection
854-
855- connection_class = ftplib.FTP
856-
857- def _create_connection(self, credentials=None):
858- """Create a new connection with the provided credentials.
859-
860- :param credentials: The credentials needed to establish the connection.
861-
862- :return: The created connection and its associated credentials.
863-
864- The input credentials are only the password as it may have been
865- entered interactively by the user and may be different from the one
866- provided in base url at transport creation time. The returned
867- credentials are username, password.
868- """
869- if credentials is None:
870- user, password = self._user, self._password
871- else:
872- user, password = credentials
873-
874- auth = config.AuthenticationConfig()
875- if user is None:
876- user = auth.get_user('ftp', self._host, port=self._port,
877- default=getpass.getuser())
878- mutter("Constructing FTP instance against %r" %
879- ((self._host, self._port, user, '********',
880- self.is_active),))
881- try:
882- connection = self.connection_class()
883- connection.connect(host=self._host, port=self._port)
884- self._login(connection, auth, user, password)
885- connection.set_pasv(not self.is_active)
886- # binary mode is the default
887- connection.voidcmd('TYPE I')
888- except socket.error as e:
889- raise errors.SocketConnectionError(self._host, self._port,
890- msg='Unable to connect to',
891- orig_error= e)
892- except ftplib.error_perm as e:
893- raise errors.TransportError(msg="Error setting up connection:"
894- " %s" % str(e), orig_error=e)
895- return connection, (user, password)
896-
897- def _login(self, connection, auth, user, password):
898- # '' is a valid password
899- if user and user != 'anonymous' and password is None:
900- password = auth.get_password('ftp', self._host,
901- user, port=self._port)
902- connection.login(user=user, passwd=password)
903-
904- def _reconnect(self):
905- """Create a new connection with the previously used credentials"""
906- credentials = self._get_credentials()
907- connection, credentials = self._create_connection(credentials)
908- self._set_connection(connection, credentials)
909-
910- def disconnect(self):
911- connection = self._get_connection()
912- if connection is not None:
913- connection.close()
914-
915- def _translate_ftp_error(self, err, path, extra=None,
916- unknown_exc=FtpPathError):
917- """Try to translate an ftplib exception to a breezy exception.
918-
919- :param err: The error to translate into a bzr error
920- :param path: The path which had problems
921- :param extra: Extra information which can be included
922- :param unknown_exc: If None, we will just raise the original exception
923- otherwise we raise unknown_exc(path, extra=extra)
924- """
925- # ftp error numbers are very generic, like "451: Requested action aborted,
926- # local error in processing" so unfortunately we have to match by
927- # strings.
928- s = str(err).lower()
929- if not extra:
930- extra = str(err)
931- else:
932- extra += ': ' + str(err)
933- if ('no such file' in s
934- or 'could not open' in s
935- or 'no such dir' in s
936- or 'could not create file' in s # vsftpd
937- or 'file doesn\'t exist' in s
938- or 'rnfr command failed.' in s # vsftpd RNFR reply if file not found
939- or 'file/directory not found' in s # filezilla server
940- # Microsoft FTP-Service RNFR reply if file not found
941- or (s.startswith('550 ') and 'unable to rename to' in extra)
942- # if containing directory doesn't exist, suggested by
943- # <https://bugs.launchpad.net/bzr/+bug/224373>
944- or (s.startswith('550 ') and "can't find folder" in s)
945- ):
946- raise errors.NoSuchFile(path, extra=extra)
947- elif ('file exists' in s):
948- raise errors.FileExists(path, extra=extra)
949- elif ('not a directory' in s):
950- raise errors.PathError(path, extra=extra)
951- elif 'directory not empty' in s:
952- raise errors.DirectoryNotEmpty(path, extra=extra)
953-
954- mutter('unable to understand error for path: %s: %s', path, err)
955-
956- if unknown_exc:
957- raise unknown_exc(path, extra=extra)
958- # TODO: jam 20060516 Consider re-raising the error wrapped in
959- # something like TransportError, but this loses the traceback
960- # Also, 'sftp' has a generic 'Failure' mode, which we use failure_exc
961- # to handle. Consider doing something like that here.
962- #raise TransportError(msg='Error for path: %s' % (path,), orig_error=e)
963- raise
964-
965- def has(self, relpath):
966- """Does the target location exist?"""
967- # FIXME jam 20060516 We *do* ask about directories in the test suite
968- # We don't seem to in the actual codebase
969- # XXX: I assume we're never asked has(dirname) and thus I use
970- # the FTP size command and assume that if it doesn't raise,
971- # all is good.
972- abspath = self._remote_path(relpath)
973- try:
974- f = self._get_FTP()
975- mutter('FTP has check: %s => %s', relpath, abspath)
976- s = f.size(abspath)
977- mutter("FTP has: %s", abspath)
978- return True
979- except ftplib.error_perm as e:
980- if ('is a directory' in str(e).lower()):
981- mutter("FTP has dir: %s: %s", abspath, e)
982- return True
983- mutter("FTP has not: %s: %s", abspath, e)
984- return False
985-
986- def get(self, relpath, retries=0):
987- """Get the file at the given relative path.
988-
989- :param relpath: The relative path to the file
990- :param retries: Number of retries after temporary failures so far
991- for this operation.
992-
993- We're meant to return a file-like object which bzr will
994- then read from. For now we do this via the magic of BytesIO
995- """
996- try:
997- mutter("FTP get: %s", self._remote_path(relpath))
998- f = self._get_FTP()
999- ret = BytesIO()
1000- f.retrbinary('RETR '+self._remote_path(relpath), ret.write, 8192)
1001- ret.seek(0)
1002- return ret
1003- except ftplib.error_perm as e:
1004- raise errors.NoSuchFile(self.abspath(relpath), extra=str(e))
1005- except ftplib.error_temp as e:
1006- if retries > _number_of_retries:
1007- raise errors.TransportError(msg="FTP temporary error during GET %s. Aborting."
1008- % self.abspath(relpath),
1009- orig_error=e)
1010- else:
1011- warning("FTP temporary error: %s. Retrying.", str(e))
1012- self._reconnect()
1013- return self.get(relpath, retries+1)
1014- except EOFError as e:
1015- if retries > _number_of_retries:
1016- raise errors.TransportError("FTP control connection closed during GET %s."
1017- % self.abspath(relpath),
1018- orig_error=e)
1019- else:
1020- warning("FTP control connection closed. Trying to reopen.")
1021- time.sleep(_sleep_between_retries)
1022- self._reconnect()
1023- return self.get(relpath, retries+1)
1024-
1025- def put_file(self, relpath, fp, mode=None, retries=0):
1026- """Copy the file-like or string object into the location.
1027-
1028- :param relpath: Location to put the contents, relative to base.
1029- :param fp: File-like or string object.
1030- :param retries: Number of retries after temporary failures so far
1031- for this operation.
1032-
1033- TODO: jam 20051215 ftp as a protocol seems to support chmod, but
1034- ftplib does not
1035- """
1036- abspath = self._remote_path(relpath)
1037- tmp_abspath = '%s.tmp.%.9f.%d.%d' % (abspath, time.time(),
1038- os.getpid(), random.randint(0, 0x7FFFFFFF))
1039- bytes = None
1040- if getattr(fp, 'read', None) is None:
1041- # hand in a string IO
1042- bytes = fp
1043- fp = BytesIO(bytes)
1044- else:
1045- # capture the byte count; .read() may be read only so
1046- # decorate it.
1047- class byte_counter(object):
1048- def __init__(self, fp):
1049- self.fp = fp
1050- self.counted_bytes = 0
1051- def read(self, count):
1052- result = self.fp.read(count)
1053- self.counted_bytes += len(result)
1054- return result
1055- fp = byte_counter(fp)
1056- try:
1057- mutter("FTP put: %s", abspath)
1058- f = self._get_FTP()
1059- try:
1060- f.storbinary('STOR '+tmp_abspath, fp)
1061- self._rename_and_overwrite(tmp_abspath, abspath, f)
1062- self._setmode(relpath, mode)
1063- if bytes is not None:
1064- return len(bytes)
1065- else:
1066- return fp.counted_bytes
1067- except (ftplib.error_temp, EOFError) as e:
1068- warning("Failure during ftp PUT of %s: %s. Deleting temporary file."
1069- % (tmp_abspath, e, ))
1070- try:
1071- f.delete(tmp_abspath)
1072- except:
1073- warning("Failed to delete temporary file on the"
1074- " server.\nFile: %s", tmp_abspath)
1075- raise e
1076- raise
1077- except ftplib.error_perm as e:
1078- self._translate_ftp_error(e, abspath, extra='could not store',
1079- unknown_exc=errors.NoSuchFile)
1080- except ftplib.error_temp as e:
1081- if retries > _number_of_retries:
1082- raise errors.TransportError(
1083- "FTP temporary error during PUT %s: %s. Aborting."
1084- % (self.abspath(relpath), e), orig_error=e)
1085- else:
1086- warning("FTP temporary error: %s. Retrying.", str(e))
1087- self._reconnect()
1088- self.put_file(relpath, fp, mode, retries+1)
1089- except EOFError:
1090- if retries > _number_of_retries:
1091- raise errors.TransportError("FTP control connection closed during PUT %s."
1092- % self.abspath(relpath), orig_error=e)
1093- else:
1094- warning("FTP control connection closed. Trying to reopen.")
1095- time.sleep(_sleep_between_retries)
1096- self._reconnect()
1097- self.put_file(relpath, fp, mode, retries+1)
1098-
1099- def mkdir(self, relpath, mode=None):
1100- """Create a directory at the given path."""
1101- abspath = self._remote_path(relpath)
1102- try:
1103- mutter("FTP mkd: %s", abspath)
1104- f = self._get_FTP()
1105- try:
1106- f.mkd(abspath)
1107- except ftplib.error_reply as e:
1108- # <https://bugs.launchpad.net/bzr/+bug/224373> Microsoft FTP
1109- # server returns "250 Directory created." which is kind of
1110- # reasonable, 250 meaning "requested file action OK", but not what
1111- # Python's ftplib expects.
1112- if e[0][:3] == '250':
1113- pass
1114- else:
1115- raise
1116- self._setmode(relpath, mode)
1117- except ftplib.error_perm as e:
1118- self._translate_ftp_error(e, abspath,
1119- unknown_exc=errors.FileExists)
1120-
1121- def open_write_stream(self, relpath, mode=None):
1122- """See Transport.open_write_stream."""
1123- self.put_bytes(relpath, "", mode)
1124- result = AppendBasedFileStream(self, relpath)
1125- _file_streams[self.abspath(relpath)] = result
1126- return result
1127-
1128- def recommended_page_size(self):
1129- """See Transport.recommended_page_size().
1130-
1131- For FTP we suggest a large page size to reduce the overhead
1132- introduced by latency.
1133- """
1134- return 64 * 1024
1135-
1136- def rmdir(self, rel_path):
1137- """Delete the directory at rel_path"""
1138- abspath = self._remote_path(rel_path)
1139- try:
1140- mutter("FTP rmd: %s", abspath)
1141- f = self._get_FTP()
1142- f.rmd(abspath)
1143- except ftplib.error_perm as e:
1144- self._translate_ftp_error(e, abspath, unknown_exc=errors.PathError)
1145-
1146- def append_file(self, relpath, f, mode=None):
1147- """Append the text in the file-like object into the final
1148- location.
1149- """
1150- text = f.read()
1151- abspath = self._remote_path(relpath)
1152- if self.has(relpath):
1153- ftp = self._get_FTP()
1154- result = ftp.size(abspath)
1155- else:
1156- result = 0
1157-
1158- if self._has_append:
1159- mutter("FTP appe to %s", abspath)
1160- self._try_append(relpath, text, mode)
1161- else:
1162- self._fallback_append(relpath, text, mode)
1163-
1164- return result
1165-
1166- def _try_append(self, relpath, text, mode=None, retries=0):
1167- """Try repeatedly to append the given text to the file at relpath.
1168-
1169- This is a recursive function. On errors, it will be called until the
1170- number of retries is exceeded.
1171- """
1172- try:
1173- abspath = self._remote_path(relpath)
1174- mutter("FTP appe (try %d) to %s", retries, abspath)
1175- ftp = self._get_FTP()
1176- cmd = "APPE %s" % abspath
1177- conn = ftp.transfercmd(cmd)
1178- conn.sendall(text)
1179- conn.close()
1180- self._setmode(relpath, mode)
1181- ftp.getresp()
1182- except ftplib.error_perm as e:
1183- # Check whether the command is not supported (reply code 502)
1184- if str(e).startswith('502 '):
1185- warning("FTP server does not support file appending natively. "
1186- "Performance may be severely degraded! (%s)", e)
1187- self._has_append = False
1188- self._fallback_append(relpath, text, mode)
1189- else:
1190- self._translate_ftp_error(e, abspath, extra='error appending',
1191- unknown_exc=errors.NoSuchFile)
1192- except ftplib.error_temp as e:
1193- if retries > _number_of_retries:
1194- raise errors.TransportError(
1195- "FTP temporary error during APPEND %s. Aborting."
1196- % abspath, orig_error=e)
1197- else:
1198- warning("FTP temporary error: %s. Retrying.", str(e))
1199- self._reconnect()
1200- self._try_append(relpath, text, mode, retries+1)
1201-
1202- def _fallback_append(self, relpath, text, mode = None):
1203- remote = self.get(relpath)
1204- remote.seek(0, os.SEEK_END)
1205- remote.write(text)
1206- remote.seek(0)
1207- return self.put_file(relpath, remote, mode)
1208-
1209- def _setmode(self, relpath, mode):
1210- """Set permissions on a path.
1211-
1212- Only set permissions if the FTP server supports the 'SITE CHMOD'
1213- extension.
1214- """
1215- if mode:
1216- try:
1217- mutter("FTP site chmod: setting permissions to %s on %s",
1218- oct(mode), self._remote_path(relpath))
1219- ftp = self._get_FTP()
1220- cmd = "SITE CHMOD %s %s" % (oct(mode),
1221- self._remote_path(relpath))
1222- ftp.sendcmd(cmd)
1223- except ftplib.error_perm as e:
1224- # Command probably not available on this server
1225- warning("FTP Could not set permissions to %s on %s. %s",
1226- oct(mode), self._remote_path(relpath), str(e))
1227-
1228- # TODO: jam 20060516 I believe ftp allows you to tell an ftp server
1229- # to copy something to another machine. And you may be able
1230- # to give it its own address as the 'to' location.
1231- # So implement a fancier 'copy()'
1232-
1233- def rename(self, rel_from, rel_to):
1234- abs_from = self._remote_path(rel_from)
1235- abs_to = self._remote_path(rel_to)
1236- mutter("FTP rename: %s => %s", abs_from, abs_to)
1237- f = self._get_FTP()
1238- return self._rename(abs_from, abs_to, f)
1239-
1240- def _rename(self, abs_from, abs_to, f):
1241- try:
1242- f.rename(abs_from, abs_to)
1243- except (ftplib.error_temp, ftplib.error_perm) as e:
1244- self._translate_ftp_error(e, abs_from,
1245- ': unable to rename to %r' % (abs_to))
1246-
1247- def move(self, rel_from, rel_to):
1248- """Move the item at rel_from to the location at rel_to"""
1249- abs_from = self._remote_path(rel_from)
1250- abs_to = self._remote_path(rel_to)
1251- try:
1252- mutter("FTP mv: %s => %s", abs_from, abs_to)
1253- f = self._get_FTP()
1254- self._rename_and_overwrite(abs_from, abs_to, f)
1255- except ftplib.error_perm as e:
1256- self._translate_ftp_error(e, abs_from,
1257- extra='unable to rename to %r' % (rel_to,),
1258- unknown_exc=errors.PathError)
1259-
1260- def _rename_and_overwrite(self, abs_from, abs_to, f):
1261- """Do a fancy rename on the remote server.
1262-
1263- Using the implementation provided by osutils.
1264- """
1265- osutils.fancy_rename(abs_from, abs_to,
1266- rename_func=lambda p1, p2: self._rename(p1, p2, f),
1267- unlink_func=lambda p: self._delete(p, f))
1268-
1269- def delete(self, relpath):
1270- """Delete the item at relpath"""
1271- abspath = self._remote_path(relpath)
1272- f = self._get_FTP()
1273- self._delete(abspath, f)
1274-
1275- def _delete(self, abspath, f):
1276- try:
1277- mutter("FTP rm: %s", abspath)
1278- f.delete(abspath)
1279- except ftplib.error_perm as e:
1280- self._translate_ftp_error(e, abspath, 'error deleting',
1281- unknown_exc=errors.NoSuchFile)
1282-
1283- def external_url(self):
1284- """See breezy.transport.Transport.external_url."""
1285- # FTP URL's are externally usable.
1286- return self.base
1287-
1288- def listable(self):
1289- """See Transport.listable."""
1290- return True
1291-
1292- def list_dir(self, relpath):
1293- """See Transport.list_dir."""
1294- basepath = self._remote_path(relpath)
1295- mutter("FTP nlst: %s", basepath)
1296- f = self._get_FTP()
1297- try:
1298- try:
1299- paths = f.nlst(basepath)
1300- except ftplib.error_perm as e:
1301- self._translate_ftp_error(e, relpath,
1302- extra='error with list_dir')
1303- except ftplib.error_temp as e:
1304- # xs4all's ftp server raises a 450 temp error when listing an
1305- # empty directory. Check for that and just return an empty list
1306- # in that case. See bug #215522
1307- if str(e).lower().startswith('450 no files found'):
1308- mutter('FTP Server returned "%s" for nlst.'
1309- ' Assuming it means empty directory',
1310- str(e))
1311- return []
1312- raise
1313- finally:
1314- # Restore binary mode as nlst switch to ascii mode to retrieve file
1315- # list
1316- f.voidcmd('TYPE I')
1317-
1318- # If FTP.nlst returns paths prefixed by relpath, strip 'em
1319- if paths and paths[0].startswith(basepath):
1320- entries = [path[len(basepath)+1:] for path in paths]
1321- else:
1322- entries = paths
1323- # Remove . and .. if present
1324- return [urlutils.escape(entry) for entry in entries
1325- if entry not in ('.', '..')]
1326-
1327- def iter_files_recursive(self):
1328- """See Transport.iter_files_recursive.
1329-
1330- This is cargo-culted from the SFTP transport"""
1331- mutter("FTP iter_files_recursive")
1332- queue = list(self.list_dir("."))
1333- while queue:
1334- relpath = queue.pop(0)
1335- st = self.stat(relpath)
1336- if stat.S_ISDIR(st.st_mode):
1337- for i, basename in enumerate(self.list_dir(relpath)):
1338- queue.insert(i, relpath+"/"+basename)
1339- else:
1340- yield relpath
1341-
1342- def stat(self, relpath):
1343- """Return the stat information for a file."""
1344- abspath = self._remote_path(relpath)
1345- try:
1346- mutter("FTP stat: %s", abspath)
1347- f = self._get_FTP()
1348- return FtpStatResult(f, abspath)
1349- except ftplib.error_perm as e:
1350- self._translate_ftp_error(e, abspath, extra='error w/ stat')
1351-
1352- def lock_read(self, relpath):
1353- """Lock the given file for shared (read) access.
1354- :return: A lock object, which should be passed to Transport.unlock()
1355- """
1356- # The old RemoteBranch ignore lock for reading, so we will
1357- # continue that tradition and return a bogus lock object.
1358- class BogusLock(object):
1359- def __init__(self, path):
1360- self.path = path
1361- def unlock(self):
1362- pass
1363- return BogusLock(relpath)
1364-
1365- def lock_write(self, relpath):
1366- """Lock the given file for exclusive (write) access.
1367- WARNING: many transports do not support this, so trying avoid using it
1368-
1369- :return: A lock object, which should be passed to Transport.unlock()
1370- """
1371- return self.lock_read(relpath)
1372-
1373-
1374-def get_test_permutations():
1375- """Return the permutations to be used in testing."""
1376- from ...tests import ftp_server
1377- return [(FtpTransport, ftp_server.FTPTestServer)]
1378
1379=== removed file 'breezy/transport/ftp/_gssapi.py'
1380--- breezy/transport/ftp/_gssapi.py 2018-05-07 12:48:12 +0000
1381+++ breezy/transport/ftp/_gssapi.py 1970-01-01 00:00:00 +0000
1382@@ -1,128 +0,0 @@
1383-# Copyright (C) 2008, 2009, 2010 Canonical Ltd
1384-#
1385-# This program is free software; you can redistribute it and/or modify
1386-# it under the terms of the GNU General Public License as published by
1387-# the Free Software Foundation; either version 2 of the License, or
1388-# (at your option) any later version.
1389-#
1390-# This program is distributed in the hope that it will be useful,
1391-# but WITHOUT ANY WARRANTY; without even the implied warranty of
1392-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1393-# GNU General Public License for more details.
1394-#
1395-# You should have received a copy of the GNU General Public License
1396-# along with this program; if not, write to the Free Software
1397-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
1398-
1399-"""Support for secure authentication using GSSAPI over FTP.
1400-
1401-See RFC2228 for details.
1402-"""
1403-
1404-from __future__ import absolute_import
1405-
1406-import base64, ftplib
1407-
1408-from ... import (
1409- errors,
1410- )
1411-from ...i18n import gettext
1412-from ...trace import (
1413- mutter,
1414- note,
1415- )
1416-from ..ftp import FtpTransport
1417-
1418-try:
1419- import kerberos
1420-except ImportError as e:
1421- mutter('failed to import kerberos lib: %s', str(e))
1422- raise errors.DependencyNotPresent('kerberos', e)
1423-
1424-if getattr(kerberos, "authGSSClientWrap", None) is None:
1425- raise errors.DependencyNotPresent('kerberos',
1426- "missing encryption function authGSSClientWrap")
1427-
1428-
1429-class GSSAPIFtp(ftplib.FTP):
1430- """Extended version of ftplib.FTP that can authenticate using GSSAPI."""
1431-
1432- def mic_putcmd(self, line):
1433- rc = kerberos.authGSSClientWrap(self.vc, base64.b64encode(line))
1434- wrapped = kerberos.authGSSClientResponse(self.vc)
1435- ftplib.FTP.putcmd(self, "MIC " + wrapped)
1436-
1437- def mic_getline(self):
1438- resp = ftplib.FTP.getline(self)
1439- if resp[:4] != '631 ':
1440- raise AssertionError
1441- rc = kerberos.authGSSClientUnwrap(self.vc, resp[4:].strip("\r\n"))
1442- response = base64.b64decode(kerberos.authGSSClientResponse(self.vc))
1443- return response
1444-
1445- def gssapi_login(self, user):
1446- # Try GSSAPI login first
1447-
1448- # Used FTP response codes:
1449- # 235 [ADAT=base64data] - indicates that the security data exchange
1450- # completed successfully.
1451- # 334 [ADAT=base64data] - indicates that the requested security
1452- # mechanism is ok, and includes security data to be used by the
1453- # client to construct the next command.
1454- # 335 [ADAT=base64data] - indicates that the security data is
1455- # acceptable, and more is required to complete the security
1456- # data exchange.
1457-
1458- resp = self.sendcmd('AUTH GSSAPI')
1459- if resp.startswith('334 '):
1460- rc, self.vc = kerberos.authGSSClientInit("ftp@%s" % self.host)
1461- if kerberos.authGSSClientStep(self.vc, "") != 1:
1462- while resp[:4] in ('334 ', '335 '):
1463- authdata = kerberos.authGSSClientResponse(self.vc)
1464- resp = self.sendcmd('ADAT ' + authdata)
1465- if resp[:9] in ('235 ADAT=', '335 ADAT='):
1466- rc = kerberos.authGSSClientStep(self.vc, resp[9:])
1467- if not ((resp.startswith('235 ') and rc == 1) or
1468- (resp.startswith('335 ') and rc == 0)):
1469- raise ftplib.error_reply(resp)
1470- note(gettext("Authenticated as %s") %
1471- kerberos.authGSSClientUserName(self.vc))
1472-
1473- # Monkey patch ftplib
1474- self.putcmd = self.mic_putcmd
1475- self.getline = self.mic_getline
1476- self.sendcmd('USER ' + user)
1477- return resp
1478- mutter("Unable to use GSSAPI authentication: %s", resp)
1479-
1480-
1481-class GSSAPIFtpTransport(FtpTransport):
1482- """FTP transport implementation that will try to use GSSAPI authentication.
1483-
1484- """
1485-
1486- connection_class = GSSAPIFtp
1487-
1488- def _login(self, connection, auth, user, password):
1489- """Login with GSSAPI Authentication.
1490-
1491- The password is used if GSSAPI Authentication is not available.
1492-
1493- The username and password can both be None, in which case the
1494- credentials specified in the URL or provided by the
1495- AuthenticationConfig() are used.
1496- """
1497- try:
1498- connection.gssapi_login(user=user)
1499- except ftplib.error_perm as e:
1500- super(GSSAPIFtpTransport, self)._login(connection, auth,
1501- user, password)
1502-
1503-
1504-def get_test_permutations():
1505- """Return the permutations to be used in testing."""
1506- from ...tests import ftp_server
1507- if ftp_server.FTPServerFeature.available():
1508- return [(GSSAPIFtpTransport, ftp_server.FTPTestServer)]
1509- else:
1510- return []

Subscribers

People subscribed via source and target branches