From 1618b68bfa740ed655ac45d7d5f4f46fed6c8c62 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 14 Jul 2017 20:41:49 +0200 Subject: HTTP signatures (#4146) * Add Request class with HTTP signature generator Spec: https://tools.ietf.org/html/draft-cavage-http-signatures-06 * Add HTTP signature verification concern * Add test for SignatureVerification concern * Add basic test for Request class * Make PuSH subscribe/unsubscribe requests use new Request class Accidentally fix lease_seconds not being set and sent properly, and change the new minimum subscription duration to 1 day * Make all PuSH workers use new Request class * Make Salmon sender use new Request class * Make FetchLinkService use new Request class * Make FetchAtomService use the new Request class * Make Remotable use the new Request class * Make ResolveRemoteAccountService use the new Request class * Add more tests * Allow +-30 seconds window for signed request to remain valid * Disable time window validation for signed requests, restore 7 days as PuSH subscription duration (which was previous default due to a bug) --- app/lib/provider_discovery.rb | 4 +-- app/lib/request.rb | 70 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 app/lib/request.rb (limited to 'app/lib') diff --git a/app/lib/provider_discovery.rb b/app/lib/provider_discovery.rb index 6d48cae2f..5e02e6806 100644 --- a/app/lib/provider_discovery.rb +++ b/app/lib/provider_discovery.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true class ProviderDiscovery < OEmbed::ProviderDiscovery - extend HttpHelper - class << self def discover_provider(url, options = {}) - res = http_client.get(url) + res = Request.new(:get, url).perform format = options[:format] raise OEmbed::NotFound, url if res.code != 200 || res.mime_type != 'text/html' diff --git a/app/lib/request.rb b/app/lib/request.rb new file mode 100644 index 000000000..e73c5ac20 --- /dev/null +++ b/app/lib/request.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +class Request + REQUEST_TARGET = '(request-target)' + + include RoutingHelper + + def initialize(verb, url, options = {}) + @verb = verb + @url = Addressable::URI.parse(url).normalize + @options = options + @headers = {} + + set_common_headers! + end + + def on_behalf_of(account) + raise ArgumentError unless account.local? + @account = account + end + + def add_headers(new_headers) + @headers.merge!(new_headers) + end + + def perform + http_client.headers(headers).public_send(@verb, @url.to_s, @options) + end + + def headers + (@account ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET) + end + + private + + def set_common_headers! + @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}" + @headers['User-Agent'] = user_agent + @headers['Host'] = @url.host + @headers['Date'] = Time.now.utc.httpdate + end + + def signature + key_id = @account.to_webfinger_s + algorithm = 'rsa-sha256' + signature = Base64.strict_encode64(@account.keypair.sign(OpenSSL::Digest::SHA256.new, signed_string)) + + "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers}\",signature=\"#{signature}\"" + end + + def signed_string + @headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n") + end + + def signed_headers + @headers.keys.join(' ').downcase + end + + def user_agent + @user_agent ||= "#{HTTP::Request::USER_AGENT} (Mastodon/#{Mastodon::Version}; +#{root_url})" + end + + def timeout + { write: 10, connect: 10, read: 10 } + end + + def http_client + HTTP.timeout(:per_operation, timeout).follow + end +end -- cgit