Skip to content

Commit ccc9330

Browse files
authored
Improve handling of old ~/.localhost directory. (#30)
1 parent 7159819 commit ccc9330

File tree

3 files changed

+66
-105
lines changed

3 files changed

+66
-105
lines changed

lib/localhost/authority.rb

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,27 @@ module Localhost
1717
class Authority
1818
# Where to store the key pair on the filesystem. This is a subdirectory
1919
# of $XDG_STATE_HOME, or ~/.local/state/ when that's not defined.
20-
def self.path
21-
File.expand_path("localhost.rb", ENV.fetch("XDG_STATE_HOME", "~/.local/state"))
20+
#
21+
# Ensures that the directory to store the certificate exists. If the legacy
22+
# directory (~/.localhost/) exists, it is moved into the new XDG Basedir
23+
# compliant directory.
24+
#
25+
# After May 2025, the old_root option may be removed.
26+
def self.path(env = ENV, old_root: nil)
27+
path = File.expand_path("localhost.rb", env.fetch("XDG_STATE_HOME", "~/.local/state"))
28+
29+
unless File.directory?(path)
30+
FileUtils.mkdir_p(path, mode: 0700)
31+
end
32+
33+
# Migrates the legacy dir ~/.localhost/ to the XDG compliant directory
34+
old_root ||= File.expand_path("~/.localhost")
35+
if File.directory?(old_root)
36+
FileUtils.mv(Dir.glob(File.join(old_root, "*")), path, force: true)
37+
FileUtils.rmdir(old_root)
38+
end
39+
40+
return path
2241
end
2342

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

182201
def load(path = @root)
183-
ensure_authority_path_exists(path)
184-
185202
certificate_path = File.join(path, "#{@hostname}.crt")
186203
key_path = File.join(path, "#{@hostname}.key")
187204

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

202219
def save(path = @root)
203-
ensure_authority_path_exists(path)
204-
205220
lockfile_path = File.join(path, "#{@hostname}.lock")
206221

207222
File.open(lockfile_path, File::RDWR|File::CREAT, 0644) do |lockfile|
@@ -218,20 +233,5 @@ def save(path = @root)
218233
)
219234
end
220235
end
221-
222-
# Ensures that the directory to store the certificate exists. If the legacy
223-
# directory (~/.localhost/) exists, it is moved into the new XDG Basedir
224-
# compliant directory.
225-
#
226-
# After May 2025, this method should be removed as the legacy directory
227-
# will no longer be contain valid certificates.
228-
def ensure_authority_path_exists(path = @root)
229-
old_root = File.expand_path("~/.localhost")
230-
231-
FileUtils.mkdir_p(path, mode: 0700) unless File.directory?(path)
232-
233-
# Migrates the legacy dir ~/.localhost/ to the XDG compliant directory
234-
FileUtils.mv("#{@old_root}/.", path, force: true) if File.directory?(old_root)
235-
end
236236
end
237237
end

test/localhost/authority.rb

Lines changed: 36 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -17,30 +17,42 @@
1717
require 'tempfile'
1818

1919
describe Localhost::Authority do
20-
def before
21-
@old_root = File.expand_path("~/.localhost")
22-
@old_root_exists = File.directory?(@old_root)
23-
24-
if @old_root_exists
25-
@tmp_folder = File.expand_path("~/.localhost_test")
26-
FileUtils.mkdir_p(@tmp_folder, mode: 0700)
27-
FileUtils.cp_r("#{@old_root}/.", @tmp_folder)
20+
def around
21+
Dir.mktmpdir do |path|
22+
@root = path
23+
24+
yield
25+
ensure
26+
@root = nil
2827
end
2928
end
30-
31-
def after
32-
if @old_root_exists
33-
FileUtils.mkdir_p(@old_root, mode: 0700)
34-
FileUtils.mv("#{@tmp_folder}/.", @old_root, force: true)
35-
FileUtils.rm_r(@tmp_folder)
29+
30+
let(:authority) {subject.new("localhost", root: @root)}
31+
32+
with ".path" do
33+
it "uses XDG_STATE_HOME" do
34+
env = {'XDG_STATE_HOME' => @root}
35+
36+
expect(Localhost::Authority.path(env)).to be == File.expand_path("localhost.rb", @root)
37+
end
38+
39+
it "copies legacy directory" do
40+
xdg_state_home = File.join(@root, ".local", "state")
41+
env = {'XDG_STATE_HOME' => xdg_state_home}
42+
43+
old_root = File.join(@root, ".localhost")
44+
Dir.mkdir(old_root)
45+
File.write(File.join(old_root, "localhost.crt"), "*fake certificate*")
46+
File.write(File.join(old_root, "localhost.key"), "*fake key*")
47+
48+
path = Localhost::Authority.path(env, old_root: old_root)
49+
expect(path).to be == File.expand_path("localhost.rb", xdg_state_home)
50+
expect(File).to be(:exist?, File.expand_path("localhost.crt", path))
51+
expect(File).to be(:exist?, File.expand_path("localhost.key", path))
52+
53+
expect(File).not.to be(:exist?, old_root)
3654
end
3755
end
38-
39-
let(:xdg_dir) { File.join(Dir.pwd, "state") }
40-
let(:authority) {
41-
ENV["XDG_STATE_HOME"] = xdg_dir
42-
subject.new
43-
}
4456

4557
with '#certificate' do
4658
it "is not valid for more than 1 year" do
@@ -52,35 +64,15 @@ def after
5264
end
5365
end
5466

55-
it "can generate key and certificate" do
56-
Dir.mktmpdir('localhost') do |dir|
57-
authority.save(dir)
58-
59-
expect(File).to be(:exist?, File.expand_path("localhost.lock", dir))
60-
expect(File).to be(:exist?, File.expand_path("localhost.crt", dir))
61-
expect(File).to be(:exist?, File.expand_path("localhost.key", dir))
62-
end
63-
end
64-
6567
it "have correct key and certificate path" do
66-
authority.save(authority.class.path)
67-
expect(File).to be(:exist?, authority.certificate_path)
68-
expect(File).to be(:exist?, authority.key_path)
68+
authority.save
6969

70-
expect(authority.key_path).to be == File.join(xdg_dir, "localhost.rb", "localhost.key")
71-
expect(authority.certificate_path).to be == File.join(xdg_dir, "localhost.rb", "localhost.crt")
72-
end
73-
74-
it "properly falls back when XDG_STATE_HOME is not set" do
75-
ENV.delete("XDG_STATE_HOME")
76-
authority = subject.new
77-
78-
authority.save(authority.class.path)
7970
expect(File).to be(:exist?, authority.certificate_path)
8071
expect(File).to be(:exist?, authority.key_path)
8172

82-
expect(authority.key_path).to be == File.join(File.expand_path("~/.local/state/"), "localhost.rb", "localhost.key")
83-
expect(authority.certificate_path).to be == File.join(File.expand_path("~/.local/state/"), "localhost.rb", "localhost.crt")
73+
expect(File).to be(:exist?, File.expand_path("localhost.lock", @root))
74+
expect(File).to be(:exist?, File.expand_path("localhost.crt", @root))
75+
expect(File).to be(:exist?, File.expand_path("localhost.key", @root))
8476
end
8577

8678
with '#store' do
@@ -94,40 +86,4 @@ def after
9486
expect(authority.server_context).to be_a OpenSSL::SSL::SSLContext
9587
end
9688
end
97-
98-
with 'client/server' do
99-
include Sus::Fixtures::Async::ReactorContext
100-
101-
let(:endpoint) {Async::IO::Endpoint.tcp("localhost", 4040)}
102-
let(:server_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: authority.server_context)}
103-
let(:client_endpoint) {Async::IO::SSLEndpoint.new(endpoint, ssl_context: authority.client_context)}
104-
105-
let(:client) {client_endpoint.connect}
106-
107-
def before
108-
@bound_endpoint = Async::IO::SharedEndpoint.bound(server_endpoint)
109-
110-
@server_task = reactor.async do
111-
@bound_endpoint.accept do |peer|
112-
peer.write("Hello World!")
113-
peer.close
114-
end
115-
end
116-
117-
super
118-
end
119-
120-
def after
121-
@server_task&.stop
122-
@bound_endpoint&.close
123-
124-
super
125-
end
126-
127-
it "can verify peer" do
128-
expect(client.read(12)).to be == "Hello World!"
129-
130-
client.close
131-
end
132-
end
13389
end

test/localhost/protocol.rb

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,6 @@
77

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

10-
require 'async/io/host_endpoint'
11-
require 'async/io/ssl_endpoint'
12-
require 'async/io/shared_endpoint'
13-
1410
require 'async/process'
1511
require 'fileutils'
1612

@@ -57,4 +53,13 @@ def make_client_endpoint(bound_endpoint)
5753
it_behaves_like AValidProtocol, "default", [], []
5854
it_behaves_like AValidProtocol, "TLSv1.2", ["-tls1_2"], ["--tlsv1.2"]
5955
it_behaves_like AValidProtocol, "TLSv1.3", ["-tls1_3"], ["--tlsv1.3"]
56+
57+
it "can connect using HTTPS" do
58+
response = client.get("/")
59+
60+
expect(response).to be(:success?)
61+
ensure
62+
response&.finish
63+
client.close
64+
end
6065
end

0 commit comments

Comments
 (0)