PEP: 381 Title: Mirroring infrastructure for PyPI Version: $Revision$
Last-Modified: $Date$ Author: Tarek Ziadé <tarek@ziade.org>, Martin v.
Löwis <martin@v.loewis.de> Status: Withdrawn Type: Standards Track
Content-Type: text/x-rst Created: 21-March-2009 Python-Version: N.A.
Post-History:

Abstract
========

This PEP describes a mirroring infrastructure for PyPI.

PEP Withdrawal
==============

The main PyPI web service was moved behind the Fastly caching CDN in May
2013:
https://mail.python.org/pipermail/distutils-sig/2013-May/020848.html

Subsequently, this arrangement was formalised as an in-kind sponsorship
with the PSF, and the PSF has also taken on the task of risk management
in the event that that sponsorship arrangement were to ever cease.

The download statistics that were previously provided directly on PyPI,
are now published indirectly via Google Big Query:
https://packaging.python.org/guides/analyzing-pypi-package-downloads/

Accordingly, the mirroring proposal described in this PEP is no longer
required, and has been marked as Withdrawn.

Rationale
=========

PyPI is hosting over 6000 projects and is used on a daily basis by
people to build applications. Especially systems like `easy_install` and
`zc.buildout` make intensive usage of PyPI.

For people making intensive use of PyPI, it can act as a single point of
failure. People have started to set up some mirrors, both private and
public. Those mirrors are active mirrors, which means that they are
browsing PyPI to get synced.

In order to make the system more reliable, this PEP describes:

-   the mirror listing and registering at PyPI
-   the pages a public mirror should maintain. These pages will be used
    by PyPI, in order to get hit counts and the last modified date.
-   how a mirror should synchronize with PyPI
-   how a client can implement a fail-over mechanism

Mirror listing and registering
==============================

People that wants to mirror PyPI make a proposal on catalog-SIG. When a
mirror is proposed on the mailing list, it is manually added in a mirror
list in the PyPI application after it has been checked to be compliant
with the mirroring rules.

The mirror list is provided as a list of host names of the form

    X.pypi.python.org

The values of X are the sequence a,b,c,...,aa,ab,... a.pypi.python.org
is the master server; the mirrors start with b. A CNAME record
last.pypi.python.org points to the last host name. Mirror operators
should use a static address, and report planned changes to that address
in advance to distutils-sig.

The new mirror also appears at `http://pypi.python.org/mirrors` which is
a human-readable page that gives the list of mirrors. This page also
explains how to register a new mirror.

Statistics page :::::::::::::::

PyPI provides statistics on downloads at `/stats`. This page is
calculated daily by PyPI, by reading all mirrors' local stats and
summing them.

The stats are presented in daily or monthly files, under `/stats/days`
and `/stats/months`. Each file is a `bzip2` file with these formats:

-   YYYY-MM-DD.bz2 for daily files
-   YYYY-MM.bz2 for monthly files

Examples:

-   /stats/days/2008-11-06.bz2
-   /stats/days/2008-11-07.bz2
-   /stats/days/2008-11-08.bz2
-   /stats/months/2008-11.bz2
-   /stats/months/2008-10.bz2

Mirror Authenticity
===================

With a distributed mirroring system, clients may want to verify that the
mirrored copies are authentic. There are multiple threats to consider:

1.  the central index may get compromised
2.  the central index is assumed to be trusted, but the mirrors might be
    tampered.
3.  a man in the middle between the central index and the end user, or
    between a mirror and the end user might tamper with datagrams.

This specification only deals with the second threat. Some provisions
are made to detect man-in-the-middle attacks. To detect the first
attack, package authors need to sign their packages using PGP keys, so
that users verify that the package comes from the author they trust.

The central index provides a DSA key at the URL /serverkey, in the PEM
format as generated by "openssl dsa -pubout" (i.e. RFC 3280
SubjectPublicKeyInfo, with the algorithm 1.3.14.3.2.12). This URL must
*not* be mirrored, and clients must fetch the official serverkey from
PyPI directly, or use the copy that came with the PyPI client software.
Mirrors should still download the key, to detect a key rollover.

For each package, a mirrored signature is provided at
/serversig/<package>. This is the DSA signature of the parallel URL
/simple/<package>, in DER form, using SHA-1 with DSA (i.e. as a RFC 3279
Dsa-Sig-Value, created by algorithm 1.2.840.10040.4.3)

Clients using a mirror need to perform the following steps to verify a
package:

1.  download the /simple page, and compute its SHA-1 hash
2.  compute the DSA signature of that hash
3.  download the corresponding /serversig, and compare it
    (byte-for-byte) with the value computed in step 2.
4.  compute and verify (against the /simple page) the MD-5 hashes of all
    files they download from the mirror.

An implementation of the verification algorithm is available from
https://svn.python.org/packages/trunk/pypi/tools/verify.py

Verification is not needed when downloading from central index, and
should be avoided to reduce the computation overhead.

About once a year, the key will be replaced with a new one. Mirrors will
have to re-fetch all /serversig pages. Clients using mirrors need to
find a trusted copy of the new server key. One way to obtain one is to
download it from https://pypi.python.org/serverkey. To detect
man-in-the-middle attacks, clients need to verify the SSL server
certificate, which will be signed by the CACert authority.

Special pages a mirror needs to provide
=======================================

A mirror is a subset copy of PyPI, so it provides the same structure by
copying it.

-   simple: rest version of the package index
-   packages: packages, stored by Python version, and letters
-   serversig: signatures for the simple pages

It also needs to provide two specific elements:

-   last-modified
-   local-stats

Last modified date ::::::::::::::::::

CPAN uses a freshness date system where the mirror's last
synchronisation date is made available.

For PyPI, each mirror needs to maintain a URL with simple text content
that represents the last synchronisation date the mirror maintains.

The date is provided in GMT time, using the ISO 8601 format
\[\#iso8601\]\_. Each mirror will be responsible to maintain its last
modified date.

This page must be located at : `/last-modified` and must be a text/plain
page.

Local statistics ::::::::::::::::

Each mirror is responsible to count all the downloads that where done
via it. This is used by PyPI to sum up all downloads, to be able to
display the grand total.

These statistics are in CSV-like form, with a header in the first line.
It needs to obey PEP 305 \[\#pep305\]\_. Basically, it should be
readable by Python's `csv` module.

The fields in this file are:

-   package: the distutils id of the package.
-   filename: the filename that has been downloaded.
-   useragent: the User-Agent of the client that has downloaded the
    package.
-   count: the number of downloads.

The content will look like this::

    # package,filename,useragent,count
    zc.buildout,zc.buildout-1.6.0.tgz,MyAgent,142
    ...

The counting starts the day the mirror is launched, and there is one
file per day, compressed using the `bzip2` format. Each file is named
like the day. For example, `2008-11-06.bz2` is the file for the 6th of
November 2008.

They are then provided in a folder called `days`. For example:

-   /local-stats/days/2008-11-06.bz2
-   /local-stats/days/2008-11-07.bz2
-   /local-stats/days/2008-11-08.bz2

This page must be located at `/local-stats`.

How a mirror should synchronize with PyPI
=========================================

A mirroring protocol called `Simple Index` was described and implemented
by Martin v. Loewis and Jim Fulton, based on how `easy_install` works.
This section synthesizes it and gives a few relevant links, plus a small
part about `User-Agent`.

The mirroring protocol ::::::::::::::::::::::

Mirrors must reduce the amount of data transferred between the central
server and the mirror. To achieve that, they MUST use the changelog()
PyPI XML-RPC call, and only refetch the packages that have been changed
since the last time. For each package P, they MUST copy documents
/simple/P/ and /serversig/P. If a package is deleted on the central
server, they MUST delete the package and all associated files. To detect
modification of package files, they MAY cache the file's ETag, and MAY
request skipping it using the If-none-match header.

Each mirroring tool MUST identify itself using a descripte User-agent
header.

The pep381client package \[\#pep381client\]\_ provides an application
that respects this protocol to browse PyPI.

User-agent request header :::::::::::::::::::::::::

In order to be able to differentiate actions taken by clients over PyPI,
a specific user agent name should be provided by all mirroring software.

This is also true for all clients like:

-   zc.buildout \[\#zc.buildout\]\_.
-   setuptools \[\#setuptools\]\_.
-   pip \[\#pip\]\_.

XXX user agent registering mechanism at PyPI ?

How a client can use PyPI and its mirrors
:::::::::::::::::::::::::::::::::::::::::

Clients that are browsing PyPI should be able to use alternative
mirrors, by getting the list of the mirrors using
`last.pypi.python.org`.

Code example::

    >>> import socket
    >>> socket.gethostbyname_ex('last.pypi.python.org')[0]
    'h.pypi.python.org'

The clients so far that could use this mechanism:

-   setuptools
-   zc.buildout (through setuptools)
-   pip

Fail-over mechanism :::::::::::::::::::

Clients that are browsing PyPI should be able to use a fail-over
mechanism when PyPI or the used mirror is not responding.

It is up to the client to decide which mirror should be used, maybe by
looking at its geographical location and its responsiveness.

This PEP does not describe how this fail-over mechanism should work, but
it is strongly encouraged that the clients try to use the nearest
mirror.

The clients so far that could use this mechanism:

-   setuptools
-   zc.buildout (through setuptools)
-   pip

Extra package indexes :::::::::::::::::::::

It is obvious that some packages will not be uploaded to PyPI, whether
because they are private or whether because the project maintainer runs
his own server where people might get the project package. However, it
is strongly encouraged that a public package index follows PyPI and
Distutils protocols.

In other words, the `register` and `upload` command should be compatible
with any package index server out there.

Software that are compatible with PyPI and Distutils so far:

-   PloneSoftwareCenter \[\#psc\]\_ which is used to run plone.org
    products section.
-   EggBasket \[\#eggbasket\]\_.

**An extra package index is not a mirror of PyPI, but can have some
mirrors itself.**

Merging several indexes :::::::::::::::::::::::

When a client needs to get some packages from several distinct indexes,
it should be able to use each one of them as a potential source of
packages. Different indexes should be defined as a sorted list for the
client to look for a package.

Each independent index can of course provide a list of its mirrors.

XXX define how to get the hostname for the mirrors of an arbitrary
index.

That permits all combinations at client level, for a reliable packaging
system with all levels of privacy.

It is up the client to deal with the merging.

References
==========

.. \[\#pep305\] http://www.python.org/dev/peps/pep-0305/\#id19

.. \[\#pep381client\] http://pypi.python.org/pypi/pep381client

.. \[\#iso8601\] http://en.wikipedia.org/wiki/ISO\_8601

.. \[\#zc.buildout\] http://pypi.python.org/pypi/zc.buildout

.. \[\#setuptools\] http://pypi.python.org/pypi/setuptools

.. \[\#pip\] http://pypi.python.org/pypi/pip

.. \[\#psc\] http://plone.org/products/plonesoftwarecenter

.. \[\#eggbasket\] http://www.chrisarndt.de/projects/eggbasket

Acknowledgments
===============

Georg Brandl.

Copyright
=========

This document has been placed in the public domain.

 .. Local Variables: mode: indented-text indent-tabs-mode: nil
sentence-end-double-space: t fill-column: 70 coding: utf-8 End:
