Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve handling of old ~/.localhost directory. #30

Merged
merged 1 commit into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 21 additions & 21 deletions lib/localhost/authority.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,27 @@ module Localhost
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.rb", ENV.fetch("XDG_STATE_HOME", "~/.local/state"))
#
# 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.
#
# After May 2025, the old_root option may be removed.
def self.path(env = ENV, old_root: nil)
path = File.expand_path("localhost.rb", env.fetch("XDG_STATE_HOME", "~/.local/state"))

unless File.directory?(path)
FileUtils.mkdir_p(path, mode: 0700)
end

# Migrates the legacy dir ~/.localhost/ to the XDG compliant directory
old_root ||= File.expand_path("~/.localhost")
if File.directory?(old_root)
FileUtils.mv(Dir.glob(File.join(old_root, "*")), path, force: true)
FileUtils.rmdir(old_root)
end

return path
end

# List all certificate authorities in the given directory:
Expand Down Expand Up @@ -180,8 +199,6 @@ def client_context(*args)
end

def load(path = @root)
ensure_authority_path_exists(path)

certificate_path = File.join(path, "#{@hostname}.crt")
key_path = File.join(path, "#{@hostname}.key")

Expand All @@ -200,8 +217,6 @@ def load(path = @root)
end

def save(path = @root)
ensure_authority_path_exists(path)

lockfile_path = File.join(path, "#{@hostname}.lock")

File.open(lockfile_path, File::RDWR|File::CREAT, 0644) do |lockfile|
Expand All @@ -218,20 +233,5 @@ 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.
#
# After May 2025, this method should be removed as the legacy directory
# will no longer be contain valid certificates.
def ensure_authority_path_exists(path = @root)
old_root = File.expand_path("~/.localhost")

FileUtils.mkdir_p(path, mode: 0700) unless File.directory?(path)

# Migrates the legacy dir ~/.localhost/ to the XDG compliant directory
FileUtils.mv("#{@old_root}/.", path, force: true) if File.directory?(old_root)
end
end
end
116 changes: 36 additions & 80 deletions test/localhost/authority.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,30 +17,42 @@
require 'tempfile'

describe Localhost::Authority do
def before
@old_root = File.expand_path("~/.localhost")
@old_root_exists = File.directory?(@old_root)

if @old_root_exists
@tmp_folder = File.expand_path("~/.localhost_test")
FileUtils.mkdir_p(@tmp_folder, mode: 0700)
FileUtils.cp_r("#{@old_root}/.", @tmp_folder)
def around
Dir.mktmpdir do |path|
@root = path

yield
ensure
@root = nil
end
end

def after
if @old_root_exists
FileUtils.mkdir_p(@old_root, mode: 0700)
FileUtils.mv("#{@tmp_folder}/.", @old_root, force: true)
FileUtils.rm_r(@tmp_folder)

let(:authority) {subject.new("localhost", root: @root)}

with ".path" do
it "uses XDG_STATE_HOME" do
env = {'XDG_STATE_HOME' => @root}

expect(Localhost::Authority.path(env)).to be == File.expand_path("localhost.rb", @root)
end

it "copies legacy directory" do
xdg_state_home = File.join(@root, ".local", "state")
env = {'XDG_STATE_HOME' => xdg_state_home}

old_root = File.join(@root, ".localhost")
Dir.mkdir(old_root)
File.write(File.join(old_root, "localhost.crt"), "*fake certificate*")
File.write(File.join(old_root, "localhost.key"), "*fake key*")

path = Localhost::Authority.path(env, old_root: old_root)
expect(path).to be == File.expand_path("localhost.rb", xdg_state_home)
expect(File).to be(:exist?, File.expand_path("localhost.crt", path))
expect(File).to be(:exist?, File.expand_path("localhost.key", path))

expect(File).not.to be(:exist?, old_root)
end
end

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
Expand All @@ -52,35 +64,15 @@ def after
end
end

it "can generate key and certificate" do
Dir.mktmpdir('localhost') do |dir|
authority.save(dir)

expect(File).to be(:exist?, File.expand_path("localhost.lock", dir))
expect(File).to be(:exist?, File.expand_path("localhost.crt", dir))
expect(File).to be(:exist?, File.expand_path("localhost.key", dir))
end
end

it "have correct key and certificate path" do
authority.save(authority.class.path)
expect(File).to be(:exist?, authority.certificate_path)
expect(File).to be(:exist?, authority.key_path)
authority.save

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")
expect(File).to be(:exist?, File.expand_path("localhost.lock", @root))
expect(File).to be(:exist?, File.expand_path("localhost.crt", @root))
expect(File).to be(:exist?, File.expand_path("localhost.key", @root))
end

with '#store' do
Expand All @@ -94,40 +86,4 @@ def after
expect(authority.server_context).to be_a OpenSSL::SSL::SSLContext
end
end

with 'client/server' do
include Sus::Fixtures::Async::ReactorContext

let(:endpoint) {Async::IO::Endpoint.tcp("localhost", 4040)}
let(:server_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: authority.server_context)}
let(:client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: authority.client_context)}

let(:client) {client_endpoint.connect}

def before
@bound_endpoint = Async::IO::SharedEndpoint.bound(server_endpoint)

@server_task = reactor.async do
@bound_endpoint.accept do |peer|
peer.write("Hello World!")
peer.close
end
end

super
end

def after
@server_task&.stop
@bound_endpoint&.close

super
end

it "can verify peer" do
expect(client.read(12)).to be == "Hello World!"

client.close
end
end
end
13 changes: 9 additions & 4 deletions test/localhost/protocol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@

require 'sus/fixtures/async/http/server_context'

require 'async/io/host_endpoint'
require 'async/io/ssl_endpoint'
require 'async/io/shared_endpoint'

require 'async/process'
require 'fileutils'

Expand Down Expand Up @@ -57,4 +53,13 @@ def make_client_endpoint(bound_endpoint)
it_behaves_like AValidProtocol, "default", [], []
it_behaves_like AValidProtocol, "TLSv1.2", ["-tls1_2"], ["--tlsv1.2"]
it_behaves_like AValidProtocol, "TLSv1.3", ["-tls1_3"], ["--tlsv1.3"]

it "can connect using HTTPS" do
response = client.get("/")

expect(response).to be(:success?)
ensure
response&.finish
client.close
end
end
Loading