diff --git a/.gitignore b/.gitignore index 09a72e0..c14184f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /gems.locked /.covered.db /external +/state diff --git a/guides/getting-started/README.md b/guides/getting-started/README.md index 9bf8966..9568785 100644 --- a/guides/getting-started/README.md +++ b/guides/getting-started/README.md @@ -18,7 +18,7 @@ $ bundle add localhost ### Files -The certificate and private key are stored in `~/.localhost/`. You can delete them and they will be regenerated. If you added the certificate to your computer's certificate store/keychain, you'll you'd need to update it. +The certificate and private key are stored in `$XDG_STATE_HOME/localhost.rb/` (defaulting to `~/.local/state/localhost.rb/`). You can delete them and they will be regenerated. If you added the certificate to your computer's certificate store/keychain, you'll you'd need to update it. ## Usage diff --git a/lib/localhost/authority.rb b/lib/localhost/authority.rb index 5275b71..debff6a 100644 --- a/lib/localhost/authority.rb +++ b/lib/localhost/authority.rb @@ -8,13 +8,16 @@ # Copyright, 2023, by Antonio Terceiro. # Copyright, 2023, by Yuuji Yaginuma. +require 'fileutils' require 'openssl' module Localhost # Represents a single public/private key pair for a given hostname. class Authority + # Where to store the key pair on the filesystem. This is a subdirectory + # of $XDG_STATE_HOME, or ~/.local/state/ when that's not defined. def self.path - File.expand_path("~/.localhost") + File.expand_path("localhost.rb", ENV.fetch("XDG_STATE_HOME", "~/.local/state")) end # List all certificate authorities in the given directory: @@ -176,27 +179,27 @@ def client_context(*args) end def load(path = @root) - if File.directory?(path) - certificate_path = File.join(path, "#{@hostname}.crt") - key_path = File.join(path, "#{@hostname}.key") - - return false unless File.exist?(certificate_path) and File.exist?(key_path) - - certificate = OpenSSL::X509::Certificate.new(File.read(certificate_path)) - key = OpenSSL::PKey::RSA.new(File.read(key_path)) - - # Certificates with old version need to be regenerated. - return false if certificate.version < 2 - - @certificate = certificate - @key = key - - return true - end + ensure_authority_path_exists(path) + + certificate_path = File.join(path, "#{@hostname}.crt") + key_path = File.join(path, "#{@hostname}.key") + + return false unless File.exist?(certificate_path) and File.exist?(key_path) + + certificate = OpenSSL::X509::Certificate.new(File.read(certificate_path)) + key = OpenSSL::PKey::RSA.new(File.read(key_path)) + + # Certificates with old version need to be regenerated. + return false if certificate.version < 2 + + @certificate = certificate + @key = key + + return true end def save(path = @root) - Dir.mkdir(path, 0700) unless File.directory?(path) + ensure_authority_path_exists(path) lockfile_path = File.join(path, "#{@hostname}.lock") @@ -214,5 +217,19 @@ def save(path = @root) ) end end + + # Ensures that the directory to store the certificate exists. If the legacy + # directory (~/.localhost/) exists, it is moved into the new XDG Basedir + # compliant directory. + def ensure_authority_path_exists(path = @root) + old_root = File.expand_path("~/.localhost") + + if File.directory?(old_root) and not File.directory?(path) + # Migrates the legacy dir ~/.localhost/ to the XDG compliant directory + File.rename(old_root, path) + elsif not File.directory?(path) + FileUtils.makedirs(path, mode: 0700) + end + end end end diff --git a/test/localhost/authority.rb b/test/localhost/authority.rb index 50df694..2d966e2 100644 --- a/test/localhost/authority.rb +++ b/test/localhost/authority.rb @@ -15,8 +15,12 @@ require 'fileutils' describe Localhost::Authority do - let(:authority) {subject.new} - + let(:xdg_dir) { File.join(Dir.pwd, "state") } + let(:authority) { + ENV["XDG_STATE_HOME"] = xdg_dir + subject.new + } + with '#certificate' do it "is not valid for more than 1 year" do certificate = authority.certificate @@ -28,7 +32,6 @@ end it "can generate key and certificate" do - FileUtils.mkdir_p("ssl") authority.save("ssl") expect(File).to be(:exist?, "ssl/localhost.lock") @@ -41,8 +44,20 @@ expect(File).to be(:exist?, authority.certificate_path) expect(File).to be(:exist?, authority.key_path) - expect(authority.key_path).to be == File.join(File.expand_path("~/.localhost"), "localhost.key") - expect(authority.certificate_path).to be == File.join(File.expand_path("~/.localhost"), "localhost.crt") + expect(authority.key_path).to be == File.join(xdg_dir, "localhost.rb", "localhost.key") + expect(authority.certificate_path).to be == File.join(xdg_dir, "localhost.rb", "localhost.crt") + end + + it "properly falls back when XDG_STATE_HOME is not set" do + ENV.delete("XDG_STATE_HOME") + authority = subject.new + + authority.save(authority.class.path) + expect(File).to be(:exist?, authority.certificate_path) + expect(File).to be(:exist?, authority.key_path) + + expect(authority.key_path).to be == File.join(File.expand_path("~/.local/state/"), "localhost.rb", "localhost.key") + expect(authority.certificate_path).to be == File.join(File.expand_path("~/.local/state/"), "localhost.rb", "localhost.crt") end with '#store' do