fedipub 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/app/controllers/fedipub/server/web_finger_controller.rb +7 -8
- data/app/controllers/fedipub/server_controller.rb +11 -0
- data/app/models/concerns/fedipub/actor_entity.rb +1 -2
- data/app/models/concerns/fedipub/application_actor.rb +25 -0
- data/app/models/concerns/fedipub/data_entity.rb +1 -2
- data/app/models/fedipub/actor.rb +14 -11
- data/app/views/fedipub/server/actors/_actor.activitypub.jbuilder +14 -0
- data/app/views/fedipub/server/nodeinfo/index.nodeinfo.jbuilder +4 -0
- data/app/views/fedipub/server/web_finger/find.jrd.jbuilder +11 -3
- data/lib/fedipub/utils/json_request.rb +65 -17
- data/lib/fedipub/version.rb +1 -1
- data/lib/fediverse/node_info.rb +1 -1
- data/lib/fediverse/notifier.rb +5 -32
- data/lib/fediverse/signature/draft_cavage12.rb +109 -0
- data/lib/fediverse/signature/rfc9421.rb +69 -0
- data/lib/fediverse/signature.rb +16 -39
- data/lib/fediverse/webfinger.rb +1 -1
- metadata +18 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 506f49df52ad99cb113b5526bace6666e2a916cfe13f0abcfe7afe49d08e295f
|
|
4
|
+
data.tar.gz: 1f9cb4fa11e555fec1ea7b0e9e0b600dc14b2d9ba9b31f5ebe7c63f22c554818
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 915f0da60e1cdde3e9f10f3f3737e21063eacadc8ca5d7ace7f46a478aa0332f884d54df9796feecf4278278f856e8ffde2bba07fc0ea8ca06d347dcf600460a
|
|
7
|
+
data.tar.gz: e44d799ce12d87b68a89befa4135c06596554452b8ac37d74bda7213d841641854dfe1567c0ceb774991120fb3ed325f76fbdc3b32f5edac9816e77950793d53
|
data/README.md
CHANGED
|
@@ -48,9 +48,14 @@ It _may_ work on other versions, but we won't provide support.
|
|
|
48
48
|
|
|
49
49
|
Extensions add extra features to Fedipub:
|
|
50
50
|
|
|
51
|
-
- [Fedipub Moderation](https://
|
|
51
|
+
- [Fedipub Moderation](https://gitlab.com/fedipub/fedipub-moderation/)
|
|
52
52
|
> A gem that provides trust & safety for Fedipub
|
|
53
53
|
|
|
54
|
+
## Projects using Fedipub
|
|
55
|
+
|
|
56
|
+
- [Manyfold](https://github.com/manyfold3d/manyfold)
|
|
57
|
+
- [Prismo](https://github.com/mbajur/prismo)
|
|
58
|
+
|
|
54
59
|
## License
|
|
55
60
|
|
|
56
61
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -62,6 +67,7 @@ See [CONTRIBUTING](CONTRIBUTING.md) to have an overview of the process and the t
|
|
|
62
67
|
### Contributors
|
|
63
68
|
|
|
64
69
|
- [echarp](https://gitlab.com/echarp)
|
|
70
|
+
- [Francesco Urdih](https://gitlab.com/francesco.urdih)
|
|
65
71
|
- [James Smith](https://gitlab.com/floppy.uk)
|
|
66
72
|
- [Manuel Tancoigne](https://gitlab.com/mtancoigne)
|
|
67
73
|
- [pessi-v](https://github.com/pessi-v)
|
|
@@ -6,17 +6,16 @@ module Fedipub
|
|
|
6
6
|
def find
|
|
7
7
|
skip_authorization
|
|
8
8
|
|
|
9
|
-
resource = params.require(:resource)
|
|
10
|
-
|
|
9
|
+
case resource = params.require(:resource)
|
|
10
|
+
when %r{^https?://#{Fedipub::Utils::Host.localhost}/?$}
|
|
11
|
+
@actor = Fedipub::Actor.application_actor
|
|
11
12
|
when %r{^https?://.+}
|
|
12
|
-
@
|
|
13
|
+
@actor = Fedipub::Actor.find_by_federation_url!(resource) # rubocop:disable Rails/DynamicFindBy
|
|
13
14
|
when /^acct:.+/
|
|
14
|
-
actor = Fedipub::Actor.find_local_by_username(username)
|
|
15
|
-
raise Fedipub::Actor::TombstonedError if actor&.tombstoned?
|
|
16
|
-
|
|
17
|
-
@user = actor&.entity
|
|
15
|
+
@actor = Fedipub::Actor.find_local_by_username(username)
|
|
16
|
+
raise Fedipub::Actor::TombstonedError if @actor&.tombstoned?
|
|
18
17
|
end
|
|
19
|
-
raise ActiveRecord::RecordNotFound if @
|
|
18
|
+
raise ActiveRecord::RecordNotFound if @actor.nil?
|
|
20
19
|
|
|
21
20
|
render formats: [:jrd]
|
|
22
21
|
end
|
|
@@ -2,6 +2,7 @@ module Fedipub
|
|
|
2
2
|
class ServerController < ::ActionController::Base # rubocop:disable Rails/ApplicationController
|
|
3
3
|
include Pundit::Authorization
|
|
4
4
|
|
|
5
|
+
before_action :verify_request_signature!
|
|
5
6
|
after_action :verify_authorized
|
|
6
7
|
|
|
7
8
|
protect_from_forgery with: :null_session
|
|
@@ -12,8 +13,18 @@ module Fedipub
|
|
|
12
13
|
Fedipub::DataEntity::TombstonedError,
|
|
13
14
|
with: :error_gone
|
|
14
15
|
|
|
16
|
+
def self.require_signature?
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
15
20
|
private
|
|
16
21
|
|
|
22
|
+
def verify_request_signature!
|
|
23
|
+
Fediverse::Signature.verify!(request: request, require_signature: ServerController.require_signature?)
|
|
24
|
+
rescue Fediverse::Signature::BadSignature
|
|
25
|
+
head :unauthorized
|
|
26
|
+
end
|
|
27
|
+
|
|
17
28
|
def error_fallback(exception, fallback_message, status)
|
|
18
29
|
message = exception&.message || fallback_message
|
|
19
30
|
respond_to do |format|
|
|
@@ -38,7 +38,7 @@ module Fedipub
|
|
|
38
38
|
#
|
|
39
39
|
# @example
|
|
40
40
|
# acts_as_fedipub_actor username_field: :username, name_field: :display_name, profile_url_method: :url_for, actor_type: 'Person'
|
|
41
|
-
# rubocop:disable Metrics/ParameterLists
|
|
41
|
+
# rubocop:disable-next Metrics/ParameterLists
|
|
42
42
|
def acts_as_fedipub_actor(
|
|
43
43
|
name_field:,
|
|
44
44
|
username_field:,
|
|
@@ -57,7 +57,6 @@ module Fedipub
|
|
|
57
57
|
auto_create_actors: auto_create_actors
|
|
58
58
|
)
|
|
59
59
|
end
|
|
60
|
-
# rubocop:enable Metrics/ParameterLists
|
|
61
60
|
|
|
62
61
|
# Define a method that will be called after the entity receives a follow request.
|
|
63
62
|
# The follow request will be passed as an argument to the method.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module Fedipub
|
|
2
|
+
# Definition for a single local Application Actor, which represents the instance/server itself.
|
|
3
|
+
#
|
|
4
|
+
# Keys from this actor are used to sign outgoing GET requests.
|
|
5
|
+
module ApplicationActor
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
APPLICATION_ACTOR_ATTRIBUTES = {
|
|
9
|
+
username: '__application',
|
|
10
|
+
actor_type: 'Application',
|
|
11
|
+
entity: nil,
|
|
12
|
+
local: true,
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
included do
|
|
16
|
+
def self.application_actor
|
|
17
|
+
find_or_create_by!(APPLICATION_ACTOR_ATTRIBUTES)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def application_actor?
|
|
21
|
+
APPLICATION_ACTOR_ATTRIBUTES.all? { |attribute, value| attributes[attribute.to_s] == value }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -126,7 +126,7 @@ module Fedipub
|
|
|
126
126
|
#
|
|
127
127
|
# @example
|
|
128
128
|
# acts_as_fedipub_data handles: 'Note', with: :note_handler, route_path_segment: :articles, actor_entity_method: :user
|
|
129
|
-
# rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
|
|
129
|
+
# rubocop:disable-next Metrics/ParameterLists, Metrics/MethodLength
|
|
130
130
|
def acts_as_fedipub_data(
|
|
131
131
|
handles:,
|
|
132
132
|
with: :handle_incoming_fediverse_data,
|
|
@@ -155,7 +155,6 @@ module Fedipub
|
|
|
155
155
|
Fediverse::Inbox.register_handler 'Create', handles, self, with
|
|
156
156
|
Fediverse::Inbox.register_handler 'Update', handles, self, with
|
|
157
157
|
end
|
|
158
|
-
# rubocop:enable Metrics/ParameterLists, Metrics/MethodLength
|
|
159
158
|
|
|
160
159
|
# Instantiates a new instance from an ActivityPub object
|
|
161
160
|
#
|
data/app/models/fedipub/actor.rb
CHANGED
|
@@ -16,18 +16,17 @@ module Fedipub
|
|
|
16
16
|
include Fedipub::HandlesDeleteRequests
|
|
17
17
|
include Fedipub::Likeable
|
|
18
18
|
include Fedipub::Announceable
|
|
19
|
+
include Fedipub::ApplicationActor
|
|
19
20
|
|
|
20
|
-
validates :federated_url, presence: { unless:
|
|
21
|
+
validates :federated_url, presence: { unless: -> { entity || application_actor? } }, uniqueness: { unless: :local? }
|
|
21
22
|
validates :username, presence: { unless: :local? }
|
|
22
23
|
validates :server, presence: { unless: :local? }
|
|
23
24
|
validates :inbox_url, presence: { unless: :local? }
|
|
24
25
|
validates :outbox_url, presence: { unless: :local? }
|
|
25
|
-
validates :followers_url, presence: { unless: :local? }
|
|
26
|
-
validates :followings_url, presence: { unless: :local? }
|
|
27
26
|
validates :profile_url, presence: { unless: :local? }
|
|
28
|
-
validates :actor_type, presence: { unless:
|
|
27
|
+
validates :actor_type, presence: { unless: -> { local? && !application_actor? } }
|
|
29
28
|
validates :entity_id, uniqueness: { scope: :entity_type }, if: :entity_type
|
|
30
|
-
validates :entity, presence: true, if: -> { local? && !tombstoned? }
|
|
29
|
+
validates :entity, presence: true, if: -> { local? && !tombstoned? && !application_actor? }
|
|
31
30
|
|
|
32
31
|
belongs_to :entity, polymorphic: true, optional: true
|
|
33
32
|
# FIXME: Handle this with something like undelete
|
|
@@ -59,7 +58,7 @@ module Fedipub
|
|
|
59
58
|
end
|
|
60
59
|
|
|
61
60
|
def federated_url
|
|
62
|
-
use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
|
|
61
|
+
use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_url(self) : attributes['federated_url'].presence
|
|
63
62
|
end
|
|
64
63
|
|
|
65
64
|
def username
|
|
@@ -75,7 +74,7 @@ module Fedipub
|
|
|
75
74
|
end
|
|
76
75
|
|
|
77
76
|
def server
|
|
78
|
-
use_entity_attributes? ? Utils::Host.localhost : attributes['server']
|
|
77
|
+
use_entity_attributes? || application_actor? ? Utils::Host.localhost : attributes['server']
|
|
79
78
|
end
|
|
80
79
|
|
|
81
80
|
def actor_type
|
|
@@ -83,19 +82,19 @@ module Fedipub
|
|
|
83
82
|
end
|
|
84
83
|
|
|
85
84
|
def inbox_url
|
|
86
|
-
use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
|
|
85
|
+
use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_inbox_url(self) : attributes['inbox_url']
|
|
87
86
|
end
|
|
88
87
|
|
|
89
88
|
def outbox_url
|
|
90
|
-
use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
|
|
89
|
+
use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.server_actor_outbox_url(self) : attributes['outbox_url']
|
|
91
90
|
end
|
|
92
91
|
|
|
93
92
|
def followers_url
|
|
94
|
-
use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
|
|
93
|
+
use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.followers_server_actor_url(self) : attributes['followers_url']
|
|
95
94
|
end
|
|
96
95
|
|
|
97
96
|
def followings_url
|
|
98
|
-
use_entity_attributes? ? Fedipub::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
|
|
97
|
+
use_entity_attributes? || application_actor? ? Fedipub::Engine.routes.url_helpers.following_server_actor_url(self) : attributes['followings_url']
|
|
99
98
|
end
|
|
100
99
|
|
|
101
100
|
def profile_url
|
|
@@ -244,6 +243,10 @@ module Fedipub
|
|
|
244
243
|
end
|
|
245
244
|
return actor if actor
|
|
246
245
|
|
|
246
|
+
# Look for pure actors (e.g. the application actor)
|
|
247
|
+
actor = Fedipub::Actor.local.find_by username: username
|
|
248
|
+
return actor if actor
|
|
249
|
+
|
|
247
250
|
# Last hope: Search for tombstoned actors
|
|
248
251
|
Fedipub::Actor.local.tombstoned.find_by username: username
|
|
249
252
|
end
|
|
@@ -25,3 +25,17 @@ if actor.public_key
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
json.merge! actor_data
|
|
28
|
+
|
|
29
|
+
# FEP-844e capability discovery
|
|
30
|
+
if actor.application_actor?
|
|
31
|
+
json.implements([
|
|
32
|
+
'https://www.w3.org/TR/activitypub/',
|
|
33
|
+
'https://datatracker.ietf.org/doc/html/rfc9421',
|
|
34
|
+
'https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12',
|
|
35
|
+
'https://w3id.org/fep/844e',
|
|
36
|
+
'https://w3id.org/fep/2677',
|
|
37
|
+
'https://w3id.org/fep/d556',
|
|
38
|
+
].map { |url| { 'href' => url } })
|
|
39
|
+
else
|
|
40
|
+
json.generator Fedipub::Actor.application_actor.federated_url
|
|
41
|
+
end
|
|
@@ -5,16 +5,24 @@ links = [
|
|
|
5
5
|
{
|
|
6
6
|
rel: 'self',
|
|
7
7
|
type: Mime[:activitypub].to_s,
|
|
8
|
-
href: @
|
|
8
|
+
href: @actor.federated_url,
|
|
9
9
|
},
|
|
10
10
|
]
|
|
11
11
|
|
|
12
|
+
if @actor.application_actor?
|
|
13
|
+
links << {
|
|
14
|
+
rel: 'https://www.w3.org/ns/activitystreams#Service',
|
|
15
|
+
type: Mime[:activitypub].to_s,
|
|
16
|
+
href: @actor.federated_url,
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
12
20
|
# User profile URL if configured
|
|
13
21
|
# TODO: Add a profile controller/action in dummy to test this
|
|
14
|
-
if @
|
|
22
|
+
if @actor.profile_url
|
|
15
23
|
links.push rel: 'https://webfinger.net/rel/profile-page',
|
|
16
24
|
type: 'text/html',
|
|
17
|
-
href: @
|
|
25
|
+
href: @actor.profile_url
|
|
18
26
|
end
|
|
19
27
|
|
|
20
28
|
# Remote following
|
|
@@ -1,41 +1,89 @@
|
|
|
1
1
|
require 'faraday'
|
|
2
2
|
require 'faraday/follow_redirects'
|
|
3
|
+
require 'fediverse/signature'
|
|
3
4
|
|
|
4
5
|
module Fedipub
|
|
5
6
|
module Utils
|
|
6
|
-
#
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
# Wrapper around HTTP calls which ensures signatures etc are applied.
|
|
8
|
+
class JsonRequest
|
|
9
|
+
include Singleton
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
extend Forwardable
|
|
13
|
+
|
|
14
|
+
def_delegators :instance, :get_json, :get, :post
|
|
15
|
+
end
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
'Content-Type' => 'application/json',
|
|
12
|
-
'Accept' => 'application/json',
|
|
13
|
-
}.freeze
|
|
17
|
+
class UnhandledResponseStatus < StandardError; end
|
|
14
18
|
|
|
15
19
|
# Makes a GET request and returns a +Hash+ from the parsed body
|
|
16
20
|
#
|
|
17
21
|
# @param url [String] Target URL
|
|
18
22
|
# @param params [Hash] Querystring parameters
|
|
19
23
|
# @param headers [Hash] Additional headers
|
|
20
|
-
# @param follow_redirects [Boolean] Whether to follow redirections
|
|
21
24
|
# @param expected_status [Integer] Expected response status. Will raise a +UnhandledResponseStatus+ when status is different
|
|
22
25
|
#
|
|
23
26
|
# @return The parsed JSON object
|
|
24
27
|
#
|
|
25
28
|
# @raise [UnhandledResponseStatus] when response status is not the expected_status
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
connection = Faraday.new url: url, params: params, headers: headers do |faraday|
|
|
30
|
-
faraday.response :follow_redirects if follow_redirects
|
|
31
|
-
faraday.adapter Faraday.default_adapter
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
response = connection.get
|
|
29
|
+
def get_json(url, params: {}, headers: {}, expected_status: 200, from: nil)
|
|
30
|
+
response = get url: url, params: params, headers: headers, from: from
|
|
35
31
|
raise UnhandledResponseStatus, "Unhandled status code #{response.status} for GET #{url}" if expected_status && response.status != expected_status
|
|
36
32
|
|
|
37
33
|
JSON.parse(response.body)
|
|
38
34
|
end
|
|
35
|
+
|
|
36
|
+
def get(url:, params: {}, headers: {}, from: nil)
|
|
37
|
+
execute_request method: :get, url: url, params: params, headers: headers, from: from || Fedipub::Actor.application_actor
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def post(url:, message:, headers: {}, from: nil)
|
|
41
|
+
execute_request method: :post, url: url, headers: headers, message: message, from: from
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Send to remote server with RFC9421 signature and double-knocking for draft-cavage-12 if that fails
|
|
47
|
+
def execute_request(method:, url:, params: {}, headers: {}, message: nil, from: nil) # rubocop:disable Metrics/ParameterLists
|
|
48
|
+
req = build_request(method: method, url: url, params: params, headers: headers, message: message)
|
|
49
|
+
response = connection.builder.build_response(
|
|
50
|
+
connection,
|
|
51
|
+
from ? Fediverse::Signature.sign(sender: from, request: req.dup) : req
|
|
52
|
+
)
|
|
53
|
+
# If signature was present and rejected, try double-knocking
|
|
54
|
+
return response unless from && response.status.in?([400, 401])
|
|
55
|
+
|
|
56
|
+
connection.builder.build_response(
|
|
57
|
+
connection,
|
|
58
|
+
Fediverse::Signature.sign(sender: from, request: req, legacy_signature: true)
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_request(method:, url:, params: {}, headers: {}, message: nil) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
|
|
63
|
+
# Extract params from URL string if they're in there instead of the hash
|
|
64
|
+
uri = URI(url)
|
|
65
|
+
params.merge! Rack::Utils.parse_nested_query(uri.query)
|
|
66
|
+
uri.query = nil
|
|
67
|
+
# Build the request
|
|
68
|
+
connection.build_request(method) do |req|
|
|
69
|
+
req.url uri
|
|
70
|
+
req.body = message
|
|
71
|
+
req.params = params
|
|
72
|
+
req.headers = {
|
|
73
|
+
'Content-Type' => Mime[:activitypub].to_s,
|
|
74
|
+
'Accept' => [Mime[:activitypub].to_s, Mime[:activitypub].send(:synonyms), "#{Mime[:json]};q=0.5"].flatten.join(', '),
|
|
75
|
+
'User-Agent' => req.headers['User-Agent'] || "Fedipub/#{Fedipub::VERSION}",
|
|
76
|
+
'Accept-Signature' => 'sig1=()',
|
|
77
|
+
}.compact.merge(headers)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def connection
|
|
82
|
+
@connection ||= Faraday.new do |faraday|
|
|
83
|
+
faraday.response :follow_redirects
|
|
84
|
+
faraday.adapter Faraday.default_adapter
|
|
85
|
+
end
|
|
86
|
+
end
|
|
39
87
|
end
|
|
40
88
|
end
|
|
41
89
|
end
|
data/lib/fedipub/version.rb
CHANGED
data/lib/fediverse/node_info.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Fediverse
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def nodeinfo_url(domain)
|
|
31
|
-
response = Fedipub::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo"
|
|
31
|
+
response = Fedipub::Utils::JsonRequest.get_json "#{base_url(domain)}/.well-known/nodeinfo"
|
|
32
32
|
entry = response['links']&.find { |link| link['rel'] == 'http://nodeinfo.diaspora.software/ns/schema/2.0' }
|
|
33
33
|
|
|
34
34
|
entry['href']
|
data/lib/fediverse/notifier.rb
CHANGED
|
@@ -21,6 +21,11 @@ module Fediverse
|
|
|
21
21
|
|
|
22
22
|
private
|
|
23
23
|
|
|
24
|
+
# Overridden by Fedipub::Moderation for filtering
|
|
25
|
+
def post_to_inbox(inbox_url:, message:, from: nil)
|
|
26
|
+
Fedipub::Utils::JsonRequest.post(url: inbox_url, message: message, from: from)
|
|
27
|
+
end
|
|
28
|
+
|
|
24
29
|
# Determines the list of inboxes that the activity should be delivered to
|
|
25
30
|
#
|
|
26
31
|
# @return [Array<Fedipub::Actor>]
|
|
@@ -53,38 +58,6 @@ module Fediverse
|
|
|
53
58
|
format: :json
|
|
54
59
|
)
|
|
55
60
|
end
|
|
56
|
-
|
|
57
|
-
def post_to_inbox(inbox_url:, message:, from: nil)
|
|
58
|
-
conn = Faraday.default_connection
|
|
59
|
-
conn.builder.build_response(
|
|
60
|
-
conn,
|
|
61
|
-
signed_request(url: inbox_url, message: message, from: from)
|
|
62
|
-
)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def signed_request(url:, message:, from:)
|
|
66
|
-
req = request(url: url, message: message)
|
|
67
|
-
req.headers['Signature'] = Fediverse::Signature.sign(sender: from, request: req) if from
|
|
68
|
-
req
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def request(url:, message:) # rubocop:todo Metrics/AbcSize
|
|
72
|
-
Faraday.default_connection.build_request(:post) do |req|
|
|
73
|
-
req.url url
|
|
74
|
-
req.body = message
|
|
75
|
-
req.headers['Content-Type'] = Mime[:activitypub].to_s
|
|
76
|
-
req.headers['Accept'] = Mime[:activitypub].to_s
|
|
77
|
-
req.headers['Host'] = URI.parse(url).host
|
|
78
|
-
req.headers['Date'] = Time.now.utc.httpdate
|
|
79
|
-
req.headers['Digest'] = digest(message)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def digest(message)
|
|
84
|
-
"SHA-256=#{Base64.strict_encode64(
|
|
85
|
-
OpenSSL::Digest.new('SHA256').digest(message)
|
|
86
|
-
)}"
|
|
87
|
-
end
|
|
88
61
|
end
|
|
89
62
|
end
|
|
90
63
|
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require 'fediverse/signature'
|
|
2
|
+
|
|
3
|
+
module Fediverse
|
|
4
|
+
module Signature
|
|
5
|
+
class DraftCavage12
|
|
6
|
+
class << self
|
|
7
|
+
def sign(sender:, request:)
|
|
8
|
+
request = set_headers(request)
|
|
9
|
+
|
|
10
|
+
parts = {
|
|
11
|
+
keyId: sender.key_id,
|
|
12
|
+
headers: signature_headers(request).join(' '),
|
|
13
|
+
signature: signature(sender: sender, request: request),
|
|
14
|
+
}
|
|
15
|
+
request.headers['Signature'] = parts.map { |k, v| "#{k}=\"#{v}\"" }.join(',')
|
|
16
|
+
request
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def verify!(request:)
|
|
20
|
+
# Do we have a signature to verify?
|
|
21
|
+
return false unless request.headers.key?('Signature')
|
|
22
|
+
|
|
23
|
+
# Have we got what we need?
|
|
24
|
+
components = signature_components(request)
|
|
25
|
+
raise Fediverse::Signature::BadSignature, 'Malformed signature' unless components['signature'] && components['headers']
|
|
26
|
+
|
|
27
|
+
# Find the sender
|
|
28
|
+
sender = find_sender_by_key_id(components['keyId'])
|
|
29
|
+
raise Fediverse::Signature::BadSignature, "Couldn't find sender" unless sender
|
|
30
|
+
|
|
31
|
+
# Build the expected payload
|
|
32
|
+
comparison_string = signature_payload(request: request, headers: components['headers'])
|
|
33
|
+
|
|
34
|
+
# Verify the payload against the signature
|
|
35
|
+
result = do_verification(components['signature'], sender, comparison_string)
|
|
36
|
+
raise Fediverse::Signature::BadSignature unless result
|
|
37
|
+
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def do_verification(signature, sender, comparison_string)
|
|
44
|
+
signature = Base64.decode64(signature)
|
|
45
|
+
key = OpenSSL::PKey::RSA.new(sender.public_key)
|
|
46
|
+
key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison_string)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find_sender_by_key_id(key_id)
|
|
50
|
+
return unless key_id
|
|
51
|
+
|
|
52
|
+
Fedipub::Actor.find_or_create_by_federation_url(
|
|
53
|
+
key_id.split('#', 2).first
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def set_headers(request) # rubocop:disable Naming/AccessorMethodName
|
|
58
|
+
request.headers['Digest'] = digest(request.body) if request.body
|
|
59
|
+
request.headers['Host'] = URI.parse(request.path).host
|
|
60
|
+
request.headers['Date'] = Time.now.utc.httpdate
|
|
61
|
+
request
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def signature_components(request)
|
|
65
|
+
request.headers['Signature'].split(',').to_h do |pair|
|
|
66
|
+
/\A(?<key>\w+)="(?<value>.*)"\z/ =~ pair
|
|
67
|
+
[key, value]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def digest(message)
|
|
72
|
+
"SHA-256=#{Base64.strict_encode64(
|
|
73
|
+
OpenSSL::Digest.new('SHA256').digest(message)
|
|
74
|
+
)}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def signature_payload(request:, headers: signature_headers(request))
|
|
78
|
+
headers = headers.split if headers.is_a?(String)
|
|
79
|
+
headers.map do |header|
|
|
80
|
+
case header
|
|
81
|
+
when '(request-target)'
|
|
82
|
+
"(request-target): #{(request.try(:http_method) || request.try(:method)).downcase} #{URI.parse(request.path).path}"
|
|
83
|
+
else
|
|
84
|
+
"#{header}: #{request.headers[header.capitalize]}"
|
|
85
|
+
end
|
|
86
|
+
end.join("\n")
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def signature_headers(request)
|
|
90
|
+
if request.body
|
|
91
|
+
%w[(request-target) host date digest]
|
|
92
|
+
else
|
|
93
|
+
%w[(request-target) host date]
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def signature(sender:, request:)
|
|
98
|
+
private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
|
|
99
|
+
Base64.strict_encode64(
|
|
100
|
+
private_key.sign(
|
|
101
|
+
OpenSSL::Digest.new('SHA256'),
|
|
102
|
+
signature_payload(request: request)
|
|
103
|
+
)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require 'linzer'
|
|
2
|
+
require 'linzer/faraday'
|
|
3
|
+
require 'linzer/rack'
|
|
4
|
+
|
|
5
|
+
Linzer::Message.register_adapter ActionDispatch::Request, Linzer::Message::Adapter::Rack::Request
|
|
6
|
+
|
|
7
|
+
module Fediverse
|
|
8
|
+
module Signature
|
|
9
|
+
class Rfc9421
|
|
10
|
+
class << self
|
|
11
|
+
def sign(sender:, request:)
|
|
12
|
+
request.headers['Content-Digest'] = digest(request.body) if request.body
|
|
13
|
+
Linzer.sign!(
|
|
14
|
+
request,
|
|
15
|
+
key: linzer_private_key(sender),
|
|
16
|
+
components: components(request),
|
|
17
|
+
params: {
|
|
18
|
+
created: Time.now.to_i,
|
|
19
|
+
alg: 'rsa-v1_5-sha256',
|
|
20
|
+
}
|
|
21
|
+
)
|
|
22
|
+
request
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def verify!(request:)
|
|
26
|
+
# Do we have a signature to verify?
|
|
27
|
+
return false if !request.headers.key?('Signature-Input') || !request.headers.key?('Signature')
|
|
28
|
+
|
|
29
|
+
# Verify the signature
|
|
30
|
+
Linzer.verify!(request) do |key_id|
|
|
31
|
+
sender = Fedipub::Actor.find_or_create_by_federation_url(key_id.split('#', 2).first)
|
|
32
|
+
raise Fediverse::Signature::BadSignature if sender.nil?
|
|
33
|
+
|
|
34
|
+
linzer_public_key(sender)
|
|
35
|
+
end
|
|
36
|
+
rescue Linzer::Error
|
|
37
|
+
raise Fediverse::Signature::BadSignature
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# Converts key to right structure for Linzer to use
|
|
43
|
+
def linzer_private_key(sender)
|
|
44
|
+
private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
|
|
45
|
+
Linzer.new_rsa_v1_5_sha256_key(private_key.to_pem, sender.key_id)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def linzer_public_key(sender)
|
|
49
|
+
public_key = OpenSSL::PKey::RSA.new(sender.public_key)
|
|
50
|
+
Linzer.new_rsa_v1_5_sha256_key(public_key.to_pem, sender.key_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def components(request)
|
|
54
|
+
if request.body
|
|
55
|
+
%w[@method @target-uri content-digest]
|
|
56
|
+
else
|
|
57
|
+
%w[@method @target-uri]
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def digest(message)
|
|
62
|
+
"sha-256=:#{Base64.strict_encode64(
|
|
63
|
+
OpenSSL::Digest.new('SHA256').digest(message)
|
|
64
|
+
)}:"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/fediverse/signature.rb
CHANGED
|
@@ -1,49 +1,26 @@
|
|
|
1
1
|
module Fediverse
|
|
2
|
-
|
|
3
|
-
class
|
|
4
|
-
def sign(sender:, request:)
|
|
5
|
-
private_key = OpenSSL::PKey::RSA.new sender.private_key, Rails.application.credentials.secret_key_base
|
|
6
|
-
headers = '(request-target) host date digest'
|
|
7
|
-
sig = Base64.strict_encode64(
|
|
8
|
-
private_key.sign(
|
|
9
|
-
OpenSSL::Digest.new('SHA256'), signature_payload(request: request, headers: headers)
|
|
10
|
-
)
|
|
11
|
-
)
|
|
12
|
-
{
|
|
13
|
-
keyId: sender.key_id,
|
|
14
|
-
headers: headers,
|
|
15
|
-
signature: sig,
|
|
16
|
-
}.map { |k, v| "#{k}=\"#{v}\"" }.join(',')
|
|
17
|
-
end
|
|
2
|
+
module Signature
|
|
3
|
+
class BadSignature < StandardError; end
|
|
18
4
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
5
|
+
class << self
|
|
6
|
+
def sign(sender:, request:, legacy_signature: false)
|
|
7
|
+
if legacy_signature
|
|
8
|
+
Fediverse::Signature::DraftCavage12.sign(sender: sender, request: request)
|
|
9
|
+
else
|
|
10
|
+
Fediverse::Signature::Rfc9421.sign(sender: sender, request: request)
|
|
25
11
|
end
|
|
26
|
-
|
|
27
|
-
headers = signature_header['headers']
|
|
28
|
-
signature = Base64.decode64(signature_header['signature'])
|
|
29
|
-
key = OpenSSL::PKey::RSA.new(sender.public_key)
|
|
30
|
-
|
|
31
|
-
comparison_string = signature_payload(request: request, headers: headers)
|
|
32
|
-
|
|
33
|
-
key.verify(OpenSSL::Digest.new('SHA256'), signature, comparison_string)
|
|
34
12
|
end
|
|
35
13
|
|
|
36
|
-
|
|
14
|
+
def verify!(request:, require_signature: false) # rubocop:disable Naming/PredicateMethod
|
|
15
|
+
success = Fediverse::Signature::Rfc9421.verify!(request: request) ||
|
|
16
|
+
Fediverse::Signature::DraftCavage12.verify!(request: request)
|
|
17
|
+
raise(BadSignature) if require_signature && !success
|
|
37
18
|
|
|
38
|
-
|
|
39
|
-
headers.split.map do |signed_header_name|
|
|
40
|
-
if signed_header_name == '(request-target)'
|
|
41
|
-
"(request-target): #{request.http_method} #{URI.parse(request.path).path}"
|
|
42
|
-
else
|
|
43
|
-
"#{signed_header_name}: #{request.headers[signed_header_name.capitalize]}"
|
|
44
|
-
end
|
|
45
|
-
end.join("\n")
|
|
19
|
+
true
|
|
46
20
|
end
|
|
47
21
|
end
|
|
48
22
|
end
|
|
49
23
|
end
|
|
24
|
+
|
|
25
|
+
require 'fediverse/signature/draft_cavage12'
|
|
26
|
+
require 'fediverse/signature/rfc9421'
|
data/lib/fediverse/webfinger.rb
CHANGED
|
@@ -121,7 +121,7 @@ module Fediverse
|
|
|
121
121
|
# @return [Hash]
|
|
122
122
|
# @raise [ActiveRecord::RecordNotFound] when the response is invalid
|
|
123
123
|
def get_json(url, params = {})
|
|
124
|
-
Fedipub::Utils::JsonRequest.get_json(url, params: params,
|
|
124
|
+
Fedipub::Utils::JsonRequest.get_json(url, params: params, headers: { accept: 'application/json' })
|
|
125
125
|
rescue Fedipub::Utils::JsonRequest::UnhandledResponseStatus => e
|
|
126
126
|
Rails.logger.debug { e.message }
|
|
127
127
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fedipub
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.10.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Manuel Tancoigne
|
|
@@ -93,6 +93,20 @@ dependencies:
|
|
|
93
93
|
- - ">="
|
|
94
94
|
- !ruby/object:Gem::Version
|
|
95
95
|
version: 1.2.0
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: linzer
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - "~>"
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0.8'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - "~>"
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0.8'
|
|
96
110
|
- !ruby/object:Gem::Dependency
|
|
97
111
|
name: ostruct
|
|
98
112
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -166,6 +180,7 @@ files:
|
|
|
166
180
|
- app/mailers/fedipub/application_mailer.rb
|
|
167
181
|
- app/models/concerns/fedipub/actor_entity.rb
|
|
168
182
|
- app/models/concerns/fedipub/announceable.rb
|
|
183
|
+
- app/models/concerns/fedipub/application_actor.rb
|
|
169
184
|
- app/models/concerns/fedipub/data_entity.rb
|
|
170
185
|
- app/models/concerns/fedipub/handles_delete_requests.rb
|
|
171
186
|
- app/models/concerns/fedipub/has_uuid.rb
|
|
@@ -260,6 +275,8 @@ files:
|
|
|
260
275
|
- lib/fediverse/notifier.rb
|
|
261
276
|
- lib/fediverse/request.rb
|
|
262
277
|
- lib/fediverse/signature.rb
|
|
278
|
+
- lib/fediverse/signature/draft_cavage12.rb
|
|
279
|
+
- lib/fediverse/signature/rfc9421.rb
|
|
263
280
|
- lib/fediverse/webfinger.rb
|
|
264
281
|
- lib/generators/fedipub/copy_client_policies/USAGE
|
|
265
282
|
- lib/generators/fedipub/copy_client_policies/copy_client_policies_generator.rb
|