launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32677
[Merge] ~pelpsi/txpkgupload:security-linters into txpkgupload:master
Simone Pelosi has proposed merging ~pelpsi/txpkgupload:security-linters into txpkgupload:master.
Commit message:
fix: resolve all pre-commit linting and formatting issues
Remove unused variable 'client' in hooks.py
Fix undefined future imports in test_filesystem.py by importing from __future__ module
Replace lambda expression with proper function definition in test_plugin.py
Remove unused 'connector' variable in test_plugin.py
Fix f-string formatting issues in charm reactive code
Quote shell command substitution in update-version-info.sh
Apply black code formatting and isort import sorting
Ensure all ruff security checks pass with existing suppressions
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~pelpsi/txpkgupload/+git/txpkgupload/+merge/488225
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~pelpsi/txpkgupload:security-linters into txpkgupload:master.
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e61001c..fe5d447 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -44,3 +44,10 @@ repos:
hooks:
- id: woke-from-source
files: ^doc/.*\.rst$
+- repo: https://github.com/astral-sh/ruff-pre-commit
+ rev: v0.12.1
+ hooks:
+ - id: ruff
+ args: [--select=S]
+ name: ruff-security
+ files: ^src/
\ No newline at end of file
diff --git a/charm/txpkgupload/reactive/txpkgupload.py b/charm/txpkgupload/reactive/txpkgupload.py
index f0d5de6..820352f 100644
--- a/charm/txpkgupload/reactive/txpkgupload.py
+++ b/charm/txpkgupload/reactive/txpkgupload.py
@@ -246,10 +246,9 @@ def configure_loadbalancer():
# For passive FTP transfer, we want our haproxy to bind to not only the
# usual FTP/SFTP ports but also to higher ports
if config.get("min_passive_port") and config.get("max_passive_port"):
- extra_ports = (
- f"bind :::"
- f"{config['min_passive_port']}-{config['max_passive_port']}"
- )
+ min_port = config["min_passive_port"]
+ max_port = config["max_passive_port"]
+ extra_ports = "bind :::{}-{}".format(min_port, max_port)
service_options_ftp.append(extra_ports)
ftp_port = config["ftp_port"]
diff --git a/scripts/update-version-info.sh b/scripts/update-version-info.sh
index d6492fe..9e7a25f 100755
--- a/scripts/update-version-info.sh
+++ b/scripts/update-version-info.sh
@@ -13,7 +13,7 @@ if [ ! -e .git ]; then
exit 1
fi
-if ! which git > /dev/null || ! test -x $(which git); then
+if ! which git > /dev/null || ! test -x "$(which git)"; then
echo "No working 'git' executable found" >&2
exit 1
fi
diff --git a/setup.py b/setup.py
index 6f3ae16..4eb111f 100755
--- a/setup.py
+++ b/setup.py
@@ -16,20 +16,22 @@
# You should have received a copy of the GNU Lesser General Public License
# along with txpkgupload. If not, see <http://www.gnu.org/licenses/>.
-from setuptools import setup, find_packages
+from setuptools import find_packages, setup
# generic helpers primarily for the long_description
def generate(*docname_or_string):
res = []
for value in docname_or_string:
- if value.endswith('.txt'):
+ if value.endswith(".txt"):
with open(value) as f:
value = f.read()
res.append(value)
- if not value.endswith('\n'):
- res.append('')
- return '\n'.join(res)
+ if not value.endswith("\n"):
+ res.append("")
+ return "\n".join(res)
+
+
# end generic helpers
@@ -39,32 +41,32 @@ with open("README.txt") as f:
description = f.readline().strip()
setup(
- name='txpkgupload',
+ name="txpkgupload",
version=__version__,
- packages=find_packages('src') + ['twisted.plugins'],
- package_dir={'': 'src'},
+ packages=find_packages("src") + ["twisted.plugins"],
+ package_dir={"": "src"},
include_package_data=True,
zip_safe=False,
- maintainer='LAZR Developers',
- maintainer_email='lazr-developers@xxxxxxxxxxxxxxxxxxx',
+ maintainer="LAZR Developers",
+ maintainer_email="lazr-developers@xxxxxxxxxxxxxxxxxxx",
description=description,
- long_description=generate('src/txpkgupload/NEWS.txt'),
- license='AGPL v3',
+ long_description=generate("src/txpkgupload/NEWS.txt"),
+ license="AGPL v3",
python_requires=">=3.5",
install_requires=[
- 'FormEncode',
+ "FormEncode",
'importlib-metadata; python_version < "3.8"',
- 'lazr.sshserver>=0.1.7',
- 'oops-datedir-repo>=0.0.21',
- 'oops-twisted>=0.0.7',
- 'PyYAML',
- 'setuptools',
- 'six>=1.12.0',
- 'Twisted[conch_nacl]',
- 'zope.interface>=3.6.0',
- ],
- url='https://launchpad.net/txpkgupload',
- download_url='https://launchpad.net/txpkgupload/+download',
+ "lazr.sshserver>=0.1.7",
+ "oops-datedir-repo>=0.0.21",
+ "oops-twisted>=0.0.7",
+ "PyYAML",
+ "setuptools",
+ "six>=1.12.0",
+ "Twisted[conch_nacl]",
+ "zope.interface>=3.6.0",
+ ],
+ url="https://launchpad.net/txpkgupload",
+ download_url="https://launchpad.net/txpkgupload/+download",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
@@ -76,18 +78,17 @@ setup(
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
- ],
+ ],
extras_require=dict(
- test=['fixtures',
- 'testtools'],
+ test=["fixtures", "testtools"],
),
entry_points={
- 'console_scripts': [
- 'build-twisted-plugin-cache = twisted.plugins.plugincache:main',
+ "console_scripts": [
+ "build-twisted-plugin-cache = twisted.plugins.plugincache:main",
]
},
# This does not play nicely with buildout because it downloads but does
# not cache the package.
# setup_requires=['eggtestinfo', 'setuptools_bzr'],
- test_suite='txpkgupload.tests',
- )
+ test_suite="txpkgupload.tests",
+)
diff --git a/src/twisted/plugins/pkgupload.py b/src/twisted/plugins/pkgupload.py
index 68b89f6..317b3e7 100644
--- a/src/twisted/plugins/pkgupload.py
+++ b/src/twisted/plugins/pkgupload.py
@@ -1,7 +1,6 @@
# Copyright 2005-2015 Canonical Ltd. This software is licensed under
# the GNU Affero General Public License version 3 (see the file LICENSE).
-from __future__ import absolute_import
from txpkgupload.plugin import PkgUploadServiceMaker
@@ -10,4 +9,5 @@ from txpkgupload.plugin import PkgUploadServiceMaker
# providers of IPlugin and IServiceMaker.
service_pkgupload = PkgUploadServiceMaker(
- "pkgupload", "A package upload frontend server.")
+ "pkgupload", "A package upload frontend server."
+)
diff --git a/src/txpkgupload/__init__.py b/src/txpkgupload/__init__.py
index 00557a9..c7379c4 100644
--- a/src/txpkgupload/__init__.py
+++ b/src/txpkgupload/__init__.py
@@ -12,7 +12,7 @@ def get_txpkgupload_root(fsroot):
If the TXPKGUPLOAD_ROOT environment variable is set, use that. If not,
use fsroot.
"""
- txpkgupload_root = os.environ.get('TXPKGUPLOAD_ROOT', None)
+ txpkgupload_root = os.environ.get("TXPKGUPLOAD_ROOT", None)
if txpkgupload_root:
return txpkgupload_root
return fsroot
diff --git a/src/txpkgupload/filesystem.py b/src/txpkgupload/filesystem.py
index 9a913e0..d912ba5 100644
--- a/src/txpkgupload/filesystem.py
+++ b/src/txpkgupload/filesystem.py
@@ -1,16 +1,14 @@
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-__metaclass__ = type
__all__ = [
- 'UploadFileSystem',
- ]
+ "UploadFileSystem",
+]
import os
class UploadFileSystem:
-
def __init__(self, rootpath):
self.rootpath = rootpath
@@ -30,10 +28,10 @@ class UploadFileSystem:
# and since UTF-8 has low risk of undetected decoding errors,
# let's try to decode SFTP paths as UTF-8.
try:
- path = path.decode('UTF-8')
+ path = path.decode("UTF-8")
except UnicodeDecodeError:
- raise NotImplementedError('Paths must be encoded using UTF-8')
- if path.startswith('/'):
+ raise NotImplementedError("Paths must be encoded using UTF-8")
+ if path.startswith("/"):
path = path[1:]
path = os.path.normpath(path)
return path
diff --git a/src/txpkgupload/hooks.py b/src/txpkgupload/hooks.py
index c6cc475..07a4b03 100644
--- a/src/txpkgupload/hooks.py
+++ b/src/txpkgupload/hooks.py
@@ -1,12 +1,11 @@
# Copyright 2009-2010 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-__metaclass__ = type
__all__ = [
- 'Hooks',
- 'InterfaceFailure',
- ]
+ "Hooks",
+ "InterfaceFailure",
+]
import os
@@ -20,12 +19,11 @@ class InterfaceFailure(Exception):
class Hooks:
-
clients = {}
LOG_MAGIC = "Post-processing finished"
_targetcount = 0
- def __init__(self, targetpath, targetstart=0, perms=None, prefix=''):
+ def __init__(self, targetpath, targetstart=0, perms=None, prefix=""):
self.targetpath = targetpath
self.perms = perms
self.prefix = prefix
@@ -38,10 +36,7 @@ class Hooks:
def new_client_hook(self, fsroot, host, port):
"""Prepare a new client record indexed by fsroot..."""
- self.clients[fsroot] = {
- "host": host,
- "port": port
- }
+ self.clients[fsroot] = {"host": host, "port": port}
log.msg("Accepting new session in fsroot: %s" % fsroot, debug=True)
log.msg("Session from %s:%s" % (host, port), debug=True)
@@ -53,11 +48,8 @@ class Hooks:
log.msg("Processing session complete in %s" % fsroot, debug=True)
- client = self.clients[fsroot]
-
timestamp = time.strftime("%Y%m%d-%H%M%S")
- path = "upload%s-%s-%06d" % (
- self.prefix, timestamp, self.targetcount)
+ path = "upload%s-%s-%06d" % (self.prefix, timestamp, self.targetcount)
target_fsroot = os.path.join(self.targetpath, path)
# Move the session directory to the target directory.
@@ -67,14 +59,16 @@ class Hooks:
else:
try:
os.rename(fsroot, target_fsroot)
- except (OSError, IOError):
+ except OSError:
if not os.path.exists(target_fsroot):
raise
# XXX cprov 20071024: We should replace os.system call by os.chmod
# and fix the default permission value accordingly in txpkgupload
if self.perms is not None:
- os.system("chmod %s -R %s" % (self.perms, target_fsroot))
+ os.system( # noqa: S605
+ "chmod %s -R %s" % (self.perms, target_fsroot)
+ )
self.clients.pop(fsroot)
# This is mainly done so that tests know when the
diff --git a/src/txpkgupload/plugin.py b/src/txpkgupload/plugin.py
index c9287a1..aa6be6e 100644
--- a/src/txpkgupload/plugin.py
+++ b/src/txpkgupload/plugin.py
@@ -1,26 +1,18 @@
# Copyright 2005-2015 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-__metaclass__ = type
__all__ = [
- 'PkgUploadServiceMaker',
- ]
+ "PkgUploadServiceMaker",
+]
-from functools import partial
import os
+from functools import partial
+import yaml
from formencode import Schema
from formencode.api import set_stdtranslation
-from formencode.validators import (
- Int,
- RequireIfPresent,
- String,
- StringBool,
- )
-from lazr.sshserver.auth import (
- LaunchpadAvatar,
- PublicKeyFromLaunchpadChecker,
- )
+from formencode.validators import Int, RequireIfPresent, String, StringBool
+from lazr.sshserver.auth import LaunchpadAvatar, PublicKeyFromLaunchpadChecker
from lazr.sshserver.service import SSHService
from lazr.sshserver.session import DoNothingSession
from twisted.application.service import IServiceMaker
@@ -29,25 +21,14 @@ from twisted.conch.ssh import filetransfer
from twisted.cred.portal import IRealm, Portal
from twisted.plugin import IPlugin
from twisted.protocols.policies import TimeoutFactory
-from twisted.python import (
- components,
- usage,
- )
+from twisted.python import components, usage
from twisted.web.xmlrpc import Proxy
-import yaml
from zope.interface import implementer
from txpkgupload import get_txpkgupload_root
-from txpkgupload.services import (
- PkgUploadServices,
- ReadyService,
- )
+from txpkgupload.services import PkgUploadServices, ReadyService
from txpkgupload.twistedftp import FTPServiceFactory
-from txpkgupload.twistedsftp import (
- PkgUploadFileTransferServer,
- SFTPServer,
- )
-
+from txpkgupload.twistedsftp import PkgUploadFileTransferServer, SFTPServer
# Ensure that formencode does not translate strings; there are encoding issues
# that are easier to side-step for now.
@@ -62,9 +43,7 @@ class ConfigOops(Schema):
directory = String(if_missing="")
reporter = String(if_missing="PKGUPLOAD")
- chained_validators = (
- RequireIfPresent("reporter", present="directory"),
- )
+ chained_validators = (RequireIfPresent("reporter", present="directory"),)
class ConfigFtp(Schema):
@@ -116,7 +95,8 @@ class Config(Schema):
# The access log location. Information such as connection, SSH login
# and session start times will be logged here.
access_log = String(
- if_empty="txpkgupload-access.log", if_missing="txpkgupload-access.log")
+ if_empty="txpkgupload-access.log", if_missing="txpkgupload-access.log"
+ )
# If true, enable additional debug logging.
debug = StringBool(if_missing=False)
@@ -153,11 +133,14 @@ class Config(Schema):
class Options(usage.Options):
-
optParameters = [
- ["config-file", "c", "etc/txpkgupload.yaml",
- "Configuration file to load."],
- ]
+ [
+ "config-file",
+ "c",
+ "etc/txpkgupload.yaml",
+ "Configuration file to load.",
+ ],
+ ]
class PkgUploadAvatar(LaunchpadAvatar):
@@ -176,7 +159,6 @@ class PkgUploadAvatar(LaunchpadAvatar):
@implementer(IRealm)
class Realm:
-
def __init__(self, authentication_proxy, fs_root, temp_dir):
self.authentication_proxy = authentication_proxy
self.fs_root = fs_root
@@ -184,8 +166,7 @@ class Realm:
def requestAvatar(self, avatar_id, mind, *interfaces):
# Fetch the user's details from the authserver
- deferred = mind.lookupUserDetails(
- self.authentication_proxy, avatar_id)
+ deferred = mind.lookupUserDetails(self.authentication_proxy, avatar_id)
# Once all those details are retrieved, we can construct the avatar.
def got_user_dict(user_dict):
@@ -203,8 +184,7 @@ def make_portal(authentication_endpoint, fs_root, temp_dir):
"""
authentication_proxy = Proxy(authentication_endpoint.encode("UTF-8"))
portal = Portal(Realm(authentication_proxy, fs_root, temp_dir))
- portal.registerChecker(
- PublicKeyFromLaunchpadChecker(authentication_proxy))
+ portal.registerChecker(PublicKeyFromLaunchpadChecker(authentication_proxy))
return portal
@@ -233,13 +213,15 @@ class PkgUploadServiceMaker:
oops_reporter = oops_config["reporter"]
services = PkgUploadServices(
- oops_dir, oops_reporter, server_argv=server_argv)
+ oops_dir, oops_reporter, server_argv=server_argv
+ )
root = get_txpkgupload_root(config["fsroot"])
temp_dir = config["temp_dir"]
if temp_dir is None:
- temp_dir = os.path.abspath(os.path.join(
- root, os.pardir, "tmp-incoming"))
+ temp_dir = os.path.abspath(
+ os.path.join(root, os.pardir, "tmp-incoming")
+ )
if not os.path.exists(temp_dir):
old_mask = os.umask(0o002)
try:
@@ -263,17 +245,20 @@ class PkgUploadServiceMaker:
sftp_config = config["sftp"]
sftp_service = SSHService(
portal=make_portal(
- sftp_config["authentication_endpoint"], root, temp_dir),
+ sftp_config["authentication_endpoint"], root, temp_dir
+ ),
private_key_path=sftp_config["host_key_private"],
public_key_path=sftp_config["host_key_public"],
- main_log='txpkgupload',
- access_log='txpkgupload.access',
+ main_log="txpkgupload",
+ access_log="txpkgupload.access",
access_log_path=config["access_log"],
strport=sftp_config["port"],
factory_decorator=partial(
- timeout_decorator, config["idle_timeout"]),
+ timeout_decorator, config["idle_timeout"]
+ ),
banner=sftp_config["banner"],
- moduli_path=sftp_config["moduli_path"])
+ moduli_path=sftp_config["moduli_path"],
+ )
sftp_service.name = "sftp"
sftp_service.setServiceParent(services)
@@ -284,6 +269,7 @@ class PkgUploadServiceMaker:
components.registerAdapter(
- SFTPServer, PkgUploadAvatar, filetransfer.ISFTPServer)
+ SFTPServer, PkgUploadAvatar, filetransfer.ISFTPServer
+)
components.registerAdapter(DoNothingSession, PkgUploadAvatar, ISession)
diff --git a/src/txpkgupload/services.py b/src/txpkgupload/services.py
index 613ff30..c0af23e 100644
--- a/src/txpkgupload/services.py
+++ b/src/txpkgupload/services.py
@@ -3,31 +3,20 @@
"""Additional services that compose txpkgupload."""
-from __future__ import (
- print_function,
- unicode_literals,
- )
-__metaclass__ = type
__all__ = [
"PkgUploadFileLogObserver",
"PkgUploadServices",
"ReadyService",
- ]
+]
import signal
import sys
from oops_datedir_repo import DateDirRepo
-from oops_twisted import (
- Config as oops_config,
- defer_publisher,
- OOPSObserver,
- )
-from twisted.application.service import (
- MultiService,
- Service,
- )
+from oops_twisted import Config as oops_config
+from oops_twisted import OOPSObserver, defer_publisher
+from twisted.application.service import MultiService, Service
from twisted.internet import reactor
from twisted.python import log
from twisted.python.components import Componentized
@@ -55,7 +44,8 @@ class ReadyService(Service):
def startService(self):
reactor.addSystemEventTrigger(
- 'after', 'startup', log.msg, 'daemon ready!')
+ "after", "startup", log.msg, "daemon ready!"
+ )
Service.startService(self)
@@ -63,7 +53,7 @@ class PkgUploadServices(MultiService):
"""Container for package upload services."""
def __init__(self, oops_dir, oops_reporter, server_argv=None):
- super(PkgUploadServices, self).__init__()
+ super().__init__()
self.oops_dir = oops_dir
self.oops_reporter = oops_reporter
self.server_argv = server_argv
@@ -90,11 +80,14 @@ class PkgUploadServices(MultiService):
if not logfilename:
logfilename = "txpkgupload.log"
self._log_file = logFile = LogFile.fromFullPath(
- logfilename, rotateLength=None, defaultMode=0o644)
+ logfilename, rotateLength=None, defaultMode=0o644
+ )
# Override if signal is set to None or SIG_DFL (0)
if not signal.getsignal(signal.SIGUSR1):
+
def reopen_log(signal, frame):
reactor.callFromThread(logFile.reopen)
+
signal.signal(signal.SIGUSR1, reopen_log)
log_observer = PkgUploadFileLogObserver(logFile)
@@ -105,13 +98,13 @@ class PkgUploadServices(MultiService):
repo = DateDirRepo(self.oops_dir)
config.publisher = defer_publisher(repo.publish)
if self.oops_reporter:
- config.template['reporter'] = self.oops_reporter
+ config.template["reporter"] = self.oops_reporter
oops_observer = OOPSObserver(config, fallback=log_observer.emit)
return oops_observer.emit
def setServiceParent(self, parent):
- super(PkgUploadServices, self).setServiceParent(parent)
+ super().setServiceParent(parent)
if isinstance(parent, Componentized):
# Customise the application's logging. We don't get much of an
# opportunity to do this otherwise.
@@ -121,4 +114,4 @@ class PkgUploadServices(MultiService):
if self._log_file is not None:
self._log_file.close()
self._log_file = None
- super(PkgUploadServices, self).disownServiceParent()
+ super().disownServiceParent()
diff --git a/src/txpkgupload/tests/__init__.py b/src/txpkgupload/tests/__init__.py
index 006d37c..8da50ec 100644
--- a/src/txpkgupload/tests/__init__.py
+++ b/src/txpkgupload/tests/__init__.py
@@ -2,4 +2,3 @@
# GNU Affero General Public License version 3 (see the file LICENSE).
"""Tests for txpkgupload."""
-
diff --git a/src/txpkgupload/tests/test_filesystem.py b/src/txpkgupload/tests/test_filesystem.py
index bb77a9f..9e33c7a 100644
--- a/src/txpkgupload/tests/test_filesystem.py
+++ b/src/txpkgupload/tests/test_filesystem.py
@@ -1,18 +1,13 @@
# Copyright 2009 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-from __future__ import absolute_import, print_function, unicode_literals
-
-__metaclass__ = type
import doctest
import os
-
DOCTEST_FLAGS = (
- doctest.ELLIPSIS |
- doctest.NORMALIZE_WHITESPACE |
- doctest.REPORT_NDIFF)
+ doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.REPORT_NDIFF
+)
# The setUp() and tearDown() functions ensure that this doctest is not umask
@@ -26,12 +21,21 @@ def tearDown(testobj):
def load_tests(loader, tests, pattern):
+ # Import future features to make them available in doctests
+ import __future__
+
globs = {
- "absolute_import": absolute_import,
- "print_function": print_function,
- "unicode_literals": unicode_literals,
- }
- tests.addTest(doctest.DocFileSuite(
- "filesystem.txt", setUp=setUp, tearDown=tearDown, globs=globs,
- optionflags=DOCTEST_FLAGS))
+ "absolute_import": __future__.absolute_import,
+ "print_function": __future__.print_function,
+ "unicode_literals": __future__.unicode_literals,
+ }
+ tests.addTest(
+ doctest.DocFileSuite(
+ "filesystem.txt",
+ setUp=setUp,
+ tearDown=tearDown,
+ globs=globs,
+ optionflags=DOCTEST_FLAGS,
+ )
+ )
return tests
diff --git a/src/txpkgupload/tests/test_plugin.py b/src/txpkgupload/tests/test_plugin.py
index 13123f9..f16fb95 100644
--- a/src/txpkgupload/tests/test_plugin.py
+++ b/src/txpkgupload/tests/test_plugin.py
@@ -1,12 +1,7 @@
# Copyright 2009-2011 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
-from __future__ import absolute_import, print_function, unicode_literals
-__metaclass__ = type
-
-from collections import defaultdict
-from functools import partial
import io
import os
import shutil
@@ -14,64 +9,42 @@ import stat
import struct
import sys
import tempfile
+from collections import defaultdict
+from functools import partial
from textwrap import dedent
-from fixtures import (
- EnvironmentVariableFixture,
- Fixture,
- TempDir,
- )
+import yaml
+from fixtures import EnvironmentVariableFixture, Fixture, TempDir
from fixtures.callmany import MultipleExceptions
from formencode import Invalid
from lazr.sshserver.auth import NoSuchPersonWithName
from testtools import TestCase
from testtools.compat import reraise
-from testtools.matchers import (
- MatchesException,
- Raises,
- )
+from testtools.matchers import MatchesException, Raises
from testtools.twistedsupport import AsynchronousDeferredRunTest
-from twisted.application.service import (
- Application,
- IService,
- MultiService,
- )
+from twisted.application.service import Application, IService, MultiService
from twisted.conch.client.default import SSHUserAuthClient
from twisted.conch.client.direct import SSHClientFactory
from twisted.conch.scripts.cftp import ClientOptions
from twisted.conch.ssh.channel import SSHChannel
from twisted.conch.ssh.common import NS
-from twisted.conch.ssh.connection import (
- MSG_CHANNEL_REQUEST,
- SSHConnection,
- )
+from twisted.conch.ssh.connection import MSG_CHANNEL_REQUEST, SSHConnection
from twisted.conch.ssh.filetransfer import (
- FileTransferClient,
FXF_CREAT,
FXF_EXCL,
FXF_READ,
FXF_TRUNC,
FXF_WRITE,
- )
-from twisted.internet import (
- defer,
- reactor,
- )
+ FileTransferClient,
+)
+from twisted.internet import defer, reactor
from twisted.internet.protocol import ClientCreator
from twisted.protocols.ftp import FTPClient
from twisted.python import log
-from twisted.web import (
- server,
- xmlrpc,
- )
-import yaml
+from twisted.web import server, xmlrpc
from txpkgupload.hooks import Hooks
-from txpkgupload.plugin import (
- Config,
- Options,
- PkgUploadServiceMaker,
- )
+from txpkgupload.plugin import Config, Options, PkgUploadServiceMaker
class DeferringFixture(Fixture):
@@ -109,7 +82,7 @@ class TestConfig(TestCase):
"fsroot": None,
"ftp": {
"port": "2121",
- },
+ },
"idle_timeout": 3600,
"max_passive_port": None,
"min_passive_port": None,
@@ -117,7 +90,7 @@ class TestConfig(TestCase):
"oops": {
"directory": "",
"reporter": "PKGUPLOAD",
- },
+ },
"sftp": {
"authentication_endpoint": None,
"banner": None,
@@ -125,9 +98,9 @@ class TestConfig(TestCase):
"host_key_public": None,
"moduli_path": "/etc/ssh/moduli",
"port": "tcp:5022",
- },
+ },
"temp_dir": None,
- }
+ }
observed = Config.to_python({})
self.assertEqual(expected, observed)
@@ -147,8 +120,13 @@ class TestConfig(TestCase):
def test_load_example(self):
# The example configuration can be loaded and validated.
filename = os.path.join(
- os.path.dirname(__file__), os.pardir, os.pardir, os.pardir,
- "etc", "txpkgupload.yaml")
+ os.path.dirname(__file__),
+ os.pardir,
+ os.pardir,
+ os.pardir,
+ "etc",
+ "txpkgupload.yaml",
+ )
Config.load(filename)
def test_load_ftp_integer(self):
@@ -163,12 +141,14 @@ class TestConfig(TestCase):
# Check that a UsageError is raised when parsing options.
self.assertThat(
partial(Config.to_python, config),
- Raises(MatchesException(Invalid, message)))
+ Raises(MatchesException(Invalid, message)),
+ )
def test_option_idle_timeout_integer(self):
self.check_exception(
{"idle_timeout": "bob"},
- "idle_timeout: Please enter an integer value")
+ "idle_timeout: Please enter an integer value",
+ )
class TestOptions(TestCase):
@@ -210,14 +190,15 @@ class PkgUploadFixture(DeferringFixture):
def _setUp(self):
self.root = self.useFixture(TempDir()).path
top = os.path.join(
- os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)
+ os.path.dirname(__file__), os.pardir, os.pardir, os.pardir
+ )
with open(os.path.join(top, "etc", "txpkgupload.yaml")) as stream:
config = yaml.safe_load(stream)
config["access_log"] = os.path.join(
- self.root, "txpkgupload-access.log")
+ self.root, "txpkgupload-access.log"
+ )
if self.extra_config is not None:
- deep_update(
- config, yaml.safe_load(io.StringIO(self.extra_config)))
+ deep_update(config, yaml.safe_load(io.StringIO(self.extra_config)))
# Make some paths absolute to cope with tests running in a different
# working directory.
for key in ("host_key_private", "host_key_public"):
@@ -229,9 +210,11 @@ class PkgUploadFixture(DeferringFixture):
options = Options()
options.parseOptions(["-c", filename])
self.service_maker = PkgUploadServiceMaker(
- "txpkgupload", "description")
+ "txpkgupload", "description"
+ )
self.service = self.service_maker.makeService(
- options, server_argv=["--logfile", self.logfile])
+ options, server_argv=["--logfile", self.logfile]
+ )
# Set up logging more or less as twistd's standard application
# runner would, and start the service.
application = Application(self.service_maker.tapname)
@@ -249,8 +232,9 @@ class PkgUploadFixture(DeferringFixture):
timeout_call = None
def check():
+ nonlocal check_call
if os.path.exists(self.logfile):
- with open(self.logfile, "r") as logfile:
+ with open(self.logfile) as logfile:
occurrences = logfile.read().count(Hooks.LOG_MAGIC)
if occurrences >= number:
if timeout_call is not None and timeout_call.active():
@@ -280,7 +264,8 @@ class FTPServer(DeferringFixture):
self.fsroot = os.path.join(self.root_dir, "incoming")
self.temp_dir = os.path.join(self.root_dir, "tmp-incoming")
top = os.path.join(
- os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)
+ os.path.dirname(__file__), os.pardir, os.pardir, os.pardir
+ )
with open(os.path.join(top, "etc", "txpkgupload.yaml")) as stream:
config = yaml.safe_load(stream)
self.port = config["ftp"]["port"]
@@ -288,19 +273,30 @@ class FTPServer(DeferringFixture):
def _setUp(self):
os.mkdir(self.fsroot)
os.mkdir(self.temp_dir)
- self.pkgupload = self.useFixture(PkgUploadFixture(dedent("""\
+ self.pkgupload = self.useFixture(
+ PkgUploadFixture(
+ dedent(
+ """\
fsroot: %s
- temp_dir: %s""") % (self.fsroot, self.temp_dir)))
+ temp_dir: %s"""
+ )
+ % (self.fsroot, self.temp_dir)
+ )
+ )
def getAnonClient(self):
creator = ClientCreator(
- reactor, FTPClient,
- username="anonymous", password="me@xxxxxxxxxxx")
+ reactor,
+ FTPClient,
+ username="anonymous",
+ password="me@xxxxxxxxxxx", # noqa: S106
+ )
return creator.connectTCP("localhost", self.port)
def getClient(self):
creator = ClientCreator(
- reactor, FTPClient, username="ubuntu", password="")
+ reactor, FTPClient, username="ubuntu", password=""
+ )
return creator.connectTCP("localhost", self.port)
def makeDirectory(self, client, path):
@@ -366,13 +362,14 @@ class SFTPConnection(SSHConnection):
"""
if channel.localClosed:
return
- log.msg('sending request %r' % (requestType))
+ log.msg("sending request %r" % (requestType))
self.transport.sendPacket(
MSG_CHANNEL_REQUEST,
- struct.pack('>L', self.channelsToRemoteChannel[channel])
+ struct.pack(">L", self.channelsToRemoteChannel[channel])
+ NS(requestType)
- + (b'\1' if wantReply else b'\0')
- + data)
+ + (b"\1" if wantReply else b"\0")
+ + data,
+ )
if wantReply:
d = defer.Deferred()
self.deferreds.setdefault(channel.id, []).append(d)
@@ -387,7 +384,7 @@ class FakeAuthServerService(xmlrpc.XMLRPC):
self.keys = defaultdict(list)
def addSSHKey(self, username, public_key_path):
- with open(public_key_path, "r") as f:
+ with open(public_key_path) as f:
public_key = f.read()
kind, keytext, _ = public_key.split(" ", 2)
if kind == "ssh-rsa":
@@ -405,7 +402,7 @@ class FakeAuthServerService(xmlrpc.XMLRPC):
"id": username,
"name": username,
"keys": self.keys[username],
- }
+ }
class SFTPServer(DeferringFixture):
@@ -415,46 +412,57 @@ class SFTPServer(DeferringFixture):
self.root_dir = root_dir
self.fsroot = os.path.join(self.root_dir, "incoming")
self.temp_dir = os.path.join(self.root_dir, "tmp-incoming")
- #self._factory = factory
+ # self._factory = factory
top = os.path.join(
- os.path.dirname(__file__), os.pardir, os.pardir, os.pardir)
+ os.path.dirname(__file__), os.pardir, os.pardir, os.pardir
+ )
with open(os.path.join(top, "etc", "txpkgupload.yaml")) as stream:
config = yaml.safe_load(stream)
- self.port = int(config["sftp"]["port"].partition(':')[2])
+ self.port = int(config["sftp"]["port"].partition(":")[2])
self.test_private_key = os.path.join(
- os.path.dirname(__file__), "txpkgupload-sftp")
+ os.path.dirname(__file__), "txpkgupload-sftp"
+ )
self.test_public_key = os.path.join(
- os.path.dirname(__file__), "txpkgupload-sftp.pub")
+ os.path.dirname(__file__), "txpkgupload-sftp.pub"
+ )
def setUpUser(self, name):
self.authserver.addSSHKey(name, self.test_public_key)
# Set up a temporary home directory for Paramiko's sake
self._home_dir = tempfile.mkdtemp()
self.addCleanup(shutil.rmtree, self._home_dir)
- os.mkdir(os.path.join(self._home_dir, '.ssh'))
+ os.mkdir(os.path.join(self._home_dir, ".ssh"))
os.symlink(
self.test_private_key,
- os.path.join(self._home_dir, '.ssh', 'id_rsa'))
- self.useFixture(EnvironmentVariableFixture('HOME', self._home_dir))
- self.useFixture(EnvironmentVariableFixture('SSH_AUTH_SOCK', None))
- self.useFixture(EnvironmentVariableFixture('BZR_SSH', 'paramiko'))
+ os.path.join(self._home_dir, ".ssh", "id_rsa"),
+ )
+ self.useFixture(EnvironmentVariableFixture("HOME", self._home_dir))
+ self.useFixture(EnvironmentVariableFixture("SSH_AUTH_SOCK", None))
+ self.useFixture(EnvironmentVariableFixture("BZR_SSH", "paramiko"))
def _setUp(self):
self.authserver = FakeAuthServerService()
self.authserver_listener = reactor.listenTCP(
- 0, server.Site(self.authserver))
+ 0, server.Site(self.authserver)
+ )
self.authserver_port = self.authserver_listener.getHost().port
self.authserver_url = "http://localhost:%d/" % self.authserver_port
self.addCleanup(self.authserver_listener.stopListening)
- self.setUpUser('joe')
+ self.setUpUser("joe")
os.mkdir(self.fsroot)
os.mkdir(self.temp_dir)
- self.pkgupload = self.useFixture(PkgUploadFixture(dedent("""\
+ self.pkgupload = self.useFixture(
+ PkgUploadFixture(
+ dedent(
+ """\
sftp:
authentication_endpoint: %s
fsroot: %s
- temp_dir: %s""") %
- (self.authserver_url, self.fsroot, self.temp_dir)))
+ temp_dir: %s"""
+ )
+ % (self.authserver_url, self.fsroot, self.temp_dir)
+ )
+ )
@defer.inlineCallbacks
def getClient(self):
@@ -465,11 +473,15 @@ class SFTPServer(DeferringFixture):
conn = SFTPConnection()
conn._sftp = defer.Deferred()
auth_client = SSHUserAuthClient("joe", options, conn)
- verifyHostKey = lambda t, h, pk, fp: defer.succeed(None)
+
+ def verifyHostKey(t, h, pk, fp):
+ return defer.succeed(None)
+
connecting = defer.Deferred()
factory = SSHClientFactory(
- connecting, options, verifyHostKey, auth_client)
- connector = reactor.connectTCP(host, self.port, factory)
+ connecting, options, verifyHostKey, auth_client
+ )
+ reactor.connectTCP(host, self.port, factory)
yield connecting
client = yield conn._sftp
defer.returnValue(client)
@@ -480,7 +492,8 @@ class SFTPServer(DeferringFixture):
@defer.inlineCallbacks
def createFile(self, client, relpath, data):
remote_file = yield client.openFile(
- relpath, FXF_WRITE | FXF_CREAT | FXF_TRUNC | FXF_EXCL, {})
+ relpath, FXF_WRITE | FXF_CREAT | FXF_TRUNC | FXF_EXCL, {}
+ )
yield remote_file.writeChunk(0, data)
yield remote_file.close()
@@ -509,7 +522,7 @@ class TestPkgUploadServiceMakerMixin:
def setUp(self):
"""Set up txpkgupload in a temp dir."""
- super(TestPkgUploadServiceMakerMixin, self).setUp()
+ super().setUp()
root_dir = self.useFixture(TempDir()).path
self.server = self.server_factory(root_dir)
self.useFixture(self.server)
@@ -524,10 +537,13 @@ class TestPkgUploadServiceMakerMixin:
self.assertIsInstance(service, MultiService)
self.assertEqual(3, len(service.services))
self.assertSequenceEqual(
- ["ftp", "ready", "sftp"], sorted(service.namedServices))
+ ["ftp", "ready", "sftp"], sorted(service.namedServices)
+ )
self.assertEqual(
- len(service.namedServices), len(service.services),
- "Not all services are named.")
+ len(service.namedServices),
+ len(service.services),
+ "Not all services are named.",
+ )
def _uploadPath(self, path):
"""Return system path of specified path inside an upload.
@@ -543,12 +559,12 @@ class TestPkgUploadServiceMakerMixin:
# Creating directories on the server makes actual directories where we
# expect them, and creates them with g+rwxs
client = yield self.server.getClient()
- yield self.server.makeDirectory(client, 'foo/bar')
+ yield self.server.makeDirectory(client, "foo/bar")
yield self.server.disconnect(client)
yield self.server.waitForClose()
- wanted_path = self._uploadPath('foo/bar')
+ wanted_path = self._uploadPath("foo/bar")
self.assertTrue(os.path.exists(wanted_path))
self.assertEqual(os.stat(wanted_path).st_mode, 0o42775)
@@ -556,14 +572,14 @@ class TestPkgUploadServiceMakerMixin:
def test_rmdir(self):
"""Check recursive RMD (aka rmdir)"""
client = yield self.server.getClient()
- yield self.server.makeDirectory(client, 'foo/bar')
- yield client.removeDirectory('foo/bar')
- yield client.removeDirectory('foo')
+ yield self.server.makeDirectory(client, "foo/bar")
+ yield client.removeDirectory("foo/bar")
+ yield client.removeDirectory("foo")
yield self.server.disconnect(client)
yield self.server.waitForClose()
- wanted_path = self._uploadPath('foo')
+ wanted_path = self._uploadPath("foo")
self.assertFalse(os.path.exists(wanted_path))
@defer.inlineCallbacks
@@ -578,15 +594,21 @@ class TestPkgUploadServiceMakerMixin:
yield self.server.disconnect(client)
yield self.server.waitForClose()
- wanted_path = self._uploadPath('foo/bar/baz')
+ wanted_path = self._uploadPath("foo/bar/baz")
with open(os.path.join(wanted_path)) as f:
fs_content = f.read()
self.assertEqual(fs_content, "fake contents")
# Expected mode is -rw-rwSr--.
self.assertEqual(
os.stat(wanted_path).st_mode,
- stat.S_IROTH | stat.S_ISGID | stat.S_IRGRP | stat.S_IWGRP
- | stat.S_IWUSR | stat.S_IRUSR | stat.S_IFREG)
+ stat.S_IROTH
+ | stat.S_ISGID
+ | stat.S_IRGRP
+ | stat.S_IWGRP
+ | stat.S_IWUSR
+ | stat.S_IRUSR
+ | stat.S_IFREG,
+ )
@defer.inlineCallbacks
def test_full_source_upload(self):
@@ -595,40 +617,49 @@ class TestPkgUploadServiceMakerMixin:
"""
client = yield self.server.getClient()
- files = ['test-source_0.1.dsc',
- 'test-source_0.1.orig.tar.gz',
- 'test-source_0.1.diff.gz',
- 'test-source_0.1_source.changes']
+ files = [
+ "test-source_0.1.dsc",
+ "test-source_0.1.orig.tar.gz",
+ "test-source_0.1.diff.gz",
+ "test-source_0.1_source.changes",
+ ]
for upload in files:
file_to_upload = "~ppa-user/ppa/ubuntu/%s" % upload
yield self.server.createFile(
- client, file_to_upload, upload.encode("ASCII"))
+ client, file_to_upload, upload.encode("ASCII")
+ )
yield self.server.disconnect(client)
yield self.server.waitForClose()
- upload_path = self._uploadPath('')
+ upload_path = self._uploadPath("")
self.assertEqual(os.stat(upload_path).st_mode, 0o42770)
- dir_name = upload_path.split('/')[-2]
+ dir_name = upload_path.split("/")[-2]
if isinstance(self.server, SFTPServer):
- self.assertEqual(dir_name.startswith('upload-sftp-2'), True)
+ self.assertEqual(dir_name.startswith("upload-sftp-2"), True)
elif isinstance(self.server, FTPServer):
- self.assertEqual(dir_name.startswith('upload-ftp-2'), True)
+ self.assertEqual(dir_name.startswith("upload-ftp-2"), True)
else:
raise AssertionError(
- "self.server is neither SFTPServer or FTPServer")
+ "self.server is neither SFTPServer or FTPServer"
+ )
for upload in files:
- wanted_path = self._uploadPath(
- "~ppa-user/ppa/ubuntu/%s" % upload)
+ wanted_path = self._uploadPath("~ppa-user/ppa/ubuntu/%s" % upload)
with open(os.path.join(wanted_path)) as f:
fs_content = f.read()
self.assertEqual(fs_content, upload)
# Expected mode is -rw-rwSr--.
self.assertEqual(
os.stat(wanted_path).st_mode,
- stat.S_IROTH | stat.S_ISGID | stat.S_IRGRP | stat.S_IWGRP
- | stat.S_IWUSR | stat.S_IRUSR | stat.S_IFREG)
+ stat.S_IROTH
+ | stat.S_ISGID
+ | stat.S_IRGRP
+ | stat.S_IWGRP
+ | stat.S_IWUSR
+ | stat.S_IRUSR
+ | stat.S_IFREG,
+ )
@defer.inlineCallbacks
def test_upload_isolation(self):
@@ -663,15 +694,19 @@ class TestPkgUploadServiceMakerMixin:
yield self.server.waitForClose(4)
# Build a list of directories representing the 4 sessions.
- upload_dirs = [leaf for leaf in sorted(os.listdir(self.server.fsroot))
- if not leaf.startswith(".")]
+ upload_dirs = [
+ leaf
+ for leaf in sorted(os.listdir(self.server.fsroot))
+ if not leaf.startswith(".")
+ ]
self.assertEqual(len(upload_dirs), 4)
# Check the contents of files on each session.
- expected_contents = ['ONE', 'TWO', 'THREE', 'FOUR']
+ expected_contents = ["ONE", "TWO", "THREE", "FOUR"]
for index in range(4):
- with open(os.path.join(
- self.server.fsroot, upload_dirs[index], "test")) as f:
+ with open(
+ os.path.join(self.server.fsroot, upload_dirs[index], "test")
+ ) as f:
content = f.read()
self.assertEqual(content, expected_contents[index])
@@ -702,12 +737,12 @@ class TestPkgUploadServiceMakerFTP(TestPkgUploadServiceMakerMixin, TestCase):
"""
if client is None:
client = yield self.server.getClient()
- yield client.cwd('foo/bar')
+ yield client.cwd("foo/bar")
yield self.server.disconnect(client)
yield self.server.waitForClose()
- wanted_path = self._uploadPath('foo/bar')
+ wanted_path = self._uploadPath("foo/bar")
self.assertTrue(os.path.exists(wanted_path))
self.assertEqual(os.stat(wanted_path).st_mode, 0o42775)
diff --git a/src/txpkgupload/tests/test_twistedsftp.py b/src/txpkgupload/tests/test_twistedsftp.py
index a1b3093..fd819b7 100644
--- a/src/txpkgupload/tests/test_twistedsftp.py
+++ b/src/txpkgupload/tests/test_twistedsftp.py
@@ -3,27 +3,24 @@
"""Tests for twistedsftp."""
-__metaclass__ = type
import os
import fixtures
-from lazr.sshserver.sftp import FileIsADirectory
import six
import testtools
+from lazr.sshserver.sftp import FileIsADirectory
from txpkgupload.twistedsftp import SFTPServer
class MockAvatar:
-
def __init__(self, fs_root, temp_dir):
self.fs_root = fs_root
self.temp_dir = temp_dir
class TestSFTPServer(testtools.TestCase):
-
def setUp(self):
temp_dir = self.useFixture(fixtures.TempDir())
fs_root = temp_dir.join("incoming")
@@ -31,47 +28,55 @@ class TestSFTPServer(testtools.TestCase):
os.mkdir(fs_root)
os.mkdir(temp_incoming)
self.sftp_server = SFTPServer(MockAvatar(fs_root, temp_incoming))
- super(TestSFTPServer, self).setUp()
+ super().setUp()
def assertPermissions(self, expected, file_name):
observed = os.stat(file_name).st_mode
self.assertEqual(
- expected, observed, "Expected %07o, got %07o, for %s" % (
- expected, observed, file_name))
+ expected,
+ observed,
+ "Expected %07o, got %07o, for %s"
+ % (expected, observed, file_name),
+ )
def test_gotVersion(self):
# gotVersion always returns an empty dict, since the server does not
# support any extended features. See ISFTPServer.
extras = self.sftp_server.gotVersion(None, None)
- self.assertEquals(extras, {})
+ self.assertEqual(extras, {})
def test_mkdir_and_rmdir(self):
- self.sftp_server.makeDirectory('foo/bar', None)
+ self.sftp_server.makeDirectory("foo/bar", None)
self.assertEqual(
os.listdir(os.path.join(self.sftp_server._current_upload))[0],
- 'foo')
- dir_name = os.path.join(self.sftp_server._current_upload, 'foo')
- self.assertEqual(os.listdir(dir_name)[0], 'bar')
+ "foo",
+ )
+ dir_name = os.path.join(self.sftp_server._current_upload, "foo")
+ self.assertEqual(os.listdir(dir_name)[0], "bar")
self.assertPermissions(0o40775, dir_name)
- self.sftp_server.removeDirectory('foo/bar')
+ self.sftp_server.removeDirectory("foo/bar")
self.assertEqual(
- os.listdir(os.path.join(self.sftp_server._current_upload,
- 'foo')), [])
- self.sftp_server.removeDirectory('foo')
+ os.listdir(os.path.join(self.sftp_server._current_upload, "foo")),
+ [],
+ )
+ self.sftp_server.removeDirectory("foo")
self.assertEqual(
- os.listdir(os.path.join(self.sftp_server._current_upload)), [])
+ os.listdir(os.path.join(self.sftp_server._current_upload)), []
+ )
def test_file_creation(self):
- upload_file = self.sftp_server.openFile('foo/bar', None, None)
+ upload_file = self.sftp_server.openFile("foo/bar", None, None)
upload_file.writeChunk(0, b"This is a test")
- file_name = os.path.join(self.sftp_server._current_upload, 'foo/bar')
- with open(file_name, 'r') as test_file:
+ file_name = os.path.join(self.sftp_server._current_upload, "foo/bar")
+ with open(file_name) as test_file:
self.assertEqual(test_file.read(), "This is a test")
self.assertPermissions(0o100644, file_name)
- dir_name = os.path.join(self.sftp_server._current_upload, 'bar/foo')
+ dir_name = os.path.join(self.sftp_server._current_upload, "bar/foo")
os.makedirs(dir_name)
- upload_file = self.sftp_server.openFile('bar/foo', None, None)
+ upload_file = self.sftp_server.openFile("bar/foo", None, None)
err = self.assertRaises(
- FileIsADirectory, upload_file.writeChunk, 0, b"This is a test")
+ FileIsADirectory, upload_file.writeChunk, 0, b"This is a test"
+ )
self.assertEqual(
- "File is a directory: %r" % six.ensure_text(dir_name), str(err))
+ "File is a directory: %r" % six.ensure_text(dir_name), str(err)
+ )
diff --git a/src/txpkgupload/twistedftp.py b/src/txpkgupload/twistedftp.py
index a652553..2baa2a7 100644
--- a/src/txpkgupload/twistedftp.py
+++ b/src/txpkgupload/twistedftp.py
@@ -3,11 +3,10 @@
"""Twisted FTP implementation of the txpkgupload upload server."""
-__metaclass__ = type
__all__ = [
- 'AnonymousShell',
- 'FTPRealm',
- ]
+ "AnonymousShell",
+ "FTPRealm",
+]
import ipaddress
import os
@@ -15,23 +14,10 @@ import re
import tempfile
import six
-from twisted.application import (
- service,
- strports,
- )
-from twisted.cred import (
- checkers,
- credentials,
- )
-from twisted.cred.portal import (
- IRealm,
- Portal,
- )
-from twisted.internet import (
- defer,
- error,
- reactor,
- )
+from twisted.application import service, strports
+from twisted.cred import checkers, credentials
+from twisted.cred.portal import IRealm, Portal
+from twisted.internet import defer, error, reactor
from twisted.protocols import ftp
from twisted.python import filepath
from zope.interface import implementer
@@ -45,7 +31,9 @@ class AccessCheck:
"""An `ICredentialsChecker` for txpkgupload FTP sessions."""
credentialInterfaces = (
- credentials.IUsernamePassword, credentials.IAnonymous)
+ credentials.IUsernamePassword,
+ credentials.IAnonymous,
+ )
def requestAvatarId(self, credentials):
# txpkgupload allows any credentials. People can use "anonymous" if
@@ -63,13 +51,13 @@ class AnonymousShell(ftp.FTPShell):
def __init__(self, fsroot, temp_dir):
self._fs_root = fsroot
self.uploadfilesystem = UploadFileSystem(
- tempfile.mkdtemp(dir=temp_dir))
+ tempfile.mkdtemp(dir=temp_dir)
+ )
self._current_upload = self.uploadfilesystem.rootpath
- os.chmod(self._current_upload, 0o770)
- self.hook = Hooks(self._fs_root, perms='g+rws', prefix='-ftp')
+ os.chmod(self._current_upload, 0o770) # noqa: S103
+ self.hook = Hooks(self._fs_root, perms="g+rws", prefix="-ftp")
self.hook.new_client_hook(self._current_upload, 0, 0)
- super(AnonymousShell, self).__init__(
- filepath.FilePath(self._current_upload))
+ super().__init__(filepath.FilePath(self._current_upload))
def openForWriting(self, file_segments):
"""Write the uploaded file to disk, safely.
@@ -82,7 +70,7 @@ class AnonymousShell(ftp.FTPShell):
"""
filename = os.sep.join(file_segments)
self._create_missing_directories(filename)
- return super(AnonymousShell, self).openForWriting(file_segments)
+ return super().openForWriting(file_segments)
def makeDirectory(self, path):
"""Make a directory using the secure `UploadFileSystem`."""
@@ -94,7 +82,7 @@ class AnonymousShell(ftp.FTPShell):
if segments:
path = self._path(segments)
path.makedirs()
- return super(AnonymousShell, self).access(segments)
+ return super().access(segments)
def logout(self):
"""Called when the client disconnects.
@@ -106,10 +94,10 @@ class AnonymousShell(ftp.FTPShell):
def _create_missing_directories(self, filename):
# Same as SFTPServer
new_dir, new_file = os.path.split(
- self.uploadfilesystem._sanitize(filename))
- if new_dir != '':
- if not os.path.exists(
- os.path.join(self._current_upload, new_dir)):
+ self.uploadfilesystem._sanitize(filename)
+ )
+ if new_dir != "":
+ if not os.path.exists(os.path.join(self._current_upload, new_dir)):
self.uploadfilesystem.mkdir(new_dir)
def list(self, path_segments, attrs):
@@ -133,10 +121,14 @@ class FTPRealm:
for iface in interfaces:
if iface is ftp.IFTPShell:
avatar = AnonymousShell(self.root, self.temp_dir)
- return ftp.IFTPShell, avatar, getattr(
- avatar, 'logout', lambda: None)
+ return (
+ ftp.IFTPShell,
+ avatar,
+ getattr(avatar, "logout", lambda: None),
+ )
raise NotImplementedError(
- "Only IFTPShell interface is supported by this realm")
+ "Only IFTPShell interface is supported by this realm"
+ )
_AFNUM_IP = 1
@@ -166,7 +158,7 @@ def decodeExtendedAddressLine(line):
@return: a 3-tuple of (protocol, host, port).
"""
- match = re.search(r'\((.*)\)', line)
+ match = re.search(r"\((.*)\)", line)
if match:
return decodeExtendedAddress(match.group(1))
else:
@@ -174,7 +166,6 @@ def decodeExtendedAddressLine(line):
class FTPWithEPSV(ftp.FTP):
-
epsvAll = False
supportedNetworkProtocols = (_AFNUM_IP, _AFNUM_IP6)
portsInUse = set()
@@ -194,7 +185,7 @@ class FTPWithEPSV(ftp.FTP):
if port and port in self.portsInUse:
self.portsInUse.remove(port)
- def getDTPPort(self, factory, interface=''):
+ def getDTPPort(self, factory, interface=""):
"""
Return a port for passive access, using C{self.passivePortRange}
attribute.
@@ -203,16 +194,19 @@ class FTPWithEPSV(ftp.FTP):
if portn in self.portsInUse:
continue
try:
- dtpPort = self.listenFactory(portn, factory,
- interface=interface)
+ dtpPort = self.listenFactory(
+ portn, factory, interface=interface
+ )
except error.CannotListenError:
continue
else:
self.portsInUse.add(dtpPort.getHost().port)
return dtpPort
- raise error.CannotListenError('', portn,
- "No port available in range %s" %
- (self.passivePortRange,))
+ raise error.CannotListenError(
+ "",
+ portn,
+ "No port available in range %s" % (self.passivePortRange,),
+ )
def getHostAddress(self):
"""
@@ -245,7 +239,7 @@ class FTPWithEPSV(ftp.FTP):
# response in order that at least clients that ignore the
# host part can work, and if it becomes necessary then we
# could do that too.)
- return defer.fail(ftp.CmdNotImplementedError('PASV'))
+ return defer.fail(ftp.CmdNotImplementedError("PASV"))
return str(address.ipv4_mapped)
@@ -262,8 +256,9 @@ class FTPWithEPSV(ftp.FTP):
server is listening on.
"""
if self.epsvAll:
- return defer.fail(ftp.BadCmdSequenceError(
- 'may not send PASV after EPSV ALL'))
+ return defer.fail(
+ ftp.BadCmdSequenceError("may not send PASV after EPSV ALL")
+ )
host = self.getHostAddress()
@@ -301,12 +296,13 @@ class FTPWithEPSV(ftp.FTP):
raise ftp.CmdArgSyntaxError(protocol)
if protocol not in self.supportedNetworkProtocols:
raise UnsupportedNetworkProtocolError(
- ','.join(str(p) for p in self.supportedNetworkProtocols))
+ ",".join(str(p) for p in self.supportedNetworkProtocols)
+ )
- def ftp_EPSV(self, protocol=''):
- if protocol == 'ALL':
+ def ftp_EPSV(self, protocol=""):
+ if protocol == "ALL":
self.epsvAll = True
- self.sendLine('200 EPSV ALL OK')
+ self.sendLine("200 EPSV ALL OK")
return defer.succeed(None)
elif protocol:
try:
@@ -315,7 +311,8 @@ class FTPWithEPSV(ftp.FTP):
return defer.fail()
except UnsupportedNetworkProtocolError as e:
self.sendLine(
- '522 Network protocol not supported, use (%s)' % e.args)
+ "522 Network protocol not supported, use (%s)" % e.args
+ )
return defer.succeed(None)
# if we have a DTP port set up, lose it.
@@ -326,9 +323,9 @@ class FTPWithEPSV(ftp.FTP):
self.dtpFactory = ftp.DTPFactory(pi=self)
self.dtpFactory.setTimeout(self.dtpTimeout)
if not protocol or protocol == _AFNUM_IP6:
- interface = '::'
+ interface = "::"
else:
- interface = ''
+ interface = ""
self.dtpPort = self.getDTPPort(self.dtpFactory, interface=interface)
port = self.dtpPort.getHost().port
@@ -337,8 +334,9 @@ class FTPWithEPSV(ftp.FTP):
def ftp_PORT(self):
if self.epsvAll:
- return defer.fail(ftp.BadCmdSequenceError(
- 'may not send PORT after EPSV ALL'))
+ return defer.fail(
+ ftp.BadCmdSequenceError("may not send PORT after EPSV ALL")
+ )
return ftp.FTP.ftp_PORT(self)
def ftp_EPRT(self, extendedAddress):
@@ -354,8 +352,9 @@ class FTPWithEPSV(ftp.FTP):
transport addresses.
"""
if self.epsvAll:
- return defer.fail(ftp.BadCmdSequenceError(
- 'may not send EPRT after EPSV ALL'))
+ return defer.fail(
+ ftp.BadCmdSequenceError("may not send EPRT after EPSV ALL")
+ )
try:
protocol, ip, port = decodeExtendedAddress(extendedAddress)
@@ -368,7 +367,8 @@ class FTPWithEPSV(ftp.FTP):
return defer.fail()
except UnsupportedNetworkProtocolError as e:
self.sendLine(
- '522 Network protocol not supported, use (%s)' % e.args)
+ "522 Network protocol not supported, use (%s)" % e.args
+ )
return defer.succeed(None)
# if we have a DTP port set up, lose it.
@@ -376,7 +376,8 @@ class FTPWithEPSV(ftp.FTP):
self.cleanupDTP()
self.dtpFactory = ftp.DTPFactory(
- pi=self, peerHost=self.transport.getPeer().host)
+ pi=self, peerHost=self.transport.getPeer().host
+ )
self.dtpFactory.setTimeout(self.dtpTimeout)
self.dtpPort = reactor.connectTCP(ip, port, self.dtpFactory)
diff --git a/src/txpkgupload/twistedsftp.py b/src/txpkgupload/twistedsftp.py
index 0faec06..4e48a04 100644
--- a/src/txpkgupload/twistedsftp.py
+++ b/src/txpkgupload/twistedsftp.py
@@ -3,24 +3,17 @@
"""Twisted SFTP implementation of the txpkgupload upload server."""
-__metaclass__ = type
__all__ = [
- 'SFTPFile',
- 'SFTPServer',
- ]
+ "SFTPFile",
+ "SFTPServer",
+]
import errno
import os
import tempfile
-from lazr.sshserver.sftp import (
- FileIsADirectory,
- FileTransferServer,
- )
-from twisted.conch.interfaces import (
- ISFTPFile,
- ISFTPServer,
- )
+from lazr.sshserver.sftp import FileIsADirectory, FileTransferServer
+from twisted.conch.interfaces import ISFTPFile, ISFTPServer
from zope.interface import implementer
from txpkgupload.filesystem import UploadFileSystem
@@ -35,9 +28,10 @@ class SFTPServer:
self._avatar = avatar
self._fs_root = avatar.fs_root
self.uploadfilesystem = UploadFileSystem(
- tempfile.mkdtemp(dir=avatar.temp_dir))
+ tempfile.mkdtemp(dir=avatar.temp_dir)
+ )
self._current_upload = self.uploadfilesystem.rootpath
- os.chmod(self._current_upload, 0o770)
+ os.chmod(self._current_upload, 0o770) # noqa: S103
def gotVersion(self, other_version, ext_data):
return {}
@@ -85,20 +79,20 @@ class SFTPServer:
def _create_missing_directories(self, filename):
new_dir, new_file = os.path.split(
- self.uploadfilesystem._sanitize(filename))
- if new_dir != '':
- if not os.path.exists(
- os.path.join(self._current_upload, new_dir)):
+ self.uploadfilesystem._sanitize(filename)
+ )
+ if new_dir != "":
+ if not os.path.exists(os.path.join(self._current_upload, new_dir)):
self.uploadfilesystem.mkdir(new_dir)
def _translate_path(self, filename):
return self.uploadfilesystem._full(
- self.uploadfilesystem._sanitize(filename))
+ self.uploadfilesystem._sanitize(filename)
+ )
@implementer(ISFTPFile)
class SFTPFile:
-
def __init__(self, filename):
self.filename = filename
@@ -111,7 +105,8 @@ class SFTPFile:
def writeChunk(self, offset, data):
try:
chunk_file = os.open(
- self.filename, os.O_CREAT | os.O_WRONLY, 0o644)
+ self.filename, os.O_CREAT | os.O_WRONLY, 0o644
+ )
except OSError as e:
if e.errno != errno.EISDIR:
raise
@@ -128,7 +123,6 @@ class SFTPFile:
class SFTPDirectory:
-
def __iter__(self):
return self
@@ -142,10 +136,9 @@ class SFTPDirectory:
class PkgUploadFileTransferServer(FileTransferServer):
-
def __init__(self, data=None, avatar=None):
FileTransferServer.__init__(self, data=data, avatar=avatar)
- self.hook = Hooks(self.client._fs_root, perms='g+rws', prefix='-sftp')
+ self.hook = Hooks(self.client._fs_root, perms="g+rws", prefix="-sftp")
self.hook.new_client_hook(self.client._current_upload, 0, 0)
def connectionLost(self, reason):
diff --git a/tox.ini b/tox.ini
index cba4d41..2d5ab90 100644
--- a/tox.ini
+++ b/tox.ini
@@ -18,3 +18,12 @@ commands =
deps =
-rbootstrap-requirements.txt
-rrequirements.txt
+
+[testenv:lint]
+description =
+ run linters with pre-commit
+deps =
+ pre-commit
+skip_install = true
+commands =
+ pre-commit run -a
\ No newline at end of file