Admin Router Access
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MageSail\Magesail\Model;
|
||||
|
||||
use Magento\Framework\App\DeploymentConfig;
|
||||
use Magento\Framework\App\Request\Http;
|
||||
|
||||
/**
|
||||
* Detects Magento admin URL path so tunnel store scope is not applied to backend requests.
|
||||
*/
|
||||
class AdminPathDetector
|
||||
{
|
||||
public function __construct(
|
||||
private readonly DeploymentConfig $deploymentConfig
|
||||
) {
|
||||
}
|
||||
|
||||
public function getConfiguredAdminFrontName(): string
|
||||
{
|
||||
return (string) $this->deploymentConfig->get('backend/frontName', 'admin');
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the request path contains the configured admin front name as a segment (any position),
|
||||
* after skipping empty, index.php, and pub segments.
|
||||
*/
|
||||
public function isAdminPath(Http $request): bool
|
||||
{
|
||||
return $this->getAdminFrontNameIfPresentInPath($request) !== null;
|
||||
}
|
||||
|
||||
public function getAdminFrontNameIfPresentInPath(Http $request): ?string
|
||||
{
|
||||
$path = trim((string) $request->getPathInfo(), '/');
|
||||
if ($path === '') {
|
||||
$uriPath = parse_url((string) $request->getRequestUri(), PHP_URL_PATH);
|
||||
$path = trim((string) $uriPath, '/');
|
||||
}
|
||||
if ($path === '') {
|
||||
return null;
|
||||
}
|
||||
$adminFront = $this->getConfiguredAdminFrontName();
|
||||
foreach (explode('/', $path) as $segment) {
|
||||
$lower = strtolower($segment);
|
||||
if ($lower === '' || $lower === 'index.php' || $lower === 'pub') {
|
||||
continue;
|
||||
}
|
||||
if (strcasecmp($segment, $adminFront) === 0) {
|
||||
return $adminFront;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MageSail\Magesail\Model;
|
||||
|
||||
use Magento\Framework\App\Request\Http;
|
||||
|
||||
/**
|
||||
* Detects MageSail tunnel proxy (header / query / server) on the current request.
|
||||
*/
|
||||
class TunnelRequestDetector
|
||||
{
|
||||
private const HEADER_STORE_CODE = 'X-MageSail-Store-Code';
|
||||
private const QUERY_PARAM_STORE = 'magesail_store';
|
||||
private const SERVER_HEADER = 'HTTP_X_MAGESAIL_STORE_CODE';
|
||||
|
||||
public function getTunnelStoreCode(Http $request): ?string
|
||||
{
|
||||
$value = $request->getParam(self::QUERY_PARAM_STORE);
|
||||
if ($value !== null && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
$value = $request->getHeader(self::HEADER_STORE_CODE);
|
||||
if ($value !== false && $value !== null && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
$value = $request->getServer(self::SERVER_HEADER);
|
||||
if ($value !== null && $value !== '' && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
$uri = (string) $request->getRequestUri();
|
||||
if ($uri !== '' && str_contains($uri, self::QUERY_PARAM_STORE . '=')) {
|
||||
$query = str_contains($uri, '?') ? substr($uri, strpos($uri, '?') + 1) : '';
|
||||
parse_str($query, $params);
|
||||
$v = $params[self::QUERY_PARAM_STORE] ?? '';
|
||||
if ($v !== '' && trim((string) $v) !== '') {
|
||||
return trim((string) $v);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MageSail\Magesail\Plugin\App\Request;
|
||||
|
||||
use Magento\Framework\App\Request\Http;
|
||||
use MageSail\Magesail\Model\AdminPathDetector;
|
||||
|
||||
/**
|
||||
* {@see Http::getFrontName()} only uses the first path segment. With store code in URL, paths look like
|
||||
* /magesail_tunnel/admin_xxx/… so Magento picks the wrong area. If any path segment is the configured admin
|
||||
* front name, use it as the area front name.
|
||||
*/
|
||||
class AdminFrontNameFromPathPlugin
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminPathDetector $adminPathDetector
|
||||
) {
|
||||
}
|
||||
|
||||
public function aroundGetFrontName(Http $subject, callable $proceed): ?string
|
||||
{
|
||||
$adminFront = $this->adminPathDetector->getConfiguredAdminFrontName();
|
||||
if ($adminFront === '') {
|
||||
return $proceed();
|
||||
}
|
||||
$path = trim($subject->getPathInfo(), '/');
|
||||
if ($path === '') {
|
||||
return $proceed();
|
||||
}
|
||||
foreach (explode('/', $path) as $part) {
|
||||
$p = strtolower($part);
|
||||
if ($p === '' || $p === 'index.php' || $p === 'pub') {
|
||||
continue;
|
||||
}
|
||||
if (strcasecmp($part, $adminFront) === 0) {
|
||||
return $adminFront;
|
||||
}
|
||||
}
|
||||
return $proceed();
|
||||
}
|
||||
}
|
||||
@@ -4,16 +4,26 @@ declare(strict_types=1);
|
||||
namespace MageSail\Magesail\Plugin\App\Request;
|
||||
|
||||
use Magento\Framework\App\Request\Http;
|
||||
use Magento\Store\Model\ScopeInterface;
|
||||
use Magento\Store\Model\StoreManager;
|
||||
use MageSail\Magesail\Model\AdminPathDetector;
|
||||
use MageSail\Magesail\Model\TunnelRequestDetector;
|
||||
|
||||
/**
|
||||
* When request has X-MageSail-Store-Code header or magesail_store query param (from tunnel proxy),
|
||||
* make getServerValue return it for MAGE_RUN_CODE and 'store' for MAGE_RUN_TYPE so Magento serves that store.
|
||||
*
|
||||
* Admin URLs must not use nginx tunnel map values: StoreResolver is built from MAGE_RUN_* init params; if they
|
||||
* stay as the tunnel store, admin can 404. For admin paths we force website scope and empty run code so the
|
||||
* default website is used (same as a normal request without per-host map).
|
||||
*/
|
||||
class StoreFromMageSailHeaderPlugin
|
||||
{
|
||||
private const HEADER_STORE_CODE = 'X-MageSail-Store-Code';
|
||||
private const QUERY_PARAM_STORE = 'magesail_store';
|
||||
public function __construct(
|
||||
private readonly AdminPathDetector $adminPathDetector,
|
||||
private readonly TunnelRequestDetector $tunnelRequestDetector
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Around getServerValue: if MageSail store header is set, return it for MAGE_RUN_CODE / MAGE_RUN_TYPE.
|
||||
@@ -26,7 +36,16 @@ class StoreFromMageSailHeaderPlugin
|
||||
*/
|
||||
public function aroundGetServerValue(Http $subject, callable $proceed, $name = null, $default = null)
|
||||
{
|
||||
$storeCode = $this->getStoreCodeFromRequest($subject);
|
||||
if ($this->adminPathDetector->isAdminPath($subject)) {
|
||||
if ($name === StoreManager::PARAM_RUN_TYPE) {
|
||||
return ScopeInterface::SCOPE_WEBSITE;
|
||||
}
|
||||
if ($name === StoreManager::PARAM_RUN_CODE) {
|
||||
return '';
|
||||
}
|
||||
return $proceed($name, $default);
|
||||
}
|
||||
$storeCode = $this->tunnelRequestDetector->getTunnelStoreCode($subject);
|
||||
if ($storeCode === null) {
|
||||
return $proceed($name, $default);
|
||||
}
|
||||
@@ -38,21 +57,4 @@ class StoreFromMageSailHeaderPlugin
|
||||
}
|
||||
return $proceed($name, $default);
|
||||
}
|
||||
|
||||
private function getStoreCodeFromRequest(Http $request): ?string
|
||||
{
|
||||
$value = $request->getParam(self::QUERY_PARAM_STORE);
|
||||
if ($value !== null && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
$value = $request->getHeader(self::HEADER_STORE_CODE);
|
||||
if ($value !== false && $value !== null && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
$value = $request->getServer('HTTP_X_MAGESAIL_STORE_CODE');
|
||||
if ($value !== null && $value !== '' && trim((string) $value) !== '') {
|
||||
return trim((string) $value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MageSail\Magesail\Plugin\Backend\App\Area;
|
||||
|
||||
use Magento\Backend\App\Area\FrontNameResolver;
|
||||
use Magento\Framework\App\Request\Http;
|
||||
use Magento\Framework\App\RequestInterface;
|
||||
use MageSail\Magesail\Model\TunnelRequestDetector;
|
||||
use MageSail\Magesail\Model\TunnelStoreState;
|
||||
|
||||
/**
|
||||
* When the tunnel is used, HTTP_HOST is often dev.myshop.local while default backend base URLs point at production.
|
||||
* {@see FrontNameResolver::getFrontName(true)} then returns false, so {@see \Magento\Framework\App\AreaList}
|
||||
* never maps the admin front name to adminhtml and /admin_* is dispatched as frontend (404).
|
||||
*
|
||||
* Bypass host validation when: (1) tunnel proxy headers/query are present, or (2) the request host matches the
|
||||
* tunnel store base URLs from provisioned state (browser opened admin URL directly without X-MageSail-Store-Code).
|
||||
*/
|
||||
class TunnelBackendFrontNameResolverPlugin
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestInterface $request,
|
||||
private readonly TunnelRequestDetector $tunnelRequestDetector,
|
||||
private readonly TunnelStoreState $tunnelStoreState
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FrontNameResolver $subject
|
||||
* @param callable(bool $checkHost=): string|false $proceed
|
||||
* @return string|false
|
||||
*/
|
||||
public function aroundGetFrontName(FrontNameResolver $subject, callable $proceed, $checkHost = false)
|
||||
{
|
||||
$result = $proceed($checkHost);
|
||||
if ($result !== false) {
|
||||
return $result;
|
||||
}
|
||||
if (!$checkHost || !$this->request instanceof Http) {
|
||||
return $result;
|
||||
}
|
||||
if (!$this->shouldBypassBackendHostCheck($this->request)) {
|
||||
return $result;
|
||||
}
|
||||
if ($subject->isHostBackend()) {
|
||||
return $result;
|
||||
}
|
||||
return $proceed(false);
|
||||
}
|
||||
|
||||
private function shouldBypassBackendHostCheck(Http $request): bool
|
||||
{
|
||||
if ($this->tunnelRequestDetector->getTunnelStoreCode($request) !== null) {
|
||||
return true;
|
||||
}
|
||||
return $this->requestHostMatchesTunnelStoreUrls($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hostname-only match (tunnel unsecure/secure may differ in scheme/port from the current request).
|
||||
*/
|
||||
private function requestHostMatchesTunnelStoreUrls(Http $request): bool
|
||||
{
|
||||
$state = $this->tunnelStoreState->read();
|
||||
if ($state === null) {
|
||||
return false;
|
||||
}
|
||||
$requestHost = $this->hostFromHttpHost($request->getServer('HTTP_HOST'));
|
||||
if ($requestHost === null) {
|
||||
return false;
|
||||
}
|
||||
foreach ([$state['unsecure_base_url'], $state['secure_base_url']] as $baseUrl) {
|
||||
if ($baseUrl === '') {
|
||||
continue;
|
||||
}
|
||||
$configuredHost = $this->hostFromBaseUrl($baseUrl);
|
||||
if ($configuredHost !== null && strcasecmp($requestHost, $configuredHost) === 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hostFromHttpHost(?string $httpHost): ?string
|
||||
{
|
||||
if ($httpHost === null || $httpHost === '') {
|
||||
return null;
|
||||
}
|
||||
$parsed = parse_url('http://' . $httpHost);
|
||||
if ($parsed === false || !isset($parsed['host'])) {
|
||||
return null;
|
||||
}
|
||||
return $parsed['host'];
|
||||
}
|
||||
|
||||
private function hostFromBaseUrl(string $baseUrl): ?string
|
||||
{
|
||||
$parsed = parse_url($baseUrl);
|
||||
if ($parsed === false || !isset($parsed['host'])) {
|
||||
return null;
|
||||
}
|
||||
return $parsed['host'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace MageSail\Magesail\Plugin\Store;
|
||||
|
||||
use Magento\Framework\App\Request\Http;
|
||||
use Magento\Framework\App\RequestInterface;
|
||||
use Magento\Framework\UrlInterface;
|
||||
use Magento\Store\Model\Store;
|
||||
use MageSail\Magesail\Model\TunnelRequestDetector;
|
||||
|
||||
/**
|
||||
* With tunnel proxy, HTTP_HOST is dev.myshop.local but store base URLs point at production (e.g. s1.x64.world).
|
||||
* Rewrite scheme/host for page/link base URLs only — never for static/media/js or asset bases break (404).
|
||||
*/
|
||||
class TunnelStoreBaseUrlPlugin
|
||||
{
|
||||
public function __construct(
|
||||
private readonly RequestInterface $request,
|
||||
private readonly TunnelRequestDetector $tunnelRequestDetector
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Store $subject
|
||||
* @param string $result
|
||||
* @return string
|
||||
*/
|
||||
public function afterGetBaseUrl(Store $subject, $result, $type = null, $secure = null)
|
||||
{
|
||||
if (!$this->request instanceof Http || $result === '' || $result === null) {
|
||||
return $result;
|
||||
}
|
||||
$effectiveType = $type ?? UrlInterface::URL_TYPE_LINK;
|
||||
if (!\in_array($effectiveType, [
|
||||
UrlInterface::URL_TYPE_LINK,
|
||||
UrlInterface::URL_TYPE_DIRECT_LINK,
|
||||
UrlInterface::URL_TYPE_WEB,
|
||||
], true)) {
|
||||
return $result;
|
||||
}
|
||||
if ($this->tunnelRequestDetector->getTunnelStoreCode($this->request) === null) {
|
||||
return $result;
|
||||
}
|
||||
$parts = parse_url($result);
|
||||
if ($parts === false || empty($parts['host'])) {
|
||||
return $result;
|
||||
}
|
||||
$requestHost = $this->request->getHttpHost();
|
||||
if ($requestHost === '' || strcasecmp((string) $parts['host'], $requestHost) === 0) {
|
||||
return $result;
|
||||
}
|
||||
$scheme = $this->request->isSecure() ? 'https' : 'http';
|
||||
$path = $parts['path'] ?? '/';
|
||||
$query = isset($parts['query']) ? '?' . $parts['query'] : '';
|
||||
$fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : '';
|
||||
return $scheme . '://' . $requestHost . $path . $query . $fragment;
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -1,6 +1,13 @@
|
||||
<?xml version="1.0"?>
|
||||
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
|
||||
<type name="Magento\Framework\App\Request\Http">
|
||||
<plugin name="magesail_store_from_header" type="MageSail\Magesail\Plugin\App\Request\StoreFromMageSailHeaderPlugin"/>
|
||||
<plugin name="magesail_admin_front_from_path" type="MageSail\Magesail\Plugin\App\Request\AdminFrontNameFromPathPlugin" sortOrder="5"/>
|
||||
<plugin name="magesail_store_from_header" type="MageSail\Magesail\Plugin\App\Request\StoreFromMageSailHeaderPlugin" sortOrder="10"/>
|
||||
</type>
|
||||
<type name="Magento\Backend\App\Area\FrontNameResolver">
|
||||
<plugin name="magesail_tunnel_backend_frontname" type="MageSail\Magesail\Plugin\Backend\App\Area\TunnelBackendFrontNameResolverPlugin"/>
|
||||
</type>
|
||||
<type name="Magento\Store\Model\Store">
|
||||
<plugin name="magesail_tunnel_store_base_url" type="MageSail\Magesail\Plugin\Store\TunnelStoreBaseUrlPlugin"/>
|
||||
</type>
|
||||
</config>
|
||||
|
||||
Reference in New Issue
Block a user