Skip to content

Commit

Permalink
Merge pull request #152 from vtsykun/feat/customer-page
Browse files Browse the repository at this point in the history
Multi host web-ui protection for agencies
  • Loading branch information
vtsykun authored Aug 22, 2023
2 parents bbf19a6 + 5e847fc commit 3bb3474
Show file tree
Hide file tree
Showing 7 changed files with 168 additions and 0 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ packeton:
- '/data/hdd1/composer'
# Default path to storage/(local cache for S3) of uploaded artifacts
artifact_storage: '%composer_home_dir%/artifact_storage'
web_protection:
## Multi host protection, disable web-ui if host !== app.example.com and ips != 127.0.0.1, 10.9.1.0/24
## But the repo metadata will be available for all hosts and ips.
repo_hosts: ['*', '!app.example.com']
allow_ips: '127.0.0.1, 10.9.1.0/24'
```

### Metadata format.
Expand Down
2 changes: 2 additions & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@
- [JWT Configuration](authentication-jwt.md)
- [LDAP Configuration](authentication-ldap.md)
- [S3 Storage Provider](usage/storage.md)
- [Custom landing page](usage/custom-page.md)
- [Security Monitoring](usage/security-monitoring.md)
- [OAuth2 Integrations](oauth2.md)
- [Pull Request review](pull-request-review.md)
- [GitHub Setup](oauth2/github-oauth.md)
- [GitHub AppBot](oauth2/githubapp.md)
- [GitLab Setup](oauth2/gitlab-integration.md)
- [Gitea Setup](oauth2/gitea.md)
- [Bitbucket Setup](oauth2/bitbucket.md)
Expand Down
18 changes: 18 additions & 0 deletions docs/dev/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,4 +105,22 @@ packeton:
sync_interval: 3600 # default auto.
info_cmd_message: "\n\u001b[37;44m#Слава\u001b[30;43mУкраїні!\u001b[0m\n\u001b[40;31m#Смерть\u001b[30;41mворогам\u001b[0m" # Info message

web_protection:
## Multi host protection, disable web-ui if host !== app.example.com and ips != 127.0.0.1, 10.9.1.0/24
## But the repo metadata will be available for all hosts and ips.
repo_hosts: ['*', '!app.example.com']
allow_ips: '127.0.0.1, 10.9.1.0/24'
status_code: 402
custom_page: > # Custom landing non-auth page. Path or HTML
<html>
<head><title>402 Payment Required</title></head>
<body>
<center><h1>402 Payment Required</h1></center>
<hr><center>nginx</center>
</body>
</html>
web_protection:
## Disable web-ui for host = repo.example.com
repo_hosts: ['repo.example.com']
```
40 changes: 40 additions & 0 deletions docs/usage/custom-page.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Custom landing page

If you are distributing packages to your customers,
you may want to create a separate domain for Composer metadata-only to hide
the default web interface and login page.

Add following lines to you configuration. `config.yaml or config/packages/*.yaml`

```yaml
packeton:
web_protection:
## Multi host protection, disable web-ui if host !== app.example.com and ips != 127.0.0.1, 10.9.1.0/24
## But the repo metadata will be available for all hosts and ips.
repo_hosts: ['*', '!app.example.com']
allow_ips: '127.0.0.1, 10.9.1.0/24'
status_code: 402
custom_page: > # Custom landing non-auth page. Path or HTML
<html>
<head><title>402 Payment Required</title></head>
<body>
<center><h1>402 Payment Required</h1></center>
<hr><center>nginx</center>
</body>
</html>
```
Where `custom_page` html content or path to html page.

Here all hosts will be hidden under this page (if ip is not match or host != app.example.com).

`app.example.com` - this is host for default Web-UI.

### Example 2

```yaml
web_protection:
repo_hosts: ['repo.example.com']
```

Here Web-UI will be hidden for `repo_hosts` host `repo.example.com`.
14 changes: 14 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,20 @@ public function getConfigTreeBuilder(): TreeBuilder
->end()
->booleanNode('anonymous_access')->defaultFalse()->end()
->booleanNode('anonymous_archive_access')->defaultFalse()->end()
->arrayNode('web_protection')
->addDefaultsIfNotSet()
->children()
->arrayNode('repo_hosts')
->example(['repo.packagist.com', '*', '!app.packagist.com'])
->scalarPrototype()->end()
->end()
->scalarNode('allow_ips')->end()
->scalarNode('custom_page')->end()
->integerNode('status_code')->end()
->scalarNode('content_type')->end()
->end()
->end()

->booleanNode('archive')
->defaultFalse()
->end()
Expand Down
1 change: 1 addition & 0 deletions src/DependencyInjection/PacketonExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->setParameter('packeton_artifact_paths', $config['artifacts']['allowed_paths'] ?? []);
$container->setParameter('packeton_artifact_storage', $config['artifacts']['artifact_storage'] ?? null);
$container->setParameter('packeton_artifact_types', $config['artifacts']['support_types'] ?? []);
$container->setParameter('packeton_web_protection', $config['web_protection'] ?? null);

$container->registerAttributeForAutoconfiguration(AsWorker::class, static function (ChildDefinition $definition, AsWorker $attribute) {
$attributes = get_object_vars($attribute);
Expand Down
87 changes: 87 additions & 0 deletions src/EventListener/ProtectHostListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

declare(strict_types=1);

namespace Packeton\EventListener;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\RequestEvent;

class ProtectHostListener
{
public static $allowedRoutes = [
'root_packages' => 1,
'root_providers' => 1,
'metadata_changes' => 1,
'root_package' => 1,
'root_package_v2' => 1,
'download_dist_package' => 1,
'track_download' => 1,
'track_download_batch' =>1,
'root_packages_slug' =>1,
'root_providers_slug' => 1,
'root_package_slug' => 1,
'root_package_v2_slug' => 1,
'mirror_root' => 1,
'mirror_metadata_v2' => 1,
'mirror_metadata_v1' => 1,
'mirror_zipball' => 1,
'mirror_provider_includes' => 1,
];

public function __construct(
#[Autowire(param: 'packeton_web_protection')]
protected ?array $protection = null
) {
}

#[AsEventListener('kernel.request', priority: 30)]
public function onKernelRequest(RequestEvent $event): void
{
if (empty($this->protection)) {
return;
}

$request = $event->getRequest();
$route = $request->attributes->get('_route');
if (isset(static::$allowedRoutes[$route])) {
return;
}

if ($protectedHosts = ($this->protection['repo_hosts'] ?? [])) {
$host = $request->getHost();
if (in_array($host, $protectedHosts, true) || (in_array('*', $protectedHosts, true) && !in_array('!'.$host, $protectedHosts, true))) {
$this->terminate($event);
}
}

if ($allowIps = ($this->protection['allow_ips'] ?? null)) {
$allowIps = array_map('trim', explode(',', $allowIps));
if (false === IpUtils::checkIp($request->getClientIp() ?? '', $allowIps)) {
$this->terminate($event);
}
}
}

private function terminate(RequestEvent $event): void
{
$route = $event->getRequest()->attributes->get('_route');

$response = new JsonResponse(['error' => 'Not Found'], 404);
if ($route === 'home' && ($customPage = $this->protection['custom_page'] ?? null)) {
$customPage = is_file($customPage) ? file_get_contents($customPage) : $customPage;
$response = new Response($customPage, $this->protection['status_code'] ?? 200);
if ($contentType = $this->protection['content_type'] ?? null) {
$response->headers->set('content-type', $contentType);
}

$event->getRequest()->attributes->set('_format', 'X-Debug');
}

$event->setResponse($response);
}
}

0 comments on commit 3bb3474

Please sign in to comment.