Gab Social. All are welcome.
This commit is contained in:
78
app/controllers/concerns/account_controller_concern.rb
Normal file
78
app/controllers/concerns/account_controller_concern.rb
Normal file
@@ -0,0 +1,78 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
FOLLOW_PER_PAGE = 12
|
||||
|
||||
included do
|
||||
layout 'public'
|
||||
|
||||
before_action :set_account
|
||||
before_action :check_account_approval
|
||||
before_action :check_account_suspension
|
||||
before_action :set_instance_presenter
|
||||
before_action :set_link_headers
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_account
|
||||
@account = Account.find_local!(username_param)
|
||||
end
|
||||
|
||||
def set_instance_presenter
|
||||
@instance_presenter = InstancePresenter.new
|
||||
end
|
||||
|
||||
def set_link_headers
|
||||
response.headers['Link'] = LinkHeader.new(
|
||||
[
|
||||
webfinger_account_link,
|
||||
atom_account_url_link,
|
||||
actor_url_link,
|
||||
]
|
||||
)
|
||||
end
|
||||
|
||||
def username_param
|
||||
params[:account_username]
|
||||
end
|
||||
|
||||
def webfinger_account_link
|
||||
[
|
||||
webfinger_account_url,
|
||||
[%w(rel lrdd), %w(type application/xrd+xml)],
|
||||
]
|
||||
end
|
||||
|
||||
def atom_account_url_link
|
||||
[
|
||||
account_url(@account, format: 'atom'),
|
||||
[%w(rel alternate), %w(type application/atom+xml)],
|
||||
]
|
||||
end
|
||||
|
||||
def actor_url_link
|
||||
[
|
||||
ActivityPub::TagManager.instance.uri_for(@account),
|
||||
[%w(rel alternate), %w(type application/activity+json)],
|
||||
]
|
||||
end
|
||||
|
||||
def webfinger_account_url
|
||||
webfinger_url(resource: @account.to_webfinger_s)
|
||||
end
|
||||
|
||||
def check_account_approval
|
||||
not_found if @account.user_pending?
|
||||
end
|
||||
|
||||
def check_account_suspension
|
||||
if @account.suspended?
|
||||
skip_session!
|
||||
expires_in(3.minutes, public: true)
|
||||
gone
|
||||
end
|
||||
end
|
||||
end
|
||||
9
app/controllers/concerns/accountable_concern.rb
Normal file
9
app/controllers/concerns/accountable_concern.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module AccountableConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def log_action(action, target)
|
||||
Admin::ActionLog.create(account: current_account, action: action, target: target)
|
||||
end
|
||||
end
|
||||
23
app/controllers/concerns/authorization.rb
Normal file
23
app/controllers/concerns/authorization.rb
Normal file
@@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Authorization
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include Pundit
|
||||
|
||||
def pundit_user
|
||||
current_account
|
||||
end
|
||||
|
||||
def authorize(*)
|
||||
super
|
||||
rescue Pundit::NotAuthorizedError
|
||||
raise GabSocial::NotPermittedError
|
||||
end
|
||||
|
||||
def authorize_with(user, record, query)
|
||||
Pundit.authorize(user, record, query)
|
||||
rescue Pundit::NotAuthorizedError
|
||||
raise GabSocial::NotPermittedError
|
||||
end
|
||||
end
|
||||
30
app/controllers/concerns/export_controller_concern.rb
Normal file
30
app/controllers/concerns/export_controller_concern.rb
Normal file
@@ -0,0 +1,30 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ExportControllerConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :authenticate_user!
|
||||
before_action :load_export
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def load_export
|
||||
@export = Export.new(current_account)
|
||||
end
|
||||
|
||||
def send_export_file
|
||||
respond_to do |format|
|
||||
format.csv { send_data export_data, filename: export_filename }
|
||||
end
|
||||
end
|
||||
|
||||
def export_data
|
||||
raise 'Override in controller'
|
||||
end
|
||||
|
||||
def export_filename
|
||||
"#{controller_name}.csv"
|
||||
end
|
||||
end
|
||||
42
app/controllers/concerns/localized.rb
Normal file
42
app/controllers/concerns/localized.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Localized
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_locale
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_locale
|
||||
I18n.locale = default_locale
|
||||
I18n.locale = current_user.locale if user_signed_in?
|
||||
rescue I18n::InvalidLocale
|
||||
I18n.locale = default_locale
|
||||
end
|
||||
|
||||
def default_locale
|
||||
if ENV['DEFAULT_LOCALE'].present?
|
||||
I18n.default_locale
|
||||
else
|
||||
request_locale || I18n.default_locale
|
||||
end
|
||||
end
|
||||
|
||||
def request_locale
|
||||
preferred_locale || compatible_locale
|
||||
end
|
||||
|
||||
def preferred_locale
|
||||
http_accept_language.preferred_language_from(available_locales)
|
||||
end
|
||||
|
||||
def compatible_locale
|
||||
http_accept_language.compatible_language_from(available_locales)
|
||||
end
|
||||
|
||||
def available_locales
|
||||
I18n.available_locales.reverse
|
||||
end
|
||||
end
|
||||
16
app/controllers/concerns/obfuscate_filename.rb
Normal file
16
app/controllers/concerns/obfuscate_filename.rb
Normal file
@@ -0,0 +1,16 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ObfuscateFilename
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
class_methods do
|
||||
def obfuscate_filename(path)
|
||||
before_action do
|
||||
file = params.dig(*path)
|
||||
next if file.nil?
|
||||
|
||||
file.original_filename = SecureRandom.hex(8) + File.extname(file.original_filename)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
58
app/controllers/concerns/rate_limit_headers.rb
Normal file
58
app/controllers/concerns/rate_limit_headers.rb
Normal file
@@ -0,0 +1,58 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module RateLimitHeaders
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :set_rate_limit_headers, if: :rate_limited_request?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_rate_limit_headers
|
||||
apply_header_limit
|
||||
apply_header_remaining
|
||||
apply_header_reset
|
||||
end
|
||||
|
||||
def rate_limited_request?
|
||||
!request.env['rack.attack.throttle_data'].nil?
|
||||
end
|
||||
|
||||
def apply_header_limit
|
||||
response.headers['X-RateLimit-Limit'] = rate_limit_limit
|
||||
end
|
||||
|
||||
def rate_limit_limit
|
||||
api_throttle_data[:limit].to_s
|
||||
end
|
||||
|
||||
def apply_header_remaining
|
||||
response.headers['X-RateLimit-Remaining'] = rate_limit_remaining
|
||||
end
|
||||
|
||||
def rate_limit_remaining
|
||||
(api_throttle_data[:limit] - api_throttle_data[:count]).to_s
|
||||
end
|
||||
|
||||
def apply_header_reset
|
||||
response.headers['X-RateLimit-Reset'] = rate_limit_reset
|
||||
end
|
||||
|
||||
def rate_limit_reset
|
||||
(request_time + reset_period_offset).iso8601(6)
|
||||
end
|
||||
|
||||
def api_throttle_data
|
||||
most_limited_type, = request.env['rack.attack.throttle_data'].min_by { |_, v| v[:limit] }
|
||||
request.env['rack.attack.throttle_data'][most_limited_type]
|
||||
end
|
||||
|
||||
def request_time
|
||||
@_request_time ||= Time.now.utc
|
||||
end
|
||||
|
||||
def reset_period_offset
|
||||
api_throttle_data[:period] - request_time.to_i % api_throttle_data[:period]
|
||||
end
|
||||
end
|
||||
22
app/controllers/concerns/session_tracking_concern.rb
Normal file
22
app/controllers/concerns/session_tracking_concern.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SessionTrackingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
UPDATE_SIGN_IN_HOURS = 24
|
||||
|
||||
included do
|
||||
before_action :set_session_activity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_session_activity
|
||||
return unless session_needs_update?
|
||||
current_session.touch
|
||||
end
|
||||
|
||||
def session_needs_update?
|
||||
!current_session.nil? && current_session.updated_at < UPDATE_SIGN_IN_HOURS.hours.ago
|
||||
end
|
||||
end
|
||||
11
app/controllers/concerns/signature_authentication.rb
Normal file
11
app/controllers/concerns/signature_authentication.rb
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SignatureAuthentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include SignatureVerification
|
||||
|
||||
def current_account
|
||||
super || signed_request_account
|
||||
end
|
||||
end
|
||||
148
app/controllers/concerns/signature_verification.rb
Normal file
148
app/controllers/concerns/signature_verification.rb
Normal file
@@ -0,0 +1,148 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Implemented according to HTTP signatures (Draft 6)
|
||||
# <https://tools.ietf.org/html/draft-cavage-http-signatures-06>
|
||||
module SignatureVerification
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
def signed_request?
|
||||
request.headers['Signature'].present?
|
||||
end
|
||||
|
||||
def signature_verification_failure_reason
|
||||
return @signature_verification_failure_reason if defined?(@signature_verification_failure_reason)
|
||||
end
|
||||
|
||||
def signed_request_account
|
||||
return @signed_request_account if defined?(@signed_request_account)
|
||||
|
||||
unless signed_request?
|
||||
@signature_verification_failure_reason = 'Request not signed'
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
if request.headers['Date'].present? && !matches_time_window?
|
||||
@signature_verification_failure_reason = 'Signed request date outside acceptable time window'
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
raw_signature = request.headers['Signature']
|
||||
signature_params = {}
|
||||
|
||||
raw_signature.split(',').each do |part|
|
||||
parsed_parts = part.match(/([a-z]+)="([^"]+)"/i)
|
||||
next if parsed_parts.nil? || parsed_parts.size != 3
|
||||
signature_params[parsed_parts[1]] = parsed_parts[2]
|
||||
end
|
||||
|
||||
if incompatible_signature?(signature_params)
|
||||
@signature_verification_failure_reason = 'Incompatible request signature'
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
account = account_from_key_id(signature_params['keyId'])
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
signature = Base64.decode64(signature_params['signature'])
|
||||
compare_signed_string = build_signed_string(signature_params['headers'])
|
||||
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
|
||||
account = stoplight_wrap_request { account.possibly_stale? ? account.refresh! : account_refresh_key(account) }
|
||||
|
||||
if account.nil?
|
||||
@signature_verification_failure_reason = "Public key not found for key #{signature_params['keyId']}"
|
||||
@signed_request_account = nil
|
||||
return
|
||||
end
|
||||
|
||||
return account unless verify_signature(account, signature, compare_signed_string).nil?
|
||||
|
||||
@signature_verification_failure_reason = "Verification failed for #{account.username}@#{account.domain} #{account.uri}"
|
||||
@signed_request_account = nil
|
||||
end
|
||||
|
||||
def request_body
|
||||
@request_body ||= request.raw_post
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def verify_signature(account, signature, compare_signed_string)
|
||||
if account.keypair.public_key.verify(OpenSSL::Digest::SHA256.new, signature, compare_signed_string)
|
||||
@signed_request_account = account
|
||||
@signed_request_account
|
||||
end
|
||||
rescue OpenSSL::PKey::RSAError
|
||||
nil
|
||||
end
|
||||
|
||||
def build_signed_string(signed_headers)
|
||||
signed_headers = 'date' if signed_headers.blank?
|
||||
|
||||
signed_headers.downcase.split(' ').map do |signed_header|
|
||||
if signed_header == Request::REQUEST_TARGET
|
||||
"#{Request::REQUEST_TARGET}: #{request.method.downcase} #{request.path}"
|
||||
elsif signed_header == 'digest'
|
||||
"digest: #{body_digest}"
|
||||
else
|
||||
"#{signed_header}: #{request.headers[to_header_name(signed_header)]}"
|
||||
end
|
||||
end.join("\n")
|
||||
end
|
||||
|
||||
def matches_time_window?
|
||||
begin
|
||||
time_sent = Time.httpdate(request.headers['Date'])
|
||||
rescue ArgumentError
|
||||
return false
|
||||
end
|
||||
|
||||
(Time.now.utc - time_sent).abs <= 12.hours
|
||||
end
|
||||
|
||||
def body_digest
|
||||
"SHA-256=#{Digest::SHA256.base64digest(request_body)}"
|
||||
end
|
||||
|
||||
def to_header_name(name)
|
||||
name.split(/-/).map(&:capitalize).join('-')
|
||||
end
|
||||
|
||||
def incompatible_signature?(signature_params)
|
||||
signature_params['keyId'].blank? ||
|
||||
signature_params['signature'].blank?
|
||||
end
|
||||
|
||||
def account_from_key_id(key_id)
|
||||
if key_id.start_with?('acct:')
|
||||
stoplight_wrap_request { ResolveAccountService.new.call(key_id.gsub(/\Aacct:/, '')) }
|
||||
elsif !ActivityPub::TagManager.instance.local_uri?(key_id)
|
||||
account = ActivityPub::TagManager.instance.uri_to_resource(key_id, Account)
|
||||
account ||= stoplight_wrap_request { ActivityPub::FetchRemoteKeyService.new.call(key_id, id: false) }
|
||||
account
|
||||
end
|
||||
end
|
||||
|
||||
def stoplight_wrap_request(&block)
|
||||
Stoplight("source:#{request.remote_ip}", &block)
|
||||
.with_fallback { nil }
|
||||
.with_threshold(1)
|
||||
.with_cool_off_time(5.minutes.seconds)
|
||||
.with_error_handler { |error, handle| error.is_a?(HTTP::Error) ? handle.call(error) : raise(error) }
|
||||
.run
|
||||
end
|
||||
|
||||
def account_refresh_key(account)
|
||||
return if account.local? || !account.activitypub?
|
||||
ActivityPub::FetchRemoteAccountService.new.call(account.uri, only_key: true)
|
||||
end
|
||||
end
|
||||
22
app/controllers/concerns/user_tracking_concern.rb
Normal file
22
app/controllers/concerns/user_tracking_concern.rb
Normal file
@@ -0,0 +1,22 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module UserTrackingConcern
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
UPDATE_SIGN_IN_HOURS = 24
|
||||
|
||||
included do
|
||||
before_action :set_user_activity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_user_activity
|
||||
return unless user_needs_sign_in_update?
|
||||
current_user.update_tracked_fields!(request)
|
||||
end
|
||||
|
||||
def user_needs_sign_in_update?
|
||||
user_signed_in? && (current_user.current_sign_in_at.nil? || current_user.current_sign_in_at < UPDATE_SIGN_IN_HOURS.hours.ago)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user