1
0
mirror of https://github.com/laravel/valet.git synced 2026-02-05 00:20:08 +01:00

Merge pull request #913 from drbyte/valet-proxy

Add proxy site-handling commands
This commit is contained in:
Matt Stauffer
2020-04-30 11:44:44 -04:00
committed by GitHub
8 changed files with 978 additions and 42 deletions

View File

@@ -68,7 +68,7 @@ private function getLinkNameByCurrentDir()
function host($path)
{
foreach ($this->files->scandir($this->sitesPath()) as $link) {
if ($resolved = realpath($this->sitesPath().'/'.$link) === $path) {
if ($resolved = realpath($this->sitesPath($link)) === $path) {
return $link;
}
}
@@ -103,7 +103,7 @@ function link($target, $link)
*/
function links()
{
$certsPath = VALET_HOME_PATH.'/Certificates';
$certsPath = $this->certificatesPath();
$this->files->ensureDirExists($certsPath, user());
@@ -119,11 +119,7 @@ function links()
*/
function parked()
{
$certsPath = VALET_HOME_PATH.'/Certificates';
$this->files->ensureDirExists($certsPath, user());
$certs = $this->getCertificates($certsPath);
$certs = $this->getCertificates();
$links = $this->getSites($this->sitesPath(), $certs);
@@ -145,14 +141,89 @@ function parked()
return $parkedLinks;
}
/**
* Get all sites which are proxies (not Links, and contain proxy_pass directive)
*
* @return \Illuminate\Support\Collection
*/
function proxies()
{
$dir = $this->nginxPath();
$tld = $this->config->read()['tld'];
$links = $this->links();
$certs = $this->getCertificates();
if (! $this->files->exists($dir)) {
return collect();
}
$proxies = collect($this->files->scandir($dir))
->filter(function ($site, $key) use ($tld) {
// keep sites that match our TLD
return ends_with($site, '.'.$tld);
})->map(function ($site, $key) use ($tld) {
// remove the TLD suffix for consistency
return str_replace('.'.$tld, '', $site);
})->reject(function ($site, $key) use ($links) {
return $links->has($site);
})->mapWithKeys(function ($site) {
$host = $this->getProxyHostForSite($site) ?: '(other)';
return [$site => $host];
})->reject(function ($host, $site) {
// If proxy host is null, it may be just a normal SSL stub, or something else; either way we exclude it from the list
return $host === '(other)';
})->map(function ($host, $site) use ($certs, $tld) {
$secured = $certs->has($site);
$url = ($secured ? 'https': 'http').'://'.$site.'.'.$tld;
return [
'site' => $site,
'secured' => $secured ? ' X': '',
'url' => $url,
'path' => $host,
];
});
return $proxies;
}
/**
* Identify whether a site is for a proxy by reading the host name from its config file.
*
* @param string $site Site name without TLD
* @param string $configContents Config file contents
* @return string|null
*/
function getProxyHostForSite($site, $configContents = null)
{
$siteConf = $configContents ?: $this->getSiteConfigFileContents($site);
$host = null;
if (preg_match('~proxy_pass\s+(?<host>https?://.*)\s*;~', $siteConf, $patterns)) {
$host = trim($patterns['host']);
}
return $host;
}
function getSiteConfigFileContents($site)
{
$config = $this->config->read();
$suffix = '.'.$config['tld'];
$file = str_replace($suffix,'',$site).$suffix;
return $this->files->get($this->nginxPath($file));
}
/**
* Get all certificates from config folder.
*
* @param string $path
* @return \Illuminate\Support\Collection
*/
function getCertificates($path)
function getCertificates($path = null)
{
$path = $path ?: $this->certificatesPath();
$this->files->ensureDirExists($path, user());
$config = $this->config->read();
return collect($this->files->scandir($path))->filter(function ($value, $key) {
@@ -196,6 +267,8 @@ function getSites($path, $certs)
{
$config = $this->config->read();
$this->files->ensureDirExists($path, user());
return collect($this->files->scandir($path))->mapWithKeys(function ($site) use ($path) {
$sitePath = $path.'/'.$site;
@@ -230,7 +303,7 @@ function unlink($name)
{
$name = $this->getRealSiteName($name);
if ($this->files->exists($path = $this->sitesPath().'/'.$name)) {
if ($this->files->exists($path = $this->sitesPath($name))) {
$this->files->unlink($path);
}
@@ -265,14 +338,43 @@ function resecureForNewTld($oldTld, $tld)
$secured = $this->secured();
foreach ($secured as $url) {
$this->unsecure($url);
}
$newUrl = str_replace('.'.$oldTld, '.'.$tld, $url);
$siteConf = $this->getSiteConfigFileContents($url);
foreach ($secured as $url) {
$this->secure(str_replace('.'.$oldTld, '.'.$tld, $url));
if (strpos($siteConf, '# valet stub: proxy.valet.conf') === 0) {
$this->unsecure($url);
$this->secure($newUrl, $this->replaceOldDomainWithNew($siteConf, '.'.$url, '.'.$newUrl));
} else {
$this->unsecure($url);
$this->secure($newUrl);
}
}
}
/**
* Parse Nginx site config file contents to swap old domain to new.
*
* @param string $siteConf Nginx site config content
* @param string $old Old domain
* @param string $new New domain
* @return string
*/
function replaceOldDomainWithNew($siteConf, $old, $new)
{
$lookups = [];
$lookups[] = '~server_name .*;~';
$lookups[] = '~error_log .*;~';
foreach ($lookups as $lookup) {
preg_match($lookup, $siteConf, $matches);
foreach ($matches as $match) {
$replaced = str_replace($old, $new, $match);
$siteConf = str_replace($match, $replaced, $siteConf);
}
}
return $siteConf;
}
/**
* Get all of the URLs that are currently secured.
*
@@ -290,9 +392,10 @@ function secured()
* Secure the given host with TLS.
*
* @param string $url
* @param string $siteConf pregenerated Nginx config file contents
* @return void
*/
function secure($url)
function secure($url, $siteConf = null)
{
$this->unsecure($url);
@@ -300,12 +403,14 @@ function secure($url)
$this->files->ensureDirExists($this->certificatesPath(), user());
$this->files->ensureDirExists($this->nginxPath(), user());
$this->createCa();
$this->createCertificate($url);
$this->files->putAsUser(
VALET_HOME_PATH.'/Nginx/'.$url, $this->buildSecureNginxServer($url)
$this->nginxPath($url), $this->buildSecureNginxServer($url, $siteConf)
);
}
@@ -316,8 +421,8 @@ function secure($url)
*/
function createCa()
{
$caPemPath = $this->caPath().'/LaravelValetCASelfSigned.pem';
$caKeyPath = $this->caPath().'/LaravelValetCASelfSigned.key';
$caPemPath = $this->caPath('LaravelValetCASelfSigned.pem');
$caKeyPath = $this->caPath('LaravelValetCASelfSigned.key');
if ($this->files->exists($caKeyPath) && $this->files->exists($caPemPath)) {
return;
@@ -353,13 +458,13 @@ function createCa()
*/
function createCertificate($url)
{
$caPemPath = $this->caPath().'/LaravelValetCASelfSigned.pem';
$caKeyPath = $this->caPath().'/LaravelValetCASelfSigned.key';
$caSrlPath = $this->caPath().'/LaravelValetCASelfSigned.srl';
$keyPath = $this->certificatesPath().'/'.$url.'.key';
$csrPath = $this->certificatesPath().'/'.$url.'.csr';
$crtPath = $this->certificatesPath().'/'.$url.'.crt';
$confPath = $this->certificatesPath().'/'.$url.'.conf';
$caPemPath = $this->caPath('LaravelValetCASelfSigned.pem');
$caKeyPath = $this->caPath('LaravelValetCASelfSigned.key');
$caSrlPath = $this->caPath('LaravelValetCASelfSigned.srl');
$keyPath = $this->certificatesPath($url, 'key');
$csrPath = $this->certificatesPath($url, 'csr');
$crtPath = $this->certificatesPath($url, 'crt');
$confPath = $this->certificatesPath($url, 'conf');
$this->buildCertificateConf($confPath, $url);
$this->createPrivateKey($keyPath);
@@ -453,16 +558,26 @@ function buildCertificateConf($path, $url)
* Build the TLS secured Nginx server for the given URL.
*
* @param string $url
* @param string $siteConf (optional) Nginx site config file content
* @return string
*/
function buildSecureNginxServer($url)
function buildSecureNginxServer($url, $siteConf = null)
{
$path = $this->certificatesPath();
if ($siteConf === null) {
$siteConf = $this->files->get(__DIR__.'/../stubs/secure.valet.conf');
}
return str_replace(
['VALET_HOME_PATH', 'VALET_SERVER_PATH', 'VALET_STATIC_PREFIX', 'VALET_SITE', 'VALET_CERT', 'VALET_KEY'],
[VALET_HOME_PATH, VALET_SERVER_PATH, VALET_STATIC_PREFIX, $url, $path.'/'.$url.'.crt', $path.'/'.$url.'.key'],
$this->files->get(__DIR__.'/../stubs/secure.valet.conf')
[
$this->valetHomePath(),
VALET_SERVER_PATH,
VALET_STATIC_PREFIX,
$url,
$this->certificatesPath($url, 'crt'),
$this->certificatesPath($url, 'key'),
],
$siteConf
);
}
@@ -474,13 +589,13 @@ function buildSecureNginxServer($url)
*/
function unsecure($url)
{
if ($this->files->exists($this->certificatesPath().'/'.$url.'.crt')) {
$this->files->unlink(VALET_HOME_PATH.'/Nginx/'.$url);
if ($this->files->exists($this->certificatesPath($url, 'crt'))) {
$this->files->unlink($this->nginxPath($url));
$this->files->unlink($this->certificatesPath().'/'.$url.'.conf');
$this->files->unlink($this->certificatesPath().'/'.$url.'.key');
$this->files->unlink($this->certificatesPath().'/'.$url.'.csr');
$this->files->unlink($this->certificatesPath().'/'.$url.'.crt');
$this->files->unlink($this->certificatesPath($url, 'conf'));
$this->files->unlink($this->certificatesPath($url, 'key'));
$this->files->unlink($this->certificatesPath($url, 'csr'));
$this->files->unlink($this->certificatesPath($url, 'crt'));
}
$this->cli->run(sprintf('sudo security delete-certificate -c "%s" /Library/Keychains/System.keychain', $url));
@@ -522,14 +637,77 @@ function unsecureAll()
info('unsecure --all was successful.');
}
/**
* Build the Nginx proxy config for the specified domain
*
* @param string $url The domain name to serve
* @param string $host The URL to proxy to, eg: http://127.0.0.1:8080
* @return string
*/
function proxyCreate($url, $host)
{
if (!preg_match('~^https?://.*$~', $host)) {
throw new \InvalidArgumentException(sprintf('"%s" is not a valid URL', $host));
}
$tld = $this->config->read()['tld'];
if (!ends_with($url, '.'.$tld)) {
$url .= '.'.$tld;
}
$siteConf = $this->files->get(__DIR__.'/../stubs/proxy.valet.conf');
$siteConf = str_replace(
['VALET_HOME_PATH', 'VALET_SERVER_PATH', 'VALET_STATIC_PREFIX', 'VALET_SITE', 'VALET_PROXY_HOST'],
[$this->valetHomePath(), VALET_SERVER_PATH, VALET_STATIC_PREFIX, $url, $host],
$siteConf
);
$this->secure($url, $siteConf);
info('Valet will now proxy [https://'.$url.'] traffic to ['.$host.'].');
}
/**
* Unsecure the given URL so that it will use HTTP again.
*
* @param string $url
* @return void
*/
function proxyDelete($url)
{
$tld = $this->config->read()['tld'];
if (!ends_with($url, '.'.$tld)) {
$url .= '.'.$tld;
}
$this->unsecure($url);
$this->files->unlink($this->nginxPath($url));
info('Valet will no longer proxy [https://'.$url.'].');
}
function valetHomePath()
{
return VALET_HOME_PATH;
}
/**
* Get the path to Nginx site configuration files.
*/
function nginxPath($additionalPath = null)
{
return $this->valetHomePath().'/Nginx'.($additionalPath ? '/'.$additionalPath : '');
}
/**
* Get the path to the linked Valet sites.
*
* @return string
*/
function sitesPath()
function sitesPath($link = null)
{
return VALET_HOME_PATH.'/Sites';
return $this->valetHomePath().'/Sites'.($link ? '/'.$link : '');
}
/**
@@ -537,9 +715,9 @@ function sitesPath()
*
* @return string
*/
function caPath()
function caPath($caFile = null)
{
return VALET_HOME_PATH.'/CA';
return $this->valetHomePath().'/CA'.($caFile ? '/'.$caFile : '');
}
/**
@@ -547,8 +725,11 @@ function caPath()
*
* @return string
*/
function certificatesPath()
function certificatesPath($url = null, $extension = null)
{
return VALET_HOME_PATH.'/Certificates';
$url = $url ? '/'.$url : '';
$extension = $extension ? '.'.$extension : '';
return $this->valetHomePath().'/Certificates'.$url.$extension;
}
}

View File

@@ -0,0 +1,95 @@
# valet stub: proxy.valet.conf
server {
listen 127.0.0.1:80;
server_name VALET_SITE www.VALET_SITE *.VALET_SITE;
return 301 https://$host$request_uri;
}
server {
listen 127.0.0.1:443 ssl http2;
server_name VALET_SITE www.VALET_SITE *.VALET_SITE;
root /;
charset utf-8;
client_max_body_size 128M;
http2_push_preload on;
location /VALET_STATIC_PREFIX/ {
internal;
alias /;
try_files $uri $uri/;
}
ssl_certificate "VALET_CERT";
ssl_certificate_key "VALET_KEY";
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "VALET_HOME_PATH/Log/VALET_SITE-error.log";
error_page 404 "VALET_SERVER_PATH";
location / {
proxy_pass VALET_PROXY_HOST;
proxy_set_header Host $host;
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-Client-Verify SUCCESS;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-SSL-Subject $ssl_client_s_dn;
proxy_set_header X-SSL-Issuer $ssl_client_i_dn;
proxy_set_header X-NginX-Proxy true;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_read_timeout 1800;
proxy_connect_timeout 1800;
chunked_transfer_encoding on;
proxy_redirect off;
proxy_buffering off;
}
location ~ /\.ht {
deny all;
}
}
server {
listen 127.0.0.1:60;
server_name VALET_SITE www.VALET_SITE *.VALET_SITE;
root /;
charset utf-8;
client_max_body_size 128M;
add_header X-Robots-Tag 'noindex, nofollow, nosnippet, noarchive';
location /VALET_STATIC_PREFIX/ {
internal;
alias /;
try_files $uri $uri/;
}
location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }
access_log off;
error_log "VALET_HOME_PATH/Log/VALET_SITE-error.log";
error_page 404 "VALET_SERVER_PATH";
location / {
proxy_pass VALET_PROXY_HOST;
proxy_set_header Host $host;
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;
}
location ~ /\.ht {
deny all;
}
}

View File

@@ -181,6 +181,35 @@
info('The ['.$url.'] site will now serve traffic over HTTP.');
})->descriptions('Stop serving the given domain over HTTPS and remove the trusted TLS certificate');
/**
* Create an Nginx proxy config for the specified domain
*/
$app->command('proxy domain host', function ($domain, $host) {
Site::proxyCreate($domain, $host);
Nginx::restart();
})->descriptions('Create an Nginx proxy site for the specified host. Useful for docker, mailhog etc.');
/**
* Delete an Nginx proxy config
*/
$app->command('unproxy domain', function ($domain) {
Site::proxyDelete($domain);
Nginx::restart();
})->descriptions('Delete an Nginx proxy config.');
/**
* Display all of the sites that are proxies.
*/
$app->command('proxies', function () {
$proxies = Site::proxies();
table(['Site', 'SSL', 'URL', 'Host'], $proxies->all());
})->descriptions('Display all of the proxy sites');
/**
* Determine which Valet driver the current directory is using.
*/