Mirroring Composer dependencies with Packeton
Tags: php, composer, tutorial, programming
Published: 2023-03-05 17:46:34

## Introduce
Packeton is an open-source private Composer repository based on Packagist.org and Satis, distributed under the MIT license. Back in 2018, there was no free alternative to the Private Packagist packagist.com with the necessary functionality, but the Satis functionality is very limited. I like the UI of the Packagist.org thought it would be cool to use it for my own private Packagist, so I forked it to make it available for everyone to reuse. The source code is available on [GitHub](https://github.com/vtsykun/packeton)

## Mirror dependencies

Mirroring dependencies can be useful for several scenarios:

- Speed-up downloading dependencies 
- Sharing access to a private Composer repository, such as Magento, with your team and customers.
- Preventing Dependency Confusion [attacks](https://blog.packagist.com/preventing-dependency-hijacking/).

Packeton will create a local copy of a remote repository, allowing you to download dependencies and metadata from the local copy instead of the remote composer repository.

### Installation

To get started with Packeton, you'll first need to install the application. You can do this using Docker by creating a `docker-compose.yml` file with the following configuration:

```yaml
version: '3.6'

services:
    packeton:
        image: packeton/packeton:latest
        container_name: packeton
        hostname: packeton
        environment:
            ADMIN_USER: admin
            ADMIN_PASSWORD: 123456 # Default password
            ADMIN_EMAIL: admin@example.com
            TRUSTED_PROXIES: 172.16.0.0/12
            # Default SQLite
            # DATABASE_URL: "mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"
        ports:
            - '127.0.0.1:8081:80'
        volumes:
            - .docker:/data
```

Run the following command to start the container: `docker-compose up -d` The application will then be accessible at `http://127.0.0.1:8081`.

Next, you may want to install an Nginx reverse proxy to make the application available at a custom domain name. Here's an example configuration for this:

```
server {
    listen *:443 http2;

    server_name packages.example.org;

    ssl_certificate /etc/nginx/ssl/example.org.crt;
    ssl_certificate_key /etc/nginx/ssl/example.org.key;
    ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4';
 
    ssl_protocols TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_session_cache  builtin:1000  shared:SSL:10m;
    ssl_session_timeout  5m;

    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_buffers 16 16k;
    gzip_http_version 1.1;
    gzip_min_length 2048;
    gzip_types text/css application/javascript text/javascript application/json;

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_pass http://127.0.0.1:8081/;
    }
}

```

Now you can login on `packages.example.org`

### Configure Composer proxy

To configure the Composer proxy, you need to add it on configuration level. For example, for a docker installation, this is a file `config.yaml` in a volume. For source code installation put config file with any name the directory `{project dir}/config/packages`.

```yaml
packeton:
    mirrors:
        orocrm:
            url: https://satis.oroinc.com/
            
            # SSH keys for create ZIP archive from Git source.
            git_ssh_keys:
                git@github.com:oroinc: '/var/www/.ssh/private_key'
#            logo: 'https://example.com/logo.png'
#            http_basic:
#                username: user
#                password: pass
    
            # Allow public access, default false
#            public_access: true
            # default false
#            sync_lazy: true
#            enable_dist_mirror: false
    
            # Additional restriction, but you can restrict it in UI
#            available_package_patterns:
#                - 'vend1/*'
#            available_packages:
#                - 'pack1/name1' # but you can restrict it in UI
            # JSON. auth.json to pass composer opts.
#            composer_auth: '{"auth.json..."}'
    
            # default auto.
            sync_interval: ~
            # Console info message
            info_cmd_message: ~
```

Now you will need to restart docker container to apply configuration. 

The proxy information now will be shown by url https://packages.example.com/proxies/orocrm
Synchronization of metadata is launched by cron, and in order to update the data after creating a proxy, you need to run the update through the console or through UI. If everything happened without errors, then you will see the result of the synchronization as below

![Job Result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bk6sk7y4fzk5a3fz56at.png)

To use this proxy you need to add this line to your composer.json.

```
  "repositories": [
    {
      "type": "composer",
      "url": "https://packages.example.org/mirror/orocrm"
    }
  ]
```

By default, this resource is available only with aurization, so you need enable auth for composer `composer config --global --auth http-basic.packages.example.org {username} {apitoken}` Where `apitoken` you can see on the profile page.

#### Dependencies Approval.

In Packeton by default dependencies are automatically enabled, when you run the first time `composer update`. In the example above, I used the repository https://satis.oroinc.com that has third-party dependencies. Example of the [application](https://github.com/oroinc/platform-application/blob/5.1.0-rc.2/composer.json#L15) that uses this repo.

The root file [packages.json](https://satis.oroinc.com/packages.json) does not contains restrictions by `available_package_patterns`. Depending on the satis settings, if `require-all` is enabled, the third party vendor can publish a package under any vendor name. For example `symfony/process`. So attacker may replace your dependencies, because if a package name is found in this custom package repository, only those versions are loaded at all. 

To select specific packages that you use in a project and remove those you don't need, you can turn on strict mode. Go to proxy settings and select the "Strict mode"

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eakg0r9l92aje8e9i6hs.png)

Then, using the Mass mirror action, you can enable and approve your packages. Put the composer info output as shown below.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kvregbfw8nuzohbtq6v.png)

Depending on the User-Agent the metadata is available in two formats for Composer 2 and 1 at once. However, the original repository only provide the metadata for Composer 1.


```json
{
    "providers-lazy-url": "/mirror/orocrm/pkg/%package%.json",
    "mirrors": [
        {
            "dist-url": "/mirror/orocrm/zipball/%package%/%version%/%reference%.%type%",
            "preferred": true
        }
    ],
    "metadata-url": "/mirror/orocrm/p2/%package%.json",
    "available-packages": [
        "oro/platform-enterprise",
        "oro/crm-enterprise",
        "oro/api-doc-bundle",
        "oro/crm-pro-ldap-bundle"
    ]
}
```

If User-Agent header is match with Composer 1, then all packages will be merge into one snapshot `includes` for spend up Composer 1 downloads.

When the User-Agent header matches to Composer1, Packeton will merge all packages into one snapshot `includes` to speed up Composer 1 downloads. Because Composer 1 is not able to perform parallel downloads, so by merging all packages into one snapshot, Packeton can reduce the number of requests Composer 1.


```json
{
    "includes": {
        "include-packeton/all$6150da03358e44ebc3b99713c643e98dc06a221e.json": {
            "sha1": "6150da03358e44ebc3b99713c643e98dc06a221e"
        }
    },
    "mirrors": [
        {
            "dist-url": "/mirror/orocrm/zipball/%package%/%version%/%reference%.%type%",
            "preferred": true
        }
    ]
}
```


## Links
- Packeton source code https://github.com/vtsykun/packeton
- Installation instruction https://docs.packeton.org/installation.html
- About Preventing Dependency Confusion with Composer https://blog.packagist.com/preventing-dependency-hijacking/