PEP 470 – 从 PyPI 中移除外部托管支持
- 作者:
- Donald Stufft <donald at stufft.io>
- BDFL-Delegate:
- Paul Moore <p.f.moore at gmail.com>
- Discussions-To:
- Distutils-SIG 列表
- 状态:
- 最终
- 类型:
- 流程
- 主题:
- 打包
- 创建:
- 2014-05-12
- 发布历史:
- 2014-05-14, 2014-06-05, 2014-10-03, 2014-10-13, 2015-08-26
- 替换:
- 438
- 决议:
- Distutils-SIG 消息
摘要
本 PEP 提出弃用和移除对在 PyPI 外部托管文件的支持,以及弃用和移除由 PEP 438 添加的功能,特别是用于对不同类型的链接进行分类的 rel 信息以及用于指示 API 版本的元标签。
理由
从历史上看,PyPI 没有托管文件或自动检索可安装文件的方法,而是专注于提供一个中央名称注册表,以防止命名冲突,并作为发现可使用项目的工具。随着时间的推移,setuptools 开始抓取这些面向人类的页面,以及从这些页面链接的页面,寻找它可以自动下载和安装的东西。最终这变成了“Simple”API,它使用类似的 URL 结构,但是它消除了所有无关的链接和信息,使 API 更有效率。此外,PyPI 增加了项目将发布文件直接上传到 PyPI 的功能,使 PyPI 除了索引之外还可以充当仓库。
这赋予了 PyPI 在 Python 生态系统中两个同样重要的角色:作为索引,以便轻松发现 Python 项目;作为中央仓库,以便轻松托管、下载和安装 Python 项目。由于 PyPI 的历史背景和它经历的有机增长,这两个角色之间的界限很模糊,这种模糊性给这两个角色的最终用户带来了困惑,这也反过来导致了尝试以不同方式使用 PyPI 的人之间的不满,最常见的情况是最终用户希望将 PyPI 用作仓库,而作者希望将 PyPI 仅仅用作索引。
这种困惑归结于项目的最终用户没有意识到项目是在 PyPI 上托管,还是依赖于外部服务。这通常表现为当外部服务出现故障,而 PyPI 没有出现故障时。人们会看到 PyPI 工作正常,其他项目也工作正常,但这个特定的项目却无法工作。他们往往没有意识到他们需要联系谁才能解决这个问题,也不知道他们的补救步骤是什么。
PEP 438 试图通过允许项目明确声明他们是否使用仓库功能来解决这个问题,如果他们没有使用仓库功能,它会让安装程序将它找到的链接分类为“内部”、“可验证的外部”或“不可验证的外部”。PEP 438 被接受并在 pip 1.4(于 2013 年 7 月 23 日发布)中实施,最终过渡在 pip 1.5(于 2014 年 1 月 2 日发布)中实施。
PEP 438 成功地促使更多人利用 PyPI 的仓库功能,考虑到为 PyPI 提供动力的全球 CDN 为很多人提供了加速,这无疑是一件好事,但是它通过为最终用户和作者引入了一个新的困惑和痛苦点来实现这一点。
通过迁移到使用显式多仓库,我们可以使这两个角色之间的界限更加明确,并移除当前处理不想将 PyPI 用作仓库的人所造成的“隐藏”的意外。
关键用户体验预期
- 轻松允许外部托管在系统、用户或虚拟环境级别进行适当配置时“正常工作”。
- 从用户体验中消除所有与令人困惑的“可验证的外部”和“不可验证的外部”区分相关的引用(在安装和发布包时)。
- PyPI 的仓库方面应该仅仅成为默认的包托管位置(即,大多数客户端工具在其默认配置中将其视为退出而不是加入)。除了这方面之外,在 PyPI 上托管不应该比托管自己的包仓库提供更好的用户体验。
- 在提供默认行为的同时,该行为应能够防范除国家级攻击者以外的大多数攻击者。
为什么需要额外的仓库?
两种常用的安装工具,pip 和 easy_install/setuptools,都支持搜索多个位置以查找满足安装要求的文件的概念,并且多年来一直如此。这意味着无需“逐步引入”新的标志或概念,并且从 PyPI 以外的仓库安装项目的解决方案将适用于最终用户的安装程序有多旧(在合理范围内)。这种概念不仅在 Python 工具中存在了一段时间,而且它是一个跨语言的概念,甚至扩展到操作系统级别,操作系统包工具几乎普遍使用多仓库支持,这使得人们很可能已经熟悉这个概念。
此外,多仓库方法是一个在允许希望包含在 PyPI 的索引部分但不想使用 PyPI 的仓库部分的项目之外的其他范围内有用的概念。这包括公司希望托管包含其内部包的仓库的地方,或者项目希望拥有多个发布“通道”的地方,例如 alpha、beta、候选发布和最终发布。这也可以用于托管无法上传到 PyPI 的文件的项目,例如多吉字节数据文件或,至少目前是,Linux Wheels。
为什么不使用 PEP 438 或类似方案?
虽然 pip 和 setuptools 多年来一直支持额外的搜索位置,但对 PEP 438 的支持仅从 pip 1.4 版本开始存在,并且尚未在 setuptools 中实现。 PEP 438 的设计确实意味着即使使用较旧的安装程序,用户仍然可以从不需要外部文件的项目中受益,但是对于需要外部文件的项目,用户仍然在不知情的情况下被提供可能不可靠或更糟的是不安全的下载文件。这个系统也是 Python 特有的,因为它源于 PyPI 的历史,这意味着大多数(如果不是全部)用户在尝试使用 Python 工具链之前,几乎肯定不了解这个概念。
此外,PEP 438 提出的分类系统在实践中已被证明对最终用户极其困惑,以至于本 PEP 的立场是目前的状况完全无法忍受。在这种系统中,用户通常会尝试安装一个项目,可能会收到错误消息(或者如果项目曾经上传过文件,但后来在没有移除旧文件的情况下切换,则可能不会收到错误消息),看到错误消息建议使用 --allow-external
,他们重新发出添加该标志的命令,最有可能收到另一个错误消息,看到这次错误消息建议也添加 --allow-unverified
,并且再次发出命令,第三次尝试,这次终于得到了他们想要安装的东西。
这种 UX 故障存在几个原因。
- 如果 pip 可以在 Simple API 上找到项目的任何文件,它会直接使用这些文件,而不是尝试找到更多文件。这通常是正确的做法,因为尝试找到更多文件会消除很大一部分 PEP 438 的好处。这意味着如果一个项目曾经上传过与用户请求的安装内容匹配的文件,那么无论它有多旧,都会使用该文件。
- PEP 438 隐含地假设大多数项目要么会将自己上传到 PyPI,要么会更新自己以直接链接到发布文件。虽然许多项目最终确实决定上传到 PyPI,但其中一些项目之所以这样做,仅仅是因为围绕 PEP 438 的 UX 太糟糕,以至于他们感觉被迫这样做。然而,更令人担忧的是,很少有项目选择直接安全地链接到文件,相反,它们仍然只链接到必须抓取才能找到实际文件的页面,因此使得安全的变体 (
--allow-external
) 基本上毫无用处。 - 即使作者希望直接链接到他们的文件,安全地做到这一点也不明显。它需要在 URL 的散列中包含一个 MD5 散列(出于历史原因)。如果他们没有包含它,那么他们的文件将被视为“未验证”。
- PEP 438 采取了以安全为中心的观点,禁止任何形式的针对未验证项目的全局加入。虽然这通常是一件好事,但它创建了非常冗长和重复的命令调用,例如
$ pip install --allow-external myproject --allow-unverified myproject myproject $ pip install --allow-all-external --allow-unverified myproject myproject
多仓库/索引支持
安装程序 SHOULD 实现或继续提供,指向安装程序多个 URL 位置的功能。用户如何指示他们希望使用额外的位置的具体机制留给每个单独的实现来决定。
此外,当使用多个仓库时,发现安装候选者的机制也由每个单独的实现决定,但是一旦配置,实现不应该阻止、警告或以其他方式对仓库的使用产生负面影响,仅仅因为它不是默认的仓库。
目前,pip 和 setuptools 都通过使用它能够从任何仓库中找到的最佳安装候选者来实现多仓库支持,本质上将其视为一个大型仓库。
安装程序 SHOULD 也实现一些机制来移除或禁用默认仓库的使用。如何实现这一点的具体细节由每个单独的实现决定。
安装程序还应实现一些机制来白名单和黑名单用户希望从特定存储库安装的项目。如何实现这一机制的具体细节取决于每个实现。
必须更新Python 包指南,其中包含一个部分详细说明了设置自己的存储库的选项,以便任何希望将来不在 PyPI 上托管的项目都可以参考该文档。这应该包括建议依赖于托管自己存储库的项目在其项目描述中记录如何安装其项目。
弃用和移除链接爬取
将向 PyPI 添加一种新的托管模式。这种托管模式将被称为pypi-only
,它将作为PEP 438已经提供的三种模式之外的补充,这三种模式分别是pypi-explicit
、pypi-scrape
、pypi-scrape-crawl
。这种新的托管模式将修改项目的简单 API 页面,使其仅列出直接托管在 PyPI 上的文件,并且不会链接到任何其他内容。
在接受本 PEP 以及添加pypi-only
模式后,所有新项目都将默认设置为 PyPI 唯一模式,并且将被锁定到此模式,无法更改此特定设置。
然后将向所有仅托管在 PyPI 上的项目发送电子邮件,通知他们在一个月后,他们的项目将自动转换为pypi-only
模式。在这些电子邮件发送后一个月,任何仍然仅托管在 PyPI 上的项目都将被永久设置为pypi-only
模式。
同时,将向依赖于 PyPI 外部托管的项目发送电子邮件。此电子邮件将警告这些项目,PyPI 上已弃用外部托管的文件,并且在该电子邮件发出后 3 个月内,将从安装程序 API 中删除所有外部链接。此电子邮件**必须**包含将项目转换为托管在 PyPI 上的说明,并且**必须**包含指向脚本或包的链接,这些脚本或包将使他们能够输入其 PyPI 凭据和包名,并自动下载和重新托管所有文件在 PyPI 上。此电子邮件**必须**还包含设置自己的索引页面的说明。此电子邮件还必须包含指向 PyPI 服务条款的链接,因为许多用户可能很久以前就注册了,并且可能不记得这些条款是什么。最后,此电子邮件还必须包含 PyPI 注册的链接列表,在这些链接中,我们能够检测到安装文件的位置。
在第一封电子邮件发出后的两个月,必须向仍然依赖于外部托管的任何项目发送另一封电子邮件。此电子邮件将包含第一封电子邮件中的所有相同信息,只是删除日期将是一个月后,而不是三个月后。
最后,一个月后,所有项目都将切换到pypi-only
模式,并且 PyPI 将被修改以删除外部链接的文件功能。
变更摘要
仓库端
- 根据PEP 438定义弃用并删除托管模式。
- 限制简单 API 仅列出存储库中包含的文件。
客户端
- 实现多个存储库支持。
- 实现一些机制来删除/禁用默认存储库。
- 弃用/删除PEP 438
影响
为了确定影响,我们查看了所有使用类似于 pip 和 setuptools 使用的 PyPI 搜索方法的项目,并搜索了 PyPI 上所有可用的文件,安全地从 PyPI 链接,不安全地从 PyPI 链接,最后是不安全地从 PyPI 外部链接。当在多个位置找到相同文件时,它被重复数据删除,并且仅在以下优先级基础上在一个位置进行计数:PyPI>安全地从 PyPI 外部>不安全地从 PyPI 外部。这为我们提供了最广泛的影响定义,这意味着此项目的任何单个文件可能不再默认可见,但是该文件可能是几年前的,或者它可能是二进制文件,而 PyPI 上有一个 sdist 文件。这意味着*实际*影响可能要小得多,但为了不错误计数,我们采用最广泛的定义。
在撰写本文时,PyPI 上托管了 65,232 个项目,其中 59 个项目依赖于安全托管在 PyPI 外部的外部文件,931 个项目依赖于不安全托管在 PyPI 外部的外部文件。这表明 1.5% 的项目将受到此更改的某种程度的影响,而 98.5% 的项目将继续像往常一样运行。此外,只有 5% 的受影响项目正在使用PEP 438提供的功能来安全地托管在 PyPI 外部,而 95% 的项目正在通过中间人攻击将用户暴露于远程代码执行中。
常见问题解答
由于 <X>,我无法在 PyPI 上托管我的项目,我该怎么办?
首先,你应该确定
我的用户使用这个 PEP 的体验比之前更糟糕,如何解释?
这个答案的一部分将是针对每个项目特定的,你需要向你的用户解释导致你决定在自己的存储库中托管,而不是使用他们在安装程序的默认存储库列表中已经有的存储库的原因。但是,这个答案的一部分还将解释,以前透明地包含外部链接的行为既是一种安全隐患(因为在大多数情况下,它允许 MITM 在最终用户机器上执行任意 Python 代码),也是一种可靠性问题,并且PEP 438试图通过使它们明确选择加入来解决这个问题,但是PEP 438带来了许多严重的可用性问题。 PEP 470代表了模型的简化,这种模型类似于 Linux 发行版中常见的模型。
切换到仓库结构会破坏我的工作流程,或者我的主机不允许这样做?
有很多廉价或免费的托管服务商乐于支持存储库所需的功能。特别是,你实际上不需要以不同的方式上传你的文件,只要你可以生成具有正确结构的托管服务,该托管服务指向你的文件实际所在的地址即可。许多这些托管服务提供使用共享域名进行免费的 HTTPS,并且可以从StartSSL获得免费的 HTTPS 证书,或者在不久的将来,可以从LetsEncrypt获得免费的 HTTPS 证书,或者可以从许多提供商那里以低廉的价格获得。
为什么不提供 <X>?
这里的答案将取决于
- 我们以前没有想到它,也没有人建议过。
- 我们对
没有足够的经验来为它设计一个合适的解决方案,并欢迎领域专家帮助我们提供它。 - 我们是一个开源项目,还没有人决定自愿设计和实现
。
总是欢迎提出建议其他功能的附加 PEP,但它们将需要有人有时间和专业知识才能准确地设计
如果我运行自己的仓库,为什么要在 PyPI 上注册?
PyPI 对 Python 生态系统执行两个关键功能。其中一个是作为 pip 或其他包管理器下载和安装的实际文件的中心存储库,而本 PEP 正是关注这个功能,并且如果你运行自己的存储库,你将替换这个功能。但是,它还提供了一个关于谁拥有什么名称的中心注册表,以防止命名冲突,可以将其视为 DNS,但针对的是 Python 包。除了确保按先到先得的方式分配名称外,它还为用户提供了一个中心位置,让他们可以搜索和发现新项目。因此,简单的答案是,你仍然应该在 PyPI 上注册你的项目,以避免命名冲突,并使人们能够轻松地发现你的项目。
被拒绝的提案
允许更容易发现外部托管的索引
本 PEP 的早期版本包括一个新功能,该功能添加到 PyPI 和安装程序中,允许项目作者在 PyPI 中输入一个 URL 列表,这些 URL 将指示安装程序忽略上传到 PyPI 的任何文件,而是返回一个错误,告诉最终用户这些额外的 URL,他们可以将其添加到他们的安装程序中以使安装正常工作。
该功能已从 PEP 的范围中移除,因为它证明很难开发出能够避免类似于PEP 438解决方案导致许多问题的 UX 问题的解决方案。如果需要,未来的 PEP 可以重新审视这个想法。
保留当前分类系统,但调整选项
本 PEP 拒绝了几个相关的提议,这些提议试图解决当前系统的一些可用性问题,但仍然保持PEP 438的基本要点。
这包括
- 默认允许安全地外部托管文件,但不允许不安全地托管。
- 默认情况下不允许安全地外部托管文件,只有全局标志才能启用它们,但不允许不安全地托管。
- 继续遵循PEP 438的建议路径,并删除不安全地外部托管的选项,但继续允许安全地外部托管的选项。
这些提议被拒绝,因为
- PEP 438中引入的分类系统是 PyPI 独有的概念,即使在 Python 包的背景下也无法通用地应用。添加其他概念会带来成本。
- 分类系统本身并不直观,要预先确定一个项目需要哪种链接分类,需要检查项目的
/simple/<project>/
页面,以及该页面可能链接到的任何 URL。 - 能够在外部托管,同时仍然被链接以进行自动发现,这在很大程度上是一种历史遗留问题,它带来了相当大的痛苦和复杂性,而回报却很少。
- 安装程序优化或清理用户界面的能力受到限制,因为需要进行隐式链接抓取,这需要进行调整。这扩展到
--allow-*
选项以及无法确定链接是否预期会失败。 - 当启用某个选项时,该机制使用非常宽泛的笔触,而 PEP 438 试图通过每个包选项来限制这一点。但是,一个存在了较长时间的项目可能在它的简单索引中列出了多个不同的 URL。至少其中一个不再受项目控制的情况并不少见。虽然未注册的域名大多数时候都相对无害,但 pip 会在每次发现阶段继续尝试从该域名安装。这意味着攻击者只需要查看依赖于不安全的外部 URL 的项目,并注册已过期的域名来攻击用户。
实施此 PEP,但不移除现有链接
这实质上是该 PEP 向后兼容的版本。它试图允许使用旧版客户端或未实现此 PEP 的客户端继续使用,就好像没有任何改变一样。此提案被拒绝,因为绝大多数这些情况都是对弃用功能的不安全使用。该 PEP 的观点是,默认情况下允许对最终用户执行不安全的操作,绝不是一个可接受的解决方案。
版权
本文档已置于公共领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0470.rst
最后修改时间:2023-09-09 17:39:29 GMT