From e313898c552ca690fda186e7525293dff28a1888 Mon Sep 17 00:00:00 2001 From: Ahmad Fikrizaman Date: Sun, 14 Feb 2021 02:07:45 +0800 Subject: [PATCH] first commit --- .gitignore | 8 + LICENSE | 21 + README.md | 90 +++ bin/butler | 312 ++++++++ docker/dnsmasq/Dockerfile | 24 + docker/dnsmasq/dnsmasq.conf | 13 + docker/dnsmasq/docker-entrypoint.sh | 27 + docker/openresty/Dockerfile | 13 + .../entrypoint/10-envsubst-on-templates.sh | 39 + .../openresty/entrypoint/docker-entrypoint.sh | 40 + install.sh | 72 ++ stubs/.env | 7 + stubs/config.json | 6 + stubs/docker-compose.yaml | 50 ++ templates/dnsmasq/dnsmasq-valet.conf.template | 3 + templates/nginx/nginx.conf.template | 53 ++ templates/nginx/valet.conf.template | 37 + valet/cli/Valet/Brew.php | 420 ++++++++++ valet/cli/Valet/CommandLine.php | 98 +++ valet/cli/Valet/Configuration.php | 261 +++++++ valet/cli/Valet/Diagnose.php | 159 ++++ valet/cli/Valet/DnsMasq.php | 112 +++ valet/cli/Valet/Filesystem.php | 342 +++++++++ valet/cli/Valet/Nginx.php | 173 +++++ valet/cli/Valet/Ngrok.php | 59 ++ valet/cli/Valet/PhpFpm.php | 259 +++++++ valet/cli/Valet/Site.php | 726 ++++++++++++++++++ valet/cli/Valet/Valet.php | 120 +++ valet/cli/drivers/BasicValetDriver.php | 154 ++++ valet/cli/drivers/BedrockValetDriver.php | 76 ++ valet/cli/drivers/CakeValetDriver.php | 52 ++ valet/cli/drivers/Concrete5ValetDriver.php | 48 ++ valet/cli/drivers/ContaoValetDriver.php | 58 ++ valet/cli/drivers/CraftValetDriver.php | 211 +++++ valet/cli/drivers/DrupalValetDriver.php | 111 +++ valet/cli/drivers/JigsawValetDriver.php | 28 + valet/cli/drivers/JoomlaValetDriver.php | 32 + valet/cli/drivers/KatanaValetDriver.php | 28 + valet/cli/drivers/KirbyValetDriver.php | 70 ++ valet/cli/drivers/LaravelValetDriver.php | 64 ++ valet/cli/drivers/Magento2ValetDriver.php | 153 ++++ valet/cli/drivers/NeosValetDriver.php | 50 ++ valet/cli/drivers/SculpinValetDriver.php | 57 ++ valet/cli/drivers/StatamicV1ValetDriver.php | 68 ++ valet/cli/drivers/StatamicValetDriver.php | 146 ++++ valet/cli/drivers/SymfonyValetDriver.php | 58 ++ valet/cli/drivers/Typo3ValetDriver.php | 197 +++++ valet/cli/drivers/ValetDriver.php | 217 ++++++ valet/cli/drivers/WordPressValetDriver.php | 51 ++ valet/cli/drivers/require.php | 30 + valet/cli/includes/compatibility.php | 12 + valet/cli/includes/facades.php | 42 + valet/cli/includes/helpers.php | 206 +++++ valet/cli/scripts/fetch-share-url.sh | 5 + valet/cli/stubs/SampleValetDriver.php | 51 ++ valet/cli/stubs/etc-dnsmasq-valet.conf | 3 + valet/cli/stubs/etc-phpfpm-error_log.ini | 5 + valet/cli/stubs/etc-phpfpm-valet.conf | 26 + valet/cli/stubs/fastcgi_params | 20 + valet/cli/stubs/nginx.conf | 47 ++ valet/cli/stubs/openssl.conf | 27 + valet/cli/stubs/php-memory-limits.ini | 10 + valet/cli/stubs/proxy.valet.conf | 89 +++ valet/cli/stubs/secure.valet.conf | 90 +++ valet/cli/stubs/valet.conf | 37 + valet/cli/templates/404.html | 14 + valet/cli/valet.php | 317 ++++++++ valet/composer.json | 46 ++ valet/server.php | 224 ++++++ valet/valet | 38 + 70 files changed, 6812 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/butler create mode 100644 docker/dnsmasq/Dockerfile create mode 100644 docker/dnsmasq/dnsmasq.conf create mode 100755 docker/dnsmasq/docker-entrypoint.sh create mode 100644 docker/openresty/Dockerfile create mode 100755 docker/openresty/entrypoint/10-envsubst-on-templates.sh create mode 100755 docker/openresty/entrypoint/docker-entrypoint.sh create mode 100755 install.sh create mode 100644 stubs/.env create mode 100644 stubs/config.json create mode 100644 stubs/docker-compose.yaml create mode 100644 templates/dnsmasq/dnsmasq-valet.conf.template create mode 100644 templates/nginx/nginx.conf.template create mode 100644 templates/nginx/valet.conf.template create mode 100755 valet/cli/Valet/Brew.php create mode 100755 valet/cli/Valet/CommandLine.php create mode 100755 valet/cli/Valet/Configuration.php create mode 100755 valet/cli/Valet/Diagnose.php create mode 100755 valet/cli/Valet/DnsMasq.php create mode 100755 valet/cli/Valet/Filesystem.php create mode 100755 valet/cli/Valet/Nginx.php create mode 100755 valet/cli/Valet/Ngrok.php create mode 100755 valet/cli/Valet/PhpFpm.php create mode 100755 valet/cli/Valet/Site.php create mode 100755 valet/cli/Valet/Valet.php create mode 100755 valet/cli/drivers/BasicValetDriver.php create mode 100755 valet/cli/drivers/BedrockValetDriver.php create mode 100755 valet/cli/drivers/CakeValetDriver.php create mode 100755 valet/cli/drivers/Concrete5ValetDriver.php create mode 100755 valet/cli/drivers/ContaoValetDriver.php create mode 100755 valet/cli/drivers/CraftValetDriver.php create mode 100755 valet/cli/drivers/DrupalValetDriver.php create mode 100755 valet/cli/drivers/JigsawValetDriver.php create mode 100755 valet/cli/drivers/JoomlaValetDriver.php create mode 100755 valet/cli/drivers/KatanaValetDriver.php create mode 100755 valet/cli/drivers/KirbyValetDriver.php create mode 100755 valet/cli/drivers/LaravelValetDriver.php create mode 100755 valet/cli/drivers/Magento2ValetDriver.php create mode 100755 valet/cli/drivers/NeosValetDriver.php create mode 100755 valet/cli/drivers/SculpinValetDriver.php create mode 100755 valet/cli/drivers/StatamicV1ValetDriver.php create mode 100755 valet/cli/drivers/StatamicValetDriver.php create mode 100755 valet/cli/drivers/SymfonyValetDriver.php create mode 100755 valet/cli/drivers/Typo3ValetDriver.php create mode 100755 valet/cli/drivers/ValetDriver.php create mode 100755 valet/cli/drivers/WordPressValetDriver.php create mode 100755 valet/cli/drivers/require.php create mode 100755 valet/cli/includes/compatibility.php create mode 100755 valet/cli/includes/facades.php create mode 100755 valet/cli/includes/helpers.php create mode 100755 valet/cli/scripts/fetch-share-url.sh create mode 100755 valet/cli/stubs/SampleValetDriver.php create mode 100755 valet/cli/stubs/etc-dnsmasq-valet.conf create mode 100755 valet/cli/stubs/etc-phpfpm-error_log.ini create mode 100755 valet/cli/stubs/etc-phpfpm-valet.conf create mode 100755 valet/cli/stubs/fastcgi_params create mode 100755 valet/cli/stubs/nginx.conf create mode 100755 valet/cli/stubs/openssl.conf create mode 100755 valet/cli/stubs/php-memory-limits.ini create mode 100755 valet/cli/stubs/proxy.valet.conf create mode 100755 valet/cli/stubs/secure.valet.conf create mode 100755 valet/cli/stubs/valet.conf create mode 100755 valet/cli/templates/404.html create mode 100755 valet/cli/valet.php create mode 100644 valet/composer.json create mode 100644 valet/server.php create mode 100755 valet/valet diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf015ce --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.env +/docker-compose.yaml +/valet-ori +/valet/vendor +/valet/composer.lock +/.valet-home +/www +.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..16edfe1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 RunCloudIO + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..8078f69 --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# About Butler + +Butler is a replacement for [Laravel Valet](https://github.com/laravel/valet) that works inside Docker. So, no more tinkering with brew, fixing things, etc when things go south. Since Butler is based on Laravel Valet, please take a look at [Laravel Valet Documentation](https://laravel.com/docs/master/valet) before using Butler. + +Butler codebase is 100% taken from Laravel Valet and a few codebase was inspired (taken) from [Laravel Sail](https://github.com/laravel/sail). Since Valet was designed for MacOS, a few tweak from main code need to be changed inside Butler to give the same experience as using Laravel Valet. + +# Butler Manifesto + +I hate it when things doesn't work on my machine after I have setup everything. Things just don't work sometimes and I believe you have face the same problem. When I create Butler, it is because brew service give ton shit of error. Not to mention, when upgrading your Mac sometimes you face new error. + +Like every programmer, instead of fixing broken things. Why not make a new solution? I like how Laravel Valet works but to deal with errors (not causing by Laravel Valet), it just consumed my daily life experience in developing my product. To combat this, Butler was born. + +To make things simple inside your development machine, Butler should make your life easy without having to install PHP, Nginx or DNSmasq inside your Mac. Thus, keeping your Mac clean and you can easily setup your development environment when you buy a new Mac with your hard earned money. + +Butler aim to replicate the simplicity of using Laravel Valet and thus I will not add other cool shit feature to Butler if it does not available inside Laravel Valet. Any **PR** that add a feature which not exist inside Valet will **be rejected** without hesitation. This project is my first project in Docker because I want to learn how to use Docker. There will be part of this code which you will feel like a **n00b** that code this project, and that is because it is. If you have any improvement to make, don't hesitate to make PR or the noob code will stay forever. + +# Todo + +- [ ] valet share +- [ ] valet fetch-share-url +- [ ] valet unsecure --all + +# TLDR; + +``` +$ ./install.sh +$ cd www/default +$ mkdir mysite +$ cd mysite +$ echo " index.php +$ # update DNS to 127.0.0.1 +$ open -a "Google Chrome" http://mysite.test +``` + +# Installation + +Requirement: + +1. [Docker](https://www.docker.com/) + +To start with Butler, clone this repository and run `install.sh` + +``` +$ git clone https://github.com/RunCloudIO/butler.git +$ cd butler +$ ./install.sh +``` +**IMPORTANT PART**. After the installation succeeded, change your DNS inside System Preferences > Network > Advanced > DNS to **`127.0.0.1`**. Failure to do so **will prevent** you from running custom domain TLD. + + +To update, just `git pull` and then run `install.sh` again. If you have **moved** the folder to a different path, simply run `install.sh` inside the new path to make sure `butler` command know about it. + +# Usage + +### Basic usage +You will have `butler` installed inside `/usr/local/bin/butler`. Thus, you can invoke `butler` command anywhere. + +`butler` command without any argument is same as running `valet` without any argument. You also can run `butler valet` if you prefer it that way. + +Valet default path was set to `/var/www/default`. So you may create your 1st project inside there, which is inside host is `www/default` directory. + +### Butler specific command + +```bash +$ butler start # start butler process +$ butler reload # reload processes if you change .env or docker-compose.yaml +$ butler reset # reset everything to original state but keep your item in mounted folder +$ butler restart # restart all butler services +$ butler stop # stop all butler services +``` + +### Using PHP and Composer + +Since you are not installing any PHP inside your Mac, you can run php using `butler php`. Thus, running `php artisan migrate` can be run using `butler php artisan migrate`. Or if you prefer the shortest way `butler artisan migrate`, or `butler art migrate`. + +Same as using composer, you can run `butler composer create-project laravel/laravel example-app` to install Laravel. + +**PLEASE TAKE NOTE** that running PHP based command (php, valet, composer, artisan) only supported on **`DEFAULT_WWW_PATH`** that you have set inside **`.env`**. + +Running PHP based command ouside of `DEFAULT_WWW_PATH` folder is equivalent to running the command inside `/var/www/`. If you need to run outside that folder, you need to manually mount your folder and interact directly with the Docker container. + +### Change PHP version + +Since we are using Docker, **changing PHP version** is **easier** than ever. You just need to update `.env` by changing `BUTLER_PHP_VERSION` to either version *8.0, 7.4, 7.3, 7.2, 7.1, or 7.0*. Then just issue `butler reload` for it to take effect. + +### Laravel Valet park and link command + +You can only run `butler valet park` or `butler valet link` inside`DEFAULT_WWW_PATH` defined in `.env`. If you run these command outside the `DEFAULT_WWW_PATH` directory, it will automatically run your command inside `/var/www` in the container. + +You may create another folder inside `DEFAULT_WWW_PATH` and register it as new parked paths or linked path. So you can divide your codebase per project basis inside here. E.g: `/var/www/{defaults,project1,project2,project3,link1,link2}`. \ No newline at end of file diff --git a/bin/butler b/bin/butler new file mode 100755 index 0000000..078ee75 --- /dev/null +++ b/bin/butler @@ -0,0 +1,312 @@ +#!/usr/bin/env bash + +BUTLERRUNPATH="REPLACEME" +VALET_HOME=".valet-home" +CURRENTDIR="$(pwd)" +UNAMEOUT="$(uname -s)" + +WHITE='\033[1;37m' +NC='\033[0m' + +# Function that outputs Butler is not running... +function butler_is_not_running { + echo -e "${WHITE}Butler is not running.${NC}" >&2 + echo "" >&2 + echo -e "${WHITE}You may Butler using the following command:${NC} 'butler up'" >&2 + + exit 1 +} + +function is_file_exist { + if test -f "$1"; then + return 0 + else + return 1 + fi +} + +function generate_ca { + CA_PATH="$BUTLERRUNPATH/$VALET_HOME/CA/LaravelValetCASelfSigned.pem" + CA_KEY_PATH="$BUTLERRUNPATH/$VALET_HOME/CA/LaravelValetCASelfSigned.key" + + if is_file_exist $CA_PATH && is_file_exist $CA_KEY_PATH ; then + return + else + # Need to change name to distinguish between real Valet certificate + oName="Laravel Valet (Butler) CA Self Signed Organization"; + cName="Laravel Valet (Butler) CA Self Signed CN"; + + # Remove existing CA and CA Key + rm $CA_PATH $CA_KEY_PATH 2> /dev/null + + echo -e "${WHITE}Installing CA Cert. Enter password to install${NC}" >&2 + # remove the old cert + sudo security delete-certificate -c $cName /Library/Keychains/System.keychain + openssl req -new -newkey rsa:2048 -days 10000 -nodes -x509 -subj "/C=MY/ST=Negeri Sembilan/O=$oName/localityName=Mantin/commonName=$cName/organizationalUnitName=Engineering/emailAddress=rootcertificate@runcloud.io/" -keyout $CA_KEY_PATH -out $CA_PATH + sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain $CA_PATH + fi +} + +function get_tld { + # We dont want to use jq. so... KISS + cat $BUTLERRUNPATH/$VALET_HOME/config.json | tr { '\n' | tr , '\n' | tr } '\n' | grep "tld" | awk -F'"' '{print $4}' +} + +function trust_certificate { + tld=$(get_tld) + + CERT_PATH="$BUTLERRUNPATH/$VALET_HOME/Certificates/$1.$tld.crt" + + if is_file_exist $CERT_PATH; then + untrust_certificate $1 + sudo security add-trusted-cert -d -r trustAsRoot -k /Library/Keychains/System.keychain $CERT_PATH + else + echo -e "${WHITE}Error:${NC} $CERT_PATH does not exists!" >&2 + fi +} + +function untrust_certificate { + tld=$(get_tld) + + sudo security delete-certificate -c "$1.$tld" /Library/Keychains/System.keychain 2> /dev/null + sudo security delete-certificate -c "*.$1.$tld" /Library/Keychains/System.keychain 2> /dev/null +} + +function untrust_old_certificates { + if [ -d $BUTLERRUNPATH/$VALET_HOME/Certificates-old ]; then + for filePath in $BUTLERRUNPATH/$VALET_HOME/Certificates-old/*.crt; do + fileName=$(basename $filePath) + domain="${fileName%.*}" + domainWithoutTLD="${domain%.*}" + untrust_certificate $domainWithoutTLD + done + fi + + rm -rf $BUTLERRUNPATH/$VALET_HOME/Certificates-old 2> /dev/null +} + +function valet_tld { + shift 1 + workdir=$(get_workdir) + TLDCHANGE="no" + # if the $1 is not empty, then we are requesting tld change + if [ ! -z $1 ]; then + TLDCHANGE="yes" + + # backup old certificates + backup_old_certificates + # untrust those cert + untrust_old_certificates + fi + + # pass to valet + docker exec -i \ + -w $workdir \ + butler_php_1 \ + valet tld "$@" + + if [ $TLDCHANGE == "yes" ]; then + trust_new_certificates + reload_dnsmasq + reload_webserver + fi +} + +function trust_new_certificates { + if [ -d $BUTLERRUNPATH/$VALET_HOME/Certificates ]; then + for filePath in $BUTLERRUNPATH/$VALET_HOME/Certificates/*.crt; do + fileName=$(basename $filePath) + domain="${fileName%.*}" + domainWithoutTLD="${domain%.*}" + trust_certificate $domainWithoutTLD + done + fi +} + +function reload_webserver { + docker restart butler_webserver_1 > /dev/null 2>&1 +} + +function reload_dnsmasq { + docker restart butler_dns_1 > /dev/null 2>&1 +} + +function backup_old_certificates { + cp -r $BUTLERRUNPATH/$VALET_HOME/Certificates $BUTLERRUNPATH/$VALET_HOME/Certificates-old +} + +function get_env_value { + VALUE=$(cat $BUTLERRUNPATH/.env | grep -o "$1=.*" | cut -f2- -d =) + echo $VALUE +} + +function get_workdir { + www_path=$(get_env_value DEFAULT_WWW_PATH) + + if [[ $CURRENTDIR/ = $www_path/* ]]; then + workdir=${CURRENTDIR//$www_path/} + + if [ "$workdir" == "" ]; then + workdir="/" + fi + + echo "/var/www$workdir" + else + echo "/var/www" + fi +} + +# Verify operating system is supported... We add it here in case we wanted to support Linux and Windows too +case "${UNAMEOUT}" in + Darwin*) MACHINE=mac;; + *) MACHINE="UNKNOWN" +esac + +if [ "$MACHINE" == "UNKNOWN" ]; then + echo -e "${WHITE}Unsupported operating system [$(uname -s)]. Butler only supports macOS${NC}" >&2 + + exit 1 +fi + +# Ensure that Docker is running... +if ! docker info > /dev/null 2>&1; then + echo -e "${WHITE}Docker is not running.${NC}" >&2 + + exit 1 +fi + + +if [ ! -d $BUTLERRUNPATH ] +then + echo -e "${WHITE}Uh oh... Butler directory does not exist. Please reinstall Butler..${NC}" >&2 + + exit 1 +fi + +cd $BUTLERRUNPATH + +# Need to make sure .env exist +if ! is_file_exist $BUTLERRUNPATH/.env; then + echo -e "${WHITE}Uh oh... .env file not exists. Please run install.sh to reinstall${NC}" >&2 + exit 1 +fi + +if [ "$1" == "start" ];then + docker-compose -p butler up -d + echo -e "${WHITE}Butler process started...${NC}" >&2 + exit 0 +elif [ "$1" == "reset" ];then + echo -e "${WHITE}Remove all butler related processes...${NC}" >&2 + docker-compose -p butler down + echo -e "${WHITE}Adding back butler processes...${NC}" >&2 + docker-compose -p butler up -d + exit 0 +elif [ "$1" == "restart" ];then + echo -e "${WHITE}Restart all butler related processes...${NC}" >&2 + docker-compose -p butler restart + exit 0 +elif [ "$1" == "reload" ];then + echo -e "${WHITE}Reload all butler related processes...${NC}" >&2 + docker-compose -p butler up -d + exit 0 +elif [ "$1" == "stop" ];then + echo -e "${WHITE}Stopping all butler related processes...${NC}" >&2 + docker-compose -p butler down + exit 0 +fi + + +PSRESULT="$(docker ps | grep butler | wc -l)" + +if [[ PSRESULT -lt 2 ]]; then + echo -e "${WHITE}Shutting down old Butler processes...${NC}" >&2 + + docker-compose -p butler down > /dev/null 2>&1 + + butler_is_not_running + + exit 1 +fi + + +# # Proxy PHP commands to the "php" binary on the application container... +if [[ "$1" == "php" || "$1" == "valet" || $1 == "composer" || $1 == "artisan" ]]; then + COMMAND=$1 + workdir=$(get_workdir) + shift 1 + + if [[ $COMMAND == "valet" && $1 == "secure" ]]; then + shift 1 + # We need to install CA cert to keychain. so we interfere valet secure and only after that we pass to valet command + # Do the CA installation here + generate_ca + + if [ -z $1 ]; then + CN=$(basename $CURRENTDIR) + else + CN=$1 + fi + + docker exec -i \ + -w $workdir \ + butler_php_1 \ + valet secure $CN + + trust_certificate $CN + reload_webserver + elif [[ $COMMAND == "valet" && $1 == "unsecure" ]]; then + shift 1 + if [ -z $1 ]; then + CN=$(basename $CURRENTDIR) + else + CN=$1 + fi + + docker exec -i \ + -w $workdir \ + butler_php_1 \ + valet unsecure $CN + + untrust_certificate $CN + reload_webserver + elif [[ $COMMAND == "valet" && $1 == "proxy" ]]; then + shift 1 + + docker exec -i \ + butler_php_1 \ + valet proxy "$@" + reload_webserver + elif [[ $COMMAND == "valet" && $1 == "unproxy" ]]; then + shift 1 + + docker exec -i \ + -w $workdir \ + butler_php_1 \ + valet unproxy "$@" + reload_webserver + elif [[ $COMMAND == "valet" && $1 == "tld" ]]; then + valet_tld "$@" + elif [[ $COMMAND == "artisan" || $command == "art" ]]; then + shift 1 + docker exec -i \ + -w $workdir \ + butler_php_1 \ + php artisan "$@" + else + docker exec -i \ + -w $workdir \ + butler_php_1 \ + $COMMAND "$@" + fi +elif [ "$1" == "tld" ];then + valet_tld "$@" +elif [ "$1" == "down" ];then + docker-compose -p butler stop +else + workdir=$(get_workdir) + + docker exec -i \ + -w $workdir \ + butler_php_1 \ + valet "$@" +fi \ No newline at end of file diff --git a/docker/dnsmasq/Dockerfile b/docker/dnsmasq/Dockerfile new file mode 100644 index 0000000..fa1a0c9 --- /dev/null +++ b/docker/dnsmasq/Dockerfile @@ -0,0 +1,24 @@ + +FROM alpine:edge +LABEL maintainer="fikri@runcloud.io" + +ENV BUILD_DEPS="gettext" \ + RUNTIME_DEPS="libintl" + +RUN set -x \ + && mkdir -p /etc/default/ \ + && mkdir -p /etc/dnsmasq-templates \ + && echo -e "ENABLED=1\nIGNORE_RESOLVCONF=yes" > /etc/default/dnsmasq \ + && apk --no-cache add dnsmasq \ + && apk add --update $RUNTIME_DEPS \ + && apk add --virtual build_deps $BUILD_DEPS \ + && cp /usr/bin/envsubst /usr/local/bin/envsubst \ + && apk del build_deps + +COPY ./dnsmasq.conf /etc/dnsmasq.conf +COPY ./docker-entrypoint.sh /docker-entrypoint.sh + +# ENTRYPOINT ["dnsmasq", "-k"] +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD [ "dnsmasq", "-k" ] \ No newline at end of file diff --git a/docker/dnsmasq/dnsmasq.conf b/docker/dnsmasq/dnsmasq.conf new file mode 100644 index 0000000..3b864b8 --- /dev/null +++ b/docker/dnsmasq/dnsmasq.conf @@ -0,0 +1,13 @@ +#dnsmasq config, for a complete example, see: +# http://oss.segetech.com/intra/srv/dnsmasq.conf +#log all dns queries +log-queries +#dont use hosts nameservers +no-resolv +#use cloudflare as default nameservers, prefer 1^4 +server=1.0.0.1 +server=1.1.1.1 +strict-order +conf-dir=/etc/dnsmasq.d/,*.conf +#serve all .poen queries using a specific nameserver +# address=/.poen/127.0.0.1 diff --git a/docker/dnsmasq/docker-entrypoint.sh b/docker/dnsmasq/docker-entrypoint.sh new file mode 100755 index 0000000..a179869 --- /dev/null +++ b/docker/dnsmasq/docker-entrypoint.sh @@ -0,0 +1,27 @@ +#!/bin/sh +# vim:sw=4:ts=4:et + +set -e + +echo $1 +exec 3>&1 + +template_dir="/etc/dnsmasq-templates" +output_dir="/etc/dnsmasq.d" +suffix=".template" + +defined_envs=$(printf '${%s} ' $(env | cut -d= -f1)) + +find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do + relative_path="${template#$template_dir/}" + output_path="$output_dir/${relative_path%$suffix}" + subdir=$(dirname "$relative_path") + # create a subdirectory where the template file exists + mkdir -p "$output_dir/$subdir" + echo >&2 "$ME: Running envsubst on $template to $output_path" + envsubst "$defined_envs" < "$template" > "$output_path" +done + +echo >&2 "$0: Configuration complete; ready for start up" + +exec "$@" \ No newline at end of file diff --git a/docker/openresty/Dockerfile b/docker/openresty/Dockerfile new file mode 100644 index 0000000..e784a57 --- /dev/null +++ b/docker/openresty/Dockerfile @@ -0,0 +1,13 @@ +FROM openresty/openresty:alpine-fat + +COPY entrypoint/docker-entrypoint.sh / +COPY entrypoint/10-envsubst-on-templates.sh /docker-entrypoint.d/10-envsubst-on-templates.sh +RUN ln -s /usr/local/openresty/bin/openresty /bin/openresty +ENTRYPOINT ["/docker-entrypoint.sh"] + +CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"] + + +# Use SIGQUIT instead of default SIGTERM to cleanly drain requests +# See https://github.com/openresty/docker-openresty/blob/master/README.md#tips--pitfalls +STOPSIGNAL SIGQUIT \ No newline at end of file diff --git a/docker/openresty/entrypoint/10-envsubst-on-templates.sh b/docker/openresty/entrypoint/10-envsubst-on-templates.sh new file mode 100755 index 0000000..8c0b504 --- /dev/null +++ b/docker/openresty/entrypoint/10-envsubst-on-templates.sh @@ -0,0 +1,39 @@ +#!/bin/sh + +set -e + +ME=$(basename $0) + +auto_envsubst() { + local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}" + local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}" + local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}" + local NGINXCONFPATH="/usr/local/openresty/nginx/conf/nginx.conf" + + local template defined_envs relative_path output_path subdir + defined_envs=$(printf '${%s} ' $(env | cut -d= -f1)) + [ -d "$template_dir" ] || return 0 + if [ ! -w "$output_dir" ]; then + echo >&2 "$ME: ERROR: $template_dir exists, but $output_dir is not writable" + return 0 + fi + find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do + relative_path="${template#$template_dir/}" + output_path="$output_dir/${relative_path%$suffix}" + subdir=$(dirname "$relative_path") + # create a subdirectory where the template file exists + mkdir -p "$output_dir/$subdir" + if [ $template == "/etc/nginx/templates/nginx.conf.template" ]; then + echo >&2 "$ME: Running envsubst on $template to $NGINXCONFPATH" + envsubst "$defined_envs" < "$template" > "$NGINXCONFPATH" + else + echo >&2 "$ME: Running envsubst on $template to $output_path" + envsubst "$defined_envs" < "$template" > "$output_path" + fi + done +} + +auto_envsubst + + +exit 0 \ No newline at end of file diff --git a/docker/openresty/entrypoint/docker-entrypoint.sh b/docker/openresty/entrypoint/docker-entrypoint.sh new file mode 100755 index 0000000..f18699a --- /dev/null +++ b/docker/openresty/entrypoint/docker-entrypoint.sh @@ -0,0 +1,40 @@ +#!/bin/sh +# vim:sw=4:ts=4:et + +set -e + +echo $1 + +if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + exec 3>&1 +else + exec 3>/dev/null +fi + +if [ "$1" = "nginx" -o "$1" = "nginx-debug" -o "$1" = "/usr/local/openresty/bin/openresty" ]; then + if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + echo >&2 "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" + + echo >&2 "$0: Looking for shell scripts in /docker-entrypoint.d/" + find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do + case "$f" in + *.sh) + if [ -x "$f" ]; then + echo >&2 "$0: Launching $f"; + "$f" + else + # warn on shell scripts without exec bit + echo >&2 "$0: Ignoring $f, not executable"; + fi + ;; + *) echo >&2 "$0: Ignoring $f";; + esac + done + + echo >&2 "$0: Configuration complete; ready for start up" + else + echo >&2 "$0: No files found in /docker-entrypoint.d/, skipping configuration" + fi +fi + +exec "$@" \ No newline at end of file diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..6d0cddd --- /dev/null +++ b/install.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +VALET_HOME="./.valet-home" +WHITE='\033[1;37m' +NC='\033[0m' + +echo -e "${WHITE} +Butler (Laravel Valet for Docker) +================================= + +This will install Butler (Laravel Valet for Docker) + +Although it is a replacement for the original Laravel Valet, it is not a drop-in replacement. +It means you need to disable current running Laravel Valet either by uninstall it or by running valet stop. + +If you are not running Laravel Valet, please make sure port 80, 443, 53 inside your MacOS is not in use. +${NC}" + + + +while true; do + read -p "Do you wish to install continue this installation? [Y/n]" yn + case $yn in + [Yy]* ) break;; + [Nn]* ) exit;; + * ) echo "Please answer yes or no.";; + esac +done + +# Ensure that Docker is running... +if ! docker info > /dev/null 2>&1; then + echo -e "${WHITE}Docker is not running.${NC}" >&2 + + exit 1 +fi + + +mkdir -p www +mkdir -p $VALET_HOME/{CA,Certificates,Drivers,Extensions,Log,Nginx,Sites,dnsmasq.d} +touch $VALET_HOME/Log/nginx-error.log +cp ./valet/cli/stubs/SampleValetDriver.php $VALET_HOME/Drivers/SampleValetDriver.php + +# copy config file if not exists +if [ ! -f $VALET_HOME/config.json ]; then + cp ./stubs/config.json $VALET_HOME/config.json +fi + +if [ ! -f .env ]; then + cp ./stubs/.env .env + sed -i '' "s|REPLACEME|$PWD/www|g" ./.env +fi + +if [ ! -f docker-compose.yaml ]; then + cp ./stubs/docker-compose.yaml ./docker-compose.yaml +fi + + +sed "s|REPLACEME|$PWD|g" ./bin/butler > ./butler +chmod +x ./butler +mv ./butler /usr/local/bin/butler +echo "Waiting for Butler services to start..." +butler start +docker exec -i -w /valet/master butler_php_1 composer install -vvv +butler install + +echo -e "${WHITE} + + +Please make sure to set 127.0.0.1 inside your DNS setting for custom domain to work + + +${NC}" \ No newline at end of file diff --git a/stubs/.env b/stubs/.env new file mode 100644 index 0000000..3e59ba5 --- /dev/null +++ b/stubs/.env @@ -0,0 +1,7 @@ +BUTLER_PHP_VERSION=8.0 +DEFAULT_WWW_PATH=REPLACEME +WORK_DIR_PATH=/var/www +VALET_PATH=/valet/master +VALET_HOME_PATH=/valet/home +VALET_STATIC_PREFIX=41c270e4-5535-4daa-b23e-c269744c2f45 +VALET_SERVER_PATH=${VALET_PATH}/server.php \ No newline at end of file diff --git a/stubs/config.json b/stubs/config.json new file mode 100644 index 0000000..3f7dfe9 --- /dev/null +++ b/stubs/config.json @@ -0,0 +1,6 @@ +{ + "tld": "test", + "paths": [ + "/var/www/default" + ] +} diff --git a/stubs/docker-compose.yaml b/stubs/docker-compose.yaml new file mode 100644 index 0000000..58ca793 --- /dev/null +++ b/stubs/docker-compose.yaml @@ -0,0 +1,50 @@ +version: '3.9' +x-enviroment: &commonEnvironment + - VALET_HOME_PATH: ${VALET_HOME_PATH} + - VALET_STATIC_PREFIX: ${VALET_STATIC_PREFIX} + - VALET_SERVER_PATH: ${VALET_SERVER_PATH} + - PATH: /valet/master:$PATH + +services: + webserver: + build: + context: docker/openresty + dockerfile: Dockerfile + ports: + - 80:80 + - 443:443 + working_dir: ${WORK_DIR_PATH} + volumes_from: + - php + volumes: + - ./templates/nginx:/etc/nginx/templates + environment: + <<: *commonEnvironment + depends_on: + - php + + php: + image: jtreminio/php:${BUTLER_PHP_VERSION} + environment: + <<: *commonEnvironment + working_dir: ${WORK_DIR_PATH} + volumes: + - composer:/./composer + - ./valet:${VALET_PATH} + - ./.valet-home:${VALET_HOME_PATH} + - ${DEFAULT_WWW_PATH}:${WORK_DIR_PATH} + + dns: + build: + context: docker/dnsmasq + dockerfile: Dockerfile + ports: + - 53:53/udp + environment: + <<: *commonEnvironment + volumes: + - ./templates/dnsmasq:/etc/dnsmasq-templates + volumes_from: + - php +volumes: + composer: \ No newline at end of file diff --git a/templates/dnsmasq/dnsmasq-valet.conf.template b/templates/dnsmasq/dnsmasq-valet.conf.template new file mode 100644 index 0000000..ba192df --- /dev/null +++ b/templates/dnsmasq/dnsmasq-valet.conf.template @@ -0,0 +1,3 @@ +# Valet +# Include all *.conf files in user's valet directory +conf-dir=${VALET_HOME_PATH}/dnsmasq.d/,*.conf \ No newline at end of file diff --git a/templates/nginx/nginx.conf.template b/templates/nginx/nginx.conf.template new file mode 100644 index 0000000..14ae2c3 --- /dev/null +++ b/templates/nginx/nginx.conf.template @@ -0,0 +1,53 @@ +user root root; +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + client_body_temp_path /var/run/openresty/nginx-client-body; + proxy_temp_path /var/run/openresty/nginx-proxy; + fastcgi_temp_path /var/run/openresty/nginx-fastcgi; + uwsgi_temp_path /var/run/openresty/nginx-uwsgi; + scgi_temp_path /var/run/openresty/nginx-scgi; + + sendfile on; + keepalive_timeout 65; + types_hash_max_size 2048; + + client_max_body_size 512M; + + server_names_hash_bucket_size 128; + + ssi on; + + gzip on; + gzip_comp_level 5; + gzip_min_length 256; + gzip_proxied any; + gzip_vary on; + gzip_types + application/atom+xml + application/javascript + application/json + application/rss+xml + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/opentype + image/svg+xml + image/x-icon + text/css + text/plain + text/x-component; + + include "${VALET_HOME_PATH}/Nginx/*"; + include servers/*; + include /etc/nginx/conf.d/valet.conf; +} diff --git a/templates/nginx/valet.conf.template b/templates/nginx/valet.conf.template new file mode 100644 index 0000000..8bee87a --- /dev/null +++ b/templates/nginx/valet.conf.template @@ -0,0 +1,37 @@ +server { + listen 0.0.0.0:80 default_server; + root /; + charset utf-8; + client_max_body_size 128M; + + location /${VALET_STATIC_PREFIX}/ { + internal; + alias /; + try_files $uri $uri/; + } + + location / { + rewrite ^ "${VALET_SERVER_PATH}" last; + } + + 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/nginx-error.log"; + + error_page 404 "${VALET_SERVER_PATH}"; + + location ~ [^/]\.php(/|$) { + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass php:9000; + fastcgi_index "${VALET_SERVER_PATH}"; + include fastcgi_params; + fastcgi_param SCRIPT_FILENAME "${VALET_SERVER_PATH}"; + fastcgi_param PATH_INFO $fastcgi_path_info; + } + + location ~ /\.ht { + deny all; + } +} diff --git a/valet/cli/Valet/Brew.php b/valet/cli/Valet/Brew.php new file mode 100755 index 0000000..648ee68 --- /dev/null +++ b/valet/cli/Valet/Brew.php @@ -0,0 +1,420 @@ +cli = $cli; + $this->files = $files; + } + + /** + * Ensure the formula exists in the current Homebrew configuration + * + * @param string $formula + * @return bool + */ + function installed($formula) + { + $result = $this->cli->runAsUser("brew info $formula --json"); + + // should be a json response, but if not installed then "Error: No available formula ..." + if (starts_with($result, 'Error: No')) { + return false; + } + + $details = json_decode($result); + + return !empty($details[0]->installed); + } + + /** + * Determine if a compatible PHP version is Homebrewed. + * + * @return bool + */ + function hasInstalledPhp() + { + $installed = $this->installedPhpFormulae()->first(function ($formula) { + return $this->supportedPhpVersions()->contains($formula); + }); + + return !empty($installed); + } + + /** + * Get a list of supported PHP versions. + * + * @return \Illuminate\Support\Collection + */ + function supportedPhpVersions() + { + return collect(static::SUPPORTED_PHP_VERSIONS); + } + + function installedPhpFormulae() + { + return collect( + explode(PHP_EOL, $this->cli->runAsUser('brew list --formula | grep php')) + ); + } + + /** + * Get the aliased formula version from Homebrew + */ + function determineAliasedVersion($formula) + { + $details = json_decode($this->cli->runAsUser("brew info $formula --json")); + + if (!empty($details[0]->aliases[0])) { + return $details[0]->aliases[0]; + } + + return 'ERROR - NO BREW ALIAS FOUND'; + } + + /** + * Determine if a compatible nginx version is Homebrewed. + * + * @return bool + */ + function hasInstalledNginx() + { + return $this->installed('nginx') + || $this->installed('nginx-full'); + } + + /** + * Return name of the nginx service installed via Homebrew. + * + * @return string + */ + function nginxServiceName() + { + return $this->installed('nginx-full') ? 'nginx-full' : 'nginx'; + } + + /** + * Ensure that the given formula is installed. + * + * @param string $formula + * @param array $options + * @param array $taps + * @return void + */ + function ensureInstalled($formula, $options = [], $taps = []) + { + if (! $this->installed($formula)) { + $this->installOrFail($formula, $options, $taps); + } + } + + /** + * Install the given formula and throw an exception on failure. + * + * @param string $formula + * @param array $options + * @param array $taps + * @return void + */ + function installOrFail($formula, $options = [], $taps = []) + { + info("Installing {$formula}..."); + + if (count($taps) > 0) { + $this->tap($taps); + } + + output('['.$formula.'] is not installed, installing it now via Brew... 🍻'); + if ($formula !== 'php' && starts_with($formula, 'php') && preg_replace('/[^\d]/', '', $formula) < '73') { + warning('Note: older PHP versions may take 10+ minutes to compile from source. Please wait ...'); + } + + $this->cli->runAsUser(trim('brew install '.$formula.' '.implode(' ', $options)), function ($exitCode, $errorOutput) use ($formula) { + output($errorOutput); + + throw new DomainException('Brew was unable to install ['.$formula.'].'); + }); + } + + /** + * Tap the given formulas. + * + * @param dynamic[string] $formula + * @return void + */ + function tap($formulas) + { + $formulas = is_array($formulas) ? $formulas : func_get_args(); + + foreach ($formulas as $formula) { + $this->cli->passthru('sudo -u "'.user().'" brew tap '.$formula); + } + } + + /** + * Restart the given Homebrew services. + * + * @param + */ + function restartService($services) + { + $services = is_array($services) ? $services : func_get_args(); + + foreach ($services as $service) { + if ($this->installed($service)) { + info("Restarting {$service}..."); + + $this->cli->quietly('sudo brew services stop '.$service); + $this->cli->quietly('sudo brew services start '.$service); + } + } + } + + /** + * Stop the given Homebrew services. + * + * @param + */ + function stopService($services) + { + $services = is_array($services) ? $services : func_get_args(); + + foreach ($services as $service) { + if ($this->installed($service)) { + info("Stopping {$service}..."); + + $this->cli->quietly('sudo brew services stop '.$service); + } + } + } + + /** + * Determine if php is currently linked. + * + * @return bool + */ + function hasLinkedPhp() + { + return $this->files->isLink(BREW_PREFIX.'/bin/php'); + } + + /** + * Get the linked php parsed. + * + * @return mixed + */ + function getParsedLinkedPhp() + { + if (! $this->hasLinkedPhp()) { + throw new DomainException("Homebrew PHP appears not to be linked. Please run [valet use php@X.Y]"); + } + + $resolvedPath = $this->files->readLink(BREW_PREFIX.'/bin/php'); + + /** + * Typical homebrew path resolutions are like: + * "../Cellar/php@7.4/7.4.13/bin/php" + * or older styles: + * "../Cellar/php/7.4.9_2/bin/php + * "../Cellar/php55/bin/php + */ + preg_match('~\w{3,}/(php)(@?\d\.?\d)?/(\d\.\d)?([_\d\.]*)?/?\w{3,}~', $resolvedPath, $matches); + + return $matches; + } + + /** + * Gets the currently linked formula by identifying the symlink in the hombrew bin directory. + * Different to ->linkedPhp() in that this will just get the linked directory name, + * whether that is php, php74 or php@7.4 + * + * @return string + */ + function getLinkedPhpFormula() + { + $matches = $this->getParsedLinkedPhp(); + return $matches[1] . $matches[2]; + } + + /** + * Determine which version of PHP is linked in Homebrew. + * + * @return string + */ + function linkedPhp() + { + $matches = $this->getParsedLinkedPhp(); + $resolvedPhpVersion = $matches[3] ?: $matches[2]; + + return $this->supportedPhpVersions()->first( + function ($version) use ($resolvedPhpVersion) { + $resolvedVersionNormalized = preg_replace('/[^\d]/', '', $resolvedPhpVersion); + $versionNormalized = preg_replace('/[^\d]/', '', $version); + return $resolvedVersionNormalized === $versionNormalized; + }, function () use ($resolvedPhpVersion) { + throw new DomainException("Unable to determine linked PHP when parsing '$resolvedPhpVersion'"); + }); + } + + /** + * Restart the linked PHP-FPM Homebrew service. + * + * @return void + */ + function restartLinkedPhp() + { + $this->restartService($this->getLinkedPhpFormula()); + } + + /** + * Create the "sudoers.d" entry for running Brew. + * + * @return void + */ + function createSudoersEntry() + { + $this->files->ensureDirExists('/etc/sudoers.d'); + + $this->files->put('/etc/sudoers.d/brew', 'Cmnd_Alias BREW = '.BREW_PREFIX.'/bin/brew * +%admin ALL=(root) NOPASSWD:SETENV: BREW'.PHP_EOL); + } + + /** + * Remove the "sudoers.d" entry for running Brew. + * + * @return void + */ + function removeSudoersEntry() + { + $this->cli->quietly('rm /etc/sudoers.d/brew'); + } + + /** + * Link passed formula. + * + * @param $formula + * @param bool $force + * + * @return string + */ + function link($formula, $force = false) + { + return $this->cli->runAsUser( + sprintf('brew link %s%s', $formula, $force ? ' --force': ''), + function ($exitCode, $errorOutput) use ($formula) { + output($errorOutput); + + throw new DomainException('Brew was unable to link [' . $formula . '].'); + } + ); + } + + /** + * Unlink passed formula. + * @param $formula + * + * @return string + */ + function unlink($formula) + { + return $this->cli->runAsUser( + sprintf('brew unlink %s', $formula), + function ($exitCode, $errorOutput) use ($formula) { + output($errorOutput); + + throw new DomainException('Brew was unable to unlink [' . $formula . '].'); + } + ); + } + + /** + * Get the currently running brew services. + * + * @return \Illuminate\Support\Collection + */ + function getRunningServices() + { + return collect(array_filter(explode(PHP_EOL, $this->cli->runAsUser( + 'brew services list | grep started | awk \'{ print $1; }\'', + function ($exitCode, $errorOutput) { + output($errorOutput); + + throw new DomainException('Brew was unable to check which services are running.'); + } + )))); + } + + /** + * Tell Homebrew to forcefully remove all PHP versions that Valet supports. + * + * @return string + */ + function uninstallAllPhpVersions() + { + $this->supportedPhpVersions()->each(function ($formula) { + $this->uninstallFormula($formula); + }); + + return 'PHP versions removed.'; + } + + /** + * Uninstall a Homebrew app by formula name. + * @param string $formula + * + * @return void + */ + function uninstallFormula($formula) + { + $this->cli->runAsUser('brew uninstall --force '.$formula); + $this->cli->run('rm -rf '.BREW_PREFIX.'/Cellar/'.$formula); + } + + /** + * Run Homebrew's cleanup commands. + * + * @return string + */ + function cleanupBrew() + { + return $this->cli->runAsUser( + 'brew cleanup && brew services cleanup', + function ($exitCode, $errorOutput) { + output($errorOutput); + } + ); + } +} diff --git a/valet/cli/Valet/CommandLine.php b/valet/cli/Valet/CommandLine.php new file mode 100755 index 0000000..ad913d1 --- /dev/null +++ b/valet/cli/Valet/CommandLine.php @@ -0,0 +1,98 @@ +runCommand($command . ' > /dev/null 2>&1'); + } + + /** + * Simple global function to run commands. + * + * @param string $command + * @return void + */ + public function quietlyAsUser($command) + { + $this->quietly('sudo -u "' . user() . '" ' . $command . ' > /dev/null 2>&1'); + } + + /** + * Pass the command to the command line and display the output. + * + * @param string $command + * @return void + */ + public function passthru($command) + { + passthru($command); + } + + /** + * Run the given command as the non-root user. + * + * @param string $command + * @param callable $onError + * @return string + */ + public function run($command, callable $onError = null) + { + return $this->runCommand($command, $onError); + } + + /** + * Run the given command. + * + * @param string $command + * @param callable $onError + * @return string + */ + public function runAsUser($command, callable $onError = null) + { + return $this->runCommand('sudo -u "' . user() . '" ' . $command, $onError); + } + + /** + * Run the given command. + * + * @param string $command + * @param callable $onError + * @return string + */ + public function runCommand($command, callable $onError = null) + { + $onError = $onError ?: function () {}; + + // Symfony's 4.x Process component has deprecated passing a command string + // to the constructor, but older versions (which Valet's Composer + // constraints allow) don't have the fromShellCommandLine method. + // For more information, see: https://github.com/laravel/valet/pull/761 + if (method_exists(Process::class, 'fromShellCommandline')) { + $process = Process::fromShellCommandline($command); + } else { + $process = new Process($command); + } + + $processOutput = ''; + $process->setTimeout(null)->run(function ($type, $line) use (&$processOutput) { + $processOutput .= $line; + }); + + if ($process->getExitCode() > 0) { + $onError($process->getExitCode(), $processOutput); + } + + return $processOutput; + } +} diff --git a/valet/cli/Valet/Configuration.php b/valet/cli/Valet/Configuration.php new file mode 100755 index 0000000..3b4ba98 --- /dev/null +++ b/valet/cli/Valet/Configuration.php @@ -0,0 +1,261 @@ +files = $files; + } + + /** + * Install the Valet configuration file. + * + * @return void + */ + function install() + { + $this->createConfigurationDirectory(); + $this->createDriversDirectory(); + $this->createSitesDirectory(); + $this->createExtensionsDirectory(); + $this->createLogDirectory(); + $this->createCertificatesDirectory(); + $this->writeBaseConfiguration(); + + $this->files->chown($this->path(), user()); + } + + /** + * Forcefully delete the Valet home configuration directory and contents. + * + * @return void + */ + function uninstall() + { + $this->files->unlink(VALET_HOME_PATH); + } + + /** + * Create the Valet configuration directory. + * + * @return void + */ + function createConfigurationDirectory() + { + $this->files->ensureDirExists(preg_replace('~/valet$~', '', VALET_HOME_PATH), user()); + + $oldPath = posix_getpwuid(fileowner(__FILE__))['dir'].'/.valet'; + + if ($this->files->isDir($oldPath)) { + rename($oldPath, VALET_HOME_PATH); + $this->prependPath(VALET_HOME_PATH.'/Sites'); + } + + $this->files->ensureDirExists(VALET_HOME_PATH, user()); + } + + /** + * Create the Valet drivers directory. + * + * @return void + */ + function createDriversDirectory() + { + if ($this->files->isDir($driversDirectory = VALET_HOME_PATH.'/Drivers')) { + return; + } + + $this->files->mkdirAsUser($driversDirectory); + + $this->files->putAsUser( + $driversDirectory.'/SampleValetDriver.php', + $this->files->get(__DIR__.'/../stubs/SampleValetDriver.php') + ); + } + + /** + * Create the Valet sites directory. + * + * @return void + */ + function createSitesDirectory() + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Sites', user()); + } + + /** + * Create the directory for the Valet extensions. + * + * @return void + */ + function createExtensionsDirectory() + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Extensions', user()); + } + + /** + * Create the directory for Nginx logs. + * + * @return void + */ + function createLogDirectory() + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Log', user()); + + $this->files->touch(VALET_HOME_PATH.'/Log/nginx-error.log'); + } + + /** + * Create the directory for SSL certificates. + * + * @return void + */ + function createCertificatesDirectory() + { + $this->files->ensureDirExists(VALET_HOME_PATH.'/Certificates', user()); + } + + /** + * Write the base, initial configuration for Valet. + */ + function writeBaseConfiguration() + { + if (! $this->files->exists($this->path())) { + $this->write(['tld' => 'test', 'paths' => []]); + } + + /** + * Migrate old configurations from 'domain' to 'tld' + */ + $config = $this->read(); + + if (isset($config['tld'])) { + return; + } + + $this->updateKey('tld', !empty($config['domain']) ? $config['domain'] : 'test'); + } + + /** + * Add the given path to the configuration. + * + * @param string $path + * @param bool $prepend + * @return void + */ + function addPath($path, $prepend = false) + { + $this->write(tap($this->read(), function (&$config) use ($path, $prepend) { + $method = $prepend ? 'prepend' : 'push'; + + $config['paths'] = collect($config['paths'])->{$method}($path)->unique()->all(); + })); + } + + /** + * Prepend the given path to the configuration. + * + * @param string $path + * @return void + */ + function prependPath($path) + { + $this->addPath($path, true); + } + + /** + * Remove the given path from the configuration. + * + * @param string $path + * @return void + */ + function removePath($path) + { + if ($path == VALET_HOME_PATH.'/Sites') { + info("Cannot remove this directory because this is where Valet stores its site definitions.\nRun [valet paths] for a list of parked paths."); + die(); + } + + $this->write(tap($this->read(), function (&$config) use ($path) { + $config['paths'] = collect($config['paths'])->reject(function ($value) use ($path) { + return $value === $path; + })->values()->all(); + })); + } + + /** + * Prune all non-existent paths from the configuration. + * + * @return void + */ + function prune() + { + if (! $this->files->exists($this->path())) { + return; + } + + $this->write(tap($this->read(), function (&$config) { + $config['paths'] = collect($config['paths'])->filter(function ($path) { + return $this->files->isDir($path); + })->values()->all(); + })); + } + + /** + * Read the configuration file as JSON. + * + * @return array + */ + function read() + { + return json_decode($this->files->get($this->path()), true); + } + + /** + * Update a specific key in the configuration file. + * + * @param string $key + * @param mixed $value + * @return array + */ + function updateKey($key, $value) + { + return tap($this->read(), function (&$config) use ($key, $value) { + $config[$key] = $value; + + $this->write($config); + }); + } + + /** + * Write the given configuration to disk. + * + * @param array $config + * @return void + */ + function write($config) + { + $this->files->putAsUser($this->path(), json_encode( + $config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES + ).PHP_EOL); + } + + /** + * Get the configuration file path. + * + * @return string + */ + function path() + { + return VALET_HOME_PATH.'/config.json'; + } +} diff --git a/valet/cli/Valet/Diagnose.php b/valet/cli/Valet/Diagnose.php new file mode 100755 index 0000000..9a8cc3a --- /dev/null +++ b/valet/cli/Valet/Diagnose.php @@ -0,0 +1,159 @@ + /dev/null 2>&1', + 'brew config', + 'brew services list', + 'brew list --formula --versions | grep -E "(php|nginx|dnsmasq|mariadb|mysql|mailhog|openssl)(@\d\..*)?\s"', + 'brew outdated', + 'brew tap', + 'php -v', + 'which -a php', + 'php --ini', + 'nginx -v', + 'curl --version', + 'php --ri curl', + '~/.composer/vendor/laravel/valet/bin/ngrok version', + 'ls -al ~/.ngrok2', + 'brew info nginx', + 'brew info php', + 'brew info openssl', + 'openssl version -a', + 'openssl ciphers', + 'sudo nginx -t', + 'which -a php-fpm', + BREW_PREFIX.'/opt/php/sbin/php-fpm -v', + 'sudo '.BREW_PREFIX.'/opt/php/sbin/php-fpm -y '.PHP_SYSCONFDIR.'/php-fpm.conf --test', + 'ls -al ~/Library/LaunchAgents | grep homebrew', + 'ls -al /Library/LaunchAgents | grep homebrew', + 'ls -al /Library/LaunchDaemons | grep homebrew', + 'ls -aln /etc/resolv.conf', + 'cat /etc/resolv.conf', + ]; + + var $cli, $files, $print, $progressBar; + + /** + * Create a new Diagnose instance. + * + * @param CommandLine $cli + * @param Filesystem $files + * @return void + */ + function __construct(CommandLine $cli, Filesystem $files) + { + $this->cli = $cli; + $this->files = $files; + } + + /** + * Run diagnostics. + */ + function run($print, $plainText) + { + $this->print = $print; + + $this->beforeRun(); + + $results = collect($this->commands)->map(function ($command) { + $this->beforeCommand($command); + + $output = $this->runCommand($command); + + if ($this->ignoreOutput($command)) return; + + $this->afterCommand($command, $output); + + return compact('command', 'output'); + })->filter()->values(); + + $output = $this->format($results, $plainText); + + $this->files->put('valet_diagnostics.txt', $output); + + $this->cli->run('pbcopy < valet_diagnostics.txt'); + + $this->files->unlink('valet_diagnostics.txt'); + + $this->afterRun(); + } + + function beforeRun() + { + if ($this->print) { + return; + } + + $this->progressBar = new ProgressBar(new ConsoleOutput, count($this->commands)); + + $this->progressBar->start(); + } + + function afterRun() + { + if ($this->progressBar) { + $this->progressBar->finish(); + } + + output(''); + } + + function runCommand($command) + { + return strpos($command, 'sudo ') === 0 + ? $this->cli->run($command) + : $this->cli->runAsUser($command); + } + + function beforeCommand($command) + { + if ($this->print) { + info(PHP_EOL."$ $command"); + } + } + + function afterCommand($command, $output) + { + if ($this->print) { + output(trim($output)); + } else { + $this->progressBar->advance(); + } + } + + function ignoreOutput($command) + { + return strpos($command, '> /dev/null 2>&1') !== false; + } + + function format($results, $plainText) + { + return $results->map(function ($result) use ($plainText) { + $command = $result['command']; + $output = trim($result['output']); + + if ($plainText) { + return implode(PHP_EOL, ["$ {$command}", $output]); + } + + return sprintf( + '
%s%s%s
%s
%s
', + PHP_EOL, $command, PHP_EOL, $output, PHP_EOL + ); + })->implode($plainText ? PHP_EOL.str_repeat('-', 20).PHP_EOL : PHP_EOL); + } +} diff --git a/valet/cli/Valet/DnsMasq.php b/valet/cli/Valet/DnsMasq.php new file mode 100755 index 0000000..38d8432 --- /dev/null +++ b/valet/cli/Valet/DnsMasq.php @@ -0,0 +1,112 @@ +cli = $cli; + $this->brew = $brew; + $this->files = $files; + $this->configuration = $configuration; + } + + /** + * Install and configure DnsMasq. + * + * @return void + */ + public function install($tld = 'test') + { + // For DnsMasq, we enable its feature of loading *.conf from /usr/local/etc/dnsmasq.d/ + // and then we put a valet config file in there to point to the user's home .config/valet/dnsmasq.d + // This allows Valet to make changes to our own files without needing to modify the core dnsmasq configs + $this->ensureUsingDnsmasqDForConfigs(); + + $this->createDnsmasqTldConfigFile($tld); + + info('Valet is configured to serve for TLD [.' . $tld . ']'); + } + + /** + * Forcefully uninstall dnsmasq. + * + * @return void + */ + public function uninstall() + { + $this->brew->stopService('dnsmasq'); + $this->brew->uninstallFormula('dnsmasq'); + $this->cli->run('rm -rf ' . BREW_PREFIX . '/etc/dnsmasq.d/dnsmasq-valet.conf'); + $tld = $this->configuration->read()['tld']; + $this->files->unlink($this->resolverPath . '/' . $tld); + } + + /** + * Tell Homebrew to restart dnsmasq + * + * @return void + */ + public function restart() + { + $this->brew->restartService('dnsmasq'); + } + + /** + * Ensure the DnsMasq configuration primary config is set to read custom configs + * + * @return void + */ + public function ensureUsingDnsmasqDForConfigs() + { + info('Updating Dnsmasq configuration...'); + + $this->files->ensureDirExists(VALET_HOME_PATH . '/dnsmasq.d', user()); + } + + /** + * Create the TLD-specific dnsmasq config file + * @param string $tld + * @return void + */ + public function createDnsmasqTldConfigFile($tld) + { + $tldConfigFile = $this->dnsmasqUserConfigDir() . 'tld-' . $tld . '.conf'; + + $this->files->putAsUser($tldConfigFile, 'address=/.' . $tld . '/127.0.0.1' . PHP_EOL); + } + + /** + * Update the TLD/domain resolved by DnsMasq. + * + * @param string $oldTld + * @param string $newTld + * @return void + */ + public function updateTld($oldTld, $newTld) + { + $this->files->unlink($this->resolverPath . '/' . $oldTld); + $this->files->unlink($this->dnsmasqUserConfigDir() . 'tld-' . $oldTld . '.conf'); + + $this->install($newTld); + } + + /** + * Get the custom configuration path. + * + * @return string + */ + public function dnsmasqUserConfigDir() + { + return VALET_HOME_PATH . '/dnsmasq.d/'; + } +} diff --git a/valet/cli/Valet/Filesystem.php b/valet/cli/Valet/Filesystem.php new file mode 100755 index 0000000..27abd28 --- /dev/null +++ b/valet/cli/Valet/Filesystem.php @@ -0,0 +1,342 @@ +chown($path, $owner); + } + } + + /** + * Ensure that the given directory exists. + * + * @param string $path + * @param string|null $owner + * @param int $mode + * @return void + */ + function ensureDirExists($path, $owner = null, $mode = 0755) + { + if (! $this->isDir($path)) { + $this->mkdir($path, $owner, $mode); + } + } + + /** + * Create a directory as the non-root user. + * + * @param string $path + * @param int $mode + * @return void + */ + function mkdirAsUser($path, $mode = 0755) + { + $this->mkdir($path, user(), $mode); + } + + /** + * Touch the given path. + * + * @param string $path + * @param string|null $owner + * @return string + */ + function touch($path, $owner = null) + { + touch($path); + + if ($owner) { + $this->chown($path, $owner); + } + + return $path; + } + + /** + * Touch the given path as the non-root user. + * + * @param string $path + * @return void + */ + function touchAsUser($path) + { + return $this->touch($path, user()); + } + + /** + * Determine if the given file exists. + * + * @param string $path + * @return bool + */ + function exists($path) + { + return file_exists($path); + } + + /** + * Read the contents of the given file. + * + * @param string $path + * @return string + */ + function get($path) + { + return file_get_contents($path); + } + + /** + * Write to the given file. + * + * @param string $path + * @param string $contents + * @param string|null $owner + * @return void + */ + function put($path, $contents, $owner = null) + { + file_put_contents($path, $contents); + + if ($owner) { + $this->chown($path, $owner); + } + } + + /** + * Write to the given file as the non-root user. + * + * @param string $path + * @param string $contents + * @return void + */ + function putAsUser($path, $contents) + { + $this->put($path, $contents, user()); + } + + /** + * Append the contents to the given file. + * + * @param string $path + * @param string $contents + * @param string|null $owner + * @return void + */ + function append($path, $contents, $owner = null) + { + file_put_contents($path, $contents, FILE_APPEND); + + if ($owner) { + $this->chown($path, $owner); + } + } + + /** + * Append the contents to the given file as the non-root user. + * + * @param string $path + * @param string $contents + * @return void + */ + function appendAsUser($path, $contents) + { + $this->append($path, $contents, user()); + } + + /** + * Copy the given file to a new location. + * + * @param string $from + * @param string $to + * @return void + */ + function copy($from, $to) + { + copy($from, $to); + } + + /** + * Copy the given file to a new location for the non-root user. + * + * @param string $from + * @param string $to + * @return void + */ + function copyAsUser($from, $to) + { + copy($from, $to); + + $this->chown($to, user()); + } + + /** + * Create a symlink to the given target. + * + * @param string $target + * @param string $link + * @return void + */ + function symlink($target, $link) + { + if ($this->exists($link)) { + $this->unlink($link); + } + + symlink($target, $link); + } + + /** + * Create a symlink to the given target for the non-root user. + * + * This uses the command line as PHP can't change symlink permissions. + * + * @param string $target + * @param string $link + * @return void + */ + function symlinkAsUser($target, $link) + { + if ($this->exists($link)) { + $this->unlink($link); + } + + CommandLineFacade::runAsUser('ln -s '.escapeshellarg($target).' '.escapeshellarg($link)); + } + + /** + * Delete the file at the given path. + * + * @param string $path + * @return void + */ + function unlink($path) + { + if (file_exists($path) || is_link($path)) { + @unlink($path); + } + } + + /** + * Change the owner of the given path. + * + * @param string $path + * @param string $user + */ + function chown($path, $user) + { + chown($path, $user); + } + + /** + * Change the group of the given path. + * + * @param string $path + * @param string $group + */ + function chgrp($path, $group) + { + chgrp($path, $group); + } + + /** + * Resolve the given path. + * + * @param string $path + * @return string + */ + function realpath($path) + { + return realpath($path); + } + + /** + * Determine if the given path is a symbolic link. + * + * @param string $path + * @return bool + */ + function isLink($path) + { + return is_link($path); + } + + /** + * Resolve the given symbolic link. + * + * @param string $path + * @return string + */ + function readLink($path) + { + return readlink($path); + } + + /** + * Remove all of the broken symbolic links at the given path. + * + * @param string $path + * @return void + */ + function removeBrokenLinksAt($path) + { + collect($this->scandir($path)) + ->filter(function ($file) use ($path) { + return $this->isBrokenLink($path.'/'.$file); + }) + ->each(function ($file) use ($path) { + $this->unlink($path.'/'.$file); + }); + } + + /** + * Determine if the given path is a broken symbolic link. + * + * @param string $path + * @return bool + */ + function isBrokenLink($path) + { + return is_link($path) && ! file_exists($path); + } + + /** + * Scan the given directory path. + * + * @param string $path + * @return array + */ + function scandir($path) + { + return collect(scandir($path)) + ->reject(function ($file) { + return in_array($file, ['.', '..']); + })->values()->all(); + } +} diff --git a/valet/cli/Valet/Nginx.php b/valet/cli/Valet/Nginx.php new file mode 100755 index 0000000..d6d0e67 --- /dev/null +++ b/valet/cli/Valet/Nginx.php @@ -0,0 +1,173 @@ +cli = $cli; + $this->brew = $brew; + $this->site = $site; + $this->files = $files; + $this->configuration = $configuration; + } + + /** + * Install the configuration files for Nginx. + * + * @return void + */ + function install() + { + if (!$this->brew->hasInstalledNginx()) { + $this->brew->installOrFail('nginx', []); + } + + $this->installConfiguration(); + $this->installServer(); + $this->installNginxDirectory(); + } + + /** + * Install the Nginx configuration file. + * + * @return void + */ + function installConfiguration() + { + info('Installing nginx configuration...'); + + $contents = $this->files->get(__DIR__.'/../stubs/nginx.conf'); + + $this->files->putAsUser( + static::NGINX_CONF, + str_replace(['VALET_USER', 'VALET_HOME_PATH'], [user(), VALET_HOME_PATH], $contents) + ); + } + + /** + * Install the Valet Nginx server configuration file. + * + * @return void + */ + function installServer() + { + $this->files->ensureDirExists(BREW_PREFIX.'/etc/nginx/valet'); + + $this->files->putAsUser( + BREW_PREFIX.'/etc/nginx/valet/valet.conf', + str_replace( + ['VALET_HOME_PATH', 'VALET_SERVER_PATH', 'VALET_STATIC_PREFIX'], + [VALET_HOME_PATH, VALET_SERVER_PATH, VALET_STATIC_PREFIX], + $this->files->get(__DIR__.'/../stubs/valet.conf') + ) + ); + + $this->files->putAsUser( + BREW_PREFIX.'/etc/nginx/fastcgi_params', + $this->files->get(__DIR__.'/../stubs/fastcgi_params') + ); + } + + /** + * Install the Nginx configuration directory to the ~/.config/valet directory. + * + * This directory contains all site-specific Nginx servers. + * + * @return void + */ + function installNginxDirectory() + { + info('Installing nginx directory...'); + + if (! $this->files->isDir($nginxDirectory = VALET_HOME_PATH.'/Nginx')) { + $this->files->mkdirAsUser($nginxDirectory); + } + + $this->files->putAsUser($nginxDirectory.'/.keep', "\n"); + + $this->rewriteSecureNginxFiles(); + } + + /** + * Check nginx.conf for errors. + */ + private function lint() + { + $this->cli->run( + 'sudo nginx -c '.static::NGINX_CONF.' -t', + function ($exitCode, $outputMessage) { + throw new DomainException("Nginx cannot start; please check your nginx.conf [$exitCode: $outputMessage]."); + } + ); + } + + /** + * Generate fresh Nginx servers for existing secure sites. + * + * @return void + */ + function rewriteSecureNginxFiles() + { + $tld = $this->configuration->read()['tld']; + + $this->site->resecureForNewTld($tld, $tld); + } + + /** + * Restart the Nginx service. + * + * @return void + */ + function restart() + { + $this->lint(); + + $this->brew->restartService($this->brew->nginxServiceName()); + } + + /** + * Stop the Nginx service. + * + * @return void + */ + function stop() + { + info('Stopping nginx...'); + + $this->cli->quietly('sudo brew services stop '. $this->brew->nginxServiceName()); + } + + /** + * Forcefully uninstall Nginx. + * + * @return void + */ + function uninstall() + { + $this->brew->stopService(['nginx', 'nginx-full']); + $this->brew->uninstallFormula('nginx nginx-full'); + $this->cli->quietly('rm -rf '.BREW_PREFIX.'/etc/nginx '.BREW_PREFIX.'/var/log/nginx'); + } +} diff --git a/valet/cli/Valet/Ngrok.php b/valet/cli/Valet/Ngrok.php new file mode 100755 index 0000000..25ee367 --- /dev/null +++ b/valet/cli/Valet/Ngrok.php @@ -0,0 +1,59 @@ +tunnelsEndpoints as $endpoint) { + $response = retry(20, function () use ($endpoint, $domain) { + $body = Request::get($endpoint)->send()->body; + + if (isset($body->tunnels) && count($body->tunnels) > 0) { + return $this->findHttpTunnelUrl($body->tunnels, $domain); + } + }, 250); + + if (!empty($response)) { + return $response; + } + } + + throw new DomainException("Tunnel not established."); + } + + /** + * Find the HTTP tunnel URL from the list of tunnels. + * + * @param array $tunnels + * @return string|null + */ + function findHttpTunnelUrl($tunnels, $domain) + { + // If there are active tunnels on the Ngrok instance we will spin through them and + // find the one responding on HTTP. Each tunnel has an HTTP and a HTTPS address + // but for local dev purposes we just desire the plain HTTP URL endpoint. + foreach ($tunnels as $tunnel) { + if ($tunnel->proto === 'http' && strpos($tunnel->config->addr, $domain) ) { + return $tunnel->public_url; + } + } + } +} diff --git a/valet/cli/Valet/PhpFpm.php b/valet/cli/Valet/PhpFpm.php new file mode 100755 index 0000000..a127c77 --- /dev/null +++ b/valet/cli/Valet/PhpFpm.php @@ -0,0 +1,259 @@ +cli = $cli; + $this->brew = $brew; + $this->files = $files; + } + + /** + * Install and configure PhpFpm. + * + * @return void + */ + function install() + { + if (! $this->brew->hasInstalledPhp()) { + $this->brew->ensureInstalled('php', [], $this->taps); + } + + $this->files->ensureDirExists(VALET_HOME_PATH . '/Log', user()); + + $this->updateConfiguration(); + + $this->restart(); + } + + /** + * Forcefully uninstall all of Valet's supported PHP versions and configurations + * + * @return void + */ + function uninstall() + { + $this->brew->uninstallAllPhpVersions(); + rename(BREW_PREFIX.'/etc/php', BREW_PREFIX.'/etc/php-valet-bak'.time()); + $this->cli->run('rm -rf '.BREW_PREFIX.'/var/log/php-fpm.log'); + } + + /** + * Update the PHP FPM configuration. + * + * @return void + */ + function updateConfiguration() + { + info('Updating PHP configuration...'); + + $fpmConfigFile = $this->fpmConfigPath(); + + $this->files->ensureDirExists(dirname($fpmConfigFile), user()); + + // rename (to disable) old FPM Pool configuration, regardless of whether it's a default config or one customized by an older Valet version + $oldFile = dirname($fpmConfigFile) . '/www.conf'; + if (file_exists($oldFile)) { + rename($oldFile, $oldFile . '-backup'); + } + + if (false === strpos($fpmConfigFile, '5.6')) { + // since PHP 7 we can simply drop in a valet-specific fpm pool config, and not touch the default config + $contents = $this->files->get(__DIR__.'/../stubs/etc-phpfpm-valet.conf'); + $contents = str_replace(['VALET_USER', 'VALET_HOME_PATH'], [user(), VALET_HOME_PATH], $contents); + } else { + // for PHP 5 we must do a direct edit of the fpm pool config to switch it to Valet's needs + $contents = $this->files->get($fpmConfigFile); + $contents = preg_replace('/^user = .+$/m', 'user = '.user(), $contents); + $contents = preg_replace('/^group = .+$/m', 'group = staff', $contents); + $contents = preg_replace('/^listen = .+$/m', 'listen = '.VALET_HOME_PATH.'/valet.sock', $contents); + $contents = preg_replace('/^;?listen\.owner = .+$/m', 'listen.owner = '.user(), $contents); + $contents = preg_replace('/^;?listen\.group = .+$/m', 'listen.group = staff', $contents); + $contents = preg_replace('/^;?listen\.mode = .+$/m', 'listen.mode = 0777', $contents); + } + $this->files->put($fpmConfigFile, $contents); + + $contents = $this->files->get(__DIR__.'/../stubs/php-memory-limits.ini'); + $destFile = dirname($fpmConfigFile); + $destFile = str_replace('/php-fpm.d', '', $destFile); + $destFile .= '/conf.d/php-memory-limits.ini'; + $this->files->ensureDirExists(dirname($destFile), user()); + $this->files->putAsUser($destFile, $contents); + + $contents = $this->files->get(__DIR__.'/../stubs/etc-phpfpm-error_log.ini'); + $contents = str_replace(['VALET_USER', 'VALET_HOME_PATH'], [user(), VALET_HOME_PATH], $contents); + $destFile = dirname($fpmConfigFile); + $destFile = str_replace('/php-fpm.d', '', $destFile); + $destFile .= '/conf.d/error_log.ini'; + $this->files->ensureDirExists(dirname($destFile), user()); + $this->files->putAsUser($destFile, $contents); + $this->files->ensureDirExists(VALET_HOME_PATH . '/Log', user()); + $this->files->touch(VALET_HOME_PATH . '/Log/php-fpm.log', user()); + } + + /** + * Restart the PHP FPM process. + * + * @return void + */ + function restart() + { + $this->brew->restartLinkedPhp(); + } + + /** + * Stop the PHP FPM process. + * + * @return void + */ + function stop() + { + call_user_func_array( + [$this->brew, 'stopService'], + Brew::SUPPORTED_PHP_VERSIONS + ); + } + + /** + * Get the path to the FPM configuration file for the current PHP version. + * + * @return string + */ + function fpmConfigPath() + { + $version = $this->brew->linkedPhp(); + + $versionNormalized = $this->normalizePhpVersion($version === 'php' ? Brew::LATEST_PHP_VERSION : $version); + $versionNormalized = preg_replace('~[^\d\.]~', '', $versionNormalized); + + return $versionNormalized === '5.6' + ? BREW_PREFIX.'/etc/php/5.6/php-fpm.conf' + : BREW_PREFIX."/etc/php/${versionNormalized}/php-fpm.d/valet-fpm.conf"; + } + + /** + * Only stop running php services + */ + function stopRunning() + { + $this->brew->stopService( + $this->brew->getRunningServices() + ->filter(function ($service) { + return substr($service, 0, 3) === 'php'; + }) + ->all() + ); + } + + /** + * Use a specific version of php + * + * @param $version + * @param $force + * @return string + */ + function useVersion($version, $force = false) + { + $version = $this->validateRequestedVersion($version); + + try { + if ($this->brew->linkedPhp() === $version && !$force) { + output(sprintf('Valet is already using version: %s. To re-link and re-configure use the --force parameter.' . PHP_EOL, + $version)); + exit(); + } + } catch (DomainException $e) + { /* ignore thrown exception when no linked php is found */ } + + if (!$this->brew->installed($version)) { + // Install the relevant formula if not already installed + $this->brew->ensureInstalled($version, [], $this->taps); + } + + // Unlink the current php if there is one + if ($this->brew->hasLinkedPhp()) { + $currentVersion = $this->brew->getLinkedPhpFormula(); + info(sprintf('Unlinking current version: %s', $currentVersion)); + $this->brew->unlink($currentVersion); + } + + info(sprintf('Linking new version: %s', $version)); + $this->brew->link($version, true); + + $this->stopRunning(); + + // remove any orphaned valet.sock files that PHP didn't clean up due to version conflicts + $this->files->unlink(VALET_HOME_PATH.'/valet.sock'); + $this->cli->quietly('sudo rm ' . VALET_HOME_PATH.'/valet.sock'); + + // ensure configuration is correct and start the linked version + $this->install(); + + return $version === 'php' ? $this->brew->determineAliasedVersion($version) : $version; + } + + + /** + * If passed php7.4 or php74 formats, normalize to php@7.4 format. + */ + function normalizePhpVersion($version) + { + return preg_replace('/(php)([0-9+])(?:.)?([0-9+])/i', '$1@$2.$3', $version); + } + + /** + * Validate the requested version to be sure we can support it. + * + * @param $version + * @return string + */ + function validateRequestedVersion($version) + { + $version = $this->normalizePhpVersion($version); + + if (!$this->brew->supportedPhpVersions()->contains($version)) { + throw new DomainException( + sprintf( + 'Valet doesn\'t support PHP version: %s (try something like \'php@7.3\' instead)', + $version + ) + ); + } + + if (strpos($aliasedVersion = $this->brew->determineAliasedVersion($version), '@')) { + return $aliasedVersion; + } + + if ($version === 'php') { + if (strpos($aliasedVersion = $this->brew->determineAliasedVersion($version), '@')) { + return $aliasedVersion; + } + + if ($this->brew->hasInstalledPhp()) { + throw new DomainException('Brew is already using PHP '.PHP_VERSION.' as \'php\' in Homebrew. To use another version, please specify. eg: php@7.3'); + } + } + + return $version; + } +} diff --git a/valet/cli/Valet/Site.php b/valet/cli/Valet/Site.php new file mode 100755 index 0000000..b7d8c76 --- /dev/null +++ b/valet/cli/Valet/Site.php @@ -0,0 +1,726 @@ +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 + */ + public 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 + */ + public function link($target, $link) + { + $this->files->ensureDirExists( + $linkPath = $this->sitesPath(), user() + ); + + $this->config->prependPath($linkPath); + + $this->files->symlink($target, $linkPath . '/' . $link); + + return $linkPath . '/' . $link; + } + + /** + * Pretty print out all links in Valet. + * + * @return \Illuminate\Support\Collection + */ + public function links() + { + $certsPath = $this->certificatesPath(); + + $this->files->ensureDirExists($certsPath, user()); + + $certs = $this->getCertificates($certsPath); + + return $this->getSites($this->sitesPath(), $certs); + } + + /** + * Pretty print out all parked links in Valet + * + * @return \Illuminate\Support\Collection + */ + public function parked() + { + $certs = $this->getCertificates(); + + $links = $this->getSites($this->sitesPath(), $certs); + + $config = $this->config->read(); + $parkedLinks = collect(); + foreach (array_reverse($config['paths']) as $path) { + if ($path === $this->sitesPath()) { + continue; + } + + // Only merge on the parked sites that don't interfere with the linked sites + $sites = $this->getSites($path, $certs)->filter(function ($site, $key) use ($links) { + return !$links->has($key); + }); + + $parkedLinks = $parkedLinks->merge($sites); + } + + return $parkedLinks; + } + + /** + * Get all sites which are proxies (not Links, and contain proxy_pass directive) + * + * @return \Illuminate\Support\Collection + */ + public 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 + */ + public function getProxyHostForSite($site, $configContents = null) + { + $siteConf = $configContents ?: $this->getSiteConfigFileContents($site); + + if (empty($siteConf)) { + return null; + } + + $host = null; + if (preg_match('~proxy_pass\s+(?https?://.*)\s*;~', $siteConf, $patterns)) { + $host = trim($patterns['host']); + } + return $host; + } + + public function getSiteConfigFileContents($site, $suffix = null) + { + $config = $this->config->read(); + $suffix = $suffix ?: '.' . $config['tld']; + $file = str_replace($suffix, '', $site) . $suffix; + return $this->files->exists($this->nginxPath($file)) ? $this->files->get($this->nginxPath($file)) : null; + } + + /** + * Get all certificates from config folder. + * + * @param string $path + * @return \Illuminate\Support\Collection + */ + public 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) { + return ends_with($value, '.crt'); + })->map(function ($cert) use ($config) { + $certWithoutSuffix = substr($cert, 0, -4); + $trimToString = '.'; + + // If we have the cert ending in our tld strip that tld specifically + // if not then just strip the last segment for backwards compatibility. + if (ends_with($certWithoutSuffix, $config['tld'])) { + $trimToString .= $config['tld']; + } + + return substr($certWithoutSuffix, 0, strrpos($certWithoutSuffix, $trimToString)); + })->flip(); + } + + /** + * @deprecated Use getSites instead which works for both normal and symlinked paths. + * + * @param string $path + * @param \Illuminate\Support\Collection $certs + * @return \Illuminate\Support\Collection + */ + public function getLinks($path, $certs) + { + return $this->getSites($path, $certs); + } + + /** + * Get list of sites and return them formatted + * Will work for symlink and normal site paths + * + * @param $path + * @param $certs + * + * @return \Illuminate\Support\Collection + */ + public 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; + + if ($this->files->isLink($sitePath)) { + $realPath = $this->files->readLink($sitePath); + } else { + $realPath = $this->files->realpath($sitePath); + } + return [$site => $realPath]; + })->filter(function ($path) { + return $this->files->isDir($path); + })->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 + */ + public 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 + */ + public 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 + */ + public function resecureForNewTld($oldTld, $tld) + { + if (!$this->files->exists($this->certificatesPath())) { + return; + } + + $secured = $this->secured(); + + foreach ($secured as $url) { + $newUrl = str_replace('.' . $oldTld, '.' . $tld, $url); + $siteConf = $this->getSiteConfigFileContents($url, '.' . $oldTld); + + if (!empty($siteConf) && strpos($siteConf, '# valet stub: proxy.valet.conf') === 0) { + // proxy config + $this->unsecure($url); + $this->secure($newUrl, $this->replaceOldDomainWithNew($siteConf, $url, $newUrl)); + } else { + // normal config + $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 + */ + public function replaceOldDomainWithNew($siteConf, $old, $new) + { + $lookups = []; + $lookups[] = '~server_name .*;~'; + $lookups[] = '~error_log .*;~'; + $lookups[] = '~ssl_certificate_key .*;~'; + $lookups[] = '~ssl_certificate .*;~'; + + 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. + * + * @return array + */ + public 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. On Butler (Valet on Docker), we add/remove CA, Cert and trusting those two certs on butler script + * + * @param string $url + * @param string $siteConf pregenerated Nginx config file contents + * @return void + */ + public function secure($url, $siteConf = null) + { + $this->unsecure($url); + + $this->files->ensureDirExists($this->caPath(), user()); + + $this->files->ensureDirExists($this->certificatesPath(), user()); + + $this->files->ensureDirExists($this->nginxPath(), user()); + + $this->createCertificate($url); + + $this->files->putAsUser( + $this->nginxPath($url), $this->buildSecureNginxServer($url, $siteConf) + ); + } + + /** + * If CA and root certificates are nonexistent, crete them and trust the root cert. + * + * @return void + */ + public function createCa() + { + $caPemPath = $this->caPath('LaravelValetCASelfSigned.pem'); + $caKeyPath = $this->caPath('LaravelValetCASelfSigned.key'); + + if ($this->files->exists($caKeyPath) && $this->files->exists($caPemPath)) { + return; + } + + // Need to change name to distinguish between real Valet certificate + $oName = 'Laravel Valet (Butler) CA Self Signed Organization'; + $cName = 'Laravel Valet (Butler) 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 + */ + public 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 = '-CAserial "' . $caSrlPath . '"'; + if (!$this->files->exists($caSrlPath)) { + $caSrlParam .= ' -CAcreateserial'; + } + + $this->cli->run(sprintf( + 'openssl x509 -req -sha256 -days 1095 -CA "%s" -CAkey "%s" %s -in "%s" -out "%s" -extensions v3_req -extfile "%s"', + $caPemPath, $caKeyPath, $caSrlParam, $csrPath, $crtPath, $confPath + )); + } + + /** + * Create the private key for the TLS certificate. + * + * @param string $keyPath + * @return void + */ + public function createPrivateKey($keyPath) + { + $this->cli->run(sprintf('openssl genrsa -out "%s" 2048', $keyPath)); + } + + /** + * Create the signing request for the TLS certificate. + * + * @param string $keyPath + * @return void + */ + public function createSigningRequest($url, $keyPath, $csrPath, $confPath) + { + $this->cli->run(sprintf( + 'openssl req -new -key "%s" -out "%s" -subj "/C=MY/ST=Negeri Sembilan/O=RunCloud/localityName=Mantin/commonName=%s/organizationalUnitName=Engineering/emailAddress=%s%s/" -config "%s"', + $keyPath, $csrPath, $url, $url, '@laravel.butler', $confPath + )); + } + + /** + * Trust the given root certificate file in the Mac Keychain. + * + * @param string $pemPath + * @return void + */ + public 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 + */ + public 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 + */ + public 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 + * @param string $siteConf (optional) Nginx site config file content + * @return string + */ + public function buildSecureNginxServer($url, $siteConf = null) + { + 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'], + [ + $this->valetHomePath(), + VALET_SERVER_PATH, + VALET_STATIC_PREFIX, + $url, + $this->certificatesPath($url, 'crt'), + $this->certificatesPath($url, 'key'), + ], + $siteConf + ); + } + + /** + * Unsecure the given URL so that it will use HTTP again. + * + * @param string $url + * @return void + */ + public function unsecure($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')); + } + } + + public function unsecureAll() + { + $tld = $this->config->read()['tld']; + + $secured = $this->parked() + ->merge($this->links()) + ->sort() + ->where('secured', ' X'); + + if ($secured->count() === 0) { + return info('No sites to unsecure. You may list all servable sites or links by running valet parked or valet links.'); + } + + info('Attempting to unsecure the following sites:'); + table(['Site', 'SSL', 'URL', 'Path'], $secured->toArray()); + + foreach ($secured->pluck('site') as $url) { + $this->unsecure($url . '.' . $tld); + } + + $remaining = $this->parked() + ->merge($this->links()) + ->sort() + ->where('secured', ' X'); + if ($remaining->count() > 0) { + warning('We were not succesful in unsecuring the following sites:'); + table(['Site', 'SSL', 'URL', 'Path'], $remaining->toArray()); + } + 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 + */ + public 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 + */ + public 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 . '].'); + } + + public function valetHomePath() + { + return VALET_HOME_PATH; + } + + /** + * Get the path to Nginx site configuration files. + */ + public function nginxPath($additionalPath = null) + { + return $this->valetHomePath() . '/Nginx' . ($additionalPath ? '/' . $additionalPath : ''); + } + + /** + * Get the path to the linked Valet sites. + * + * @return string + */ + public function sitesPath($link = null) + { + return $this->valetHomePath() . '/Sites' . ($link ? '/' . $link : ''); + } + + /** + * Get the path to the Valet CA certificates. + * + * @return string + */ + public function caPath($caFile = null) + { + return $this->valetHomePath() . '/CA' . ($caFile ? '/' . $caFile : ''); + } + + /** + * Get the path to the Valet TLS certificates. + * + * @return string + */ + public function certificatesPath($url = null, $extension = null) + { + $url = $url ? '/' . $url : ''; + $extension = $extension ? '.' . $extension : ''; + + return $this->valetHomePath() . '/Certificates' . $url . $extension; + } +} diff --git a/valet/cli/Valet/Valet.php b/valet/cli/Valet/Valet.php new file mode 100755 index 0000000..d707f6e --- /dev/null +++ b/valet/cli/Valet/Valet.php @@ -0,0 +1,120 @@ +cli = $cli; + $this->files = $files; + } + + /** + * Symlink the Valet Bash script into the user's local bin. + * + * @return void + */ + public function symlinkToUsersBin() + { + $this->unlinkFromUsersBin(); + + $this->cli->runAsUser('ln -s "' . realpath(__DIR__ . '/../../valet') . '" ' . $this->valetBin); + } + + /** + * Remove the symlink from the user's local bin. + * + * @return void + */ + public function unlinkFromUsersBin() + { + $this->cli->quietlyAsUser('rm ' . $this->valetBin); + } + + /** + * Get the paths to all of the Valet extensions. + * + * @return array + */ + public function extensions() + { + if (!$this->files->isDir(VALET_HOME_PATH . '/Extensions')) { + return []; + } + + return collect($this->files->scandir(VALET_HOME_PATH . '/Extensions')) + ->reject(function ($file) { + return is_dir($file); + }) + ->map(function ($file) { + return VALET_HOME_PATH . '/Extensions/' . $file; + }) + ->values()->all(); + } + + /** + * Determine if this is the latest version of Valet. + * + * @param string $currentVersion + * @return bool + * @throws \Httpful\Exception\ConnectionErrorException + */ + public function onLatestVersion($currentVersion) + { + $response = Request::get('https://api.github.com/repos/RunCloudIO/butler/releases/latest')->send(); + + return version_compare($currentVersion, trim($response->body->tag_name, 'v'), '>='); + } + + /** + * Create the "sudoers.d" entry for running Valet. + * + * @return void + */ + public function createSudoersEntry() + { + $this->files->ensureDirExists('/etc/sudoers.d'); + + $this->files->put('/etc/sudoers.d/valet', 'Cmnd_Alias VALET = ' . BREW_PREFIX . '/bin/valet * +%admin ALL=(root) NOPASSWD:SETENV: VALET' . PHP_EOL); + } + + /** + * Remove the "sudoers.d" entry for running Valet. + * + * @return void + */ + public function removeSudoersEntry() + { + $this->cli->quietly('rm /etc/sudoers.d/valet'); + } + + /** + * Run composer global diagnose + */ + public function composerGlobalDiagnose() + { + $this->cli->runAsUser('composer global diagnose'); + } + + /** + * Run composer global update + */ + public function composerGlobalUpdate() + { + $this->cli->runAsUser('composer global update'); + } +} diff --git a/valet/cli/drivers/BasicValetDriver.php b/valet/cli/drivers/BasicValetDriver.php new file mode 100755 index 0000000..df87d71 --- /dev/null +++ b/valet/cli/drivers/BasicValetDriver.php @@ -0,0 +1,154 @@ +isActualFile($staticFilePath = $sitePath.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $_SERVER['PHP_SELF'] = $uri; + $_SERVER['SERVER_ADDR'] = '127.0.0.1'; + $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + + $dynamicCandidates = [ + $this->asActualFile($sitePath, $uri), + $this->asPhpIndexFileInDirectory($sitePath, $uri), + $this->asHtmlIndexFileInDirectory($sitePath, $uri), + ]; + + foreach ($dynamicCandidates as $candidate) { + if ($this->isActualFile($candidate)) { + $_SERVER['SCRIPT_FILENAME'] = $candidate; + $_SERVER['SCRIPT_NAME'] = str_replace($sitePath, '', $candidate); + $_SERVER['DOCUMENT_ROOT'] = $sitePath; + return $candidate; + } + } + + $fixedCandidatesAndDocroots = [ + $this->asRootPhpIndexFile($sitePath) => $sitePath, + $this->asPublicPhpIndexFile($sitePath) => $sitePath . '/public', + $this->asPublicHtmlIndexFile($sitePath) => $sitePath . '/public', + ]; + + foreach ($fixedCandidatesAndDocroots as $candidate => $docroot) { + if ($this->isActualFile($candidate)) { + $_SERVER['SCRIPT_FILENAME'] = $candidate; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['DOCUMENT_ROOT'] = $docroot; + return $candidate; + } + } + } + + /** + * Concatenate the site path and URI as a single file name. + * + * @param string $sitePath + * @param string $uri + * @return string + */ + protected function asActualFile($sitePath, $uri) + { + return $sitePath.$uri; + } + + /** + * Format the site path and URI with a trailing "index.php". + * + * @param string $sitePath + * @param string $uri + * @return string + */ + protected function asPhpIndexFileInDirectory($sitePath, $uri) + { + return $sitePath.rtrim($uri, '/').'/index.php'; + } + + /** + * Format the site path and URI with a trailing "index.html". + * + * @param string $sitePath + * @param string $uri + * @return string + */ + protected function asHtmlIndexFileInDirectory($sitePath, $uri) + { + return $sitePath.rtrim($uri, '/').'/index.html'; + } + + /** + * Format the incoming site path as root "index.php" file path. + * + * @param string $sitePath + * @return string + */ + protected function asRootPhpIndexFile($sitePath) + { + return $sitePath.'/index.php'; + } + + /** + * Format the incoming site path as a "public/index.php" file path. + * + * @param string $sitePath + * @return string + */ + protected function asPublicPhpIndexFile($sitePath) + { + return $sitePath.'/public/index.php'; + } + + /** + * Format the incoming site path as a "public/index.php" file path. + * + * @param string $sitePath + * @return string + */ + protected function asPublicHtmlIndexFile($sitePath) + { + return $sitePath.'/public/index.html'; + } +} diff --git a/valet/cli/drivers/BedrockValetDriver.php b/valet/cli/drivers/BedrockValetDriver.php new file mode 100755 index 0000000..528c23a --- /dev/null +++ b/valet/cli/drivers/BedrockValetDriver.php @@ -0,0 +1,76 @@ +isActualFile($staticFilePath)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $_SERVER['PHP_SELF'] = $uri; + $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + + if (strpos($uri, '/wp/') === 0) { + return is_dir($sitePath.'/web'.$uri) + ? $sitePath.'/web'.$this->forceTrailingSlash($uri).'/index.php' + : $sitePath.'/web'.$uri; + } + + return $sitePath.'/web/index.php'; + } + + /** + * Redirect to uri with trailing slash. + * + * @param string $uri + * @return string + */ + private function forceTrailingSlash($uri) + { + if (substr($uri, -1 * strlen('/wp/wp-admin')) == '/wp/wp-admin') { + header('Location: '.$uri.'/'); die; + } + + return $uri; + } +} diff --git a/valet/cli/drivers/CakeValetDriver.php b/valet/cli/drivers/CakeValetDriver.php new file mode 100755 index 0000000..70304a1 --- /dev/null +++ b/valet/cli/drivers/CakeValetDriver.php @@ -0,0 +1,52 @@ +isActualFile($staticFilePath = $sitePath.'/webroot/'.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $_SERVER['DOCUMENT_ROOT'] = $sitePath.'/webroot'; + $_SERVER['SCRIPT_FILENAME'] = $sitePath.'/webroot/index.php'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + $_SERVER['PHP_SELF'] = '/index.php'; + + return $sitePath.'/webroot/index.php'; + } +} diff --git a/valet/cli/drivers/Concrete5ValetDriver.php b/valet/cli/drivers/Concrete5ValetDriver.php new file mode 100755 index 0000000..0b32bfe --- /dev/null +++ b/valet/cli/drivers/Concrete5ValetDriver.php @@ -0,0 +1,48 @@ +isActualFile($staticFilePath = $sitePath.'/web'.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + if ($uri === '/install.php') { + return $sitePath.'/web/install.php'; + } + + if (0 === strncmp($uri, '/app_dev.php', 12)) { + $_SERVER['SCRIPT_NAME'] = '/app_dev.php'; + $_SERVER['SCRIPT_FILENAME'] = $sitePath.'/app_dev.php'; + + return $sitePath.'/web/app_dev.php'; + } + + return $sitePath.'/web/app.php'; + } +} diff --git a/valet/cli/drivers/CraftValetDriver.php b/valet/cli/drivers/CraftValetDriver.php new file mode 100755 index 0000000..9b994ce --- /dev/null +++ b/valet/cli/drivers/CraftValetDriver.php @@ -0,0 +1,211 @@ +frontControllerDirectory($sitePath); + + if ($this->isActualFile($staticFilePath = $sitePath.'/'.$frontControllerDirectory.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $frontControllerDirectory = $this->frontControllerDirectory($sitePath); + + // Default index path + $indexPath = $sitePath.'/'.$frontControllerDirectory.'/index.php'; + $scriptName = '/index.php'; + + // Check if the first URL segment matches any of the defined locales + $locales = [ + 'ar', + 'ar_sa', + 'bg', + 'bg_bg', + 'ca_es', + 'cs', + 'cy_gb', + 'da', + 'da_dk', + 'de', + 'de_at', + 'de_ch', + 'de_de', + 'el', + 'el_gr', + 'en', + 'en_as', + 'en_au', + 'en_bb', + 'en_be', + 'en_bm', + 'en_bw', + 'en_bz', + 'en_ca', + 'en_dsrt', + 'en_dsrt_us', + 'en_gb', + 'en_gu', + 'en_gy', + 'en_hk', + 'en_ie', + 'en_in', + 'en_jm', + 'en_mh', + 'en_mp', + 'en_mt', + 'en_mu', + 'en_na', + 'en_nz', + 'en_ph', + 'en_pk', + 'en_sg', + 'en_shaw', + 'en_tt', + 'en_um', + 'en_us', + 'en_us_posix', + 'en_vi', + 'en_za', + 'en_zw', + 'en_zz', + 'es', + 'es_cl', + 'es_es', + 'es_mx', + 'es_us', + 'es_ve', + 'et', + 'fi', + 'fi_fi', + 'fil', + 'fr', + 'fr_be', + 'fr_ca', + 'fr_ch', + 'fr_fr', + 'fr_ma', + 'he', + 'hr', + 'hr_hr', + 'hu', + 'hu_hu', + 'id', + 'id_id', + 'it', + 'it_ch', + 'it_it', + 'ja', + 'ja_jp', + 'ko', + 'ko_kr', + 'lt', + 'lv', + 'ms', + 'ms_my', + 'nb', + 'nb_no', + 'nl', + 'nl_be', + 'nl_nl', + 'nn', + 'nn_no', + 'no', + 'pl', + 'pl_pl', + 'pt', + 'pt_br', + 'pt_pt', + 'ro', + 'ro_ro', + 'ru', + 'ru_ru', + 'sk', + 'sl', + 'sr', + 'sv', + 'sv_se', + 'th', + 'th_th', + 'tr', + 'tr_tr', + 'uk', + 'vi', + 'zh', + 'zh_cn', + 'zh_tw', + ]; + $parts = explode('/', $uri); + + if (count($parts) > 1 && in_array($parts[1], $locales)) { + $indexLocalizedPath = $sitePath.'/'.$frontControllerDirectory.'/'.$parts[1].'/index.php'; + + // Check if index.php exists in the localized folder, this is optional in Craft 3 + if (file_exists($indexLocalizedPath)) { + $indexPath = $indexLocalizedPath; + $scriptName = '/'.$parts[1].'/index.php'; + } + } + + $_SERVER['SCRIPT_FILENAME'] = $indexPath; + $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + $_SERVER['SCRIPT_NAME'] = $scriptName; + $_SERVER['PHP_SELF'] = $scriptName; + $_SERVER['DOCUMENT_ROOT'] = $sitePath.'/'.$frontControllerDirectory; + + return $indexPath; + } +} diff --git a/valet/cli/drivers/DrupalValetDriver.php b/valet/cli/drivers/DrupalValetDriver.php new file mode 100755 index 0000000..a59b7ae --- /dev/null +++ b/valet/cli/drivers/DrupalValetDriver.php @@ -0,0 +1,111 @@ +addSubdirectory($sitePath); + + /** + * /misc/drupal.js = Drupal 7 + * /core/lib/Drupal.php = Drupal 8 + */ + if (file_exists($sitePath.'/misc/drupal.js') || + file_exists($sitePath.'/core/lib/Drupal.php')) { + return true; + } + } + + /** + * Determine if the incoming request is for a static file. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string|false + */ + public function isStaticFile($sitePath, $siteName, $uri) + { + $sitePath = $this->addSubdirectory($sitePath); + + if (file_exists($sitePath.$uri) && + ! is_dir($sitePath.$uri) && + pathinfo($sitePath.$uri)['extension'] != 'php') { + return $sitePath.$uri; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $sitePath = $this->addSubdirectory($sitePath); + + if (!isset($_GET['q']) && !empty($uri) && $uri !== '/' && strpos($uri, '/jsonapi/') === false) { + $_GET['q'] = $uri; + } + + $matches = []; + if (preg_match('/^\/(.*?)\.php/', $uri, $matches)) { + $filename = $matches[0]; + if (file_exists($sitePath.$filename) && ! is_dir($sitePath.$filename)) { + $_SERVER['SCRIPT_FILENAME'] = $sitePath.$filename; + $_SERVER['SCRIPT_NAME'] = $filename; + return $sitePath.$filename; + } + } + + // Fallback + $_SERVER['SCRIPT_FILENAME'] = $sitePath.'/index.php'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + return $sitePath.'/index.php'; + } + + /** + * Add any matching subdirectory to the site path. + */ + public function addSubdirectory($sitePath) + { + $paths = array_map(function ($subDir) use ($sitePath) { + return "$sitePath/$subDir"; + }, $this->possibleSubdirectories()); + + $foundPaths = array_filter($paths, function ($path) { + return file_exists($path); + }); + + // If paths are found, return the first one. + if (!empty($foundPaths)) { + return array_shift($foundPaths); + } + + // If there are no matches, return the original path. + return $sitePath; + } + + /** + * Return an array of possible subdirectories. + * + * @return array + */ + private function possibleSubdirectories() + { + return ['docroot', 'public', 'web']; + } +} diff --git a/valet/cli/drivers/JigsawValetDriver.php b/valet/cli/drivers/JigsawValetDriver.php new file mode 100755 index 0000000..e628835 --- /dev/null +++ b/valet/cli/drivers/JigsawValetDriver.php @@ -0,0 +1,28 @@ +isActualFile($staticFilePath = $sitePath.$uri)) { + return $staticFilePath; + } elseif ($this->isActualFile($staticFilePath = $sitePath.'/public'.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $scriptName = '/index.php'; + + if ($this->isActualFile($sitePath.'/index.php')) { + $indexPath = $sitePath.'/index.php'; + } + + if ($isAboveWebroot = $this->isActualFile($sitePath.'/public/index.php')) { + $indexPath = $sitePath.'/public/index.php'; + } + + if (preg_match('/^\/panel/', $uri) && $this->isActualFile($sitePath.'/panel/index.php')) { + $scriptName = '/panel/index.php'; + $indexPath = $sitePath.'/panel/index.php'; + } + + $sitePathPrefix = ($isAboveWebroot) ? $sitePath.'/public' : $sitePath; + + $_SERVER['SERVER_NAME'] = $_SERVER['HTTP_HOST']; + $_SERVER['SCRIPT_NAME'] = $scriptName; + $_SERVER['SCRIPT_FILENAME'] = $sitePathPrefix.$scriptName; + + return $indexPath; + } +} diff --git a/valet/cli/drivers/LaravelValetDriver.php b/valet/cli/drivers/LaravelValetDriver.php new file mode 100755 index 0000000..d77bf83 --- /dev/null +++ b/valet/cli/drivers/LaravelValetDriver.php @@ -0,0 +1,64 @@ +isActualFile($storagePath = $sitePath.'/storage/app/public'.$storageUri)) { + return $storagePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + // Shortcut for getting the "local" hostname as the HTTP_HOST + if (isset($_SERVER['HTTP_X_ORIGINAL_HOST'], $_SERVER['HTTP_X_FORWARDED_HOST'])) { + $_SERVER['HTTP_HOST'] = $_SERVER['HTTP_X_FORWARDED_HOST']; + } + + return $sitePath.'/public/index.php'; + } +} diff --git a/valet/cli/drivers/Magento2ValetDriver.php b/valet/cli/drivers/Magento2ValetDriver.php new file mode 100755 index 0000000..8580960 --- /dev/null +++ b/valet/cli/drivers/Magento2ValetDriver.php @@ -0,0 +1,153 @@ +checkMageMode($sitePath); + + $uri = $this->handleForVersions($uri); + $route = parse_url(substr($uri, 1))['path']; + + $pub = ''; + if ('developer' === $this->mageMode) { + $pub = 'pub/'; + } + + if (!$this->isPubDirectory($sitePath, $route, $pub)) { + return false; + } + + $magentoPackagePubDir = $sitePath; + if ('developer' !== $this->mageMode) { + $magentoPackagePubDir .= '/pub'; + } + + $file = $magentoPackagePubDir . '/' . $route; + + if (file_exists($file)) { + return $magentoPackagePubDir . $uri; + } + + if (strpos($route, $pub . 'static/') === 0) { + $route = preg_replace('#' . $pub . 'static/#', '', $route, 1); + $_GET['resource'] = $route; + include $magentoPackagePubDir . '/' . $pub . 'static.php'; + exit; + } + + if (strpos($route, $pub . 'media/') === 0) { + include $magentoPackagePubDir . '/' . $pub . 'get.php'; + exit; + } + + return false; + } + + /** + * Rewrite URLs that look like "versions12345/" to remove + * the versions12345/ part + * + * @param string $route + */ + private function handleForVersions($route) + { + return preg_replace('/version\d*\//', '', $route); + } + + /** + * Determine the current MAGE_MODE + * + * @param string $sitePath + */ + private function checkMageMode($sitePath) + { + if (null !== $this->mageMode) { + // We have already figure out mode, no need to check it again + return; + } + if (!file_exists($sitePath . '/index.php')) { + $this->mageMode = 'production'; // Can't use developer mode without index.php in project root + return; + } + $mageConfig = []; + if (file_exists($sitePath . '/app/etc/env.php')) { + $mageConfig = require $sitePath . '/app/etc/env.php'; + } + if (array_key_exists('MAGE_MODE', $mageConfig)) { + $this->mageMode = $mageConfig['MAGE_MODE']; + } + } + + /** + * Checks to see if route is referencing any directory inside pub. This is a dynamic check so that if any new + * directories are added to pub this driver will not need to be updated. + * + * @param string $sitePath + * @param string $route + * @param string $pub + * @return bool + */ + private function isPubDirectory($sitePath, $route, $pub = '') + { + $sitePath .= '/pub/'; + $dirs = glob($sitePath . '*', GLOB_ONLYDIR); + + $dirs = str_replace($sitePath, '', $dirs); + foreach ($dirs as $dir) { + if (strpos($route, $pub . $dir . '/') === 0) { + return true; + } + } + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + $this->checkMageMode($sitePath); + + if ('developer' === $this->mageMode) { + $_SERVER['DOCUMENT_ROOT'] = $sitePath; + return $sitePath . '/index.php'; + } + $_SERVER['DOCUMENT_ROOT'] = $sitePath . '/pub'; + return $sitePath . '/pub/index.php'; + } +} diff --git a/valet/cli/drivers/NeosValetDriver.php b/valet/cli/drivers/NeosValetDriver.php new file mode 100755 index 0000000..d2fa2f1 --- /dev/null +++ b/valet/cli/drivers/NeosValetDriver.php @@ -0,0 +1,50 @@ +isActualFile($staticFilePath = $sitePath.'/Web'.$uri)) { + return $staticFilePath; + } + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + putenv('FLOW_CONTEXT=Development'); + putenv('FLOW_REWRITEURLS=1'); + $_SERVER['SCRIPT_FILENAME'] = $sitePath.'/Web/index.php'; + $_SERVER['SCRIPT_NAME'] = '/index.php'; + return $sitePath.'/Web/index.php'; + } +} diff --git a/valet/cli/drivers/SculpinValetDriver.php b/valet/cli/drivers/SculpinValetDriver.php new file mode 100755 index 0000000..ba21bb7 --- /dev/null +++ b/valet/cli/drivers/SculpinValetDriver.php @@ -0,0 +1,57 @@ +isModernSculpinProject($sitePath) || + $this->isLegacySculpinProject($sitePath); + } + + private function isModernSculpinProject($sitePath) + { + return is_dir($sitePath.'/source') && + is_dir($sitePath.'/output_dev') && + $this->composerRequiresSculpin($sitePath); + } + + private function isLegacySculpinProject($sitePath) + { + return is_dir($sitePath.'/.sculpin'); + } + + private function composerRequiresSculpin($sitePath) + { + if (! file_exists($sitePath.'/composer.json')) { + return false; + } + + $composer_json_source = file_get_contents($sitePath.'/composer.json'); + $composer_json = json_decode($composer_json_source, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + return false; + } + + return isset($composer_json['require']['sculpin/sculpin']); + } + + /** + * Mutate the incoming URI. + * + * @param string $uri + * @return string + */ + public function mutateUri($uri) + { + return rtrim('/output_dev'.$uri, '/'); + } +} diff --git a/valet/cli/drivers/StatamicV1ValetDriver.php b/valet/cli/drivers/StatamicV1ValetDriver.php new file mode 100755 index 0000000..aba3605 --- /dev/null +++ b/valet/cli/drivers/StatamicV1ValetDriver.php @@ -0,0 +1,68 @@ +isActualFile($staticFilePath = $sitePath.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + if (strpos($uri, '/admin.php') === 0) { + $_SERVER['SCRIPT_NAME'] = '/admin.php'; + + return $sitePath.'/admin.php'; + } + + if ($uri === '/admin') { + $_SERVER['SCRIPT_NAME'] = '/admin/index.php'; + + return $sitePath.'/admin/index.php'; + } + + $_SERVER['SCRIPT_NAME'] = '/index.php'; + + return $sitePath.'/index.php'; + } +} diff --git a/valet/cli/drivers/StatamicValetDriver.php b/valet/cli/drivers/StatamicValetDriver.php new file mode 100755 index 0000000..dc91610 --- /dev/null +++ b/valet/cli/drivers/StatamicValetDriver.php @@ -0,0 +1,146 @@ +isActualFile($staticFilePath = $sitePath.$uri)) { + return $staticFilePath; + } elseif ($this->isActualFile($staticFilePath = $sitePath.'/public'.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + if ($_SERVER['REQUEST_METHOD'] === 'GET' && $this->isActualFile($staticPath = $this->getStaticPath($sitePath))) { + return $staticPath; + } + + if ($uri === '/installer.php') { + return $sitePath.'/installer.php'; + } + + $scriptName = '/index.php'; + + if ($this->isActualFile($sitePath.'/index.php')) { + $indexPath = $sitePath.'/index.php'; + } + + if ($isAboveWebroot = $this->isActualFile($sitePath.'/public/index.php')) { + $indexPath = $sitePath.'/public/index.php'; + } + + $sitePathPrefix = ($isAboveWebroot) ? $sitePath.'/public' : $sitePath; + + if ($locale = $this->getUriLocale($uri)) { + if ($this->isActualFile($localeIndexPath = $sitePathPrefix . '/' . $locale . '/index.php')) { + // Force trailing slashes on locale roots. + if ($uri === '/' . $locale) { + header('Location: ' . $uri . '/'); + die; + } + + $indexPath = $localeIndexPath; + $scriptName = '/' . $locale . '/index.php'; + } + } + + $_SERVER['SCRIPT_NAME'] = $scriptName; + $_SERVER['SCRIPT_FILENAME'] = $sitePathPrefix . $scriptName; + + return $indexPath; + } + + /** + * Get the locale from this URI + * + * @param string $uri + * @return string|null + */ + public function getUriLocale($uri) + { + $parts = explode('/', $uri); + $locale = $parts[1]; + + if (count($parts) < 2 || ! in_array($locale, $this->getLocales())) { + return; + } + + return $locale; + } + + /** + * Get the list of possible locales used in the first segment of a URI + * + * @return array + */ + public function getLocales() + { + return [ + 'af', 'ax', 'al', 'dz', 'as', 'ad', 'ao', 'ai', 'aq', 'ag', 'ar', 'am', 'aw', 'au', 'at', 'az', 'bs', 'bh', + 'bd', 'bb', 'by', 'be', 'bz', 'bj', 'bm', 'bt', 'bo', 'bq', 'ba', 'bw', 'bv', 'br', 'io', 'bn', 'bg', 'bf', + 'bi', 'cv', 'kh', 'cm', 'ca', 'ky', 'cf', 'td', 'cl', 'cn', 'cx', 'cc', 'co', 'km', 'cg', 'cd', 'ck', 'cr', + 'ci', 'hr', 'cu', 'cw', 'cy', 'cz', 'dk', 'dj', 'dm', 'do', 'ec', 'eg', 'sv', 'gq', 'er', 'ee', 'et', 'fk', + 'fo', 'fj', 'fi', 'fr', 'gf', 'pf', 'tf', 'ga', 'gm', 'ge', 'de', 'gh', 'gi', 'gr', 'gl', 'gd', 'gp', 'gu', + 'gt', 'gg', 'gn', 'gw', 'gy', 'ht', 'hm', 'va', 'hn', 'hk', 'hu', 'is', 'in', 'id', 'ir', 'iq', 'ie', 'im', + 'il', 'it', 'jm', 'jp', 'je', 'jo', 'kz', 'ke', 'ki', 'kp', 'kr', 'kw', 'kg', 'la', 'lv', 'lb', 'ls', 'lr', + 'ly', 'li', 'lt', 'lu', 'mo', 'mk', 'mg', 'mw', 'my', 'mv', 'ml', 'mt', 'mh', 'mq', 'mr', 'mu', 'yt', 'mx', + 'fm', 'md', 'mc', 'mn', 'me', 'ms', 'ma', 'mz', 'mm', 'na', 'nr', 'np', 'nl', 'nc', 'nz', 'ni', 'ne', 'ng', + 'nu', 'nf', 'mp', 'no', 'om', 'pk', 'pw', 'ps', 'pa', 'pg', 'py', 'pe', 'ph', 'pn', 'pl', 'pt', 'pr', 'qa', + 're', 'ro', 'ru', 'rw', 'bl', 'sh', 'kn', 'lc', 'mf', 'pm', 'vc', 'ws', 'sm', 'st', 'sa', 'sn', 'rs', 'sc', + 'sl', 'sg', 'sx', 'sk', 'si', 'sb', 'so', 'za', 'gs', 'ss', 'es', 'lk', 'sd', 'sr', 'sj', 'sz', 'se', 'ch', + 'sy', 'tw', 'tj', 'tz', 'th', 'tl', 'tg', 'tk', 'to', 'tt', 'tn', 'tr', 'tm', 'tc', 'tv', 'ug', 'ua', 'ae', + 'gb', 'us', 'um', 'uy', 'uz', 'vu', 've', 'vn', 'vg', 'vi', 'wf', 'eh', 'ye', 'zm', 'zw', 'en', 'zh', + ]; + } + + /** + * Get the path to a statically cached page + * + * @param string $sitePath + * @return string + */ + protected function getStaticPath($sitePath) + { + $parts = parse_url($_SERVER['REQUEST_URI']); + $query = isset($parts['query']) ? $parts['query'] : ''; + + return $sitePath . '/static' . $parts['path'] . '_' . $query . '.html'; + } +} diff --git a/valet/cli/drivers/SymfonyValetDriver.php b/valet/cli/drivers/SymfonyValetDriver.php new file mode 100755 index 0000000..92d0489 --- /dev/null +++ b/valet/cli/drivers/SymfonyValetDriver.php @@ -0,0 +1,58 @@ +isActualFile($staticFilePath = $sitePath.'/web/'.$uri)) { + return $staticFilePath; + } elseif ($this->isActualFile($staticFilePath = $sitePath.'/public/'.$uri)) { + return $staticFilePath; + } + + return false; + } + + /** + * Get the fully resolved path to the application's front controller. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + if (file_exists($frontControllerPath = $sitePath.'/web/app_dev.php')) { + return $frontControllerPath; + } elseif (file_exists($frontControllerPath = $sitePath.'/web/app.php')) { + return $frontControllerPath; + } elseif (file_exists($frontControllerPath = $sitePath.'/public/index.php')) { + return $frontControllerPath; + } + } +} diff --git a/valet/cli/drivers/Typo3ValetDriver.php b/valet/cli/drivers/Typo3ValetDriver.php new file mode 100755 index 0000000..108f4ce --- /dev/null +++ b/valet/cli/drivers/Typo3ValetDriver.php @@ -0,0 +1,197 @@ +documentRoot . '/typo3'; + return file_exists($typo3Dir) && is_dir($typo3Dir); + } + + /** + * Determine if the incoming request is for a static file. That is, it is + * no PHP script file and the URI points to a valid file (no folder) on + * the disk. Access to those static files will be authorized. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string|false + */ + public function isStaticFile($sitePath, $siteName, $uri) + { + // May the file contains a cache busting version string like filename.12345678.css + // If that is the case, the file cannot be found on disk, so remove the version + // identifier before retrying below. + if (!$this->isActualFile($filePath = $sitePath . $this->documentRoot . $uri)) + { + $uri = preg_replace("@^(.+)\.(\d+)\.(js|css|png|jpg|gif|gzip)$@", "$1.$3", $uri); + } + + // Now that any possible version string is cleared from the filename, the resulting + // URI should be a valid file on disc. So assemble the absolut file name with the + // same schema as above and if it exists, authorize access and return its path. + if ($this->isActualFile($filePath = $sitePath . $this->documentRoot . $uri)) + { + return $this->isAccessAuthorized($uri) ? $filePath : false; + } + + // This file cannot be found in the current project and thus cannot be served. + return false; + } + + /** + * Determines if the given URI is blacklisted so that access is prevented. + * + * @param string $uri + * @return boolean + */ + private function isAccessAuthorized($uri) + { + foreach ($this->forbiddenUriPatterns as $forbiddenUriPattern) + { + if (preg_match("@$forbiddenUriPattern@", $uri)) + { + return false; + } + } + return true; + } + + /** + * Get the fully resolved path to the application's front controller. + * This can be the currently requested PHP script, a folder that + * contains an index.php or the global index.php otherwise. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return string + */ + public function frontControllerPath($sitePath, $siteName, $uri) + { + // without modifying the URI, redirect if necessary + $this->handleRedirectBackendShorthandUris($uri); + + // from now on, remove trailing / for convenience for all the following join operations + $uri = rtrim($uri, '/'); + + // try to find the responsible script file for the requested folder / script URI + if (file_exists($absoluteFilePath = $sitePath . $this->documentRoot . $uri)) + { + if (is_dir($absoluteFilePath)) + { + if (file_exists($absoluteFilePath . '/index.php')) + { + // this folder can be served by index.php + return $this->serveScript($sitePath, $siteName, $uri . '/index.php'); + } + + if (file_exists($absoluteFilePath . '/index.html')) + { + // this folder can be served by index.html + return $absoluteFilePath . '/index.html'; + } + } + else if (pathinfo($absoluteFilePath, PATHINFO_EXTENSION) === 'php') + { + // this file can be served directly + return $this->serveScript($sitePath, $siteName, $uri); + } + } + + // the global index.php will handle all other cases + return $this->serveScript($sitePath, $siteName, '/index.php'); + } + + /** + * Direct access to installtool via domain.dev/typo3/install/ will be redirected to + * sysext install script. domain.dev/typo3 will be redirected to /typo3/, because + * the generated JavaScript URIs on the login screen would be broken on /typo3. + * + * @param string $uri + */ + private function handleRedirectBackendShorthandUris($uri) + { + if (rtrim($uri, '/') === '/typo3/install') + { + header('Location: /typo3/sysext/install/Start/Install.php'); + die(); + } + + if ($uri === '/typo3') + { + header('Location: /typo3/'); + die(); + } + } + + /** + * Configures the $_SERVER globals for serving the script at + * the specified URI and returns it absolute file path. + * + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @param string $script + * @return string + */ + private function serveScript($sitePath, $siteName, $uri) + { + $docroot = $sitePath . $this->documentRoot; + $abspath = $docroot . $uri; + + $_SERVER['SERVER_NAME'] = $siteName . '.dev'; + $_SERVER['DOCUMENT_ROOT'] = $docroot; + $_SERVER['DOCUMENT_URI'] = $uri; + $_SERVER['SCRIPT_FILENAME'] = $abspath; + $_SERVER['SCRIPT_NAME'] = $uri; + $_SERVER['PHP_SELF'] = $uri; + + return $abspath; + } +} diff --git a/valet/cli/drivers/ValetDriver.php b/valet/cli/drivers/ValetDriver.php new file mode 100755 index 0000000..c766835 --- /dev/null +++ b/valet/cli/drivers/ValetDriver.php @@ -0,0 +1,217 @@ +serves($sitePath, $siteName, $driver->mutateUri($uri))) { + return $driver; + } + } + } + + /** + * Get the custom driver class from the site path, if one exists. + * + * @param string $sitePath + * @return string + */ + public static function customSiteDriver($sitePath) + { + if (! file_exists($sitePath.'/LocalValetDriver.php')) { + return; + } + + require_once $sitePath.'/LocalValetDriver.php'; + + return 'LocalValetDriver'; + } + + /** + * Get all of the driver classes in a given path. + * + * @param string $path + * @return array + */ + public static function driversIn($path) + { + if (! is_dir($path)) { + return []; + } + + $drivers = []; + + $dir = new RecursiveDirectoryIterator($path); + $iterator = new RecursiveIteratorIterator($dir); + $regex = new RegexIterator($iterator, '/^.+ValetDriver\.php$/i', RecursiveRegexIterator::GET_MATCH); + + foreach ($regex as $file) { + require_once $file[0]; + + $drivers[] = basename($file[0], '.php'); + } + + return $drivers; + } + + /** + * Mutate the incoming URI. + * + * @param string $uri + * @return string + */ + public function mutateUri($uri) + { + return $uri; + } + + /** + * Serve the static file at the given path. + * + * @param string $staticFilePath + * @param string $sitePath + * @param string $siteName + * @param string $uri + * @return void + */ + public function serveStaticFile($staticFilePath, $sitePath, $siteName, $uri) + { + /** + * Back story... + * + * PHP docs *claim* you can set default_mimetype = "" to disable the default + * Content-Type header. This works in PHP 7+, but in PHP 5.* it sends an + * *empty* Content-Type header, which is significantly different than + * sending *no* Content-Type header. + * + * However, if you explicitly set a Content-Type header, then explicitly + * remove that Content-Type header, PHP seems to not re-add the default. + * + * I have a hard time believing this is by design and not coincidence. + * + * Burn. it. all. + */ + header('Content-Type: text/html'); + header_remove('Content-Type'); + + header('X-Accel-Redirect: /' . VALET_STATIC_PREFIX . $staticFilePath); + } + + /** + * Determine if the path is a file and not a directory. + * + * @param string $path + * @return bool + */ + protected function isActualFile($path) + { + return ! is_dir($path) && file_exists($path); + } + + /** + * Load server environment variables if available. + * Processes any '*' entries first, and then adds site-specific entries + * + * @param string $sitePath + * @param string $siteName + * @return void + */ + public function loadServerEnvironmentVariables($sitePath, $siteName) + { + $varFilePath = $sitePath . '/.valet-env.php'; + if (! file_exists($varFilePath)) { + $varFilePath = VALET_HOME_PATH . '/.valet-env.php'; + } + if (! file_exists($varFilePath)) { + return; + } + + $variables = include $varFilePath; + + $variablesToSet = isset($variables['*']) ? $variables['*'] : []; + + if (isset($variables[$siteName])) { + $variablesToSet = array_merge($variablesToSet, $variables[$siteName]); + } + + foreach ($variablesToSet as $key => $value) { + if (! is_string($key)) continue; + $_SERVER[$key] = $value; + $_ENV[$key] = $value; + putenv($key . '=' . $value); + } + } + +} diff --git a/valet/cli/drivers/WordPressValetDriver.php b/valet/cli/drivers/WordPressValetDriver.php new file mode 100755 index 0000000..ae7c557 --- /dev/null +++ b/valet/cli/drivers/WordPressValetDriver.php @@ -0,0 +1,51 @@ +forceTrailingSlash($uri) + ); + } + + /** + * Redirect to uri with trailing slash. + * + * @param string $uri + * @return string + */ + private function forceTrailingSlash($uri) + { + if (substr($uri, -1 * strlen('/wp-admin')) == '/wp-admin') { + header('Location: '.$uri.'/'); die; + } + + return $uri; + } +} diff --git a/valet/cli/drivers/require.php b/valet/cli/drivers/require.php new file mode 100755 index 0000000..e64f7f5 --- /dev/null +++ b/valet/cli/drivers/require.php @@ -0,0 +1,30 @@ +make(static::containerKey()); + + return call_user_func_array([$resolvedInstance, $method], $parameters); + } +} + +class Brew extends Facade {} +class Nginx extends Facade {} +class CommandLine extends Facade {} +class Configuration extends Facade {} +class Diagnose extends Facade {} +class DnsMasq extends Facade {} +class Filesystem extends Facade {} +class Ngrok extends Facade {} +class PhpFpm extends Facade {} +class Site extends Facade {} +class Valet extends Facade {} diff --git a/valet/cli/includes/helpers.php b/valet/cli/includes/helpers.php new file mode 100755 index 0000000..8bfcf51 --- /dev/null +++ b/valet/cli/includes/helpers.php @@ -0,0 +1,206 @@ +runAsUser('printf $(brew --prefix)')); + +/** + * Output the given text to the console. + * + * @param string $output + * @return void + */ +function info($output) +{ + output('' . $output . ''); +} + +/** + * Output the given text to the console. + * + * @param string $output + * @return void + */ +function warning($output) +{ + output('' . $output . ''); +} + +/** + * Output a table to the console. + * + * @param array $headers + * @param array $rows + * @return void + */ +function table(array $headers = [], array $rows = []) +{ + $table = new Table(new ConsoleOutput); + + $table->setHeaders($headers)->setRows($rows); + + $table->render(); +} + +/** + * Output the given text to the console. + * + * @param string $output + * @return void + */ +function output($output) +{ + if (isset($_ENV['APP_ENV']) && $_ENV['APP_ENV'] === 'testing') { + return; + } + + (new ConsoleOutput)->writeln($output); +} + +if (!function_exists('resolve')) { + /** + * Resolve the given class from the container. + * + * @param string $class + * @return mixed + */ + function resolve($class) + { + return Container::getInstance()->make($class); + } +} + +/** + * Swap the given class implementation in the container. + * + * @param string $class + * @param mixed $instance + * @return void + */ +function swap($class, $instance) +{ + Container::getInstance()->instance($class, $instance); +} + +if (!function_exists('retry')) { + /** + * Retry the given function N times. + * + * @param int $retries + * @param callable $retries + * @param int $sleep + * @return mixed + */ + function retry($retries, $fn, $sleep = 0) + { + beginning: + try { + return $fn(); + } catch (Exception $e) { + if (!$retries) { + throw $e; + } + + $retries--; + + if ($sleep > 0) { + usleep($sleep * 1000); + } + + goto beginning; + } + } +} + +/** + * Verify that the script is currently running as "sudo". + * + * @return void + */ +function should_be_sudo() +{ + if (!isset($_SERVER['SUDO_USER'])) { + throw new Exception('This command must be run with sudo.'); + } +} + +if (!function_exists('tap')) { + /** + * Tap the given value. + * + * @param mixed $value + * @param callable $callback + * @return mixed + */ + function tap($value, callable $callback) + { + $callback($value); + + return $value; + } +} + +if (!function_exists('ends_with')) { + /** + * Determine if a given string ends with a given substring. + * + * @param string $haystack + * @param string|array $needles + * @return bool + */ + function ends_with($haystack, $needles) + { + foreach ((array) $needles as $needle) { + if (substr($haystack, -strlen($needle)) === (string) $needle) { + return true; + } + } + return false; + } +} + +if (!function_exists('starts_with')) { + /** + * Determine if a given string starts with a given substring. + * + * @param string $haystack + * @param string|string[] $needles + * @return bool + */ + function starts_with($haystack, $needles) + { + foreach ((array) $needles as $needle) { + if ((string) $needle !== '' && strncmp($haystack, $needle, strlen($needle)) === 0) { + return true; + } + } + + return false; + } +} + +/** + * Get the user + */ +function user() +{ + if (!isset($_SERVER['SUDO_USER'])) { + return $_SERVER['USER']; + } + + return $_SERVER['SUDO_USER']; +} diff --git a/valet/cli/scripts/fetch-share-url.sh b/valet/cli/scripts/fetch-share-url.sh new file mode 100755 index 0000000..febc26a --- /dev/null +++ b/valet/cli/scripts/fetch-share-url.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +php $DIR/../valet.php fetch-share-url $1 | pbcopy diff --git a/valet/cli/stubs/SampleValetDriver.php b/valet/cli/stubs/SampleValetDriver.php new file mode 100755 index 0000000..f2f3a83 --- /dev/null +++ b/valet/cli/stubs/SampleValetDriver.php @@ -0,0 +1,51 @@ + + + Valet - Not Found + + + + + 404 - Not Found + + diff --git a/valet/cli/valet.php b/valet/cli/valet.php new file mode 100755 index 0000000..b5e17e7 --- /dev/null +++ b/valet/cli/valet.php @@ -0,0 +1,317 @@ +#!/usr/bin/env php +command('install', function () { + DnsMasq::install(Configuration::read()['tld']); + + output(PHP_EOL . 'Valet installed successfully!'); +})->descriptions('Install the Valet services'); + +/** + * Most commands are available only if valet is installed. + */ +if (is_dir(VALET_HOME_PATH)) { + /** + * Upgrade helper: ensure the tld config exists + */ + if (empty(Configuration::read()['tld'])) { + Configuration::writeBaseConfiguration(); + } + + /** + * Get or set the TLD currently being used by Valet. + */ + $app->command('tld [tld]', function ($tld = null) { + if ($tld === null) { + return output(Configuration::read()['tld']); + } + + DnsMasq::updateTld( + $oldTld = Configuration::read()['tld'], $tld = trim($tld, '.') + ); + + Configuration::updateKey('tld', $tld); + + Site::resecureForNewTld($oldTld, $tld); + + info('Your Valet TLD has been updated to [' . $tld . '].'); + }, ['domain'])->descriptions('Get or set the TLD used for Valet sites.'); + + /** + * Add the current working directory to the paths configuration. + */ + $app->command('park [path]', function ($path = null) { + Configuration::addPath($path ?: getcwd()); + + info(($path === null ? "This" : "The [{$path}]") . " directory has been added to Valet's paths."); + })->descriptions('Register the current working (or specified) directory with Valet'); + + /** + * Get all the current sites within paths parked with 'park {path}' + */ + $app->command('parked', function () { + $parked = Site::parked(); + + table(['Site', 'SSL', 'URL', 'Path'], $parked->all()); + })->descriptions('Display all the current sites within parked paths'); + + /** + * Remove the current working directory from the paths configuration. + */ + $app->command('forget [path]', function ($path = null) { + Configuration::removePath($path ?: getcwd()); + + info(($path === null ? "This" : "The [{$path}]") . " directory has been removed from Valet's paths."); + }, ['unpark'])->descriptions('Remove the current working (or specified) directory from Valet\'s list of paths'); + + /** + * Register a symbolic link with Valet. + */ + $app->command('link [name] [--secure]', function ($name, $secure) { + $linkPath = Site::link(getcwd(), $name = $name ?: basename(getcwd())); + + info('A [' . $name . '] symbolic link has been created in [' . $linkPath . '].'); + + if ($secure) { + $this->runCommand('secure ' . $name); + } + })->descriptions('Link the current working directory to Valet'); + + /** + * Display all of the registered symbolic links. + */ + $app->command('links', function () { + $links = Site::links(); + + table(['Site', 'SSL', 'URL', 'Path'], $links->all()); + })->descriptions('Display all of the registered Valet links'); + + /** + * Unlink a link from the Valet links directory. + */ + $app->command('unlink [name]', function ($name) { + info('The [' . Site::unlink($name) . '] symbolic link has been removed.'); + })->descriptions('Remove the specified Valet link'); + + /** + * Secure the given domain with a trusted TLS certificate. + */ + $app->command('secure [domain]', function ($domain = null) { + $url = ($domain ?: Site::host(getcwd())) . '.' . Configuration::read()['tld']; + + Site::secure($url); + + info('The [' . $url . '] site has been secured with a fresh TLS certificate.'); + })->descriptions('Secure the given domain with a trusted TLS certificate'); + + /** + * Stop serving the given domain over HTTPS and remove the trusted TLS certificate. + */ + $app->command('unsecure [domain]', function ($domain = null) { + $url = ($domain ?: Site::host(getcwd())) . '.' . Configuration::read()['tld']; + + Site::unsecure($url); + + 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); + + })->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); + + })->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. + */ + $app->command('which', function () { + require __DIR__ . '/drivers/require.php'; + + $driver = ValetDriver::assign(getcwd(), basename(getcwd()), '/'); + + if ($driver) { + info('This site is served by [' . get_class($driver) . '].'); + } else { + warning('Valet could not determine which driver to use for this site.'); + } + })->descriptions('Determine which Valet driver serves the current working directory'); + + /** + * Display all of the registered paths. + */ + $app->command('paths', function () { + $paths = Configuration::read()['paths']; + + if (count($paths) > 0) { + output(json_encode($paths, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + } else { + info('No paths have been registered.'); + } + })->descriptions('Get all of the paths registered with Valet'); + + /** + * Generate a publicly accessible URL for your project. + */ + $app->command('share', function () { + warning("It looks like you are running `cli/valet.php` directly, please use the `valet` script in the project root instead."); + })->descriptions('Generate a publicly accessible URL for your project'); + + /** + * Echo the currently tunneled URL. + */ + // $app->command('fetch-share-url [domain]', function ($domain = null) { + // output(Ngrok::currentTunnelUrl($domain ?: Site::host(getcwd()) . '.' . Configuration::read()['tld'])); + // })->descriptions('Get the URL to the current Ngrok tunnel'); + + /** + * Start the daemon services. + */ + $app->command('start', function () { + + })->descriptions('Start the Butler services'); + + /** + * Restart the daemon services. + */ + $app->command('restart', function () { + + })->descriptions('Restart the Butler services'); + + /** + * Reload the daemon services. + */ + $app->command('reload', function () { + + })->descriptions('Reload the Butler services (Use this after .env change)'); + + /** + * Stop the daemon services. + */ + $app->command('stop', function () { + + })->descriptions('Stop the Butler services'); + + /** + * Determine if this is the latest release of Valet. + */ + $app->command('on-latest-version', function () use ($version) { + if (Valet::onLatestVersion($version)) { + output('Yes'); + } else { + output(sprintf('Your version of Valet (%s) is not the latest version available.', $version)); + output('Upgrade instructions can be found in the docs: https://laravel.com/docs/valet#upgrading-valet'); + } + })->descriptions('Determine if this is the latest version of Valet'); + + /** + * Configure or display the directory-listing setting. + */ + $app->command('directory-listing [status]', function ($status = null) { + $key = 'directory-listing'; + $config = Configuration::read(); + + if (in_array($status, ['on', 'off'])) { + $config[$key] = $status; + Configuration::write($config); + return output('Directory listing setting is now: ' . $status); + } + + $current = isset($config[$key]) ? $config[$key] : 'off'; + output('Directory listing is ' . $current); + })->descriptions('Determine directory-listing behavior. Default is off, which means a 404 will display.', [ + 'status' => 'on or off. (default=off) will show a 404 page; [on] will display a listing if project folder exists but requested URI not found', + ]); + + /** + * Output diagnostics to aid in debugging Valet. + */ + $app->command('diagnose [-p|--print] [--plain]', function ($print, $plain) { + info('Running diagnostics... (this may take a while)'); + + Diagnose::run($print, $plain); + + info('Diagnostics output has been copied to your clipboard.'); + })->descriptions('Output diagnostics to aid in debugging Valet.', [ + '--print' => 'print diagnostics output while running', + '--plain' => 'format clipboard output as plain text', + ]); +} + +/** + * Load all of the Valet extensions. + */ +foreach (Valet::extensions() as $extension) { + include $extension; +} + +/** + * Run the application. + */ +$app->run(); diff --git a/valet/composer.json b/valet/composer.json new file mode 100644 index 0000000..82aa7d9 --- /dev/null +++ b/valet/composer.json @@ -0,0 +1,46 @@ +{ + "name": "laravel/valet", + "description": "A more enjoyable local development experience for Mac.", + "keywords": ["laravel", "zonda", "wwdhhd"], + "license": "MIT", + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Adam Wathan", + "email": "adam.wathan@gmail.com" + } + ], + "autoload": { + "files": [ + "cli/includes/compatibility.php", + "cli/includes/facades.php", + "cli/includes/helpers.php" + ], + "psr-4": { + "Valet\\": "cli/Valet/" + } + }, + "require": { + "php": ">=5.6", + "illuminate/container": "~5.1|^6.0|^7.0|^8.0", + "mnapoli/silly": "~1.0", + "symfony/process": "~3.0|~4.0|~5.0", + "nategood/httpful": "~0.2", + "tightenco/collect": "^5.3|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.2.3", + "yoast/phpunit-polyfills": "^0.2.0" + }, + "bin": [ + "valet" + ], + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + } +} diff --git a/valet/server.php b/valet/server.php new file mode 100644 index 0000000..dc1de22 --- /dev/null +++ b/valet/server.php @@ -0,0 +1,224 @@ +Index of $uri"; + echo "
"; + echo implode("
\n", array_map(function ($path) use ($uri, $is_root) { + $file = basename($path); + return ($is_root) ? "/$file" : "$uri/$file/"; + }, $paths)); + + exit; +} + +/** + * You may use wildcard DNS providers xip.io or nip.io as a tool for testing your site via an IP address. + * It's simple to use: First determine the IP address of your local computer (like 192.168.0.10). + * Then simply use http://project.your-ip.xip.io - ie: http://laravel.192.168.0.10.xip.io + */ +function valet_support_wildcard_dns($domain, $config) +{ + $services = [ + '.*.*.*.*.xip.io', + '.*.*.*.*.nip.io', + '-*-*-*-*.nip.io', + ]; + + if (isset($config['tunnel_services'])) { + $services = array_merge($services, (array) $config['tunnel_services']); + } + + $patterns = []; + foreach ($services as $service) { + $pattern = preg_quote($service, '#'); + $pattern = str_replace('\*', '.*', $pattern); + $patterns[] = '(.*)' . $pattern; + } + + $pattern = implode('|', $patterns); + + if (preg_match('#(?:' . $pattern . ')\z#u', $domain, $matches)) { + $domain = array_pop($matches); + } + + if (strpos($domain, ':') !== false) { + $domain = explode(':', $domain)[0]; + } + + return $domain; +} + +/** + * @param array $config Valet configuration array + * + * @return string|null If set, default site path for uncaught urls + * */ +function valet_default_site_path($config) +{ + if (isset($config['default']) && is_string($config['default']) && is_dir($config['default'])) { + return $config['default']; + } + + return null; +} + +/** + * Load the Valet configuration. + */ +$valetConfig = json_decode( + file_get_contents(VALET_HOME_PATH . '/config.json'), true +); + +/** + * Parse the URI and site / host for the incoming request. + */ +$uri = rawurldecode( + explode("?", $_SERVER['REQUEST_URI'])[0] +); + +$siteName = basename( + // Filter host to support wildcard dns feature + valet_support_wildcard_dns($_SERVER['HTTP_HOST'], $valetConfig), + '.' . $valetConfig['tld'] +); + +if (strpos($siteName, 'www.') === 0) { + $siteName = substr($siteName, 4); +} + +/** + * Determine the fully qualified path to the site. + * Inspects registered path directories, case-sensitive. + */ +$valetSitePath = null; +$domain = array_slice(explode('.', $siteName), -1)[0]; + +foreach ($valetConfig['paths'] as $path) { + if ($handle = opendir($path)) { + while (false !== ($file = readdir($handle))) { + if (!is_dir($path . '/' . $file)) { + continue; + } + + if (in_array($file, ['.', '..', '.DS_Store'])) { + continue; + } + + // match dir for lowercase, because Nginx only tells us lowercase names + if (strtolower($file) === $siteName) { + $valetSitePath = $path . '/' . $file; + break; + } + if (strtolower($file) === $domain) { + $valetSitePath = $path . '/' . $file; + break; + } + } + closedir($handle); + + if ($valetSitePath) { + break; + } + } +} + +if (is_null($valetSitePath) && is_null($valetSitePath = valet_default_site_path($valetConfig))) { + show_valet_404(); +} + +$valetSitePath = realpath($valetSitePath); + +/** + * Find the appropriate Valet driver for the request. + */ +$valetDriver = null; + +require __DIR__ . '/cli/drivers/require.php'; + +$valetDriver = ValetDriver::assign($valetSitePath, $siteName, $uri); + +if (!$valetDriver) { + show_valet_404(); +} + +/** + * ngrok uses the X-Original-Host to store the forwarded hostname. + */ +if (isset($_SERVER['HTTP_X_ORIGINAL_HOST']) && !isset($_SERVER['HTTP_X_FORWARDED_HOST'])) { + $_SERVER['HTTP_X_FORWARDED_HOST'] = $_SERVER['HTTP_X_ORIGINAL_HOST']; +} + +/** + * Attempt to load server environment variables. + */ +$valetDriver->loadServerEnvironmentVariables( + $valetSitePath, $siteName +); + +/** + * Allow driver to mutate incoming URL. + */ +$uri = $valetDriver->mutateUri($uri); + +/** + * Determine if the incoming request is for a static file. + */ +$isPhpFile = pathinfo($uri, PATHINFO_EXTENSION) === 'php'; + +if ($uri !== '/' && !$isPhpFile && $staticFilePath = $valetDriver->isStaticFile($valetSitePath, $siteName, $uri)) { + return $valetDriver->serveStaticFile($staticFilePath, $valetSitePath, $siteName, $uri); +} + +/** + * Attempt to dispatch to a front controller. + */ +$frontControllerPath = $valetDriver->frontControllerPath( + $valetSitePath, $siteName, $uri +); + +if (!$frontControllerPath) { + if (isset($valetConfig['directory-listing']) && $valetConfig['directory-listing'] == 'on') { + show_directory_listing($valetSitePath, $uri); + } + + show_valet_404(); +} + +chdir(dirname($frontControllerPath)); + +require $frontControllerPath; diff --git a/valet/valet b/valet/valet new file mode 100755 index 0000000..aefd810 --- /dev/null +++ b/valet/valet @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +SOURCE="${BASH_SOURCE[0]}" + +# If the current source is a symbolic link, we need to resolve it to an +# actual directory name. We'll use PHP to do this easier than we can +# do it in pure Bash. So, we'll call into PHP CLI here to resolve. +if [[ -L "$SOURCE" ]] +then + DIR=$(php -r "echo dirname(realpath('$SOURCE'));") +else + DIR="$( cd "$( dirname "$SOURCE" )" && pwd )" +fi + +# If we are in the global Composer "bin" directory, we need to bump our +# current directory up two, so that we will correctly proxy into the +# Valet CLI script which is written in PHP. Will use PHP to do it. +if [ ! -f "$DIR/cli/valet.php" ] +then + DIR=$(php -r "echo realpath('$DIR/../laravel/valet');") +fi + +if [[ "$EUID" -ne 0 ]] +then + sudo USER="$USER" --preserve-env "$SOURCE" "$@" + exit +fi + +# If the command is the "share" command we will need to resolve out any +# symbolic links for the site. Before starting Ngrok, we will fire a +# process to retrieve the live Ngrok tunnel URL in the background. +if [[ "$1" = "share" ]] +then + # This should be a code to share using ngrok. + echo "valet (Butler) share isn't supported yet in this version" +else + php "$DIR/cli/valet.php" "$@" +fi