cli = $cli; $this->files = $files; $this->config = $config; } /** * Get the name of the site. * * @param string|null $name * @return string */ private function getRealSiteName($name) { if (! is_null($name)) { return $name; } if (is_string($link = $this->getLinkNameByCurrentDir())) { return $link; } return basename(getcwd()); } /** * Get link name based on the current directory. * * @return null|string */ private function getLinkNameByCurrentDir() { $count = count($links = $this->links()->where('path', getcwd())); if ($count == 1) { return $links->shift()['site']; } if ($count > 1) { throw new DomainException("There are {$count} links related to the current directory, please specify the name: valet unlink ."); } } /** * Get the real hostname for the given path, checking links. * * @param string $path * @return string|null */ function host($path) { foreach ($this->files->scandir($this->sitesPath()) as $link) { if ($resolved = realpath($this->sitesPath().'/'.$link) === $path) { return $link; } } return basename($path); } /** * Link the current working directory with the given name. * * @param string $target * @param string $link * @return string */ function link($target, $link) { $this->files->ensureDirExists( $linkPath = $this->sitesPath(), user() ); $this->config->prependPath($linkPath); $this->files->symlinkAsUser($target, $linkPath.'/'.$link); return $linkPath.'/'.$link; } /** * Pretty print out all links in Valet. * * @return \Illuminate\Support\Collection */ function links() { $certsPath = VALET_HOME_PATH.'/Certificates'; $this->files->ensureDirExists($certsPath, user()); $certs = $this->getCertificates($certsPath); return $this->getLinks($this->sitesPath(), $certs); } /** * Get all certificates from config folder. * * @param string $path * @return \Illuminate\Support\Collection */ function getCertificates($path) { return collect($this->files->scandir($path))->filter(function ($value, $key) { return ends_with($value, '.crt'); })->map(function ($cert) { $certWithoutSuffix = substr($cert, 0, -4); return substr($certWithoutSuffix, 0, strrpos($certWithoutSuffix, '.')); })->flip(); } /** * Get list of links and present them formatted. * * @param string $path * @param \Illuminate\Support\Collection $certs * @return \Illuminate\Support\Collection */ function getLinks($path, $certs) { $config = $this->config->read(); return collect($this->files->scandir($path))->mapWithKeys(function ($site) use ($path) { return [$site => $this->files->readLink($path.'/'.$site)]; })->map(function ($path, $site) use ($certs, $config) { $secured = $certs->has($site); $url = ($secured ? 'https': 'http').'://'.$site.'.'.$config['tld']; return [ 'site' => $site, 'secured' => $secured ? ' X': '', 'url' => $url, 'path' => $path, ]; }); } /** * Unlink the given symbolic link. * * @param string $name * @return void */ function unlink($name) { $name = $this->getRealSiteName($name); if ($this->files->exists($path = $this->sitesPath().'/'.$name)) { $this->files->unlink($path); } return $name; } /** * Remove all broken symbolic links. * * @return void */ function pruneLinks() { $this->files->ensureDirExists($this->sitesPath(), user()); $this->files->removeBrokenLinksAt($this->sitesPath()); } /** * Resecure all currently secured sites with a fresh tld. * * @param string $oldTld * @param string $tld * @return void */ function resecureForNewTld($oldTld, $tld) { if (! $this->files->exists($this->certificatesPath())) { return; } $secured = $this->secured(); foreach ($secured as $url) { $this->unsecure($url); } foreach ($secured as $url) { $this->secure(str_replace('.'.$oldTld, '.'.$tld, $url)); } } /** * Get all of the URLs that are currently secured. * * @return array */ function secured() { return collect($this->files->scandir($this->certificatesPath())) ->map(function ($file) { return str_replace(['.key', '.csr', '.crt', '.conf'], '', $file); })->unique()->values()->all(); } /** * Secure the given host with TLS. * * @param string $url * @return void */ function secure($url) { $this->unsecure($url); $this->files->ensureDirExists($this->caPath(), user()); $this->files->ensureDirExists($this->certificatesPath(), user()); $this->createCa(); $this->createCertificate($url); $this->files->putAsUser( VALET_HOME_PATH.'/Nginx/'.$url, $this->buildSecureNginxServer($url) ); } /** * If CA and root certificates are nonexistent, crete them and trust the root cert. * * @return void */ function createCa() { $caPemPath = $this->caPath().'/LaravelValetCASelfSigned.pem'; $caKeyPath = $this->caPath().'/LaravelValetCASelfSigned.key'; if ($this->files->exists($caKeyPath) && $this->files->exists($caPemPath)) { return; } $oName = 'Laravel Valet CA Self Signed Organization'; $cName = 'Laravel Valet CA Self Signed CN'; if ($this->files->exists($caKeyPath)) { $this->files->unlink($caKeyPath); } if ($this->files->exists($caPemPath)) { $this->files->unlink($caPemPath); } $this->cli->run(sprintf( 'sudo security delete-certificate -c "%s" /Library/Keychains/System.keychain', $cName )); $this->cli->runAsUser(sprintf( 'openssl req -new -newkey rsa:2048 -days 730 -nodes -x509 -subj "/C=/ST=/O=%s/localityName=/commonName=%s/organizationalUnitName=Developers/emailAddress=%s/" -keyout %s -out %s', $oName, $cName, 'rootcertificate@laravel.valet', $caKeyPath, $caPemPath )); $this->trustCa($caPemPath); } /** * Create and trust a certificate for the given URL. * * @param string $url * @return void */ 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'; $this->buildCertificateConf($confPath, $url); $this->createPrivateKey($keyPath); $this->createSigningRequest($url, $keyPath, $csrPath, $confPath); $caSrlParam = ' -CAcreateserial'; if ($this->files->exists($caSrlPath)) { $caSrlParam = ' -CAserial ' . $caSrlPath; } $this->cli->runAsUser(sprintf( 'openssl x509 -req -sha256 -days 730 -CA %s -CAkey %s%s -in %s -out %s -extensions v3_req -extfile %s', $caPemPath, $caKeyPath, $caSrlParam, $csrPath, $crtPath, $confPath )); $this->trustCertificate($crtPath); } /** * Create the private key for the TLS certificate. * * @param string $keyPath * @return void */ function createPrivateKey($keyPath) { $this->cli->runAsUser(sprintf('openssl genrsa -out %s 2048', $keyPath)); } /** * Create the signing request for the TLS certificate. * * @param string $keyPath * @return void */ function createSigningRequest($url, $keyPath, $csrPath, $confPath) { $this->cli->runAsUser(sprintf( 'openssl req -new -key %s -out %s -subj "/C=/ST=/O=/localityName=/commonName=%s/organizationalUnitName=/emailAddress=%s%s/" -config %s', $keyPath, $csrPath, $url, $url, '@laravel.valet', $confPath )); } /** * Trust the given root certificate file in the Mac Keychain. * * @param string $pemPath * @return void */ function trustCa($caPemPath) { $this->cli->run(sprintf( 'sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain %s', $caPemPath )); } /** * Trust the given certificate file in the Mac Keychain. * * @param string $crtPath * @return void */ function trustCertificate($crtPath) { $this->cli->run(sprintf( 'sudo security add-trusted-cert -d -r trustAsRoot -k /Library/Keychains/System.keychain %s', $crtPath )); } /** * Build the SSL config for the given URL. * * @param string $url * @return string */ function buildCertificateConf($path, $url) { $config = str_replace('VALET_DOMAIN', $url, $this->files->get(__DIR__.'/../stubs/openssl.conf')); $this->files->putAsUser($path, $config); } /** * Build the TLS secured Nginx server for the given URL. * * @param string $url * @return string */ function buildSecureNginxServer($url) { $path = $this->certificatesPath(); 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') ); } /** * Unsecure the given URL so that it will use HTTP again. * * @param string $url * @return void */ function unsecure($url) { if ($this->files->exists($this->certificatesPath().'/'.$url.'.crt')) { $this->files->unlink(VALET_HOME_PATH.'/Nginx/'.$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->cli->run(sprintf('sudo security delete-certificate -c "%s" /Library/Keychains/System.keychain', $url)); $this->cli->run(sprintf('sudo security delete-certificate -c "*.%s" /Library/Keychains/System.keychain', $url)); $this->cli->run(sprintf( 'sudo security find-certificate -e "%s%s" -a -Z | grep SHA-1 | sudo awk \'{system("security delete-certificate -Z "$NF" /Library/Keychains/System.keychain")}\'', $url, '@laravel.valet' )); } /** * Get the path to the linked Valet sites. * * @return string */ function sitesPath() { return VALET_HOME_PATH.'/Sites'; } /** * Get the path to the Valet CA certificates. * * @return string */ function caPath() { return VALET_HOME_PATH.'/CA'; } /** * Get the path to the Valet TLS certificates. * * @return string */ function certificatesPath() { return VALET_HOME_PATH.'/Certificates'; } }