diff --git a/docs/reverse-proxy.md b/docs/reverse-proxy.md index 803f591a..b4de46fd 100644 --- a/docs/reverse-proxy.md +++ b/docs/reverse-proxy.md @@ -82,6 +82,34 @@ server { } ``` +#### Nginx with a sub-path + +In case you already have a site, and you want Packeton to share the domain name, +you can setup Nginx to serve Packeton under a sub-path by adding the following +server section into the http section of nginx.conf: + +``` +server { + .... + location ~ ^/packeton(/?)(.*) { + resolver 1.1.1.1 valid=30s; + set $upstream_pkg pack4.example.com; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Prefix /packeton; + proxy_set_header X-Forwarded-Host portal.example.com; + + proxy_pass https://$upstream_pkg/$2$is_args$args; + } +} +``` + +Where `X-Forwarded-Host` real host and `X-Forwarded-Prefix` site prefix. + +Then you MUST set something like `TRUSTED_PROXIES=172.16.0.0/12,127.0.0.1` correctly in your .env vars. 172.16.0.0/12 - IPs of proxy servers + #### Apache ``` diff --git a/public/packeton/js/view.js b/public/packeton/js/view.js index 5fab44d7..f56d9e2b 100644 --- a/public/packeton/js/view.js +++ b/public/packeton/js/view.js @@ -127,22 +127,26 @@ if (submit.is('.loading')) { return; } - data = $('.package .force-update').serializeArray(); + + let $buttonUpdate = $('.package .force-update'); + let jobUrl = $buttonUpdate.attr('data-job-url'); + + data = $buttonUpdate.serializeArray(); if (updateAll) { data.push({name: 'updateAll', value: '1'}); } $.ajax({ - url: $('.package .force-update').attr('action'), + url: $buttonUpdate.attr('action'), dataType: 'json', cache: false, data: data, - type: $('.package .force-update').attr('method'), + type: $buttonUpdate.attr('method'), success: function (data) { if (data.job) { let checkJobStatus = function () { $.ajax({ - url: '/jobs/' + data.job, + url: jobUrl.replace('fffffaafaaffff', data.job), cache: false, success: function (data) { if (data.status == 'completed' || data.status == 'errored' || data.status == 'failed' || data.status == 'package_deleted') { diff --git a/src/Composer/Cache/MetadataCache.php b/src/Composer/Cache/MetadataCache.php index 5af8fd83..5af33cde 100644 --- a/src/Composer/Cache/MetadataCache.php +++ b/src/Composer/Cache/MetadataCache.php @@ -5,6 +5,7 @@ namespace Packeton\Composer\Cache; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Routing\RequestContext; use Symfony\Contracts\Cache\CacheInterface; class MetadataCache @@ -12,6 +13,7 @@ class MetadataCache public function __construct( private readonly RequestStack $requestStack, private readonly CacheInterface $packagesCachePool, + private readonly RequestContext $requestContext, private readonly int $maxTtl = 1800 // TTL default / 2 ) { } @@ -22,7 +24,7 @@ public function get(string $key, callable $callback, ?int $lastModify = null, ?c // But for will protection must be used trusted_hosts $httpKey = $this->requestStack->getMainRequest()?->getSchemeAndHttpHost(); - $cacheKey = sha1($key . $httpKey); + $cacheKey = sha1($key . $httpKey . $this->requestContext->getBaseUrl()); $item = $this->packagesCachePool->getItem($cacheKey); @[$ctime, $data] = $item->get(); diff --git a/src/Controller/OAuth/OAuthController.php b/src/Controller/OAuth/OAuthController.php index 7dccfa86..6cba6c49 100644 --- a/src/Controller/OAuth/OAuthController.php +++ b/src/Controller/OAuth/OAuthController.php @@ -30,7 +30,7 @@ public function __construct( public function oauthLogin(string $alias, Request $request): Response { if ($this->getUser()) { - return $this->redirect('/'); + return $this->redirectToRoute('home'); } $this->state->set('controller', 'oauth_check'); diff --git a/src/Controller/SecurityController.php b/src/Controller/SecurityController.php index ec9707dc..6d79f7a4 100644 --- a/src/Controller/SecurityController.php +++ b/src/Controller/SecurityController.php @@ -35,7 +35,7 @@ public function loginAction(AuthenticationUtils $authenticationUtils, Integratio $error = $authenticationUtils->getLastAuthenticationError(); $lastUsername = $authenticationUtils->getLastUsername(); if ($this->getUser()) { - return $this->redirect('/'); + return $this->redirectToRoute('home'); } return $this->render('user/login.html.twig', [ diff --git a/src/Controller/SubRepositoryController.php b/src/Controller/SubRepositoryController.php index 205036b2..787f3f61 100644 --- a/src/Controller/SubRepositoryController.php +++ b/src/Controller/SubRepositoryController.php @@ -67,14 +67,16 @@ public function deleteAction(Request $request, #[Vars] SubRepository $entity): R public function switchAction(Request $request, #[Vars] SubRepository $entity): Response { $request->getSession()->set('_sub_repo', $entity->getId()); - return new RedirectResponse('/'); + + return $this->redirectToRoute('home'); } #[Route('/switch-root', name: 'subrepository_switch_root')] public function switchRoot(Request $request): Response { $request->getSession()->remove('_sub_repo'); - return new RedirectResponse('/'); + + return $this->redirectToRoute('home'); } protected function handleUpdate(Request $request, SubRepository $group, string $flashMessage): Response diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index c135cfdd..b0f29d57 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -367,7 +367,8 @@ public function addSSHKeyAction(Request $request, #[Vars] ?SshCredentials $key = $em->flush(); $this->addFlash('success', $key ? 'Ssh key updated successfully.' : 'Ssh key added successfully.'); - return new RedirectResponse('/'); + + return $this->redirectToRoute('home'); } } @@ -401,7 +402,8 @@ public function deleteSSHKeyAction(Request $request, #[Vars] SshCredentials $key $em = $this->registry->getManager(); $em->remove($key); $em->flush(); - return new RedirectResponse('/'); + + return $this->redirectToRoute('home'); } #[Route('/users/{name}', name: 'user_profile')] diff --git a/src/EventListener/PackagistListener.php b/src/EventListener/PackagistListener.php index 6ab3dd52..380de392 100644 --- a/src/EventListener/PackagistListener.php +++ b/src/EventListener/PackagistListener.php @@ -19,14 +19,18 @@ use Packeton\Model\ProviderManager; use Packeton\Service\DistConfig; use Packeton\Service\SubRepositoryHelper; +use Packeton\Trait\RequestContextTrait; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\Routing\RequestContext; #[AsEventListener(event: 'formHandler')] #[AsDoctrineListener(event: 'onFlush')] #[AsEntityListener(event: 'postLoad', entity: 'Packeton\Entity\Version')] class PackagistListener { + use RequestContextTrait; + private static $trackLastModifyClasses = [ GroupAclPermission::class => true, Group::class => true, @@ -40,6 +44,7 @@ public function __construct( private readonly RequestStack $requestStack, private readonly ProviderManager $providerManager, private readonly SubRepositoryHelper $subRepositoryHelper, + private readonly RequestContext $requestContext, ) { } @@ -59,7 +64,7 @@ public function postLoad(Version $version, PostLoadEventArgs $event) if (isset($dist['url']) && \str_starts_with($dist['url'], DistConfig::HOSTNAME_PLACEHOLDER)) { $currentHost = $request->getSchemeAndHttpHost(); $slug = $this->subRepositoryHelper->getCurrentSlug(); - $replacement = null !== $slug ? $currentHost . '/' . $slug : $currentHost; + $replacement = rtrim($currentHost . $this->generateUrl('/') . $slug . '/', '/'); $dist['url'] = \str_replace(DistConfig::HOSTNAME_PLACEHOLDER, $replacement, $dist['url']); $version->distNormalized = $dist; diff --git a/src/Integrations/Security/OAuth2Authenticator.php b/src/Integrations/Security/OAuth2Authenticator.php index 93784664..1ee769d4 100644 --- a/src/Integrations/Security/OAuth2Authenticator.php +++ b/src/Integrations/Security/OAuth2Authenticator.php @@ -8,10 +8,12 @@ use Packeton\Entity\User; use Packeton\Integrations\IntegrationRegistry; use Packeton\Integrations\LoginInterface; +use Packeton\Trait\RequestContextTrait; use Psr\Log\LoggerInterface; use Symfony\Component\HttpClient\Exception\ClientException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\RequestContext; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -24,10 +26,13 @@ class OAuth2Authenticator implements InteractiveAuthenticatorInterface { + use RequestContextTrait; + public function __construct( protected IntegrationRegistry $integrations, protected ManagerRegistry $registry, - protected LoggerInterface $logger + protected LoggerInterface $logger, + protected RequestContext $requestContext, ) { } @@ -157,6 +162,9 @@ public function isInteractive(): bool // Used to remove referral header. protected function getJSRedirectTemplate($route): string { + $path = $this->generateUrl('/packeton/js/redirect.js'); + $route = $this->generateUrl($route); + $text = << @@ -167,7 +175,7 @@ protected function getJSRedirectTemplate($route): string

Processing redirect $route

- + TXT; diff --git a/src/Package/InMemoryDumper.php b/src/Package/InMemoryDumper.php index c3ca16d2..f6de258a 100644 --- a/src/Package/InMemoryDumper.php +++ b/src/Package/InMemoryDumper.php @@ -17,11 +17,15 @@ use Packeton\Security\Acl\PackagesAclChecker; use Packeton\Service\DistConfig; use Packeton\Service\SubRepositoryHelper; +use Packeton\Trait\RequestContextTrait; +use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\User\UserInterface; class InMemoryDumper { + use RequestContextTrait; + private MetadataFormat $metadataFormat; private ?string $infoMessage; @@ -31,6 +35,7 @@ public function __construct( private readonly RouterInterface $router, private readonly SubRepositoryHelper $subRepositoryHelper, private readonly DistConfig $distConfig, + private readonly RequestContext $requestContext, ?array $config = null, ) { $this->infoMessage = $config['info_cmd_message'] ?? null; @@ -97,21 +102,23 @@ private function dumpRootPackages(?UserInterface $user = null, ?int $apiVersion [$providers, $packagesData, $availablePackages] = $this->dumpUserPackages($user, $apiVersion, $subRepo); $rootFile = ['packages' => []]; - $url = $this->router->generate('track_download', ['name' => 'VND/PKG']); + $slug = $subRepo && !$this->subRepositoryHelper->isAutoHost() ? '/'. $subRepo->getSlug() : ''; + $slugName = '' === $slug ? null : $subRepo->getSlug(); + $url = $this->generateRoute('track_download', $slugName, ['name' => 'VND/PKG']); - $rootFile['notify'] = $slug . str_replace('VND/PKG', '%package%', $url); - $rootFile['notify-batch'] = $slug . $this->router->generate('track_download_batch'); + $rootFile['notify'] = str_replace('VND/PKG', '%package%', $url); + $rootFile['notify-batch'] = $this->generateRoute('track_download_batch', $slugName); $rootFile['metadata-changes-url'] = $this->router->generate('metadata_changes'); - $rootFile['providers-url'] = $slug . '/p/%package%$%hash%.json'; + $rootFile['providers-url'] = $this->generateUrl($slug . '/p/%package%$%hash%.json'); if ($this->distConfig->mirrorEnabled()) { $ref = '0000000000000000000000000000000000000000.zip'; - $zipball = $slug . $this->router->generate('download_dist_package', ['package' => 'VND/PKG', 'hash' => $ref]); + $zipball = $this->generateRoute('download_dist_package', $slugName, ['package' => 'VND/PKG', 'hash' => $ref]); $rootFile['mirrors'][] = ['dist-url' => \str_replace(['VND/PKG', $ref], ['%package%', '%reference%.%type%'], $zipball), 'preferred' => true]; } - $rootFile['metadata-url'] = $slug . '/p2/%package%.json'; + $rootFile['metadata-url'] = $this->generateUrl($slug . '/p2/%package%.json'); if ($subRepo) { $rootFile['_comment'] = "Subrepository {$subRepo->getSlug()}"; } @@ -129,7 +136,7 @@ private function dumpRootPackages(?UserInterface $user = null, ?int $apiVersion if ($this->metadataFormat->lazyProviders($apiVersion)) { unset($rootFile['provider-includes'], $rootFile['providers-url']); - $rootFile['providers-lazy-url'] = $slug . '/p/%package%.json'; + $rootFile['providers-lazy-url'] = $this->generateUrl($slug . '/p/%package%.json'); } if (false === $this->metadataFormat->metadataUrl($apiVersion)) { diff --git a/src/Trait/RequestContextTrait.php b/src/Trait/RequestContextTrait.php new file mode 100644 index 00000000..67004b19 --- /dev/null +++ b/src/Trait/RequestContextTrait.php @@ -0,0 +1,20 @@ +requestContext->getBaseUrl(), '/') . $path; + } + + private function generateRoute(string $name, ?string $slugName, array $params = []): string + { + return $slugName === null ? + $this->router->generate($name, $params) + : $this->router->generate($name . '_slug', $params + ['slug' => $slugName]); + } +} diff --git a/templates/package/package.html.twig b/templates/package/package.html.twig index cbdefd7b..d8a13a30 100644 --- a/templates/package/package.html.twig +++ b/templates/package/package.html.twig @@ -80,8 +80,9 @@
{% for version in package.versions %} {%- if (version.dist) and (version.type != 'metapackage') -%} + {% set distURL = version.distNormalized['url'] ?? version.dist['url']|replace({'__host_unset__': ''}) %} - + {{ version.version }} {%- else -%} diff --git a/templates/package/viewPackage.html.twig b/templates/package/viewPackage.html.twig index 4b84b98c..9b0e6c64 100644 --- a/templates/package/viewPackage.html.twig +++ b/templates/package/viewPackage.html.twig @@ -119,7 +119,7 @@ {% endif %} {% if package.updatable and (is_granted('ROLE_UPDATE_PACKAGES') or package.maintainers.contains(app.user)) %} -
+