federails 0.1.0 → 0.2.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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +11 -0
  3. data/app/controllers/federails/application_controller.rb +4 -0
  4. data/app/controllers/federails/client/activities_controller.rb +1 -1
  5. data/app/controllers/federails/client/actors_controller.rb +1 -1
  6. data/app/controllers/federails/client/followings_controller.rb +9 -1
  7. data/app/controllers/federails/server/activities_controller.rb +5 -5
  8. data/app/controllers/federails/server/actors_controller.rb +1 -1
  9. data/app/controllers/federails/server/followings_controller.rb +2 -1
  10. data/app/controllers/federails/server/nodeinfo_controller.rb +2 -2
  11. data/app/controllers/federails/server/web_finger_controller.rb +2 -2
  12. data/app/helpers/federails/application_helper.rb +8 -0
  13. data/app/models/concerns/federails/entity.rb +12 -1
  14. data/app/models/concerns/federails/has_uuid.rb +35 -0
  15. data/app/models/federails/activity.rb +8 -13
  16. data/app/models/federails/actor.rb +38 -1
  17. data/app/models/federails/following.rb +9 -0
  18. data/app/views/federails/client/actors/show.html.erb +1 -1
  19. data/app/views/federails/server/activities/{_activity.json.jbuilder → _activity.activitypub.jbuilder} +6 -1
  20. data/app/views/federails/server/actors/{_actor.json.jbuilder → _actor.activitypub.jbuilder} +11 -1
  21. data/app/views/federails/server/web_finger/{find.json.jbuilder → find.jrd.jbuilder} +5 -1
  22. data/config/initializers/mime_types.rb +21 -0
  23. data/config/routes.rb +1 -1
  24. data/db/migrate/20241002094500_add_uuids.rb +13 -0
  25. data/db/migrate/20241002094501_add_keypair_to_actors.rb +8 -0
  26. data/lib/federails/configuration.rb +4 -0
  27. data/lib/federails/version.rb +1 -1
  28. data/lib/federails.rb +2 -1
  29. data/lib/fediverse/notifier.rb +44 -5
  30. data/lib/fediverse/signature.rb +49 -0
  31. data/lib/fediverse/webfinger.rb +31 -7
  32. metadata +20 -15
  33. /data/app/views/federails/server/activities/{outbox.json.jbuilder → outbox.activitypub.jbuilder} +0 -0
  34. /data/app/views/federails/server/activities/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
  35. /data/app/views/federails/server/actors/{followers.json.jbuilder → followers.activitypub.jbuilder} +0 -0
  36. /data/app/views/federails/server/actors/{following.json.jbuilder → following.activitypub.jbuilder} +0 -0
  37. /data/app/views/federails/server/actors/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
  38. /data/app/views/federails/server/followings/{_following.json.jbuilder → _following.activitypub.jbuilder} +0 -0
  39. /data/app/views/federails/server/followings/{show.json.jbuilder → show.activitypub.jbuilder} +0 -0
  40. /data/app/views/federails/server/nodeinfo/{index.json.jbuilder → index.nodeinfo.jbuilder} +0 -0
  41. /data/app/views/federails/server/nodeinfo/{show.json.jbuilder → show.nodeinfo.jbuilder} +0 -0
  42. /data/app/views/federails/server/web_finger/{host_meta.xml.erb → host_meta.xrd.erb} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc485556cd2ae749668f8f6a6ffec973361bdef6970e421845b9c2de64e8cecf
4
- data.tar.gz: 34630a4b116e47b3d4c3ec4fd7599ba0264f4dd76fdff5db795d2be90c50c2cc
3
+ metadata.gz: 070da5d2cc3d016475a69f797c6280595618d5f60de95cf7506bb2a1e9cbce4c
4
+ data.tar.gz: a954535a092fa71fdb911f973363d8372b6d56e42b32190394e92026567c6913
5
5
  SHA512:
6
- metadata.gz: b296b6db88b1077326722a187f6074fc8f9bbeb4f43d0f90a2c7811fb7bfd14b0e247f64149ce41c0e53128ce45d3e7e61ecfb0da8d47e64b6b7f188dfa33c8c
7
- data.tar.gz: 948085a88295cf6c6699ed1dbea2a64a30d8695bb1a9d596cb83d66753eb4e26b5129d0d00574d21f230544b75a3f859411e64c785878abc2ce85021ef41a2b5
6
+ metadata.gz: 69fba1438beef26a150079c2a39eb185903025dcad3c24f5dfee941d844710d12938cf5546f0dc488f725096a2c4e6f32dfba4d1d3c825b9ba9418f063bb4ec9
7
+ data.tar.gz: 01ef6821f7b1aa9422f013e23ed20b1225f0e630d4925ca81c7490f43d790a1d77a3b93e8d72783d93aee59f1760b48bbdd71a59feca353bca0a1277a6bd03c0
data/README.md CHANGED
@@ -102,6 +102,17 @@ Federails.configure do |config|
102
102
  end
103
103
  ```
104
104
 
105
+ #### Remote following
106
+
107
+ By default, remote follow requests (where you press a follow button on another server and get redirected home to complete the follow)
108
+ will use the built-in client paths. If you're not using the client, or want to provide your own user interface, you can set the path like this, assuming that `new_follow_url` is a valid route in your app. A `uri` query parameter template will be automatically appended, you don't need to specify that.
109
+
110
+ ```
111
+ Federails.configure do |config|
112
+ config.remote_follow_url_method = :new_follow_url
113
+ end
114
+ ```
115
+
105
116
  ### Migrations
106
117
 
107
118
  Copy the migrations:
@@ -11,6 +11,10 @@ module Federails
11
11
  def error_fallback(exception, fallback_message, status)
12
12
  message = exception&.message || fallback_message
13
13
  respond_to do |format|
14
+ format.jrd { head status }
15
+ format.xrd { head status }
16
+ format.activitypub { head status }
17
+ format.nodeinfo { head status }
14
18
  format.json { render json: { error: message }, status: status }
15
19
  format.html { raise exception }
16
20
  end
@@ -8,7 +8,7 @@ module Federails
8
8
  # GET /app/activities.json
9
9
  def index
10
10
  @activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Client::ActivityPolicy::Scope).all
11
- @activities = @activities.where actor_id: params[:actor_id] if params[:actor_id]
11
+ @activities = @activities.where actor: Actor.find_param(params[:actor_id]) if params[:actor_id]
12
12
  end
13
13
 
14
14
  # GET /app/feed
@@ -25,7 +25,7 @@ module Federails
25
25
 
26
26
  # Use callbacks to share common setup or constraints between actions.
27
27
  def set_actor
28
- @actor = Federails::Actor.find(params[:id])
28
+ @actor = Federails::Actor.find_param(params[:id])
29
29
  authorize @actor, policy_class: Federails::Client::ActorPolicy
30
30
  end
31
31
 
@@ -4,6 +4,14 @@ module Federails
4
4
  before_action :authenticate_user!
5
5
  before_action :set_following, only: [:accept, :destroy]
6
6
 
7
+ # GET /app/followings/new?uri={uri}
8
+ def new
9
+ # Find actor (and fetch if necessary)
10
+ actor = Actor.find_or_create_by_federation_url(params[:uri])
11
+ # Redirect to local profile page which will have a follow button on it
12
+ redirect_to federails.client_actor_url(actor)
13
+ end
14
+
7
15
  # PUT /app/followings/:id/accept
8
16
  # PUT /app/followings/:id/accept.json
9
17
  def accept
@@ -62,7 +70,7 @@ module Federails
62
70
 
63
71
  # Use callbacks to share common setup or constraints between actions.
64
72
  def set_following
65
- @following = Following.find(params[:id])
73
+ @following = Following.find_param(params[:id])
66
74
  authorize @following, policy_class: Federails::Client::FollowingPolicy
67
75
  end
68
76
 
@@ -8,7 +8,7 @@ module Federails
8
8
  # GET /federation/activities
9
9
  # GET /federation/actors/1/outbox.json
10
10
  def outbox
11
- @actor = Actor.find(params[:actor_id])
11
+ @actor = Actor.find_param(params[:actor_id])
12
12
  @activities = policy_scope(Federails::Activity, policy_scope_class: Federails::Server::ActivityPolicy::Scope).where(actor: @actor).order(created_at: :desc)
13
13
  @total_activities = @activities.count
14
14
  @activities = @activities.page(params[:page])
@@ -20,12 +20,12 @@ module Federails
20
20
  # POST /federation/actors/1/inbox
21
21
  def create
22
22
  payload = payload_from_params
23
- return render json: {}, status: :unprocessable_entity unless payload
23
+ return head :unprocessable_entity unless payload
24
24
 
25
25
  if Fediverse::Inbox.dispatch_request(payload)
26
- render json: {}, status: :created
26
+ head :created
27
27
  else
28
- render json: {}, status: :unprocessable_entity
28
+ head :unprocessable_entity
29
29
  end
30
30
  end
31
31
 
@@ -33,7 +33,7 @@ module Federails
33
33
 
34
34
  # Use callbacks to share common setup or constraints between actions.
35
35
  def set_activity
36
- @activity = Activity.find_by!(actor_id: params[:actor_id], id: params[:id])
36
+ @activity = Actor.find_param(params[:actor_id]).activities.find_param(params[:id])
37
37
  end
38
38
 
39
39
  # Only allow a list of trusted parameters through.
@@ -21,7 +21,7 @@ module Federails
21
21
 
22
22
  # Use callbacks to share common setup or constraints between actions.
23
23
  def set_actor
24
- @actor = Actor.find(params[:id])
24
+ @actor = Actor.find_param(params[:id])
25
25
  authorize @actor, policy_class: Federails::Server::ActorPolicy
26
26
  end
27
27
 
@@ -10,7 +10,8 @@ module Federails
10
10
 
11
11
  # Use callbacks to share common setup or constraints between actions.
12
12
  def set_following
13
- @following = Following.find_by!(actor_id: params[:actor_id], id: params[:id])
13
+ actor = Actor.find_param(params[:actor_id])
14
+ @following = Following.find_by!(actor: actor, uuid: params[:id])
14
15
  authorize @following, policy_class: Federails::Server::FollowingPolicy
15
16
  end
16
17
  end
@@ -2,7 +2,7 @@ module Federails
2
2
  module Server
3
3
  class NodeinfoController < ServerController
4
4
  def index
5
- render formats: [:json]
5
+ render formats: [:nodeinfo]
6
6
  end
7
7
 
8
8
  def show # rubocop:todo Metrics/AbcSize
@@ -15,7 +15,7 @@ module Federails
15
15
  @active_month += model.where(created_at: ((30.days.ago)...Time.current)).count
16
16
  @active_halfyear += model.where(created_at: ((180.days.ago)...Time.current)).count
17
17
  end
18
- render formats: [:json]
18
+ render formats: [:nodeinfo]
19
19
  end
20
20
  end
21
21
  end
@@ -15,11 +15,11 @@ module Federails
15
15
  end
16
16
  raise ActiveRecord::RecordNotFound if @user.nil?
17
17
 
18
- render formats: [:json]
18
+ render formats: [:jrd]
19
19
  end
20
20
 
21
21
  def host_meta
22
- render content_type: 'application/xrd+xml', formats: [:xml]
22
+ render formats: [:xrd]
23
23
  end
24
24
 
25
25
  # TODO: complete missing endpoints
@@ -1,4 +1,12 @@
1
1
  module Federails
2
2
  module ApplicationHelper
3
+ def remote_follow_url
4
+ method_name = Federails.configuration.remote_follow_url_method.to_s
5
+ if method_name.starts_with? 'federails.'
6
+ send(method_name.gsub('federails.', ''))
7
+ else
8
+ Rails.application.routes.url_helpers.send(method_name)
9
+ end
10
+ end
3
11
  end
4
12
  end
@@ -2,7 +2,18 @@ module Federails
2
2
  module Entity
3
3
  extend ActiveSupport::Concern
4
4
 
5
- included do
5
+ included do # rubocop:todo Metrics/BlockLength
6
+ include ActiveSupport::Callbacks
7
+ define_callbacks :followed
8
+
9
+ # Define a method that will be called after the entity receives a follow request
10
+ # @param method [Symbol] The name of the method to call, or a block that will be called directly
11
+ # @example
12
+ # after_followed :accept_follow
13
+ def self.after_followed(method)
14
+ set_callback :followed, :after, method
15
+ end
16
+
6
17
  has_one :actor, class_name: 'Federails::Actor', as: :entity, dependent: :destroy
7
18
 
8
19
  after_create :create_actor
@@ -0,0 +1,35 @@
1
+ module Federails
2
+ module HasUuid
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ before_validation :generate_uuid
7
+ validates :uuid, presence: true, uniqueness: true
8
+
9
+ def self.find_param(param)
10
+ find_by!(uuid: param)
11
+ end
12
+ end
13
+
14
+ def to_param
15
+ uuid
16
+ end
17
+
18
+ # Override UUID accessor to provide lazy initialization of UUIDs for old data
19
+ def uuid
20
+ if self[:uuid].blank?
21
+ generate_uuid
22
+ save!
23
+ end
24
+ self[:uuid]
25
+ end
26
+
27
+ private
28
+
29
+ def generate_uuid
30
+ return if self[:uuid].present?
31
+
32
+ (self.uuid = SecureRandom.uuid) while self[:uuid].blank? || self.class.exists?(uuid: self[:uuid])
33
+ end
34
+ end
35
+ end
@@ -1,5 +1,7 @@
1
1
  module Federails
2
2
  class Activity < ApplicationRecord
3
+ include Federails::HasUuid
4
+
3
5
  belongs_to :entity, polymorphic: true
4
6
  belongs_to :actor
5
7
 
@@ -13,22 +15,15 @@ module Federails
13
15
 
14
16
  after_create_commit :post_to_inboxes
15
17
 
16
- def recipients # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
18
+ def recipients
17
19
  return [] unless actor.local?
18
20
 
19
- actors = []
20
- case action
21
- when 'Create'
22
- actors.push(entity.target_actor) if entity_type == 'Federails::Following'
23
- # FIXME: Move this to dummy, somehow
24
- actors.push(*actor.followers) if entity_type == 'Note'
25
- when 'Accept'
26
- actors.push(entity.actor) if entity_type == 'Federails::Following'
27
- when 'Undo'
28
- actors.push(entity.target_actor) if entity_type == 'Federails::Following'
21
+ case entity_type
22
+ when 'Federails::Following'
23
+ [(action == 'Accept' ? entity.actor : entity.target_actor)]
24
+ else
25
+ actor.followers
29
26
  end
30
-
31
- actors
32
27
  end
33
28
 
34
29
  private
@@ -3,6 +3,8 @@ require 'fediverse/webfinger'
3
3
 
4
4
  module Federails
5
5
  class Actor < ApplicationRecord # rubocop:disable Metrics/ClassLength
6
+ include Federails::HasUuid
7
+
6
8
  validates :federated_url, presence: { unless: :entity }, uniqueness: { unless: :entity }
7
9
  validates :username, presence: { unless: :entity }
8
10
  validates :server, presence: { unless: :entity }
@@ -112,7 +114,7 @@ module Federails
112
114
 
113
115
  def find_by_federation_url(federated_url)
114
116
  local_route = Utils::Host.local_route federated_url
115
- return find local_route[:id] if local_route && local_route[:controller] == 'federails/server/actors' && local_route[:action] == 'show'
117
+ return find_param(local_route[:id]) if local_route && local_route[:controller] == 'federails/server/actors' && local_route[:action] == 'show'
116
118
 
117
119
  actor = find_by federated_url: federated_url
118
120
  return actor if actor
@@ -148,5 +150,40 @@ module Federails
148
150
  end
149
151
  end
150
152
  end
153
+
154
+ def public_key
155
+ ensure_key_pair_exists!
156
+ self[:public_key]
157
+ end
158
+
159
+ def private_key
160
+ ensure_key_pair_exists!
161
+ self[:private_key]
162
+ end
163
+
164
+ def key_id
165
+ "#{federated_url}#main-key"
166
+ end
167
+
168
+ private
169
+
170
+ def ensure_key_pair_exists!
171
+ return if self[:private_key].present? || !local?
172
+
173
+ update!(generate_key_pair)
174
+ end
175
+
176
+ def generate_key_pair
177
+ rsa_key = OpenSSL::PKey::RSA.new 2048
178
+ cipher = OpenSSL::Cipher.new('AES-128-CBC')
179
+ {
180
+ private_key: if Rails.application.credentials.secret_key_base
181
+ rsa_key.to_pem(cipher, Rails.application.credentials.secret_key_base)
182
+ else
183
+ rsa_key.to_pem
184
+ end,
185
+ public_key: rsa_key.public_key.to_pem,
186
+ }
187
+ end
151
188
  end
152
189
  end
@@ -1,5 +1,7 @@
1
1
  module Federails
2
2
  class Following < ApplicationRecord
3
+ include Federails::HasUuid
4
+
3
5
  enum status: { pending: 0, accepted: 1 }
4
6
 
5
7
  validates :target_actor_id, uniqueness: { scope: [:actor_id, :target_actor_id] }
@@ -9,6 +11,7 @@ module Federails
9
11
  # FIXME: Handle this with something like undelete
10
12
  has_many :activities, as: :entity, dependent: :destroy
11
13
 
14
+ after_create :after_follow
12
15
  after_create :create_activity
13
16
  after_destroy :destroy_activity
14
17
 
@@ -32,6 +35,12 @@ module Federails
32
35
 
33
36
  private
34
37
 
38
+ def after_follow
39
+ target_actor&.entity&.run_callbacks :followed, :after do
40
+ self
41
+ end
42
+ end
43
+
35
44
  def create_activity
36
45
  Activity.create! actor: actor, action: 'Create', entity: self
37
46
  end
@@ -61,7 +61,7 @@
61
61
  <p>
62
62
  <b>Home page:</b>
63
63
  <% if @actor.local? && Federails::Configuration.user_profile_url_method %>
64
- <%= link_to @actor.send(Federails::Configuration.user_username_field), send(Federails::Configuration.user_profile_url_method, @actor.user) %>
64
+ <%= link_to @actor.send(Federails::Configuration.user_username_field), Rails.application.routes.url_helpers.send(Federails::Configuration.user_profile_url_method, @actor.user) %>
65
65
  <% elsif @actor.profile_url %>
66
66
  <%= link_to @actor.name, @actor.profile_url %>
67
67
  <% else %>
@@ -6,4 +6,9 @@ json.type activity.action
6
6
  json.actor activity.actor.federated_url
7
7
  json.to ['https://www.w3.org/ns/activitystreams#Public']
8
8
  json.cc [activity.actor.followers_url]
9
- json.object activity.entity.federated_url
9
+
10
+ if activity.entity.respond_to? :to_activitypub_object
11
+ json.object activity.entity.to_activitypub_object
12
+ elsif activity.entity.respond_to? :federated_url
13
+ json.object activity.entity.federated_url
14
+ end
@@ -1,4 +1,7 @@
1
- json.set! '@context', 'https://www.w3.org/ns/activitystreams'
1
+ json.set! '@context', [
2
+ 'https://www.w3.org/ns/activitystreams',
3
+ 'https://w3id.org/security/v1',
4
+ ]
2
5
 
3
6
  json.id actor.federated_url
4
7
  json.name actor.name
@@ -9,3 +12,10 @@ json.outbox actor.outbox_url
9
12
  json.followers actor.followers_url
10
13
  json.following actor.followings_url
11
14
  json.url actor.profile_url
15
+ if actor.public_key
16
+ json.publicKey do
17
+ json.id actor.key_id
18
+ json.owner actor.federated_url
19
+ json.publicKeyPem actor.public_key
20
+ end
21
+ end
@@ -4,7 +4,7 @@ links = [
4
4
  # Federation actor URL
5
5
  {
6
6
  rel: 'self',
7
- type: 'application/activity+json',
7
+ type: Mime[:activitypub].to_s,
8
8
  href: @user.actor.federated_url,
9
9
  },
10
10
  ]
@@ -17,4 +17,8 @@ if @user.actor.profile_url
17
17
  href: @user.actor.profile_url
18
18
  end
19
19
 
20
+ # Remote following
21
+ links.push rel: 'http://ostatus.org/schema/1.0/subscribe',
22
+ template: "#{remote_follow_url}?uri={uri}"
23
+
20
24
  json.links links
@@ -0,0 +1,21 @@
1
+ # Webfinger: https://datatracker.ietf.org/doc/html/rfc7033
2
+ Mime::Type.register 'application/jrd+json', :jrd
3
+ Mime::Type.register 'application/xrd+xml', :xrd
4
+
5
+ # ActivityPub: https://www.w3.org/TR/activitypub/#retrieving-objects
6
+ Mime::Type.register 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', :activitypub, ['application/activity+json', 'application/json']
7
+
8
+ # Nodeinfo: https://github.com/jhass/nodeinfo/blob/main/PROTOCOL.md#retrieval
9
+ Mime::Type.register 'application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"', :nodeinfo
10
+
11
+ # Get current request parsers. Apparently we need to do it this way and can't add in-place, see
12
+ # https://api.rubyonrails.org/classes/ActionDispatch/Http/Parameters/ClassMethods.html#method-i-parameter_parsers-3D
13
+ parsers = ActionDispatch::Request.parameter_parsers
14
+ # Copy the default JSON parsing for JSON types
15
+ [:jrd, :activitypub, :nodeinfo].each do |mime_type|
16
+ parsers[Mime[mime_type].symbol] = parsers[:json]
17
+ end
18
+ # XRD just needs a simple XML parser
19
+ parsers[Mime[:xrd].symbol] = ->(raw_post) { Hash.from_xml(raw_post) || {} }
20
+ # Store updated parsers
21
+ ActionDispatch::Request.parameter_parsers = parsers
data/config/routes.rb CHANGED
@@ -18,7 +18,7 @@ Federails::Engine.routes.draw do
18
18
  resources :activities, only: [:index]
19
19
  end
20
20
  get :feed, to: 'activities#feed'
21
- resources :followings, only: [:create, :destroy] do
21
+ resources :followings, only: [:new, :create, :destroy] do
22
22
  collection do
23
23
  post :follow, to: 'followings#follow'
24
24
  end
@@ -0,0 +1,13 @@
1
+ class AddUuids < ActiveRecord::Migration[7.0]
2
+ def change
3
+ [
4
+ :federails_actors,
5
+ :federails_activities,
6
+ :federails_followings,
7
+ ].each do |table|
8
+ change_table table do |t|
9
+ t.string :uuid, default: nil, index: { unique: true }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,8 @@
1
+ class AddKeypairToActors < ActiveRecord::Migration[7.0]
2
+ def change
3
+ change_table :federails_actors do |t|
4
+ t.text :public_key
5
+ t.text :private_key
6
+ end
7
+ end
8
+ end
@@ -42,6 +42,10 @@ module Federails
42
42
  mattr_accessor :client_routes_path
43
43
  @@client_routes_path = :app
44
44
 
45
+ # Route method for remote-following requests
46
+ mattr_accessor :remote_follow_url_method
47
+ @@remote_follow_url_method = 'federails.new_client_following_url'
48
+
45
49
  # Method to use for links to user profiles
46
50
  # @deprecated Set profile_url_method option on acts_as_federails_actor instead
47
51
  mattr_accessor :user_profile_url_method
@@ -1,3 +1,3 @@
1
1
  module Federails
2
- VERSION = '0.1.0'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
data/lib/federails.rb CHANGED
@@ -27,10 +27,11 @@ module Federails
27
27
  :user_class, # @deprecated
28
28
  :server_routes_path,
29
29
  :client_routes_path,
30
+ :remote_follow_url_method,
30
31
  :user_profile_url_method, # @deprecated
31
32
  :user_name_field, # @deprecated
32
33
  :user_username_field, # @deprecated
33
- ].each { |key| Configuration.send :"#{key}=", config[key] }
34
+ ].each { |key| Configuration.send :"#{key}=", config[key] if config.key?(key) }
34
35
  end
35
36
  end
36
37
  # rubocop:enable Style/ClassVars
@@ -1,21 +1,60 @@
1
+ require 'fediverse/signature'
2
+
1
3
  module Fediverse
2
4
  class Notifier
3
5
  class << self
4
6
  def post_to_inboxes(activity)
5
7
  actors = activity.recipients
6
-
7
8
  Rails.logger.debug('Nobody to notice') && return if actors.count.zero?
8
9
 
9
- message = Federails::ApplicationController.renderer.new.render(
10
+ message = payload(activity)
11
+ actors.each do |recipient|
12
+ Rails.logger.debug { "Sending activity ##{activity.id} to #{recipient.inbox_url}" }
13
+ post_to_inbox(to: recipient, message: message, from: activity.actor)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def payload(activity)
20
+ Federails::ApplicationController.renderer.new.render(
10
21
  template: 'federails/server/activities/show',
11
22
  assigns: { activity: activity },
12
23
  format: :json
13
24
  )
14
- actors.each do |actor|
15
- Rails.logger.debug { "Sending activity ##{activity.id} to #{actor.inbox_url}" }
16
- Faraday.post actor.inbox_url, message, 'Content-Type' => 'application/json', 'Accept' => 'application/json'
25
+ end
26
+
27
+ def post_to_inbox(to:, message:, from: nil)
28
+ conn = Faraday.default_connection
29
+ conn.builder.build_response(
30
+ conn,
31
+ signed_request(to: to, message: message, from: from)
32
+ )
33
+ end
34
+
35
+ def signed_request(to:, message:, from:)
36
+ req = request(to: to, message: message)
37
+ req.headers['Signature'] = Fediverse::Signature.sign(sender: from, request: req) if from
38
+ req
39
+ end
40
+
41
+ def request(to:, message:) # rubocop:todo Metrics/AbcSize
42
+ Faraday.default_connection.build_request(:post) do |req|
43
+ req.url to.inbox_url
44
+ req.body = message
45
+ req.headers['Content-Type'] = Mime[:activitypub].to_s
46
+ req.headers['Accept'] = Mime[:activitypub].to_s
47
+ req.headers['Host'] = URI.parse(to.inbox_url).host
48
+ req.headers['Date'] = Time.now.utc.httpdate
49
+ req.headers['Digest'] = digest(message)
17
50
  end
18
51
  end
52
+
53
+ def digest(message)
54
+ "SHA-256=#{Base64.strict_encode64(
55
+ OpenSSL::Digest.new('SHA256').digest(message)
56
+ )}"
57
+ end
19
58
  end
20
59
  end
21
60
  end
@@ -0,0 +1,49 @@
1
+ module Fediverse
2
+ class Signature
3
+ class << self
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
18
+
19
+ def verify(sender:, request:)
20
+ raise 'Unsigned headers' unless request.headers['Signature']
21
+
22
+ signature_header = request.headers['Signature'].split(',').to_h do |pair|
23
+ /\A(?<key>[\w]+)="(?<value>.*)"\z/ =~ pair
24
+ [key, value]
25
+ 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
+ end
35
+
36
+ private
37
+
38
+ def signature_payload(request:, headers:)
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")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -30,28 +30,52 @@ module Fediverse
30
30
 
31
31
  # Returns actor id
32
32
  def webfinger(username, domain)
33
- scheme = Federails.configuration.force_ssl ? 'https' : 'http'
34
- json = get_json "#{scheme}://#{domain}/.well-known/webfinger", resource: "acct:#{username}@#{domain}"
33
+ json = webfinger_response(username, domain)
35
34
  link = json['links'].find { |l| l['type'] == 'application/activity+json' }
36
35
 
37
36
  link['href'] if link
38
37
  end
39
38
 
39
+ # Returns remote follow link template, or complete link if actor_url is provided
40
+ def remote_follow_url(username, domain, actor_url: nil)
41
+ json = webfinger_response(username, domain)
42
+ link = json['links'].find { |l| l['rel'] == 'http://ostatus.org/schema/1.0/subscribe' }
43
+ return nil if link&.dig('template').nil?
44
+
45
+ if actor_url
46
+ link['template'].gsub('{uri}', CGI.escape(actor_url))
47
+ else
48
+ link['template']
49
+ end
50
+ end
51
+
40
52
  private
41
53
 
54
+ def webfinger_response(username, domain)
55
+ scheme = Federails.configuration.force_ssl ? 'https' : 'http'
56
+ get_json "#{scheme}://#{domain}/.well-known/webfinger", resource: "acct:#{username}@#{domain}"
57
+ end
58
+
59
+ def server_and_port(id)
60
+ uri = URI.parse id
61
+ if uri.port && [80, 443].exclude?(uri.port)
62
+ "#{uri.host}:#{uri.port}"
63
+ else
64
+ uri.host
65
+ end
66
+ end
67
+
42
68
  def webfinger_to_actor(data)
43
- uri = URI.parse data['id']
44
- server = uri.host
45
- server += ":#{uri.port}" if uri.port && [80, 443].exclude?(uri.port)
46
69
  Federails::Actor.new federated_url: data['id'],
47
70
  username: data['preferredUsername'],
48
71
  name: data['name'],
49
- server: server,
72
+ server: server_and_port(data['id']),
50
73
  inbox_url: data['inbox'],
51
74
  outbox_url: data['outbox'],
52
75
  followers_url: data['followers'],
53
76
  followings_url: data['following'],
54
- profile_url: data['url']
77
+ profile_url: data['url'],
78
+ public_key: data.dig('publicKey', 'publicKeyPem')
55
79
  end
56
80
 
57
81
  def get_json(url, payload = {})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: federails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Manuel Tancoigne
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-04 00:00:00.000000000 Z
11
+ date: 2024-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: faraday
@@ -134,6 +134,7 @@ files:
134
134
  - app/jobs/federails/notify_inbox_job.rb
135
135
  - app/mailers/federails/application_mailer.rb
136
136
  - app/models/concerns/federails/entity.rb
137
+ - app/models/concerns/federails/has_uuid.rb
137
138
  - app/models/federails/activity.rb
138
139
  - app/models/federails/actor.rb
139
140
  - app/models/federails/application_record.rb
@@ -166,24 +167,27 @@ files:
166
167
  - app/views/federails/client/followings/index.json.jbuilder
167
168
  - app/views/federails/client/followings/show.html.erb
168
169
  - app/views/federails/client/followings/show.json.jbuilder
169
- - app/views/federails/server/activities/_activity.json.jbuilder
170
- - app/views/federails/server/activities/outbox.json.jbuilder
171
- - app/views/federails/server/activities/show.json.jbuilder
172
- - app/views/federails/server/actors/_actor.json.jbuilder
173
- - app/views/federails/server/actors/followers.json.jbuilder
174
- - app/views/federails/server/actors/following.json.jbuilder
175
- - app/views/federails/server/actors/show.json.jbuilder
176
- - app/views/federails/server/followings/_following.json.jbuilder
177
- - app/views/federails/server/followings/show.json.jbuilder
178
- - app/views/federails/server/nodeinfo/index.json.jbuilder
179
- - app/views/federails/server/nodeinfo/show.json.jbuilder
180
- - app/views/federails/server/web_finger/find.json.jbuilder
181
- - app/views/federails/server/web_finger/host_meta.xml.erb
170
+ - app/views/federails/server/activities/_activity.activitypub.jbuilder
171
+ - app/views/federails/server/activities/outbox.activitypub.jbuilder
172
+ - app/views/federails/server/activities/show.activitypub.jbuilder
173
+ - app/views/federails/server/actors/_actor.activitypub.jbuilder
174
+ - app/views/federails/server/actors/followers.activitypub.jbuilder
175
+ - app/views/federails/server/actors/following.activitypub.jbuilder
176
+ - app/views/federails/server/actors/show.activitypub.jbuilder
177
+ - app/views/federails/server/followings/_following.activitypub.jbuilder
178
+ - app/views/federails/server/followings/show.activitypub.jbuilder
179
+ - app/views/federails/server/nodeinfo/index.nodeinfo.jbuilder
180
+ - app/views/federails/server/nodeinfo/show.nodeinfo.jbuilder
181
+ - app/views/federails/server/web_finger/find.jrd.jbuilder
182
+ - app/views/federails/server/web_finger/host_meta.xrd.erb
183
+ - config/initializers/mime_types.rb
182
184
  - config/routes.rb
183
185
  - db/migrate/20200712133150_create_federails_actors.rb
184
186
  - db/migrate/20200712143127_create_federails_followings.rb
185
187
  - db/migrate/20200712174938_create_federails_activities.rb
186
188
  - db/migrate/20240731145400_change_actor_entity_rel_to_polymorphic.rb
189
+ - db/migrate/20241002094500_add_uuids.rb
190
+ - db/migrate/20241002094501_add_keypair_to_actors.rb
187
191
  - lib/federails.rb
188
192
  - lib/federails/configuration.rb
189
193
  - lib/federails/engine.rb
@@ -192,6 +196,7 @@ files:
192
196
  - lib/fediverse/inbox.rb
193
197
  - lib/fediverse/notifier.rb
194
198
  - lib/fediverse/request.rb
199
+ - lib/fediverse/signature.rb
195
200
  - lib/fediverse/webfinger.rb
196
201
  - lib/generators/federails/install/USAGE
197
202
  - lib/generators/federails/install/install_generator.rb