mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-24 15:16:22 +00:00
140 lines
4.6 KiB
Ruby
140 lines
4.6 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Api
|
|
module V1
|
|
# API para recibir información sin autenticación
|
|
class ProtectedController < BaseController
|
|
# Permitir conexiones desde varios sitios, estamos chequeando más
|
|
# adelante.
|
|
skip_forgery_protection
|
|
|
|
# Aplicar algunos chequeos básicos. Deberíamos registrar de
|
|
# alguna forma los errores pero tampoco queremos que nos usen
|
|
# recursos.
|
|
#
|
|
# Devolvemos un error 428: Precondition Required dando la
|
|
# oportunidad a les visitantes de reintentar en el caso de falsos
|
|
# positivos. No devolvemos contenido para que el servidor web
|
|
# capture y muestra la página de error específica de cada sitio o
|
|
# la genérica de Sutty.
|
|
#
|
|
# XXX: Ordenar en orden ascendiente según uso de recursos.
|
|
before_action :cookie_is_valid?, unless: :performed?
|
|
before_action :valid_authenticity_token_in_cookie?, unless: :performed?
|
|
before_action :site_exists?, unless: :performed?
|
|
before_action :site_is_origin?, unless: :performed?
|
|
before_action :destination_exists?, unless: :performed?
|
|
before_action :from_is_address?, unless: :performed?
|
|
before_action :gave_consent?, unless: :performed?
|
|
|
|
# Reescribir performed? para registrar lo que pasó en el camino a
|
|
# menos que esté todo bien.
|
|
def performed?
|
|
if (performed = super) && response.status >= 400
|
|
log_entry if ENV['DEBUG_FORMS'].present?
|
|
end
|
|
|
|
performed
|
|
end
|
|
|
|
private
|
|
|
|
def site_cookie
|
|
@site_cookie ||= cookies.encrypted[site_id]
|
|
end
|
|
|
|
# Comprueba que no se haya reutilizado una cookie vencida
|
|
#
|
|
# XXX: Si el navegador envió una cookie vencida es porque la está
|
|
# reutilizando, probablemente de forma maliciosa? Pero también
|
|
# puede ser que haya tardado más de media hora en enviar el
|
|
# formulario.
|
|
def cookie_is_valid?
|
|
return if (site_cookie&.dig('expires') || 0) > Time.now.to_i
|
|
|
|
@reason = 'expired_or_invalid_cookie'
|
|
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
|
end
|
|
|
|
# Queremos comprobar que la cookie corresponda con la sesión. La
|
|
# cookie puede haber vencido, así que es uno de los chequeos más
|
|
# simples que hacemos.
|
|
#
|
|
# TODO: Pensar una forma de redirigir al origen sin vaciar el
|
|
# formulario para que le usuarie recargue la cookie.
|
|
def valid_authenticity_token_in_cookie?
|
|
return if valid_authenticity_token? session, site_cookie['csrf']
|
|
|
|
@reason = 'invalid_auth_token'
|
|
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
|
end
|
|
|
|
# Comprueba que el sitio existe
|
|
#
|
|
# TODO: Responder con una zip bomb!
|
|
def site_exists?
|
|
return unless site.nil?
|
|
|
|
@reason = 'site_does_not_exist'
|
|
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
|
end
|
|
|
|
# Comprueba que el mensaje fue enviado desde el sitio o uno
|
|
# de los sitios permitidos.
|
|
#
|
|
# XXX: Este header se puede falsificar de todas formas pero al
|
|
# menos es una trampa.
|
|
def site_is_origin?
|
|
return if origin? && site.urls(slash: false).any? { |u| origin.to_s.start_with? u }
|
|
|
|
@reason = 'site_is_not_origin'
|
|
render plain: Rails.env.production? ? nil : @reason, status: :precondition_required
|
|
end
|
|
|
|
# Detecta si la dirección de contacto es válida. Además es
|
|
# opcional.
|
|
def from_is_address?
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# No aceptar nada si no dió su consentimiento
|
|
def gave_consent?
|
|
raise NotImplementedError
|
|
end
|
|
|
|
def post?
|
|
params[:layout].present?
|
|
end
|
|
|
|
def form?
|
|
params[:form].present?
|
|
end
|
|
|
|
# Los campos que se envían tienen que corresponder con un
|
|
# formulario de contacto o un layout
|
|
def destination_exists?
|
|
raise NotImplementedError
|
|
end
|
|
|
|
# Encuentra el sitio o devuelve nulo
|
|
def site
|
|
@site ||= Site.find_by(name: site_id)
|
|
end
|
|
|
|
# Genera un registro con información básica para debug, quizás no
|
|
# quede asociado a ningún sitio.
|
|
#
|
|
# TODO: Chequear qué pasa con los archivos adjuntos.
|
|
#
|
|
# @return [TrueClass]
|
|
def log_entry
|
|
LogEntry.create site: site || Site.default, text: {
|
|
reason: @reason,
|
|
status: response.status,
|
|
headers: request.headers.to_h.select { |k, _| /\A[A-Z]/ =~ k },
|
|
params: params
|
|
}
|
|
end
|
|
end
|
|
end
|
|
end
|