mirror of
https://github.com/l5yth/potato-mesh.git
synced 2026-08-12 03:42:57 +02:00
Handle federation HTTPS CRL verification failures (#293)
This commit is contained in:
@@ -99,10 +99,7 @@ module PotatoMesh
|
||||
|
||||
instance_uri_candidates(domain, "/api/instances").each do |uri|
|
||||
begin
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.open_timeout = PotatoMesh::Config.remote_instance_http_timeout
|
||||
http.read_timeout = PotatoMesh::Config.remote_instance_http_timeout
|
||||
http.use_ssl = uri.scheme == "https"
|
||||
http = build_remote_http_client(uri)
|
||||
response = http.start do |connection|
|
||||
request = Net::HTTP::Post.new(uri)
|
||||
request["Content-Type"] = "application/json"
|
||||
@@ -215,10 +212,7 @@ module PotatoMesh
|
||||
end
|
||||
|
||||
def perform_instance_http_request(uri)
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.open_timeout = PotatoMesh::Config.remote_instance_http_timeout
|
||||
http.read_timeout = PotatoMesh::Config.remote_instance_http_timeout
|
||||
http.use_ssl = uri.scheme == "https"
|
||||
http = build_remote_http_client(uri)
|
||||
http.start do |connection|
|
||||
response = connection.request(Net::HTTP::Get.new(uri))
|
||||
case response
|
||||
@@ -247,6 +241,46 @@ module PotatoMesh
|
||||
[nil, errors]
|
||||
end
|
||||
|
||||
# Build an HTTP client configured for communication with a remote instance.
|
||||
#
|
||||
# @param uri [URI::Generic] target URI describing the remote endpoint.
|
||||
# @return [Net::HTTP] HTTP client ready to execute the request.
|
||||
def build_remote_http_client(uri)
|
||||
http = Net::HTTP.new(uri.host, uri.port)
|
||||
http.open_timeout = PotatoMesh::Config.remote_instance_http_timeout
|
||||
http.read_timeout = PotatoMesh::Config.remote_instance_http_timeout
|
||||
http.use_ssl = uri.scheme == "https"
|
||||
return http unless http.use_ssl?
|
||||
|
||||
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
||||
http.min_version = :TLS1_2 if http.respond_to?(:min_version=)
|
||||
store = remote_instance_cert_store
|
||||
http.cert_store = store if store
|
||||
http
|
||||
end
|
||||
|
||||
# Construct a certificate store that disables strict CRL enforcement.
|
||||
#
|
||||
# OpenSSL may fail remote requests when certificate revocation lists are
|
||||
# unavailable from the issuing authority. The returned store mirrors the
|
||||
# default system trust store while clearing CRL-related flags so that
|
||||
# federation announcements gracefully succeed when CRLs cannot be fetched.
|
||||
#
|
||||
# @return [OpenSSL::X509::Store, nil] configured store or nil when setup fails.
|
||||
def remote_instance_cert_store
|
||||
return @remote_instance_cert_store if defined?(@remote_instance_cert_store) && @remote_instance_cert_store
|
||||
|
||||
store = OpenSSL::X509::Store.new
|
||||
store.set_default_paths
|
||||
store.flags = 0 if store.respond_to?(:flags=)
|
||||
@remote_instance_cert_store = store
|
||||
rescue OpenSSL::X509::StoreError => e
|
||||
debug_log(
|
||||
"Failed to initialize certificate store for federation HTTP: #{e.message}",
|
||||
)
|
||||
@remote_instance_cert_store = nil
|
||||
end
|
||||
|
||||
def validate_well_known_document(document, domain, pubkey)
|
||||
unless document.is_a?(Hash)
|
||||
return [false, "document is not an object"]
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require "spec_helper"
|
||||
require "net/http"
|
||||
require "openssl"
|
||||
require "uri"
|
||||
|
||||
RSpec.describe PotatoMesh::App::Federation do
|
||||
subject(:federation_helpers) do
|
||||
Class.new do
|
||||
extend PotatoMesh::App::Federation
|
||||
|
||||
class << self
|
||||
def debug_messages
|
||||
@debug_messages ||= []
|
||||
end
|
||||
|
||||
def debug_log(message)
|
||||
debug_messages << message
|
||||
end
|
||||
|
||||
def reset_debug_messages
|
||||
@debug_messages = []
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
federation_helpers.instance_variable_set(:@remote_instance_cert_store, nil)
|
||||
federation_helpers.reset_debug_messages
|
||||
end
|
||||
|
||||
describe ".remote_instance_cert_store" do
|
||||
it "initializes the store with default paths and disables CRL checks" do
|
||||
store_double = Class.new do
|
||||
attr_reader :default_paths_called, :assigned_flags
|
||||
|
||||
def set_default_paths
|
||||
@default_paths_called = true
|
||||
end
|
||||
|
||||
def flags=(value)
|
||||
@assigned_flags = value
|
||||
end
|
||||
|
||||
def respond_to_missing?(method_name, include_private = false)
|
||||
method_name == :flags= || super
|
||||
end
|
||||
end.new
|
||||
|
||||
allow(OpenSSL::X509::Store).to receive(:new).and_return(store_double)
|
||||
|
||||
result = federation_helpers.remote_instance_cert_store
|
||||
|
||||
expect(result).to eq(store_double)
|
||||
expect(store_double.default_paths_called).to be(true)
|
||||
expect(store_double.assigned_flags).to eq(0)
|
||||
end
|
||||
|
||||
it "memoizes the generated store" do
|
||||
first = federation_helpers.remote_instance_cert_store
|
||||
second = federation_helpers.remote_instance_cert_store
|
||||
expect(second).to equal(first)
|
||||
end
|
||||
|
||||
it "logs and returns nil when initialization fails" do
|
||||
allow(OpenSSL::X509::Store).to receive(:new).and_raise(OpenSSL::X509::StoreError, "boom")
|
||||
|
||||
expect(federation_helpers.remote_instance_cert_store).to be_nil
|
||||
expect(federation_helpers.debug_messages.last).to include("Failed to initialize certificate store")
|
||||
end
|
||||
end
|
||||
|
||||
describe ".build_remote_http_client" do
|
||||
let(:timeout) { 15 }
|
||||
|
||||
before do
|
||||
allow(PotatoMesh::Config).to receive(:remote_instance_http_timeout).and_return(timeout)
|
||||
end
|
||||
|
||||
it "configures SSL settings for HTTPS endpoints" do
|
||||
uri = URI.parse("https://remote.example.com/api")
|
||||
store = OpenSSL::X509::Store.new
|
||||
allow(federation_helpers).to receive(:remote_instance_cert_store).and_return(store)
|
||||
|
||||
http = federation_helpers.build_remote_http_client(uri)
|
||||
|
||||
expect(http.use_ssl?).to be(true)
|
||||
expect(http.open_timeout).to eq(timeout)
|
||||
expect(http.read_timeout).to eq(timeout)
|
||||
expect(http.cert_store).to eq(store)
|
||||
expect(http.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
|
||||
if http.respond_to?(:min_version)
|
||||
expect(http.min_version).to eq(:TLS1_2)
|
||||
end
|
||||
end
|
||||
|
||||
it "omits SSL configuration for HTTP endpoints" do
|
||||
uri = URI.parse("http://remote.example.com/api")
|
||||
|
||||
http = federation_helpers.build_remote_http_client(uri)
|
||||
|
||||
expect(http.use_ssl?).to be(false)
|
||||
expect(http.cert_store).to be_nil
|
||||
expect(http.open_timeout).to eq(timeout)
|
||||
expect(http.read_timeout).to eq(timeout)
|
||||
end
|
||||
|
||||
it "leaves the certificate store unset when unavailable" do
|
||||
uri = URI.parse("https://remote.example.com/api")
|
||||
allow(federation_helpers).to receive(:remote_instance_cert_store).and_return(nil)
|
||||
|
||||
http = federation_helpers.build_remote_http_client(uri)
|
||||
|
||||
expect(http.cert_store).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user