-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Respond to /.well-known/acme-challenge requests using ACME_CHALLENGE_…
…* envs (#44)
- Loading branch information
Showing
14 changed files
with
359 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
module Kontena | ||
class AcmeChallenges | ||
include Kontena::Logging | ||
|
||
def self.env | ||
ENV | ||
end | ||
|
||
# @return [Boolean] | ||
def self.configured? | ||
env.any? { |env, value| env.start_with? 'ACME_CHALLENGE_' } | ||
end | ||
|
||
def self.load_env(env) | ||
challenges = {} | ||
|
||
env.each do |env, value| | ||
_, prefix, suffix = env.partition(/^ACME_CHALLENGE_/) | ||
|
||
if prefix | ||
challenges[suffix] = value | ||
end | ||
end | ||
|
||
challenges | ||
end | ||
|
||
# Setup from ENV | ||
def self.boot(env = ENV) | ||
manager = new(load_env(env)) | ||
end | ||
|
||
attr_reader :challenges | ||
|
||
# @param challenges [Hash{String => String}] ACME challenge token => keyAuthorization | ||
def initialize(challenges) | ||
@challenges = challenges | ||
end | ||
|
||
# @return [Boolean] | ||
def challenges? | ||
!challenges.empty? | ||
end | ||
|
||
# @param challenge [String] ACME challenge token from /.well-known/acme-challenge/... | ||
# @return [String, nil] ACME challenge keyAuthorization | ||
def respond(challenge) | ||
@challenges[challenge] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
module Kontena::Actors | ||
class AcmeChallengeServer < Concurrent::Actor::RestartingContext | ||
include Kontena::Logging | ||
|
||
PORT = 54321 | ||
|
||
class Servlet < WEBrick::HTTPServlet::AbstractServlet | ||
def initialize(server, acme_challenges) | ||
super(server) | ||
@acme_challenges = acme_challenges | ||
end | ||
|
||
def do_GET(req, res) | ||
parts = req.path.split('/') | ||
|
||
raise WEBrick::HTTPStatus::NotFound unless parts.length == 4 | ||
|
||
challenge = parts[-1] | ||
|
||
if key_authorization = @acme_challenges.respond(challenge) | ||
res.status = 200 | ||
res.content_type = 'text/plain' | ||
res.body = key_authorization | ||
else | ||
res.status = 404 | ||
res.content_type = 'text/plain' | ||
res.body = "No key authorization for challenge: #{challenge}\n" | ||
end | ||
end | ||
end | ||
|
||
def initialize(acme_challenges, port: PORT, webrick_options: {}) | ||
@acme_challenges = acme_challenges | ||
|
||
info "initialize" | ||
|
||
@server = WEBrick::HTTPServer.new( | ||
BindAddress: '127.0.0.1', | ||
Port: port, | ||
**webrick_options | ||
) | ||
@server.mount '/.well-known/acme-challenge', Servlet, @acme_challenges | ||
end | ||
|
||
# @param [Symbol,Array] msg | ||
def on_message(msg) | ||
case msg | ||
when :start | ||
start | ||
else | ||
pass | ||
end | ||
end | ||
|
||
def start | ||
# this blocks the actor executor thread in the accept() loop | ||
# the webrick servlets run as separate threads managed by webrick | ||
@server.start | ||
end | ||
|
||
def default_executor | ||
Concurrent.global_io_executor | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
describe Kontena::AcmeChallenges do | ||
let(:challenge_token) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0' } | ||
let(:key_authorization) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI' } | ||
|
||
subject { described_class.new( | ||
challenge_token => key_authorization, | ||
) } | ||
|
||
describe '#configured?' do | ||
let(:env) { { } } | ||
|
||
before do | ||
allow(described_class).to receive(:env).and_return(env) | ||
end | ||
|
||
context 'with an empty env' do | ||
it 'is false' do | ||
expect(described_class.configured?).to be_falsey | ||
end | ||
end | ||
|
||
context 'with non-challenge envs' do | ||
let(:env) { { 'FOO' => 'bar' } } | ||
|
||
it 'is false' do | ||
expect(described_class.configured?).to be_falsey | ||
end | ||
end | ||
|
||
context 'with challenge envs' do | ||
let(:env) { { "ACME_CHALLENGE_#{challenge_token}" => key_authorization } } | ||
|
||
it 'is true' do | ||
expect(described_class.configured?).to be_truthy | ||
end | ||
end | ||
end | ||
|
||
describe '#boot' do | ||
it 'loads nothing from env empty env' do | ||
challenges = described_class.boot({}) | ||
|
||
expect(challenges.challenges?).to be_falsey | ||
end | ||
|
||
it 'loads challenge from ACME_CHALLENGE_*' do | ||
challenges = described_class.boot({"ACME_CHALLENGE_#{challenge_token}" => key_authorization}) | ||
|
||
expect(challenges.challenges).to eq(challenge_token => key_authorization) | ||
end | ||
end | ||
|
||
describe '#respond' do | ||
it 'returns nil for a non-existant challenge' do | ||
expect(subject.respond('foo')).to be_nil | ||
end | ||
|
||
it 'returns the key authorization for a known challenge' do | ||
expect(subject.respond(challenge_token)).to eq key_authorization | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
describe Kontena::Actors::AcmeChallengeServer do | ||
let(:acme_challenges) { instance_double(Kontena::AcmeChallenges) } | ||
|
||
subject { described_class.new(acme_challenges, | ||
webrick_options: { :DoNotListen => true }, | ||
) } | ||
let(:server) { subject.instance_variable_get('@server') } | ||
|
||
describe 'HTTP' do | ||
let(:http_request) { instance_double(WEBrick::HTTPRequest, | ||
request_method: http_method, | ||
unparsed_uri: http_path, | ||
path: WEBrick::HTTPUtils::normalize_path(http_path), | ||
) } | ||
let(:http_response) { WEBrick::HTTPResponse.new(server.config) } | ||
|
||
before do | ||
# XXX: should not be mocking the WEBrick::HTTPRequest | ||
allow(http_request).to receive(:script_name=) | ||
allow(http_request).to receive(:path_info=) | ||
end | ||
|
||
context 'GET /' do | ||
let(:http_method) { 'GET' } | ||
let(:http_path) { '/' } | ||
|
||
it 'responds with HTTP 404' do | ||
expect{server.service(http_request, http_response)}.to raise_error(WEBrick::HTTPStatus::NotFound) | ||
end | ||
end | ||
|
||
context 'GET /.well-known/acme-challenge' do | ||
let(:http_method) { 'GET' } | ||
let(:http_path) { '/.well-known/acme-challenge' } | ||
|
||
it 'responds with HTTP 404' do | ||
expect{server.service(http_request, http_response)}.to raise_error(WEBrick::HTTPStatus::NotFound) | ||
end | ||
end | ||
|
||
context 'GET /.well-known/acme-challenge/:token' do | ||
let(:acme_token) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0' } | ||
let(:acme_authorization) { 'LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI' } | ||
let(:http_method) { 'GET' } | ||
let(:http_path) { "/.well-known/acme-challenge/#{acme_token}" } | ||
|
||
context 'with a matching ACME_CHALLENGE' do | ||
before do | ||
expect(acme_challenges).to receive(:respond).with(acme_token).and_return(acme_authorization) | ||
end | ||
|
||
it 'responds with HTTP 200' do | ||
server.service(http_request, http_response) | ||
|
||
expect(http_response.status).to eq 200 | ||
expect(http_response.body).to eq acme_authorization | ||
end | ||
end | ||
|
||
context 'without any matching ACME_CHALLENGE' do | ||
before do | ||
expect(acme_challenges).to receive(:respond).with(acme_token).and_return(nil) | ||
end | ||
|
||
it 'responds with HTTP 440' do | ||
server.service(http_request, http_response) | ||
|
||
expect(http_response.status).to eq 404 | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.