diff --git a/cli/Valet/Cloudflared.php b/cli/Valet/Cloudflared.php new file mode 100644 index 00000000..aa60ae8d --- /dev/null +++ b/cli/Valet/Cloudflared.php @@ -0,0 +1,70 @@ +cli->run('pgrep -fl cloudflared'))) as $process) { + // Get the URL shared in this process + preg_match('/(?\d+)\s.+--http-host-header\s(?[^\s]+).*/', $process, $pgrepMatches); + + if (! array_key_exists('domain', $pgrepMatches) || ! array_key_exists('pid', $pgrepMatches)) { + continue; + } + + if ($pgrepMatches['domain'] !== $domain) { + continue; + } + + // Get the localhost URL for the metrics server + $lsof = $this->cli->run("lsof -iTCP -P -a -p {$pgrepMatches['pid']}"); + preg_match('/TCP\s(?[^\s]+:\d+)\s\(LISTEN\)/', $lsof, $lsofMatches); + + if (! array_key_exists('server', $lsofMatches)) { + continue; + } + + try { + // Get the cloudflared share URL from the metrics server output + $body = (new Client())->get("http://{$lsofMatches['server']}/metrics")->getBody(); + preg_match('/userHostname="(?.+)"/', $body->getContents(), $userHostnameMatches); + } catch (\Exception $e) { + return false; + } + + if (array_key_exists('url', $userHostnameMatches)) { + return $userHostnameMatches['url']; + } + } + + return false; + } + + /** + * Return whether cloudflared is installed. + */ + public function installed(): bool + { + return $this->brew->installed('cloudflared'); + } + + /** + * Make sure cloudflared is installed. + */ + public function ensureInstalled(): void + { + $this->brew->ensureInstalled('cloudflared'); + } +} diff --git a/cli/Valet/Expose.php b/cli/Valet/Expose.php index cd00d5e1..a0f90998 100644 --- a/cli/Valet/Expose.php +++ b/cli/Valet/Expose.php @@ -9,7 +9,7 @@ class Expose { public function __construct(public Composer $composer, public CommandLine $cli) {} - public function currentTunnelUrl(?string $domain = null): ?string + public function currentTunnelUrl(string $domain): ?string { $endpoint = 'http://127.0.0.1:4040/api/tunnels'; diff --git a/cli/Valet/Ngrok.php b/cli/Valet/Ngrok.php index 58583d17..ffa67a9d 100644 --- a/cli/Valet/Ngrok.php +++ b/cli/Valet/Ngrok.php @@ -18,7 +18,7 @@ public function __construct(public CommandLine $cli, public Brew $brew) {} /** * Get the current tunnel URL from the Ngrok API. */ - public function currentTunnelUrl(?string $domain = null): string + public function currentTunnelUrl(string $domain): string { // wait a second for ngrok to start before attempting to find available tunnels sleep(1); diff --git a/cli/app.php b/cli/app.php index 4fcb1ba6..37e05ebb 100644 --- a/cli/app.php +++ b/cli/app.php @@ -47,6 +47,12 @@ function (ConsoleCommandEvent $event) { Upgrader::onEveryRun(); +$share_tools = [ + 'cloudflared', + 'expose', + 'ngrok' +]; + /** * Install Valet and any required services. */ @@ -378,83 +384,60 @@ function (ConsoleCommandEvent $event) { /** * Echo the currently tunneled URL. */ - $app->command('fetch-share-url [domain]', function ($domain = null) { + $app->command('fetch-share-url [domain]', function ($domain = null) use ($share_tools) { $tool = Configuration::read()['share-tool'] ?? null; - switch ($tool) { - case 'expose': - if ($url = Expose::currentTunnelUrl($domain ?: Site::host(getcwd()))) { - output($url); - } - break; - case 'ngrok': - try { - output(Ngrok::currentTunnelUrl(Site::domain($domain))); - } catch (\Throwable $e) { - warning($e->getMessage()); - } - break; - default: - info('Please set your share tool with `valet share-tool expose` or `valet share-tool ngrok`.'); + if ($tool && in_array($tool, $share_tools) && class_exists($tool)) { + try { + output($tool::currentTunnelUrl(Site::domain($domain))); + } catch (\Throwable $e) { + warning($e->getMessage()); + } + } else { + info('Please set your share tool with `valet share-tool`.'); - return Command::FAILURE; + return Command::FAILURE; } - })->descriptions('Get the URL to the current share tunnel (for Expose or ngrok)'); + })->descriptions('Get the URL to the current share tunnel'); /** * Echo or set the name of the currently-selected share tool (either "ngrok" or "expose"). */ - $app->command('share-tool [tool]', function (InputInterface $input, OutputInterface $output, $tool = null) { + $app->command('share-tool [tool]', function (InputInterface $input, OutputInterface $output, $tool = null) + use ($share_tools) { if ($tool === null) { return output(Configuration::read()['share-tool'] ?? '(not set)'); } - if ($tool !== 'expose' && $tool !== 'ngrok') { - warning($tool.' is not a valid share tool. Please use `ngrok` or `expose`.'); + $share_tools_list = preg_replace('/,\s([^,]+)$/', ' or $1', + join(', ', array_map(fn($t) => "`$t`", $share_tools))); + + if (! in_array($tool, $share_tools) || ! class_exists($tool)) { + warning("$tool is not a valid share tool. Please use $share_tools_list."); return Command::FAILURE; } Configuration::updateKey('share-tool', $tool); - info('Share tool set to '.$tool.'.'); - - if ($tool === 'expose') { - if (Expose::installed()) { - // @todo: Check it's the right version (has /api/tunnels/) - // E.g. if (Expose::installedVersion) - // if (version_compare(Expose::installedVersion(), $minimumExposeVersion) < 0) { - // prompt them to upgrade - return; - } + info("Share tool set to $tool."); + if (! $tool::installed()) { $helper = $this->getHelperSet()->get('question'); - $question = new ConfirmationQuestion('Would you like to install Expose now? [y/N] ', false); + $question = new ConfirmationQuestion( + 'Would you like to install '.ucfirst($tool).' now? [y/N] ', + false); if ($helper->ask($input, $output, $question) === false) { - info('Proceeding without installing Expose.'); + info('Proceeding without installing '.ucfirst($tool).'.'); return; } - Expose::ensureInstalled(); - - return; + $tool::ensureInstalled(); } - if (! Ngrok::installed()) { - info("\nIn order to share with ngrok, you'll need a version\nof ngrok installed and managed by Homebrew."); - $helper = $this->getHelperSet()->get('question'); - $question = new ConfirmationQuestion('Would you like to install ngrok via Homebrew now? [y/N] ', false); - - if ($helper->ask($input, $output, $question) === false) { - info('Proceeding without installing ngrok.'); - - return; - } - - Ngrok::ensureInstalled(); - } - })->descriptions('Get the name of the current share tool (Expose or ngrok).'); + return Command::SUCCESS; + })->descriptions('Get the name of the current share tool.'); /** * Set the ngrok auth token. diff --git a/cli/includes/facades.php b/cli/includes/facades.php index 3e929ed2..30f6876c 100644 --- a/cli/includes/facades.php +++ b/cli/includes/facades.php @@ -38,3 +38,4 @@ class Site extends Facade {} class Status extends Facade {} class Upgrader extends Facade {} class Valet extends Facade {} +class Cloudflared extends Facade {} diff --git a/valet b/valet index f1e4c643..9f6e4c21 100755 --- a/valet +++ b/valet @@ -41,12 +41,41 @@ else 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. +# symbolic links for the site. Before starting the share tool, we will fire a +# process to retrieve the live the share tool tunnel URL in the background. if [[ "$1" = "share" ]] then SHARETOOL="$("$PHP" "$DIR/cli/valet.php" share-tool)" + # Check for parameters to pass through to share tool (these will start with '-' or '--') + PARAMS=(${@:2}) + + for PARAM in ${PARAMS[@]} + do + if [[ ${PARAM:0:1} == '-' ]]; then + PARAMS=("${PARAMS[@]/$PARAM}") # Quotes when working with strings + fi + done + + PARAMS=${PARAMS[@]} + + HOST="${PWD##*/}" + + # Find the first linked site for the current dir, if one exists + for linkname in ~/.config/valet/Sites/*; do + if [[ "$(readlink $linkname)" = "$PWD" ]] + then + HOST="${linkname##*/}" + break + fi + done + + # Lowercase the host to match how the rest of our domains are looked up + HOST=$(echo "$HOST" | tr '[:upper:]' '[:lower:]') + TLD=$("$PHP" "$DIR/cli/valet.php" tld) + $(grep --quiet --no-messages 443 ~/.config/valet/Nginx/$HOST*) + SECURED=$? + if [[ $SHARETOOL = "ngrok" ]] then # ngrok @@ -59,41 +88,13 @@ then exit fi - # Check for parameters to pass through to ngrok (these will start with '-' or '--') - PARAMS=(${@:2}) - for PARAM in ${PARAMS[@]} - do - if [[ ${PARAM:0:1} != '-' ]]; then - PARAMS=("${PARAMS[@]/$PARAM}") # Quotes when working with strings - fi - done - - PARAMS=${PARAMS[@]} - - HOST="${PWD##*/}" - - # Find the first linked site for the current dir, if one exists - for linkname in ~/.config/valet/Sites/*; do - if [[ "$(readlink $linkname)" = "$PWD" ]] - then - HOST="${linkname##*/}" - break - fi - done - - TLD=$("$PHP" "$DIR/cli/valet.php" tld) - # Decide the correct PORT: uses 60 for secure, else 80 - if grep --quiet --no-messages 443 ~/.config/valet/Nginx/$HOST* - then + if [[ $SECURED -eq 0 ]]; then PORT=60 else PORT=80 fi - # Lowercase the host to match how the rest of our domains are looked up - HOST=$(echo "$HOST" | tr '[:upper:]' '[:lower:]') - sudo -u "$USER" "$BREW_PREFIX/bin/ngrok" http "$HOST.$TLD:$PORT" --host-header=rewrite $PARAMS exit @@ -102,48 +103,34 @@ then then # expose - # Check for parameters to pass through to Expose (these will start with '-' or '--') - PARAMS=(${@:2}) - for PARAM in ${PARAMS[@]} - do - if [[ ${PARAM:0:1} != '-' ]]; then - PARAMS=("${PARAMS[@]/$PARAM}") #Quotes when working with strings - fi - done - - PARAMS=${PARAMS[@]} - - HOST="${PWD##*/}" - - # Find the first linked site for the current dir, if one exists - for linkname in ~/.config/valet/Sites/*; do - if [[ "$(readlink $linkname)" = "$PWD" ]] - then - HOST="${linkname##*/}" - break - fi - done - - TLD=$("$PHP" "$DIR/cli/valet.php" tld) - # Decide the correct PORT: uses 443 for secure, else 80 - if grep --quiet --no-messages 443 ~/.config/valet/Nginx/$HOST* - then + if [[ $SECURED -eq 0 ]]; then PORT=443 else PORT=80 fi - # Lowercase the host to match how the rest of our domains are looked up - HOST=$(echo "$HOST" | tr '[:upper:]' '[:lower:]') - sudo -u "$USER" expose share "$HOST.$TLD:$PORT" $PARAMS exit + elif [[ $SHARETOOL = "cloudflared" ]] + then + # cloudflared + if [[ $SECURED -eq 0 ]]; then + SCHEME="https" + else + SCHEME="http" + fi + + sudo -u "$USER" cloudflared tunnel --no-tls-verify --url "$SCHEME://localhost" \ + --http-host-header "$HOST.$TLD" $PARAMS + + exit + else echo '' - echo "Please use 'valet share-tool ngrok' or 'valet share-tool expose'" + echo "Please use 'valet share-tool cloudflared', 'valet share-tool expose' or 'valet share-tool ngrok'" echo "to set your preferred share tool." exit fi