From 3397aaca8e208a234fafa4182ca74b129c21a286 Mon Sep 17 00:00:00 2001 From: Aaron Kromer Date: Tue, 18 Dec 2018 11:03:43 -0500 Subject: [PATCH 1/3] Initially attempt at porting puma-dev command First thor command so not exactly sure yet the best way to set it up or register it. This initial attempt uses the `Thor::Group` to convert the procedural script into better defined groups. --- lib/radius/cli/app.rb | 3 + lib/radius/cli/helpers.rb | 5 + lib/radius/cli/puma_dev.rb | 243 +++++++++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 lib/radius/cli/puma_dev.rb diff --git a/lib/radius/cli/app.rb b/lib/radius/cli/app.rb index 6e51fd2..e2ec616 100644 --- a/lib/radius/cli/app.rb +++ b/lib/radius/cli/app.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'thor' +require "radius/cli/puma_dev" module Radius module Cli @@ -8,6 +9,8 @@ class App < Thor def self.exit_on_failure? true end + + register ::Radius::Cli::PumaDev, "puma_dev", "puma_dev", PumaDev::DESCRIPTION end end end diff --git a/lib/radius/cli/helpers.rb b/lib/radius/cli/helpers.rb index 91b643b..977f640 100644 --- a/lib/radius/cli/helpers.rb +++ b/lib/radius/cli/helpers.rb @@ -6,6 +6,7 @@ module Radius module Cli + # Common script / command setup helpers module Helpers APP_ROOT = Pathname.getwd.ascend.find { |path| path.join("Gemfile").exist? } @@ -19,6 +20,10 @@ def app_root APP_ROOT end + def app_domain + ENV['APP_DOMAIN'] + end + def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end diff --git a/lib/radius/cli/puma_dev.rb b/lib/radius/cli/puma_dev.rb new file mode 100644 index 0000000..b63c9cc --- /dev/null +++ b/lib/radius/cli/puma_dev.rb @@ -0,0 +1,243 @@ +# frozen_string_literal: true + +require "dotenv" +require_relative "helpers" + +module Radius + module Cli + # CLI command for setting up puma-dev for local HTTPS development + class PumaDev < Thor::Group + include Helpers + + DESCRIPTION = "Configure current app for local HTTPS development through puma-dev" + + desc DESCRIPTION + + class_option :cert, + type: :boolean, + default: false, + desc: "Add Puma-dev CA as trusted user cert to login keychain" + + class_option :force, + type: :boolean, + default: false, + desc: "Force overwriting links and ENV settings" + + class_option :setup, + type: :boolean, + default: false, + desc: "Initial puma-dev setup" + + class_option :verbose, + type: :boolean, + default: false, + desc: "Print progress to standard out" + + def toggle_verbosity + $stdout.reopen(IO::NULL) unless options[:verbose] + end + + def pow_conflict_check + return unless system("which pow 1>/dev/null 2>&1") + + abort <<~INFO + + CONFLICT: aborting puma-dev setup because pow is installed + Please uninstall pow then setup puma-dev. + See https://github.com/puma/puma-dev + INFO + end + + def puma_dev_check + @cmd = `which puma-dev`.chomp + abort "Unable to configure puma-dev: not installed" unless $CHILD_STATUS.success? + end + + def load_env + Dotenv.load '.env' + end + + def resetup_check + @cert = Pathname("~/Library/Application Support/io.puma.dev/cert.pem").expand_path + return unless options[:setup] && cert.exist? + + puts "puma-dev appears to already be setup." + printf "Re-install? (y/n) " + resetup = $stdin.gets(chomp: true).downcase.start_with?("y") + self.options = options.merge(setup: resetup) + end + + def puma_dev_setup + return unless options[:setup] + + puts "Initial puma-dev setup requires sudo access for DNS settings" + system! "sudo #{cmd} -setup" + puts "Configuring to run in background..." + system! "#{cmd} -install" + options[:cert] = true + system "#{cmd} -launchd" + puts <<~MESSAGE + + You'll probably need to reboot before puma-dev runs in the background automatically + MESSAGE + end + + def find_keychain + known_keychains = %w[ + ~/Library/Keychains/login.keychain-db + ~/Library/Keychains/login.keychain + ] + @keychain = known_keychains.map { |chain| Pathname(chain).expand_path } + .find(&:exist?) + end + + def verify_cert + return unless options[:cert] + + puts "Verifying Puma-dev CA cert..." + rewrite = if keychain + verify_cmd = %W[security verify-cert -r #{cert} -k #{keychain} -L -p ssl] + !system(*verify_cmd) + else + warn "Unable to locate keychain from list: #{known_keychains}" + false + end + self.options = options.merge(cert: rewrite) + end + + def setup_cert + return unless options[:cert] && cert.exist? + + puts "Adding trusted Puma-dev CA cert..." + if keychain + add_cert_cmd = %W[ + security add-trusted-cert -r trustRoot -p ssl -k #{keychain} #{cert} + ] + system!(*add_cert_cmd) + else + warn "Unable to locate keychain from list: #{known_keychains}" + end + end + + def configure_ssl + return unless options[:cert] && cert.exist? + + puts "\nConfiguring SSL..." + @combined_cert = Pathname("~/.ssh/pumadev.pem").expand_path + if combined_cert.exist? && !options[:force] + puts "Using existing custom CA SSL cert for puma-dev" + return + end + + base_cert = Pathname("/usr/local/etc/openssl/cert.pem") + abort "Missing OS root cert: #{base_cert}" unless base_cert.exist? + abort "Unable to read root cert: #{base_cert}" unless base_cert.readable? + unless cert.exist? + abort <<~INFO + Missing puma-dev cert: #{cert} + Try setting up puma-dev: #{$PROGRAM_NAME} --setup + INFO + end + abort "Unable to read puma-dev cert: #{cert}" unless cert.readable? + combined_cert.delete if options[:force] && combined_cert.exist? + puts "Creating custom CA SSL cert for puma-dev..." + File.open(combined_cert, "w") do |cert_file| + cert_file.write base_cert.read + cert_file.write "\n" + cert_file.write cert.read + end + end + + def configure_app + puts "\nConfiguring app..." + end + + def configure_app_ssl + return unless combined_cert + + cert_env = "SSL_CERT_FILE=\"#{combined_cert.to_path}\"" + env_contents = File.read(env_file) + env_with_ssl = if env_contents.include?("SSL_CERT_FILE") + puts "Updating SSL_CERT_FILE environment variable" + env_contents.gsub(/^.*SSL_CERT_FILE.*/, cert_env) + else + puts "Adding SSL_CERT_FILE environment variable" + env_contents + "\n#{cert_env}" + end + File.write env_file, env_with_ssl + end + + def force_app_ssl + return unless options[:force] + + puts "Enabling local dev SSL by default..." + File.write( + env_file, + File.read(env_file) + .gsub( + %r{http://(?[\w.]+)\.test}, + 'https://\k.test', + ) + .gsub( + /^.*DISABLE_FORCE_SSL=".*"/, + 'DISABLE_FORCE_SSL="false"', + ), + ) + end + + def configure_powrc + powrc = app_root.join(".powrc") + chruby_path = Pathname(ENV.fetch("CHRUBY_PATH", "/usr/local/opt/chruby/share/chruby/chruby.sh")) + if powrc.exist? && !options[:force] + puts "Using existing .powrc" + return + end + + case + when chruby_path.exist? + powrc.write <<~EOF + source /usr/local/opt/chruby/share/chruby/chruby.sh + chruby $(cat .ruby-version) + EOF + when system("which", "rbenv", err: :out, out: IO::NULL) # rubocop:disable Lint/EmptyWhen + # NOTE: Unclear if `rbenv` systems need a customization / what it should be + when system("which", "rvm", err: :out, out: IO::NULL) + powrc.write <<~EOF + if [ -f "$rvm_path/scripts/rvm" ] && [ -f ".ruby-version" ]; then + source "$rvm_path/scripts/rvm" + rvm use `cat .ruby-version`@`cat .ruby-gemset` + fi + EOF + else + warn "Unknown Ruby version manager. Please install chruby, rbenv, or rvm." + end + end + + def link_project + puts "\nLinking project..." + link_dir = Pathname("~/.puma-dev").expand_path + link_path = link_dir.join(app_domain) + mkdir_p link_dir, verbose: options[:verbose] if options[:force] || !link_dir.exist? + if options[:force] && (link_path.symlink? || link_path.exist?) + puts "App link exists. Deleting existing link: #{link_path}" + link_path.delete + end + system! "#{cmd} link -n #{app_domain} #{app_root}" unless link_path.exist? + puts "Project linked at: #{app_domain}.test" + end + + def restart_puma_dev + puts "Restarting puma-dev..." + system! "#{cmd} -stop" + end + + private + + attr_reader :cert, :cmd, :combined_cert, :keychain + + def env_file + app_root.join(".env") + end + end + end +end From bee3943b3c8f9a27d86f2214f8d28a07174ff06d Mon Sep 17 00:00:00 2001 From: Aaron Kromer Date: Tue, 18 Dec 2018 14:38:47 -0500 Subject: [PATCH 2/3] Note command in CHANGELOG --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa5e34..912453a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,7 @@ ### Initial release: -- TODO +- Add `puma_dev` command + + This command is designed to setup a dotenv Rails project to use HTTPS via + [puma-dev](https://github.com/puma/puma-dev). From 08493e7ddf5ee9ccbc9b21773ebbd0742bb8570e Mon Sep 17 00:00:00 2001 From: Aaron Kromer Date: Wed, 19 Dec 2018 09:28:00 -0500 Subject: [PATCH 3/3] Fix ordering issue with puma-dev cert checks In the case puma-dev is never setup the script will skip everything and not inform the user. This move the cert check to the setup step because we cannot register it with the OS unless it exists. After that point we know the cert exists so we do not need to check it again. --- lib/radius/cli/puma_dev.rb | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/lib/radius/cli/puma_dev.rb b/lib/radius/cli/puma_dev.rb index b63c9cc..98bde96 100644 --- a/lib/radius/cli/puma_dev.rb +++ b/lib/radius/cli/puma_dev.rb @@ -106,7 +106,14 @@ def verify_cert end def setup_cert - return unless options[:cert] && cert.exist? + return unless options[:cert] + + unless cert.exist? + abort <<~INFO + Missing puma-dev cert: #{cert} + Try setting up puma-dev: #{$PROGRAM_NAME} --setup + INFO + end puts "Adding trusted Puma-dev CA cert..." if keychain @@ -120,7 +127,7 @@ def setup_cert end def configure_ssl - return unless options[:cert] && cert.exist? + return unless options[:cert] puts "\nConfiguring SSL..." @combined_cert = Pathname("~/.ssh/pumadev.pem").expand_path @@ -132,12 +139,6 @@ def configure_ssl base_cert = Pathname("/usr/local/etc/openssl/cert.pem") abort "Missing OS root cert: #{base_cert}" unless base_cert.exist? abort "Unable to read root cert: #{base_cert}" unless base_cert.readable? - unless cert.exist? - abort <<~INFO - Missing puma-dev cert: #{cert} - Try setting up puma-dev: #{$PROGRAM_NAME} --setup - INFO - end abort "Unable to read puma-dev cert: #{cert}" unless cert.readable? combined_cert.delete if options[:force] && combined_cert.exist? puts "Creating custom CA SSL cert for puma-dev..."