From 6b67f13aaf84560f0311109a49432b4a92898dd7 Mon Sep 17 00:00:00 2001 From: f Date: Thu, 29 Jul 2021 10:53:54 -0300 Subject: [PATCH 01/39] Origin y Referer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Algunos navegadores no envían Origin, la forma de obtenerlo es mirar en el Referer. --- app/controllers/api/v1/base_controller.rb | 24 ++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 2d58187c..649c6078 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -23,8 +23,30 @@ module Api end end + # Referer + # + # @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer} + # @return [String,Nil] + def referer + request.referer + end + alias referrer referer + + # Origin + # + # @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin} + # @return [String,Nil] def origin - request.headers['Origin'] + request.origin + end + + # Genera un header Origin a partir del Referer si existe. + # + # @return [String,Nil] + def origin_from_referer + return if referer.blank? + + referer.split('/', 4).tap { |u| u.pop if u.size > 3 }.join('/') end # Los navegadores antiguos no envían Origin From 5d9c379e0ed914f5f5601b017cc017a099d4bdfb Mon Sep 17 00:00:00 2001 From: f Date: Thu, 29 Jul 2021 10:55:21 -0300 Subject: [PATCH 02/39] =?UTF-8?q?Volver=20al=20referer=20o=20al=20sitio=20?= =?UTF-8?q?despu=C3=A9s=20de=20enviar=20formularios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/controllers/api/v1/contact_controller.rb | 2 +- app/controllers/api/v1/posts_controller.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/contact_controller.rb b/app/controllers/api/v1/contact_controller.rb index deacf4a7..1132ac08 100644 --- a/app/controllers/api/v1/contact_controller.rb +++ b/app/controllers/api/v1/contact_controller.rb @@ -23,7 +23,7 @@ module Api contact_params.to_h.symbolize_keys, params[:redirect] - redirect_to params[:redirect] || origin.to_s + redirect_to params[:redirect] || referer || site.url end private diff --git a/app/controllers/api/v1/posts_controller.rb b/app/controllers/api/v1/posts_controller.rb index e2a839dc..42c82ea7 100644 --- a/app/controllers/api/v1/posts_controller.rb +++ b/app/controllers/api/v1/posts_controller.rb @@ -17,7 +17,7 @@ module Api site.touch if service.create_anonymous.persisted? # Redirigir a la URL de agradecimiento - redirect_to params[:redirect_to] || origin.to_s + redirect_to params[:redirect_to] || referer || site.url end private From 113498b4bf2d4f452fb367f7db2e0944d2020b5d Mon Sep 17 00:00:00 2001 From: f Date: Thu, 29 Jul 2021 10:55:53 -0300 Subject: [PATCH 03/39] Usar el referer como fallback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mucha gente no puede enviar el formulario de contacto porque su navegador no envía el Origin, con esto al menos podemos recuperarlo del Referer. --- app/controllers/api/v1/protected_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/api/v1/protected_controller.rb b/app/controllers/api/v1/protected_controller.rb index 4e49f3a8..5a14bb28 100644 --- a/app/controllers/api/v1/protected_controller.rb +++ b/app/controllers/api/v1/protected_controller.rb @@ -85,7 +85,9 @@ module Api # 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 } + return if site.urls(slash: false).any? do |u| + (origin || origin_from_referer).to_s.start_with? u + end @reason = 'site_is_not_origin' render plain: Rails.env.production? ? nil : @reason, status: :precondition_required From 3e0b94fbce25d184b96e7f6c8a402e2ad1104acf Mon Sep 17 00:00:00 2001 From: f Date: Thu, 29 Jul 2021 11:01:44 -0300 Subject: [PATCH 04/39] Una nota para el futuro --- app/controllers/api/v1/invitades_controller.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/controllers/api/v1/invitades_controller.rb b/app/controllers/api/v1/invitades_controller.rb index eb2a4f24..236778ca 100644 --- a/app/controllers/api/v1/invitades_controller.rb +++ b/app/controllers/api/v1/invitades_controller.rb @@ -46,6 +46,10 @@ module Api # # En desarrollo devuelve el Origin enviado. # + # XXX: Si el sitio tiene varias URLs, hay que devolver la más + # similar al Origin o vamos a estar generando errores de CORS. + # + # @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin} # @return [String] def return_origin Rails.env.production? ? Site.find_by(name: site_id).url : origin From 7cfd55e12669938efd67c636137e07e34b247031 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:48:49 -0300 Subject: [PATCH 05/39] =?UTF-8?q?Cada=20Deploy=20tiene=20un=20hostname=20?= =?UTF-8?q?=C3=BAnico=20guardado=20en=20la=20base=20de=20datos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Con esto simplificamos la gestión de nombres de dominio, porque podemos validar en la base de datos que solo hay uno por Deploy. La migración recupera la información que antes guardábamos serializada como JSON en la base de datos. --- .../20210801060844_add_hostname_to_deploys.rb | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 db/migrate/20210801060844_add_hostname_to_deploys.rb diff --git a/db/migrate/20210801060844_add_hostname_to_deploys.rb b/db/migrate/20210801060844_add_hostname_to_deploys.rb new file mode 100644 index 00000000..8183564b --- /dev/null +++ b/db/migrate/20210801060844_add_hostname_to_deploys.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Recupera la funcionalidad que estamos deprecando. +module AddValuesToDeploy + extend ActiveSupport::Concern + + included do + store :values, accessors: %i[hostname onion], coder: JSON + end +end + +# Convertir todos los valores serializados de Deploy en una columna, +# porque al final el único uso que tuvo fue para guardar los hostnames +# alternativos. +# +# ¡El hostname es único para poder evitar que haya duplicados! +class AddHostnameToDeploys < ActiveRecord::Migration[6.1] + # Crea una columna temporal y guarda todos los valores. Los traspasa + # y luego elimina la columna. + def up + Deploy.include AddValuesToDeploy + # Ya que estamos hacer limpieza. + Deploy.where(site_id: nil).destroy_all + + add_column :deploys, :hostname_tmp, :string + + Site.find_each do |site| + site.deploys.find_each do |deploy| + deploy.hostname_tmp = deploy.values[:hostname] || deploy.values[:onion] || deploy.hostname + end + end + + rename_column :deploys, :hostname_tmp, :hostname + remove_column :deploys, :values + + add_index :deploys, :hostname, unique: true + # A esta altura todos los dominios deberían estar migrados. + change_column :deploys, :hostname, :string, null: false + end + + # Recupera los valores desde la columna creada. + def down + Deploy.include AddValuesToDeploy + + rename_column :deploys, :hostname, :hostname_tmp + add_column :deploys, :values, :text + + Site.find_each do |site| + site.deploys.find_each do |deploy| + deploy.values[(deploy.is_a? DeployHiddenService ? :onion : :hostname)] = deploy.hostname_tmp + end + end + + remove_column :deploys, :hostname_tmp + end +end From e1664d0c1299e242bcfb87aa377e6b95db820dea Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:50:44 -0300 Subject: [PATCH 06/39] Refactorizar Deploy * Validar los hostnames * Generar los hostnames por defecto * Cada Deploy sabe su propia URL --- app/models/deploy.rb | 87 ++++++++++++++++++++++++++++++++++++++------ app/models/site.rb | 71 +++++++++++++++++------------------- 2 files changed, 110 insertions(+), 48 deletions(-) diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 3f034ad5..cd709072 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -1,44 +1,85 @@ # frozen_string_literal: true require 'open3' + # Este modelo implementa los distintos tipos de alojamiento que provee # Sutty. # -# Los datos se guardan en la tabla `deploys`. Para guardar los -# atributos, cada modelo tiene que definir su propio `store -# :attributes`. +# TODO: Cambiar el nombre a algo que no sea industrial/militar. class Deploy < ApplicationRecord + # Un sitio puede tener muchas formas de publicarse. belongs_to :site + + # Siempre generar el hostname + after_initialize :default_hostname! + # Eliminar los archivos generados por el deploy. + before_destroy :remove_destination! + + # Registro de las tareas ejecutadas has_many :build_stats, dependent: :destroy + # Siempre tienen que pertenecer a un sitio + validates :site, presence: true + # El hostname tiene que ser único en toda la plataforma + validates :hostname, uniqueness: true + # Cada deploy puede implementar su propia validación + validates :hostname, hostname: true, unless: :implements_hostname_validation? + + # Genera el hostname + # + # @return [String] + def default_hostname + raise NotImplementedError + end + + # Devolver la URL + # + # @return [String] + def url + "https://#{hostname}" + end + + # Ejecutar la tarea + # + # @return [Boolean] def deploy raise NotImplementedError end - def limit - raise NotImplementedError - end - + # El espacio ocupado por este deploy. + # + # @return [Integer] def size raise NotImplementedError end + # Empezar a contar el tiempo + # + # @return [Time] def time_start @start = Time.now end + # Detener el contador + # + # @return [Time] def time_stop @stop = Time.now end + # Obtener la demora de la tarea + # + # @return [Float] def time_spent_in_seconds (@stop - @start).round(3) end - def home_dir - site.path - end - + # El directorio donde se almacenan las gemas. + # + # TODO: En un momento podíamos tenerlas todas compartidas y ahorrar + # espacio, pero bundler empezó a mezclar cosas. + # + # @return [String] def gems_dir @gems_dir ||= Rails.root.join('_storage', 'gems', site.name) end @@ -77,9 +118,33 @@ class Deploy < ApplicationRecord private + # Genera el hostname + # + # @return [Boolean] + def default_hostname! + self.hostname = default_hostname + true + end + + # Elimina los archivos generados por el deploy + # + # @return [Boolean] + def remove_destination! + raise NotImplementedError + end + + # Convierte el comando en una versión resumida. + # # @param [String] # @return [String] def readable_cmd(cmd) cmd.split(' -', 2).first.tr(' ', '_') end + + # Cada deploy puede decidir su propia validación + # + # @return [Boolean] + def implements_hostname_validation? + false + end end diff --git a/app/models/site.rb b/app/models/site.rb index 58f20745..2fc13448 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -85,47 +85,44 @@ class Site < ApplicationRecord @repository ||= Site::Repository.new path end - def hostname - sub = name || I18n.t('deploys.deploy_local.ejemplo') - - if sub.ends_with? '.' - sub.gsub(/\.\Z/, '') - else - "#{sub}.#{Site.domain}" - end + # El primer deploy del sitio + # + # @return [DeployLocal] + def deploy_local + @deploy_local ||= deploys.order(created_at: :asc).find_by(type: 'DeployLocal') end - # Devuelve la URL siempre actualizada a través del hostname + # Todas las URLs posibles para este sitio, ordenados según fecha de + # creación. # - # @param slash Boolean Agregar / al final o no - # @return String La URL con o sin / al final - def url(slash: true) - "https://#{hostname}#{slash ? '/' : ''}" - end - - # Obtiene los dominios alternativos - # - # @return Array - def alternative_hostnames - deploys.where(type: 'DeployAlternativeDomain').map(&:hostname).map do |h| - h.end_with?('.') ? h[0..-2] : "#{h}.#{Site.domain}" - end - end - - # Obtiene todas las URLs alternativas para este sitio - # - # @return Array - def alternative_urls(slash: true) - alternative_hostnames.map do |h| - "https://#{h}#{slash ? '/' : ''}" - end - end - - # Todas las URLs posibles para este sitio - # - # @return Array + # @param :slash [Boolean] Con o sin / al final, por defecto con + # @return [Array] def urls(slash: true) - alternative_urls(slash: slash) << url(slash: slash) + deploys.order(created_at: :asc).map(&:url).map do |url| + slash ? "#{url}/" : url + end + end + + # Todos los hostnames, ordenados según fecha de creación. + # + # @return [Array] + def hostnames + deploys.order(created_at: :asc).pluck(:hostname) + end + + # Obtiene la URL principal + # + # @param :slash [Boolean] + # @return [String] + def url(slash: true) + deploy_local.url.tap do |url| + "#{url}/" if slash + end + end + + # TODO: Usar DeployCanonical + def hostname + deploy_local.hostname end def invitade?(usuarie) From e1749d6c7010ac2d203d84b05fe15b7e9d42eadc Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:51:44 -0300 Subject: [PATCH 07/39] Refactorizar DeployLocal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit En realidad no hay mucho cambio interno, pero se deprecaron algunos métodos de Deploy y se ajustaron otros. --- app/models/deploy_local.rb | 55 ++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index 02b837f0..d2a73cbe 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -1,11 +1,9 @@ # frozen_string_literal: true -# Alojamiento local, solo genera el sitio, con lo que no necesita hacer -# nada más +# Alojamiento local, genera el sitio como si corriéramos `jekyll build`. class DeployLocal < Deploy - store :values, accessors: %i[], coder: JSON - - before_destroy :remove_destination! + # Asegurarse que el hostname es el permitido. + before_save :default_hostname! # Realizamos la construcción del sitio usando Jekyll y un entorno # limpio para no pasarle secretos @@ -20,42 +18,48 @@ class DeployLocal < Deploy jekyll_build end - # Sólo permitimos un deploy local - def limit - 1 - end - # Obtener el tamaño de todos los archivos y directorios (los # directorios son archivos :) + # + # @return [Integer] def size paths = [destination, File.join(destination, '**', '**')] Dir.glob(paths).map do |file| - if File.symlink? file - 0 - else - File.size(file) - end + File.symlink?(file) ? 0 : File.size(file) end.inject(:+) end + # La ubicación del sitio luego de generarlo. + # + # @return [String] def destination - File.join(Rails.root, '_deploy', site.hostname) + File.join(Rails.root, '_deploy', hostname) + end + + # El hostname es el nombre del sitio más el dominio principal. + # + # @return [String] + def default_hostname + "#{site.name}.#{Site.domain}" end private + # Crea el directorio destino si no existe. def mkdir FileUtils.mkdir_p destination end # Un entorno que solo tiene lo que necesitamos + # + # @return [Hash] def env # XXX: This doesn't support Windows paths :B paths = [File.dirname(`which bundle`), '/usr/bin', '/bin'] { - 'HOME' => home_dir, + 'HOME' => site.path, 'PATH' => paths.join(':'), 'SPREE_API_KEY' => site.tienda_api_key, 'SPREE_URL' => site.tienda_url, @@ -66,10 +70,15 @@ class DeployLocal < Deploy } end + # @return [String] def yarn_lock File.join(site.path, 'yarn.lock') end + # Determina si este proyecto se gestiona con Yarn, buscando si el + # archivo yarn.lock existe. + # + # @return [Boolean] def yarn_lock? File.exist? yarn_lock end @@ -79,12 +88,15 @@ class DeployLocal < Deploy end # Corre yarn dentro del repositorio + # + # @return [Boolean,Nil] def yarn - return unless yarn_lock? - - run 'yarn' + run 'yarn' unless yarn_lock? end + # Instala las dependencias. + # + # @return [Boolean] def bundle if Rails.env.production? run %(bundle install --no-cache --path="#{gems_dir}") @@ -93,6 +105,9 @@ class DeployLocal < Deploy end end + # Genera el sitio. + # + # @return [Boolean] def jekyll_build run %(bundle exec jekyll build --trace --profile --destination "#{escaped_destination}") end From fa9884afddb5075bf4542b9c35a462d95beba246 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:53:00 -0300 Subject: [PATCH 08/39] Refactorizar DeployWww --- app/models/deploy_www.rb | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/app/models/deploy_www.rb b/app/models/deploy_www.rb index 5602b0fc..9ef94df7 100644 --- a/app/models/deploy_www.rb +++ b/app/models/deploy_www.rb @@ -2,33 +2,39 @@ # Vincula la versión del sitio con www a la versión sin class DeployWww < Deploy - store :values, accessors: %i[], coder: JSON - - before_destroy :remove_destination! - + # La forma de hacer este deploy es generar un link simbólico entre el + # directorio canónico y el actual. + # + # XXX: Asume que el origen y el destino se encuentran en el mismo + # directorio (¿por qué no estarían?) + # + # @return [Boolean] def deploy - File.symlink?(destination) || - File.symlink(site.hostname, destination).zero? - end - - def limit - 1 + return true if File.symlink? destination + + File.symlink(site.deploy_local.hostname, destination).zero? end + # @return [Integer] def size File.size destination end + # @return [String] def destination - File.join(Rails.root, '_deploy', fqdn) + File.join(Rails.root, '_deploy', hostname) end - def fqdn - "www.#{site.hostname}" + # El hostname por defecto incluye WWW + # + # @return [String] + def default_hostname + "www.#{site.deploy_local.hostname}" end private + # Elimina el link simbólico si se elimina este deploy. def remove_destination! FileUtils.rm_f destination end From 27b449433343d1669afd380f5abf078c5b8c4e84 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:53:37 -0300 Subject: [PATCH 09/39] Refactorizar DeployZip --- app/models/deploy_zip.rb | 48 ++++++++++++++++++++++++++++------------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/app/models/deploy_zip.rb b/app/models/deploy_zip.rb index ec8973d1..c25352b9 100644 --- a/app/models/deploy_zip.rb +++ b/app/models/deploy_zip.rb @@ -2,22 +2,24 @@ require 'zip' -# Genera un ZIP a partir del sitio ya construido +# Genera un ZIP a partir del sitio ya generado y lo coloca para descarga +# dentro del sitio público. # # TODO: Firmar con minisign class DeployZip < Deploy - store :values, accessors: %i[], coder: JSON + # El hostname es el nombre del archivo. + validates :hostname, format: { with: /\.zip\z/ } # Una vez que el sitio está generado, tomar todos los archivos y - # y generar un zip accesible públicamente. + # y generar un ZIP accesible públicamente. # - # rubocop:disable Metrics/MethodLength + # @return [Boolean] def deploy - FileUtils.rm_f path + remove_destination! time_start Dir.chdir(destination) do - Zip::File.open(path, Zip::File::CREATE) do |z| + Zip::File.open(hostname, Zip::File::CREATE) do |z| Dir.glob('./**/**').each do |f| File.directory?(f) ? z.mkdir(f) : z.add(f, f) end @@ -31,25 +33,43 @@ class DeployZip < Deploy File.exist? path end - # rubocop:enable Metrics/MethodLength - def limit - 1 + # La URL de descarga del archivo. + # + # @return [String] + def url + "#{site.deploy_local.url}/#{hostname}" end + # Devuelve el tamaño del ZIP en bytes + # + # @return [Integer] def size File.size path end + # El archivo ZIP se guarda dentro del sitio local para poder + # descargarlo luego. + # + # @return [String] def destination - File.join(Rails.root, '_deploy', site.hostname) + site.deploy_local.destination end - def file - "#{site.hostname}.zip" + # El "hostname" es la ubicación del archivo. + # + # @return [String] + def default_hostname + "#{site.deploy_local.hostname}.zip" end - def path - File.join(destination, file) + private + + def remove_destination! + FileUtils.rm_f path + end + + def implements_hostname_validation? + true end end From e49d8484a240ac09021d9d00c14a1b9df7b4baf2 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:53:52 -0300 Subject: [PATCH 10/39] Refactorizar DeployPrivate --- app/models/deploy_private.rb | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/models/deploy_private.rb b/app/models/deploy_private.rb index 3a6595f9..30aca8b8 100644 --- a/app/models/deploy_private.rb +++ b/app/models/deploy_private.rb @@ -11,12 +11,31 @@ class DeployPrivate < DeployLocal jekyll_build end - # Hacer el deploy a un directorio privado + # La URL del sitio dentro del panel. + # + # @return [String] + def url + Rails.application.routes.url_for(controller: :private, action: :show, site_id: site) + end + + # Hacer el deploy a un directorio privado. + # + # @return [String] def destination File.join(Rails.root, '_private', site.name) end + # El hostname no se usa para nada, porque el sitio es solo accesible a + # través del panel de Sutty. + # + # @return [String] + def default_hostname + "#{site.name}.private.#{Site.domain}" + end + # No usar recursos en compresión y habilitar los datos privados + # + # @return [Hash] def env @env ||= super.merge({ 'JEKYLL_ENV' => 'development', From 0d8f0ec5ee98a447a16e14746148283996dfe763 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:54:12 -0300 Subject: [PATCH 11/39] Refactorizar DeployHiddenService --- app/models/deploy_hidden_service.rb | 36 ++++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/app/models/deploy_hidden_service.rb b/app/models/deploy_hidden_service.rb index d4d2b822..642209af 100644 --- a/app/models/deploy_hidden_service.rb +++ b/app/models/deploy_hidden_service.rb @@ -1,18 +1,32 @@ # frozen_string_literal: true -# Genera una versión onion +# Alojar el sitio como un servicio oculto de Tor, que en realidad es un +# link simbólico al DeployLocal. class DeployHiddenService < DeployWww - def deploy - return true if fqdn.blank? - - super - end - - def fqdn - values[:onion] - end + validates :hostname, format: { with: /\A[a-f0-9]{56}.onion\z/ } + # Los servicios ocultos son su propio transporte cifrado y + # autenticado. + # + # @return [String] def url - 'http://' + fqdn + "http://#{hostname}" + end + + # Los onions no son creados por Sutty sino por Tor y enviados luego a + # través de la API. El hostname por defecto es un nombre temporal que + # se parece a una dirección OnionV3. + # + # TODO: En el panel se muestra esta dirección como si fuera la real. + # + # @return [String] + def default_hostname + "#{SecureRandom.hex(28)}.onion" + end + + private + + def implements_hostname_validation? + true end end From 2be57ad3af24f0ba0088e4726f3aa6308fb4b3ad Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 21:54:31 -0300 Subject: [PATCH 12/39] Refactorizar DeployAlternativeDomain Les usuaries pueden incorporar sus propios nombres de dominio y ya no dependemos de verificar si tienen punto al final para saber si son subdominios de Sutty o no. --- app/models/deploy_alternative_domain.rb | 32 ++++++++++++------------- 1 file changed, 15 insertions(+), 17 deletions(-) diff --git a/app/models/deploy_alternative_domain.rb b/app/models/deploy_alternative_domain.rb index e4960e65..7dbd35e6 100644 --- a/app/models/deploy_alternative_domain.rb +++ b/app/models/deploy_alternative_domain.rb @@ -1,23 +1,21 @@ # frozen_string_literal: true -# Soportar dominios alternativos -class DeployAlternativeDomain < Deploy - store :values, accessors: %i[hostname], coder: JSON +# Soportar dominios alternativos. +class DeployAlternativeDomain < DeployWww + validates :hostname, domainname: true - # Generar un link simbólico del sitio principal al alternativo - def deploy - File.symlink?(destination) || - File.symlink(site.hostname, destination).zero? + # No hay un hostname por defecto + # + # @return [Nil] + def default_hostname; end + + private + + def implements_hostname_validation? + true end - # No hay límite para los dominios alternativos - def limit; end - - def size - File.size destination - end - - def destination - File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, '')) - end + # No hay un hostname por defecto. Debe ser informado por les + # usuaries. + def default_hostname!; end end From bb2697f63ef467b7b8398f41b01388f61a83ed3b Mon Sep 17 00:00:00 2001 From: f Date: Sun, 1 Aug 2021 22:05:59 -0300 Subject: [PATCH 13/39] Arreglar vistas --- app/jobs/deploy_job.rb | 14 ++++---------- app/mailers/deploy_mailer.rb | 2 +- app/views/deploy_mailer/deployed.html.haml | 2 +- app/views/deploy_mailer/deployed.text.haml | 2 +- app/views/deploys/_deploy_hidden_service.haml | 7 +++---- app/views/deploys/_deploy_local.haml | 2 +- app/views/deploys/_deploy_www.haml | 2 +- app/views/deploys/_deploy_zip.haml | 7 +------ config/locales/en.yml | 8 ++++---- config/locales/es.yml | 8 ++++---- 10 files changed, 21 insertions(+), 33 deletions(-) diff --git a/app/jobs/deploy_job.rb b/app/jobs/deploy_job.rb index f1ceca9e..bb8c301b 100644 --- a/app/jobs/deploy_job.rb +++ b/app/jobs/deploy_job.rb @@ -4,6 +4,8 @@ class DeployJob < ApplicationJob class DeployException < StandardError; end + attr_reader :site + # rubocop:disable Metrics/MethodLength def perform(site, notify = true) ActiveRecord::Base.connection_pool.with_connection do @@ -17,9 +19,9 @@ class DeployJob < ApplicationJob @site.update status: 'building' # Asegurarse que DeployLocal sea el primero! - @deployed = { deploy_local: deploy_locally } + @deployed = { deploy_local: site.deploy_local.deploy } - # No es opcional + # TODO: No es opcional? unless @deployed[:deploy_local] @site.update status: 'waiting' notify_usuaries if notify @@ -37,14 +39,6 @@ class DeployJob < ApplicationJob private - def deploy_local - @deploy_local ||= @site.deploys.find_by(type: 'DeployLocal') - end - - def deploy_locally - deploy_local.deploy - end - def deploy_others @site.deploys.where.not(type: 'DeployLocal').find_each do |d| @deployed[d.type.underscore.to_sym] = d.deploy diff --git a/app/mailers/deploy_mailer.rb b/app/mailers/deploy_mailer.rb index 1d0c7308..03d72d4e 100644 --- a/app/mailers/deploy_mailer.rb +++ b/app/mailers/deploy_mailer.rb @@ -13,7 +13,7 @@ class DeployMailer < ApplicationMailer @usuarie = Usuarie.find(params[:usuarie]) @site = @usuarie.sites.find(params[:site]) @deploys = which_ones - @deploy_local = @site.deploys.find_by(type: 'DeployLocal') + @deploy_local = @site.deploy_local # Informamos a cada quien en su idioma y damos una dirección de # respuesta porque a veces les usuaries nos escriben diff --git a/app/views/deploy_mailer/deployed.html.haml b/app/views/deploy_mailer/deployed.html.haml index e8b2e7af..40c6a98f 100644 --- a/app/views/deploy_mailer/deployed.html.haml +++ b/app/views/deploy_mailer/deployed.html.haml @@ -1,6 +1,6 @@ %h1= t('.hi') -= sanitize_markdown t('.explanation', fqdn: @deploy_local.site.hostname), += sanitize_markdown t('.explanation', url: @site.deploy_local.url), tags: %w[p a strong em] %table diff --git a/app/views/deploy_mailer/deployed.text.haml b/app/views/deploy_mailer/deployed.text.haml index 53a9b008..83696b03 100644 --- a/app/views/deploy_mailer/deployed.text.haml +++ b/app/views/deploy_mailer/deployed.text.haml @@ -1,6 +1,6 @@ = '# ' + t('.hi') \ -= t('.explanation', fqdn: @deploy_local.site.hostname) += t('.explanation', url: @site.deploy_local.url) \ = Terminal::Table.new do |table| - table << [t('.th.type'), t('.th.status')] diff --git a/app/views/deploys/_deploy_hidden_service.haml b/app/views/deploys/_deploy_hidden_service.haml index d6388123..38b33b5d 100644 --- a/app/views/deploys/_deploy_hidden_service.haml +++ b/app/views/deploys/_deploy_hidden_service.haml @@ -14,10 +14,9 @@ '0', '1' = deploy.label :_destroy, class: 'custom-control-label' do %h3= t('.title') - = sanitize_markdown t('.help', public_url: deploy.object.site.url), + = sanitize_markdown t('.help', public_url: site.deploy_local.url), tags: %w[p strong em a] - - if deploy.object.fqdn - = sanitize_markdown t('.help_2', url: deploy.object.url), - tags: %w[p strong em a] + = sanitize_markdown t('.help_2', url: deploy.object.url), + tags: %w[p strong em a] %hr/ diff --git a/app/views/deploys/_deploy_local.haml b/app/views/deploys/_deploy_local.haml index 69a6a52e..c60165fa 100644 --- a/app/views/deploys/_deploy_local.haml +++ b/app/views/deploys/_deploy_local.haml @@ -6,7 +6,7 @@ .row .col %h3= t('.title') - = sanitize_markdown t('.help', fqdn: deploy.object.site.hostname), + = sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a] = deploy.hidden_field :type diff --git a/app/views/deploys/_deploy_www.haml b/app/views/deploys/_deploy_www.haml index 9cf186a6..5e4a08e5 100644 --- a/app/views/deploys/_deploy_www.haml +++ b/app/views/deploys/_deploy_www.haml @@ -15,6 +15,6 @@ '0', '1' = deploy.label :_destroy, class: 'custom-control-label' do %h3= t('.title') - = sanitize_markdown t('.help', fqdn: deploy.object.fqdn), + = sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a] %hr/ diff --git a/app/views/deploys/_deploy_zip.haml b/app/views/deploys/_deploy_zip.haml index c1588929..5071e3a6 100644 --- a/app/views/deploys/_deploy_zip.haml +++ b/app/views/deploys/_deploy_zip.haml @@ -15,10 +15,5 @@ '0', '1' = deploy.label :_destroy, class: 'custom-control-label' do %h3= t('.title') - -# TODO: secar la generación de URLs - - name = site.name || t('.ejemplo') - = sanitize_markdown t('.help', - fqdn: deploy.object.site.hostname, - file: deploy.object.file || "#{name}.zip"), - tags: %w[p strong em a] + = sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a] %hr/ diff --git a/config/locales/en.yml b/config/locales/en.yml index fc194eab..25b662bf 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -70,7 +70,7 @@ en: hi: "Hi!" explanation: | This e-mail is to notify you that Sutty has built your site, which is - available at . + available at <%{url}/>. You'll find details below. th: @@ -195,7 +195,7 @@ en: deploy_local: title: 'Host at Sutty' help: | - The site will be available at . + The site will be available at <%{url}/>. We're working out the details to allow you to use your own site domains, you can [help us](https://sutty.nl/en/index.html#contact)! @@ -211,7 +211,7 @@ en: title: 'Add www to the address' help: | When you enable this option, your site will also be available - under . + under <%{url}/>. The www prefix has been a way of referring to computers that are available on the World Wide Web. Since @@ -222,7 +222,7 @@ en: help: | ZIP files contain and compress all your site's files. With this option you can download and also share your entire site - through the address, keep it as backup + through the <%{url}> address, keep it as backup or have a strategy of solidary hosting, where many people share a copy of your site. diff --git a/config/locales/es.yml b/config/locales/es.yml index e8185391..eac8a49d 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -70,7 +70,7 @@ es: hi: "¡Hola!" explanation: | Este correo es para notificarte que Sutty ha generado tu sitio y - ya está disponible en la dirección . + ya está disponible en la dirección <%{url}>. A continuación encontrarás el detalle de lo que hicimos. th: @@ -197,7 +197,7 @@ es: deploy_local: title: 'Alojar en Sutty' help: | - El sitio estará disponible en . + El sitio estará disponible en <%{url}/>. Estamos desarrollando la posibilidad de agregar tus propios dominios, ¡ayudanos! @@ -213,7 +213,7 @@ es: title: 'Agregar www a la dirección' help: | Cuando habilitas esta opción, tu sitio también estará disponible - como . + como <%{url}/>. El prefijo www para las direcciones web ha sido una forma de referirse a las computadoras que están disponibles en la _World @@ -226,7 +226,7 @@ es: help: | Los archivos ZIP contienen y comprimen todos los archivos de tu sitio. Con esta opción podrás descargar y compartir tu sitio - entero a través de la dirección y + entero a través de la dirección <%{url}> y guardarla como copia de seguridad o una estrategia de alojamiento solidario, donde muchas personas comparten una copia de tu sitio. From f8f9e722be6302d43b0ca6f23124578c43112d0d Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 17:37:19 -0300 Subject: [PATCH 14/39] Permitir asignar un hostname al iniciar --- app/models/deploy.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/models/deploy.rb b/app/models/deploy.rb index cd709072..0da07be9 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -118,11 +118,12 @@ class Deploy < ApplicationRecord private - # Genera el hostname + # Genera el hostname pero permitir la inicialización del valor. Luego + # validamos que sea el formato correcto. # # @return [Boolean] def default_hostname! - self.hostname = default_hostname + self.hostname ||= default_hostname true end From f50e202d3aaa5e0b3eff42e83b3b20cbdf8d6b3d Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 17:38:01 -0300 Subject: [PATCH 15/39] Los servicios ocultos son direcciones en base32 --- app/models/deploy_hidden_service.rb | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/models/deploy_hidden_service.rb b/app/models/deploy_hidden_service.rb index 642209af..8ca4b082 100644 --- a/app/models/deploy_hidden_service.rb +++ b/app/models/deploy_hidden_service.rb @@ -3,7 +3,7 @@ # Alojar el sitio como un servicio oculto de Tor, que en realidad es un # link simbólico al DeployLocal. class DeployHiddenService < DeployWww - validates :hostname, format: { with: /\A[a-f0-9]{56}.onion\z/ } + validates :hostname, format: { with: /\A[a-z2-7]{56}.onion\z/ } # Los servicios ocultos son su propio transporte cifrado y # autenticado. @@ -17,11 +17,9 @@ class DeployHiddenService < DeployWww # través de la API. El hostname por defecto es un nombre temporal que # se parece a una dirección OnionV3. # - # TODO: En el panel se muestra esta dirección como si fuera la real. - # # @return [String] def default_hostname - "#{SecureRandom.hex(28)}.onion" + "temporal#{random_base32}.onion" end private @@ -29,4 +27,16 @@ class DeployHiddenService < DeployWww def implements_hostname_validation? true end + + # Adaptado de base32 + # + # @see {https://github.com/stesla/base32/blob/master/lib/base32.rb} + # @see {https://github.com/stesla/base32/blob/master/LICENSE} + def random_base32(length = 48) + table = 'abcdefghijklmnopqrstuvwxyz234567' + + OpenSSL::Random.random_bytes(length).each_byte.map do |b| + table[b % 32] + end.join + end end From 4ea2169c68c0734201872174f000bba599ac2394 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 17:40:13 -0300 Subject: [PATCH 16/39] Poder acceder directo al archivo zip --- app/models/deploy_zip.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/models/deploy_zip.rb b/app/models/deploy_zip.rb index c25352b9..d09c7552 100644 --- a/app/models/deploy_zip.rb +++ b/app/models/deploy_zip.rb @@ -63,6 +63,10 @@ class DeployZip < Deploy "#{site.deploy_local.hostname}.zip" end + def path + File.join(destination, hostname) + end + private def remove_destination! From b9d7a27105b794dc452117fe1fc032d2dde49f57 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 18:25:54 -0300 Subject: [PATCH 17/39] Arreglar y agregar tests --- test/factories/site.rb | 10 ++-- test/jobs/deploy_job_test.rb | 6 +-- test/models/deploy_alternative_domain_test.rb | 44 ++++++++++++++++ test/models/deploy_hidden_service_test.rb | 36 +++++++++++++ test/models/deploy_local_test.rb | 50 +++++++++++++------ test/models/deploy_www_test.rb | 26 ++++++---- test/models/deploy_zip_test.rb | 33 ++++++++---- 7 files changed, 159 insertions(+), 46 deletions(-) create mode 100644 test/models/deploy_alternative_domain_test.rb create mode 100644 test/models/deploy_hidden_service_test.rb diff --git a/test/factories/site.rb b/test/factories/site.rb index d9d8720f..7d91f841 100644 --- a/test/factories/site.rb +++ b/test/factories/site.rb @@ -9,11 +9,11 @@ FactoryBot.define do licencia after :build do |site| - site.deploys << build(:deploy_local, site: site) - end - - after :create do |site| - site.deploys << create(:deploy_local, site: site) + # XXX: Generamos un DeployLocal normalmente y no a través de una + # Factory porque necesitamos que el sitio se genere solo. + # + # @see {https://github.com/thoughtbot/factory_bot/wiki/How-factory_bot-interacts-with-ActiveRecord} + site.deploys.build(type: 'DeployLocal') end end end diff --git a/test/jobs/deploy_job_test.rb b/test/jobs/deploy_job_test.rb index a0fabfc4..d5ef694e 100644 --- a/test/jobs/deploy_job_test.rb +++ b/test/jobs/deploy_job_test.rb @@ -2,11 +2,7 @@ class DeployJobTest < ActiveSupport::TestCase test 'se puede compilar' do - rol = create :rol - site = rol.site - site.deploys << create(:deploy_zip, site: site) - - site.save + site = create :site DeployJob.perform_async(site.id) diff --git a/test/models/deploy_alternative_domain_test.rb b/test/models/deploy_alternative_domain_test.rb new file mode 100644 index 00000000..b7bd6d07 --- /dev/null +++ b/test/models/deploy_alternative_domain_test.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DeployAlternativeDomainTest < ActiveSupport::TestCase + setup do + @site = create :site + @deploy_alt = @site.deploys.build type: 'DeployAlternativeDomain' + end + + teardown do + @site&.destroy + end + + def random_tld + PAK::ValidatesHostname::ALLOWED_TLDS.sample + end + + test 'el hostname se ingresa manualmente' do + assert_nil @deploy_alt.hostname + end + + test 'el hostname es obligatorio' do + assert_not @deploy_alt.valid? + end + + test 'el hostname es válido' do + assert_not @deploy_alt.update(hostname: ' ') + assert_not @deploy_alt.update(hostname: 'custom.domain.root.') + assert_not @deploy_alt.update(hostname: 'custom.domain') + assert @deploy_alt.update(hostname: "custom.domain.#{random_tld}") + end + + test 'el hostname tiene que ser único' do + assert_not @deploy_alt.update(hostname: @site.hostname) + end + + test 'se puede deployear' do + assert @site.deploy_local.deploy + assert @deploy_alt.update(hostname: "#{SecureRandom.hex}.sutty.#{random_tld}") + assert @deploy_alt.deploy + assert File.symlink?(@deploy_alt.destination) + end +end diff --git a/test/models/deploy_hidden_service_test.rb b/test/models/deploy_hidden_service_test.rb new file mode 100644 index 00000000..799f3df3 --- /dev/null +++ b/test/models/deploy_hidden_service_test.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'test_helper' + +class DeployHiddenServiceTest < ActiveSupport::TestCase + setup do + @site = create :site + @deploy_hidden = @site.deploys.build type: 'DeployHiddenService' + end + + teardown do + @site&.destroy + end + + test 'el hostname es válido' do + assert_not @deploy_hidden.update(hostname: ' ') + assert_not @deploy_hidden.update(hostname: 'custom.domain.root.') + assert_not @deploy_hidden.update(hostname: 'custom.domain') + assert @deploy_hidden.update(hostname: "#{@deploy_hidden.send(:random_base32, 56)}.onion") + end + + test 'los hostnames pueden ser temporales' do + assert @deploy_hidden.hostname.start_with? 'temporal' + end + + test 'el hostname tiene que ser único' do + assert @deploy_hidden.save + assert_not @site.deploys.create(type: 'DeployHiddenService', hostname: @deploy_hidden.hostname).valid? + end + + test 'se puede deployear' do + assert @site.deploy_local.deploy + assert @deploy_hidden.deploy + assert File.symlink?(@deploy_hidden.destination) + end +end diff --git a/test/models/deploy_local_test.rb b/test/models/deploy_local_test.rb index 7e8712d7..25781bac 100644 --- a/test/models/deploy_local_test.rb +++ b/test/models/deploy_local_test.rb @@ -1,24 +1,44 @@ # frozen_string_literal: true +require 'test_helper' + class DeployLocalTest < ActiveSupport::TestCase + setup do + @site = create :site + end + + teardown do + @site&.destroy + end + + test 'se pueden crear' do + assert @site.deploy_local.valid? + assert_equal @site.hostname, @site.deploy_local.hostname + end + + test 'no se puede cambiar el hostname' do + hostname = @site.deploy_local.hostname + @site.deploy_local.hostname = SecureRandom.hex + + assert @site.deploy_local.save + assert_equal hostname, @site.deploy_local.hostname + end + test 'se puede deployear' do - site = create :site - local = create :deploy_local, site: site - deploy = create :deploy_zip, site: site + deploy_local = @site.deploy_local - # Primero tenemos que generar el sitio - local.deploy + assert deploy_local.deploy + assert File.directory?(deploy_local.destination) + assert File.exist?(File.join(deploy_local.destination, 'index.html')) + assert_equal 3, deploy_local.build_stats.count - escaped_path = Shellwords.escape(deploy.path) + assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive? + assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive? + end - assert deploy.deploy - assert File.file?(deploy.path) - assert_equal 'application/zip', - `file --mime-type "#{escaped_path}"`.split(' ').last - assert_equal 1, deploy.build_stats.count - assert deploy.build_stats.map(&:bytes).inject(:+).positive? - assert deploy.build_stats.map(&:seconds).inject(:+).positive? - - local.destroy + test 'al eliminarlos se elimina el directorio' do + deploy_local = @site.deploy_local + assert deploy_local.destroy + assert_not File.directory?(deploy_local.destination) end end diff --git a/test/models/deploy_www_test.rb b/test/models/deploy_www_test.rb index 8ae00ebe..08ff8c38 100644 --- a/test/models/deploy_www_test.rb +++ b/test/models/deploy_www_test.rb @@ -3,17 +3,23 @@ require 'test_helper' class DeployWwwTest < ActiveSupport::TestCase + setup do + @site = create :site + @deploy_www = @site.deploys.create type: 'DeployWww' + end + + teardown do + @site&.destroy + end + + test 'el hostname empieza con www' do + assert @deploy_www.hostname.start_with?('www.') + end + test 'se puede deployear' do - site = create :site - local = create :deploy_local, site: site - deploy = create :deploy_www, site: site + assert @site.deploy_local.deploy - # Primero tenemos que generar el sitio - local.deploy - - assert deploy.deploy - assert File.symlink?(deploy.destination) - - local.destroy + assert @deploy_www.deploy + assert File.symlink?(@deploy_www.destination) end end diff --git a/test/models/deploy_zip_test.rb b/test/models/deploy_zip_test.rb index d5ffe51f..2e4e55f6 100644 --- a/test/models/deploy_zip_test.rb +++ b/test/models/deploy_zip_test.rb @@ -3,18 +3,29 @@ require 'test_helper' class DeployZipTest < ActiveSupport::TestCase + setup do + @site = create :site + @deploy_zip = @site.deploys.create(type: 'DeployZip') + end + + teardown do + @site&.destroy + end + + test 'el nombre es el hostname.zip' do + assert_equal "#{@site.hostname}.zip", @deploy_zip.hostname + end + test 'se puede deployear' do - deploy_local = create :deploy_local + # Primero tenemos que generar el sitio + assert @site.deploy_local.deploy - assert deploy_local.deploy - assert File.directory?(deploy_local.destination) - assert File.exist?(File.join(deploy_local.destination, 'index.html')) - assert_equal 3, deploy_local.build_stats.count - - assert deploy_local.build_stats.map(&:bytes).compact.inject(:+).positive? - assert deploy_local.build_stats.map(&:seconds).compact.inject(:+).positive? - - assert deploy_local.destroy - assert_not File.directory?(deploy_local.destination) + assert @deploy_zip.deploy + assert File.file?(@deploy_zip.path) + assert_equal 'application/zip', + `file --mime-type "#{@deploy_zip.path}"`.split.last + assert_equal 1, @deploy_zip.build_stats.count + assert @deploy_zip.build_stats.map(&:bytes).inject(:+).positive? + assert @deploy_zip.build_stats.map(&:seconds).inject(:+).positive? end end From c08cfb16376ba50acb8769d9b9b88cb199b03045 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 18:26:52 -0300 Subject: [PATCH 18/39] Actualizar el hostname si cambia el name --- app/models/deploy_local.rb | 6 +++++- app/models/site.rb | 9 ++++++--- test/models/site_test.rb | 4 ++-- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index 3bfc7d97..be8de366 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -3,7 +3,7 @@ # Alojamiento local, genera el sitio como si corriéramos `jekyll build`. class DeployLocal < Deploy # Asegurarse que el hostname es el permitido. - before_save :default_hostname! + before_save :reset_hostname!, :default_hostname! # Realizamos la construcción del sitio usando Jekyll y un entorno # limpio para no pasarle secretos @@ -46,6 +46,10 @@ class DeployLocal < Deploy private + def reset_hostname! + self.hostname = nil + end + # Crea el directorio destino si no existe. def mkdir FileUtils.mkdir_p destination diff --git a/app/models/site.rb b/app/models/site.rb index 2fc13448..c9357657 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -58,7 +58,7 @@ class Site < ApplicationRecord after_initialize :load_jekyll after_create :load_jekyll, :static_file_migration! # Cambiar el nombre del directorio - before_update :update_name! + before_update :update_name!, :update_deploy_local_hostname!, if: :name_changed? before_save :add_private_key_if_missing! # Guardar la configuración si hubo cambios after_save :sync_attributes_with_config! @@ -450,8 +450,6 @@ class Site < ApplicationRecord end def update_name! - return unless name_changed? - FileUtils.mv path_was, path reload_jekyll! end @@ -469,6 +467,11 @@ class Site < ApplicationRecord config.hostname = hostname end + # Si cambia el nombre queremos actualizarlo en el DeployLocal + def update_deploy_local_hostname! + deploy_local&.update hostname: name + end + # Migra los archivos a Sutty def static_file_migration! Site::StaticFileMigration.new(site: self).migrate! diff --git a/test/models/site_test.rb b/test/models/site_test.rb index e53fff06..481c9bbc 100644 --- a/test/models/site_test.rb +++ b/test/models/site_test.rb @@ -93,9 +93,9 @@ class SiteTest < ActiveSupport::TestCase test 'tienen un hostname que puede cambiar' do assert_equal "#{site.name}.#{Site.domain}", site.hostname - site.name = name = SecureRandom.hex + site.update(name: (new_name = SecureRandom.hex)) - assert_equal "#{name}.#{Site.domain}", site.hostname + assert_equal "#{new_name}.#{Site.domain}", site.hostname end test 'se pueden traer los datos de una plantilla' do From 45559ad58f30dd3034a3f01e44cfe7dbcaf55506 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 18:30:07 -0300 Subject: [PATCH 19/39] Refactorizar DeployJob --- app/jobs/deploy_job.rb | 49 +++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/app/jobs/deploy_job.rb b/app/jobs/deploy_job.rb index a5e05e45..31803ae4 100644 --- a/app/jobs/deploy_job.rb +++ b/app/jobs/deploy_job.rb @@ -4,64 +4,63 @@ class DeployJob < ApplicationJob class DeployException < StandardError; end - attr_reader :site + attr_reader :site, :deployed # rubocop:disable Metrics/MethodLength - def perform(site, notify = true, time = Time.now) + def perform(site_id, notify = true, time = Time.now) ActiveRecord::Base.connection_pool.with_connection do - @site = Site.find(site) + @site = Site.find(site_id) + @deployed = {} # Si ya hay una tarea corriendo, aplazar esta. Si estuvo # esperando más de 10 minutos, recuperar el estado anterior. # # Como el trabajo actual se aplaza al siguiente, arrastrar la # hora original para poder ir haciendo timeouts. - if @site.building? + if site.building? if 10.minutes.ago >= time - @site.update status: 'waiting' + site.update status: 'waiting' raise DeployException, - "#{@site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original" + "#{site.name} la tarea estuvo más de 10 minutos esperando, volviendo al estado original" end - DeployJob.perform_in(60, site, notify, time) + DeployJob.perform_in(60, site_id, notify, time) return end - @site.update status: 'building' + site.update status: 'building' # Asegurarse que DeployLocal sea el primero! - @deployed = { deploy_local: site.deploy_local.deploy } + deployed[:deploy_local] = site.deploy_local.deploy - # TODO: No es opcional? - unless @deployed[:deploy_local] - @site.update status: 'waiting' - notify_usuaries if notify - - # Hacer fallar la tarea - raise DeployException, deploy_local.build_stats.last.log - end - - deploy_others + deploy_others if deployed[:deploy_local] # Volver a la espera - @site.update status: 'waiting' + site.update status: 'waiting' notify_usuaries if notify + + # Hacer fallar la tarea para enterarnos. + raise DeployException, site.deploy_local.build_stats.last.log unless deployed[:deploy_local] end end # rubocop:enable Metrics/MethodLength private + # Correr todas las tareas que no sean el deploy local. def deploy_others - @site.deploys.where.not(type: 'DeployLocal').find_each do |d| - @deployed[d.type.underscore.to_sym] = d.deploy + site.deploys.where.not(type: 'DeployLocal').find_each do |d| + deployed[d.type.underscore.to_sym] = d.deploy end end + # Notificar a todes les usuaries no temporales. + # + # TODO: Poder configurar quiénes quieren recibir notificaciones. def notify_usuaries - @site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie| - DeployMailer.with(usuarie: usuarie, site: @site.id) - .deployed(@deployed) + site.roles.where(rol: 'usuarie', temporal: false).pluck(:usuarie_id).each do |usuarie| + DeployMailer.with(usuarie: usuarie, site: site.id) + .deployed(deployed) .deliver_now end end From 70c8bcbcd1b7db1240ea6cf498db3402f1c82391 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 18:59:23 -0300 Subject: [PATCH 20/39] Detectar cuando el servicio oculto es opcional --- app/controllers/api/v1/sites_controller.rb | 4 ++-- app/models/deploy_hidden_service.rb | 18 ++++++++++++++++-- app/views/deploys/_deploy_hidden_service.haml | 5 +++-- test/models/deploy_hidden_service_test.rb | 2 +- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/controllers/api/v1/sites_controller.rb b/app/controllers/api/v1/sites_controller.rb index 6abff704..6613dce5 100644 --- a/app/controllers/api/v1/sites_controller.rb +++ b/app/controllers/api/v1/sites_controller.rb @@ -16,7 +16,7 @@ module Api # # @return [Array] lista de nombres de sitios sin onion aun def hidden_services - render json: DeployHiddenService.where(values: nil).includes(:site).pluck(:name) + render json: DeployHiddenService.temporary.includes(:site).pluck(:name) end # Tor va a enviar el onion junto con el nombre del sitio y tenemos @@ -28,7 +28,7 @@ module Api site = Site.find_by(name: params[:name]) if site - usuarie = GitAuthor.new email: 'tor@' + Site.domain, name: 'Tor' + usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor' service = SiteService.new site: site, usuarie: usuarie, params: params service.add_onion diff --git a/app/models/deploy_hidden_service.rb b/app/models/deploy_hidden_service.rb index 8ca4b082..184ca2c2 100644 --- a/app/models/deploy_hidden_service.rb +++ b/app/models/deploy_hidden_service.rb @@ -5,6 +5,12 @@ class DeployHiddenService < DeployWww validates :hostname, format: { with: /\A[a-z2-7]{56}.onion\z/ } + # Sufijo para todos los dominios temporales. + TEMPORARY_SUFFIX = 'temporary' + + # Traer todos los servicios ocultos temporales. + scope :temporary, -> { where("hostname not like '#{TEMPORARY_SUFFIX}%'") } + # Los servicios ocultos son su propio transporte cifrado y # autenticado. # @@ -19,7 +25,14 @@ class DeployHiddenService < DeployWww # # @return [String] def default_hostname - "temporal#{random_base32}.onion" + "#{TEMPORARY_SUFFIX}#{random_base32}.onion" + end + + # Detecta si es una dirección temporal. + # + # @return [Boolean] + def temporary? + hostname.start_with? TEMPORARY_SUFFIX end private @@ -32,8 +45,9 @@ class DeployHiddenService < DeployWww # # @see {https://github.com/stesla/base32/blob/master/lib/base32.rb} # @see {https://github.com/stesla/base32/blob/master/LICENSE} - def random_base32(length = 48) + def random_base32(length = nil) table = 'abcdefghijklmnopqrstuvwxyz234567' + length ||= 56 - TEMPORARY_SUFFIX.length OpenSSL::Random.random_bytes(length).each_byte.map do |b| table[b % 32] diff --git a/app/views/deploys/_deploy_hidden_service.haml b/app/views/deploys/_deploy_hidden_service.haml index 38b33b5d..73992602 100644 --- a/app/views/deploys/_deploy_hidden_service.haml +++ b/app/views/deploys/_deploy_hidden_service.haml @@ -17,6 +17,7 @@ = sanitize_markdown t('.help', public_url: site.deploy_local.url), tags: %w[p strong em a] - = sanitize_markdown t('.help_2', url: deploy.object.url), - tags: %w[p strong em a] + - unless deploy.object.temporary? + = sanitize_markdown t('.help_2', url: deploy.object.url), + tags: %w[p strong em a] %hr/ diff --git a/test/models/deploy_hidden_service_test.rb b/test/models/deploy_hidden_service_test.rb index 799f3df3..cfa09b41 100644 --- a/test/models/deploy_hidden_service_test.rb +++ b/test/models/deploy_hidden_service_test.rb @@ -20,7 +20,7 @@ class DeployHiddenServiceTest < ActiveSupport::TestCase end test 'los hostnames pueden ser temporales' do - assert @deploy_hidden.hostname.start_with? 'temporal' + assert @deploy_hidden.hostname.start_with? 'temporary' end test 'el hostname tiene que ser único' do From 11e45bbc050be8637807775bd9e8d68b6260c3b1 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 19:00:27 -0300 Subject: [PATCH 21/39] Solo crear el DeployLocal una vez --- app/views/deploys/_deploy_local.haml | 4 +++- test/controllers/sites_controller_test.rb | 7 +------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/app/views/deploys/_deploy_local.haml b/app/views/deploys/_deploy_local.haml index c60165fa..7409b47f 100644 --- a/app/views/deploys/_deploy_local.haml +++ b/app/views/deploys/_deploy_local.haml @@ -9,4 +9,6 @@ = sanitize_markdown t('.help', url: deploy.object.url), tags: %w[p strong em a] - = deploy.hidden_field :type + -# No duplicarlos una vez que existen. + - unless deploy.object.persisted? + = deploy.hidden_field :type diff --git a/test/controllers/sites_controller_test.rb b/test/controllers/sites_controller_test.rb index a7e2f68b..c9c98dff 100644 --- a/test/controllers/sites_controller_test.rb +++ b/test/controllers/sites_controller_test.rb @@ -119,12 +119,7 @@ class SitesControllerTest < ActionDispatch::IntegrationTest title: name, description: name * 2, design_id: design.id, - licencia_id: Licencia.all.second.id, - deploys_attributes: { - '0' => { - type: 'DeployLocal' - } - } + licencia_id: Licencia.all.second.id } } From e14e53c2a182871d1115eeedc477297288c66a5b Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 19:56:48 -0300 Subject: [PATCH 22/39] No hacer malabares con los nombres de dominio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Esta era toda la razón para las modificaciones hechas hasta ahora, no tener que tener que determinar el dominio por si tiene o no tiene punto final o contiene el dominio del sitio. --- app/controllers/api/v1/base_controller.rb | 17 +++++++++-------- .../api/v1/contact_controller_test.rb | 6 +++--- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 649c6078..a6dd81d5 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -11,16 +11,17 @@ module Api private - # Realiza la inversa de Site#hostname + # Por retrocompatibilidad con la forma en que estábamos + # gestionando los hostnames históricamente, necesitamos poder + # encontrar el sitio a partir de cualquiera de sus hostnames. # - # TODO: El sitio sutty.nl no aplica a ninguno de estos y le - # tuvimos que poner 'sutty.nl..sutty.nl' para pasar el test. + # Aunque en realidad con el hostname a partir del Origin nos + # bastaría. + # + # TODO: Generar API v2 que use solo el hostname y no haya que + # pasar site_id como parámetro redundante. def site_id - @site_id ||= if params[:site_id].end_with? Site.domain - params[:site_id].sub(/\.#{Site.domain}\z/, '') - else - params[:site_id] + '.' - end + @site_id ||= Deploy.where(hostname: params[:site_id]).pluck(:site_id).first end # Referer diff --git a/test/controllers/api/v1/contact_controller_test.rb b/test/controllers/api/v1/contact_controller_test.rb index f84e0152..95726bae 100644 --- a/test/controllers/api/v1/contact_controller_test.rb +++ b/test/controllers/api/v1/contact_controller_test.rb @@ -106,7 +106,7 @@ module Api test 'se puede enviar mensajes a dominios propios' do ActionMailer::Base.deliveries.clear - @site.update name: 'example.org.' + @site.update name: 'example' redirect = "#{@site.url}?thanks" @@ -114,8 +114,8 @@ module Api create :rol, site: @site end - get v1_site_contact_cookie_url(@site.hostname, **@host) - post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host), + get v1_site_contact_cookie_url(@site.name, **@host) + post v1_site_contact_url(site_id: @site.name, form: :contacto, **@host), headers: { origin: @site.url }, params: { name: SecureRandom.hex, From 61b3b973134720cee2f024895829d072a52f6458 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 7 Aug 2021 19:57:49 -0300 Subject: [PATCH 23/39] Obtener los AccessLog para cada Site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A través del hostname, ¡magia! Esto nos habilita a obtener estadísticas más adelante... --- app/models/deploy.rb | 2 ++ app/models/site.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 0da07be9..37790eb3 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -9,6 +9,8 @@ require 'open3' class Deploy < ApplicationRecord # Un sitio puede tener muchas formas de publicarse. belongs_to :site + # Puede tener muchos access logs a través del hostname + has_many :access_logs, primary_key: 'hostname', foreign_key: 'host' # Siempre generar el hostname after_initialize :default_hostname! diff --git a/app/models/site.rb b/app/models/site.rb index c9357657..d0c420f1 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -39,6 +39,7 @@ class Site < ApplicationRecord has_many :log_entries, dependent: :destroy has_many :deploys, dependent: :destroy + has_many :access_logs, through: :deploys has_many :build_stats, through: :deploys has_many :roles, dependent: :destroy has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') }, From 22a7a811b4a32b970f9ed04f213fb6598fafe6f3 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 8 Aug 2021 21:18:13 -0300 Subject: [PATCH 24/39] Acomodar la API de formularios de contacto a los deploys nuevos. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La API no cambia por retrocompatibilidad pero ameritaría una v2 sabiendo más cosas sobre CORS. --- app/controllers/api/v1/base_controller.rb | 17 +++++++++- .../api/v1/invitades_controller.rb | 9 ++---- .../api/v1/protected_controller.rb | 5 --- app/models/deploy.rb | 7 ++++ .../api/v1/contact_controller_test.rb | 32 +++++++++++++++++-- 5 files changed, 56 insertions(+), 14 deletions(-) diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index a6dd81d5..3a63826d 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -21,7 +21,22 @@ module Api # TODO: Generar API v2 que use solo el hostname y no haya que # pasar site_id como parámetro redundante. def site_id - @site_id ||= Deploy.where(hostname: params[:site_id]).pluck(:site_id).first + @site_id ||= Deploy.site_name_from_hostname(params[:site_id]) + end + + # @return [Site] + def site + @site ||= Site.find_by_name(site_id) + end + + # Obtiene el hostname desde el Origin, con el hostname local como + # fallback. + # + # @return [String] + def origin_hostname + URI.parse(origin || origin_from_referer).host + rescue StandardError + "#{site_id}.#{Site.domain}" end # Referer diff --git a/app/controllers/api/v1/invitades_controller.rb b/app/controllers/api/v1/invitades_controller.rb index 236778ca..09d2031d 100644 --- a/app/controllers/api/v1/invitades_controller.rb +++ b/app/controllers/api/v1/invitades_controller.rb @@ -44,15 +44,10 @@ module Api # Genera el Origin correcto a partir de la URL del sitio. # - # En desarrollo devuelve el Origin enviado. - # - # XXX: Si el sitio tiene varias URLs, hay que devolver la más - # similar al Origin o vamos a estar generando errores de CORS. - # # @see {https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin} # @return [String] def return_origin - Rails.env.production? ? Site.find_by(name: site_id).url : origin + site&.deploys&.find_by_hostname(origin_hostname)&.url end # La cookie no es accesible a través de JS y todo su contenido @@ -63,6 +58,8 @@ module Api # TODO: Volver configurable por sitio expires = ENV.fetch('COOKIE_DURATION', '30').to_i.minutes + # TODO: ¿Son necesarios estos headers en la descarga de una + # imagen? ¿No será mejor moverlos al envío de datos? headers['Access-Control-Allow-Origin'] = return_origin headers['Access-Control-Allow-Credentials'] = true headers['Vary'] = 'Origin' diff --git a/app/controllers/api/v1/protected_controller.rb b/app/controllers/api/v1/protected_controller.rb index 5a14bb28..b3b4dfc7 100644 --- a/app/controllers/api/v1/protected_controller.rb +++ b/app/controllers/api/v1/protected_controller.rb @@ -118,11 +118,6 @@ module Api 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. # diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 37790eb3..e7e0844c 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -27,6 +27,13 @@ class Deploy < ApplicationRecord # Cada deploy puede implementar su propia validación validates :hostname, hostname: true, unless: :implements_hostname_validation? + # Retrocompatibilidad: Encuentra el site_name a partir del hostname. + # + # @return [String,Nil] + def self.site_name_from_hostname(hostname) + where(hostname: hostname).includes(:site).pluck(:name).first + end + # Genera el hostname # # @return [String] diff --git a/test/controllers/api/v1/contact_controller_test.rb b/test/controllers/api/v1/contact_controller_test.rb index 95726bae..2bf12f1f 100644 --- a/test/controllers/api/v1/contact_controller_test.rb +++ b/test/controllers/api/v1/contact_controller_test.rb @@ -114,8 +114,8 @@ module Api create :rol, site: @site end - get v1_site_contact_cookie_url(@site.name, **@host) - post v1_site_contact_url(site_id: @site.name, form: :contacto, **@host), + get v1_site_contact_cookie_url(@site.hostname, **@host) + post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host), headers: { origin: @site.url }, params: { name: SecureRandom.hex, @@ -130,6 +130,34 @@ module Api assert_equal redirect, response.headers['Location'] assert_equal 2, ActionMailer::Base.deliveries.size end + + test 'algunos navegadores no soportan Origin' do + ActionMailer::Base.deliveries.clear + + @site.update name: 'example' + + redirect = "#{@site.url}?thanks" + + 10.times do + create :rol, site: @site + end + + get v1_site_contact_cookie_url(@site.hostname, **@host) + post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host), + headers: { referer: @site.url }, + params: { + name: SecureRandom.hex, + pronouns: SecureRandom.hex, + contact: SecureRandom.hex, + from: "#{SecureRandom.hex}@sutty.nl", + body: SecureRandom.hex, + consent: true, + redirect: redirect + } + + assert_equal redirect, response.headers['Location'] + assert_equal 2, ActionMailer::Base.deliveries.size + end end end end From b2508de39b6139544c413414ffe74ad02974f0ad Mon Sep 17 00:00:00 2001 From: f Date: Sun, 8 Aug 2021 21:38:33 -0300 Subject: [PATCH 25/39] Redirigir al nombre del sitio sin incluir dominio --- app/controllers/application_controller.rb | 46 ++++++++++++++++---- app/controllers/collaborations_controller.rb | 4 +- app/controllers/sites_controller.rb | 6 ++- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index acd0134d..b3d355bb 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -8,13 +8,14 @@ class ApplicationController < ActionController::Base before_action :prepare_exception_notifier before_action :configure_permitted_parameters, if: :devise_controller? + before_action :redirect_to_site_name!, only: %i[index show edit new], if: :site_id_contains_hostname? around_action :set_locale rescue_from ActionController::RoutingError, with: :page_not_found rescue_from ActionController::ParameterMissing, with: :page_not_found before_action do - Rack::MiniProfiler.authorize_request if current_usuarie&.email&.ends_with?('@' + ENV.fetch('SUTTY', 'sutty.nl')) + Rack::MiniProfiler.authorize_request if Rails.env.development? end # No tenemos índice de sutty, vamos directamente a ver el listado de @@ -29,15 +30,11 @@ class ApplicationController < ActionController::Base /[a-f0-9]{8}-([a-f0-9]{4}-){3}[a-f0-9]{12}/ =~ string end - # Encontrar un sitio por su nombre + # Encontrar un sitio por su nombre. def find_site - id = params[:site_id] || params[:id] - - unless (site = current_usuarie.sites.find_by_name(id)) - raise SiteNotFound + current_usuarie.sites.find_by_name(site_id).tap do |site| + raise SiteNotFound unless site end - - site end # Devuelve el idioma actual y si no lo encuentra obtiene uno por @@ -62,6 +59,39 @@ class ApplicationController < ActionController::Base render 'application/page_not_found', status: :not_found end + # Retrocompatibilidad con sitios cuyo nombre era su hostname. + # + # @see Deploy + def site_id_contains_hostname? + site_id&.end_with? '.' + end + + # Redirigir a la misma URL con el site_id cambiado. + # + # TODO: Eliminar cuando detectemos que no hay más redirecciones. + def redirect_to_site_name! + params.permit! + params[:site_id] = Deploy.site_name_from_hostname(site_id[0..-2]) + + redirect_to params, status: :moved_permanently + end + + # Los controladores dentro de SitesController van a usar site_id + # mientras que SiteController va a usar ID. + # + # @see SitesController + # @return [String,Nil] + def site_id + @site_id ||= params[:site_id] + end + + # El sitio actual + # + # @return [Site] + def site + @site ||= find_site + end + protected def configure_permitted_parameters diff --git a/app/controllers/collaborations_controller.rb b/app/controllers/collaborations_controller.rb index 2caa1272..ab13ef58 100644 --- a/app/controllers/collaborations_controller.rb +++ b/app/controllers/collaborations_controller.rb @@ -7,7 +7,7 @@ class CollaborationsController < ApplicationController include Pundit def collaborate - @site = Site.find_by_name(params[:site_id]) + @site = find_site authorize Collaboration.new(@site) @invitade = current_usuarie || @site.usuaries.build @@ -21,7 +21,7 @@ class CollaborationsController < ApplicationController # # * Si le usuarie existe y no está logueade, pedirle la contraseña def accept_collaboration - @site = Site.find_by_name(params[:site_id]) + @site = find_site authorize Collaboration.new(@site) @invitade = current_usuarie diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index bdaa9011..d9914232 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -139,8 +139,10 @@ class SitesController < ApplicationController private - def site - @site ||= find_site + # En los controladores dentro de este controlador vamos a usar :id + # para obtener el nombre. + def site_id + @site_id ||= params[:id] end def site_params From aebe48c7840b8db31f89b77180f1a9ad0a312b05 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 8 Aug 2021 21:43:57 -0300 Subject: [PATCH 26/39] Simplificar el procesamiento de CORS Y ahora permitir cualquier URL posible :D --- config/initializers/cors.rb | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/config/initializers/cors.rb b/config/initializers/cors.rb index 812a11e4..44484d18 100644 --- a/config/initializers/cors.rb +++ b/config/initializers/cors.rb @@ -13,29 +13,19 @@ Rails.application.config.middleware.insert_before 0, Rack::Cors do # El problema sería que otros sitios con JS malicioso hagan pedidos # a nuestra API desde otros sitios infectados. # - # XXX: La primera parte del dominio tiene que coincidir con el - # nombre del sitio. - # # XXX: Al terminar de entender esto nos pasó que el servidor recibe # la petición de todas maneras, con lo que no estamos previniendo # que nos hablen, sino que lean información. Solo va a funcionar si # el servidor no tiene el Preflight cacheado. # # TODO: Limitar el acceso desde Nginx también. - # - # TODO: Poder consultar por sitios por todas sus URLs posibles. origins do |source, _| # Cacheamos la respuesta para no tener que volver a procesarla # cada vez. Rails.cache.fetch(source, expires_in: 1.hour) do - uri = URI(source) - - if (name = uri&.host&.split('.', 2)&.first).present? - Site.where(name: [name, uri.host + '.']).pluck(:name).first.present? - else - false - end - rescue URI::Error + hostname = URI(source)&.host + hostname.present? && Deploy.find_by_hostname(hostname).present? + rescue StandardError false end end From 77e583c7cb6275fc1d3f6fc71efb104036056130 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 8 Aug 2021 21:55:22 -0300 Subject: [PATCH 27/39] =?UTF-8?q?Simplificar=20emisi=C3=B3n=20y=20renovaci?= =?UTF-8?q?=C3=B3n=20de=20certificados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://0xacab.org/sutty/sutty/-/issues/123#note_299672 --- app/controllers/api/v1/sites_controller.rb | 26 +--------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/app/controllers/api/v1/sites_controller.rb b/app/controllers/api/v1/sites_controller.rb index 6613dce5..5537d078 100644 --- a/app/controllers/api/v1/sites_controller.rb +++ b/app/controllers/api/v1/sites_controller.rb @@ -9,7 +9,7 @@ module Api # Lista de nombres de dominios a emitir certificados def index - render json: sites_names + alternative_names + api_names + render json: Deploy.all.pluck(:hostname) end # Sitios con hidden service de Tor @@ -25,8 +25,6 @@ module Api # @params [String] name # @params [String] onion def add_onion - site = Site.find_by(name: params[:name]) - if site usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor' service = SiteService.new site: site, usuarie: usuarie, @@ -36,28 +34,6 @@ module Api head :ok end - - private - - # Nombres de los sitios - def sites_names - Site.all.order(:name).pluck(:name) - end - - # Dominios alternativos - def alternative_names - DeployAlternativeDomain.all.map(&:hostname) - end - - # Obtener todos los sitios con API habilitada, es decir formulario - # de contacto y/o colaboración anónima. - # - # TODO: Optimizar - def api_names - Site.where(contact: true) - .or(Site.where(colaboracion_anonima: true)) - .select("'api.' || name as name").map(&:name) - end end end end From 75e7fe76fecedcfcd1a9f73f6b3b27c3cbeacac9 Mon Sep 17 00:00:00 2001 From: f Date: Sun, 8 Aug 2021 21:55:22 -0300 Subject: [PATCH 28/39] =?UTF-8?q?Simplificar=20emisi=C3=B3n=20y=20renovaci?= =?UTF-8?q?=C3=B3n=20de=20certificados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://0xacab.org/sutty/sutty/-/issues/123#note_299672 --- app/controllers/api/v1/sites_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/api/v1/sites_controller.rb b/app/controllers/api/v1/sites_controller.rb index 5537d078..596e2531 100644 --- a/app/controllers/api/v1/sites_controller.rb +++ b/app/controllers/api/v1/sites_controller.rb @@ -25,7 +25,7 @@ module Api # @params [String] name # @params [String] onion def add_onion - if site + if (site = Site.find_by_name(params[:name])) usuarie = GitAuthor.new email: "tor@#{Site.domain}", name: 'Tor' service = SiteService.new site: site, usuarie: usuarie, params: params From baf6d203b870e3fe705a7ff67fd17d52da88c4c9 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 9 Aug 2021 19:44:41 -0300 Subject: [PATCH 29/39] Dentro de SitesController algunos usan site_id --- app/controllers/sites_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index d9914232..d7aa82a0 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -142,7 +142,7 @@ class SitesController < ApplicationController # En los controladores dentro de este controlador vamos a usar :id # para obtener el nombre. def site_id - @site_id ||= params[:id] + @site_id ||= params[:site_id] || params[:id] end def site_params From dcaac06fa4baa30ce1b55bde3b8c1ffdac88f4f8 Mon Sep 17 00:00:00 2001 From: f Date: Mon, 9 Aug 2021 20:12:43 -0300 Subject: [PATCH 30/39] =?UTF-8?q?Los=20sitios=20pueden=20tener=20una=20ubi?= =?UTF-8?q?caci=C3=B3n=20can=C3=B3nica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/site.rb | 76 +------------- app/models/site/deployment.rb | 99 +++++++++++++++++++ config/locales/en.yml | 2 + config/locales/es.yml | 2 + ...809155434_add_canonical_deploy_to_sites.rb | 26 +++++ .../api/v1/contact_controller_test.rb | 5 +- .../api/v1/sites_controller_test.rb | 2 +- test/models/site_test.rb | 8 +- 8 files changed, 140 insertions(+), 80 deletions(-) create mode 100644 app/models/site/deployment.rb create mode 100644 db/migrate/20210809155434_add_canonical_deploy_to_sites.rb diff --git a/app/models/site.rb b/app/models/site.rb index d0c420f1..7668cb6d 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -7,6 +7,7 @@ class Site < ApplicationRecord include Site::Forms include Site::FindAndReplace include Site::Api + include Site::Deployment include Tienda # Cifrar la llave privada que cifra y decifra campos ocultos. Sutty @@ -15,19 +16,11 @@ class Site < ApplicationRecord # protege de acceso al panel de Sutty! encrypts :private_key - # TODO: Hacer que los diferentes tipos de deploy se auto registren - # @see app/services/site_service.rb - DEPLOYS = %i[local private www zip hidden_service].freeze - - validates :name, uniqueness: true, hostname: { - allow_root_label: true - } - validates :design_id, presence: true + validates_uniqueness_of :name validates_inclusion_of :status, in: %w[waiting enqueued building] validates_presence_of :title validates :description, length: { in: 50..160 } - validate :deploy_local_presence validate :compatible_layouts, on: :update attr_reader :incompatible_layouts @@ -38,9 +31,6 @@ class Site < ApplicationRecord belongs_to :licencia has_many :log_entries, dependent: :destroy - has_many :deploys, dependent: :destroy - has_many :access_logs, through: :deploys - has_many :build_stats, through: :deploys has_many :roles, dependent: :destroy has_many :usuaries, -> { where('roles.rol = ?', 'usuarie') }, through: :roles @@ -59,13 +49,11 @@ class Site < ApplicationRecord after_initialize :load_jekyll after_create :load_jekyll, :static_file_migration! # Cambiar el nombre del directorio - before_update :update_name!, :update_deploy_local_hostname!, if: :name_changed? + before_update :update_name!, if: :name_changed? before_save :add_private_key_if_missing! # Guardar la configuración si hubo cambios after_save :sync_attributes_with_config! - accepts_nested_attributes_for :deploys, allow_destroy: true - # El sitio en Jekyll attr_reader :jekyll @@ -86,46 +74,6 @@ class Site < ApplicationRecord @repository ||= Site::Repository.new path end - # El primer deploy del sitio - # - # @return [DeployLocal] - def deploy_local - @deploy_local ||= deploys.order(created_at: :asc).find_by(type: 'DeployLocal') - end - - # Todas las URLs posibles para este sitio, ordenados según fecha de - # creación. - # - # @param :slash [Boolean] Con o sin / al final, por defecto con - # @return [Array] - def urls(slash: true) - deploys.order(created_at: :asc).map(&:url).map do |url| - slash ? "#{url}/" : url - end - end - - # Todos los hostnames, ordenados según fecha de creación. - # - # @return [Array] - def hostnames - deploys.order(created_at: :asc).pluck(:hostname) - end - - # Obtiene la URL principal - # - # @param :slash [Boolean] - # @return [String] - def url(slash: true) - deploy_local.url.tap do |url| - "#{url}/" if slash - end - end - - # TODO: Usar DeployCanonical - def hostname - deploy_local.hostname - end - def invitade?(usuarie) !invitades.find_by(id: usuarie.id).nil? end @@ -468,29 +416,11 @@ class Site < ApplicationRecord config.hostname = hostname end - # Si cambia el nombre queremos actualizarlo en el DeployLocal - def update_deploy_local_hostname! - deploy_local&.update hostname: name - end - # Migra los archivos a Sutty def static_file_migration! Site::StaticFileMigration.new(site: self).migrate! end - # Valida si el sitio tiene al menos una forma de alojamiento asociada - # y es la local - # - # TODO: Volver opcional el alojamiento local, pero ahora mismo está - # atado a la generación del sitio así que no puede faltar - def deploy_local_presence - # Usamos size porque queremos saber la cantidad de deploys sin - # guardar también - return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal') - - errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence')) - end - # Valida que al cambiar de plantilla no tengamos artículos en layouts # inexistentes. def compatible_layouts diff --git a/app/models/site/deployment.rb b/app/models/site/deployment.rb new file mode 100644 index 00000000..f88d2a0c --- /dev/null +++ b/app/models/site/deployment.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +class Site + # Abstrae todo el comportamiento de publicación del sitio en un + # módulo. + module Deployment + extend ActiveSupport::Concern + + included do + # TODO: Hacer que los diferentes tipos de deploy se auto registren + # @see app/services/site_service.rb + DEPLOYS = %i[local private www zip hidden_service].freeze + + validates :name, + format: { with: /\A[0-9a-z\-]+\z/, + message: I18n.t('activerecord.errors.models.site.attributes.name.no_subdomains') } + validates :name, hostname: true + validates_presence_of :canonical_deploy + validate :deploy_local_presence + + has_one :canonical_deploy, class_name: 'Deploy' + has_many :deploys, dependent: :destroy + has_many :access_logs, through: :deploys + has_many :build_stats, through: :deploys + + before_validation :deploy_local_is_default_canonical_deploy!, unless: :canonical_deploy_id? + before_update :update_deploy_local_hostname!, if: :name_changed? + + accepts_nested_attributes_for :deploys, allow_destroy: true + + # El primer deploy del sitio, si no existe en la base de datos es + # porque recién estamos creando el sitio y todavía no se guardó. + # + # @return [DeployLocal] + def deploy_local + @deploy_local ||= deploys.order(created_at: :asc).find_by(type: 'DeployLocal') || deploys.find do |d| + d.type == 'DeployLocal' + end + end + + # Obtiene la URL principal + # + # @param :slash [Boolean] + # @return [String] + def canonical_url(slash: true) + canonical_deploy.url.tap do |url| + "#{url}/" if slash + end + end + alias_method :url, :canonical_url + + # Devuelve todas las URLs posibles + # + # @param :slash [Boolean] + # @return [Array] + def urls(slash: true) + deploys.map(&:url).map do |url| + slash ? "#{url}/" : url + end + end + + # Obtiene el hostname principal + # + # @return [String] + def hostname + canonical_deploy.hostname + end + + private + + # Si cambia el nombre queremos actualizarlo en el DeployLocal y + # recargar el deploy canónico para tomar el nombre que + # corresponda. + def update_deploy_local_hostname! + deploy_local.update(hostname: name) + canonical_deploy.reload if canonical_deploy == deploy_local + end + + # Si no asignamos un deploy canónico en el momento le asignamos el + # deploy local + def deploy_local_is_default_canonical_deploy! + self.canonical_deploy ||= deploy_local + end + + # Valida si el sitio tiene al menos una forma de alojamiento asociada + # y es la local + # + # TODO: Volver opcional el alojamiento local, pero ahora mismo está + # atado a la generación del sitio así que no puede faltar + def deploy_local_presence + # Usamos size porque queremos saber la cantidad de deploys sin + # guardar también + return if deploys.size.positive? && deploys.map(&:type).include?('DeployLocal') + + errors.add(:deploys, I18n.t('activerecord.errors.models.site.attributes.deploys.deploy_local_presence')) + end + end + end +end diff --git a/config/locales/en.yml b/config/locales/en.yml index 25b662bf..b59a5f21 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -145,6 +145,8 @@ en: models: site: attributes: + name: + no_subdomains: 'Name cannot contain dots' deploys: deploy_local_presence: 'We need to be build the site!' design_id: diff --git a/config/locales/es.yml b/config/locales/es.yml index eac8a49d..68ee3669 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -145,6 +145,8 @@ es: models: site: attributes: + name: + no_subdomains: 'El nombre no puede contener puntos' deploys: deploy_local_presence: '¡Necesitamos poder generar el sitio!' design_id: diff --git a/db/migrate/20210809155434_add_canonical_deploy_to_sites.rb b/db/migrate/20210809155434_add_canonical_deploy_to_sites.rb new file mode 100644 index 00000000..f0b24053 --- /dev/null +++ b/db/migrate/20210809155434_add_canonical_deploy_to_sites.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Los sitios pueden tener muchos tipos de publicación pero solo uno es +# el principal. Al usar un campo específico, podemos validar mejor su +# presencia y modificación. +# +# El valor por defecto es 0 para poder crear la columna sin modificarla +# después, pero la idea es que nunca haya ceros. +class AddCanonicalDeployToSites < ActiveRecord::Migration[6.1] + def up + add_belongs_to :sites, :canonical_deploy, index: true, null: false, default: 0 + + # Si el sitio tenía un dominio alternativo, usar ese en lugar del + # local, asumiendo que es el primero de todos los posibles. + Site.find_each do |site| + deploy = site.deploys.order(created_at: :asc).find_by_type('DeployAlternativeDomain') + deploy ||= site.deploy_local + + site.update canonical_deploy_id: deploy.id + end + end + + def down + remove_belongs_to :sites, :canonical_deploy, index: true + end +end diff --git a/test/controllers/api/v1/contact_controller_test.rb b/test/controllers/api/v1/contact_controller_test.rb index 2bf12f1f..21159c72 100644 --- a/test/controllers/api/v1/contact_controller_test.rb +++ b/test/controllers/api/v1/contact_controller_test.rb @@ -21,13 +21,14 @@ module Api end test 'el sitio tiene que existir' do + hostname = @site.hostname @site.destroy - get v1_site_contact_cookie_url(@site.hostname, **@host) + get v1_site_contact_cookie_url(hostname, **@host) assert_not cookies[@site.name] - post v1_site_contact_url(site_id: @site.hostname, form: :contacto, **@host), + post v1_site_contact_url(site_id: hostname, form: :contacto, **@host), params: { name: SecureRandom.hex, pronouns: SecureRandom.hex, diff --git a/test/controllers/api/v1/sites_controller_test.rb b/test/controllers/api/v1/sites_controller_test.rb index 5623edca..5007e4f4 100644 --- a/test/controllers/api/v1/sites_controller_test.rb +++ b/test/controllers/api/v1/sites_controller_test.rb @@ -23,7 +23,7 @@ module Api test 'se puede obtener un listado de todos' do get v1_sites_url(host: "api.#{Site.domain}"), headers: @authorization, as: :json - assert_equal Site.all.pluck(:name), JSON.parse(response.body) + assert_equal Deploy.all.pluck(:hostname), JSON.parse(response.body) end end end diff --git a/test/models/site_test.rb b/test/models/site_test.rb index 481c9bbc..cfef44e7 100644 --- a/test/models/site_test.rb +++ b/test/models/site_test.rb @@ -26,18 +26,18 @@ class SiteTest < ActiveSupport::TestCase assert_not site2.valid? end - test 'el nombre del sitio puede contener subdominios' do + test 'el nombre del sitio no puede contener subdominios' do @site = build :site, name: 'hola.chau' site.validate - assert_not site.errors.messages[:name].present? + assert site.errors.messages[:name].present? end - test 'el nombre del sitio puede terminar con punto' do + test 'el nombre del sitio no puede terminar con punto' do @site = build :site, name: 'hola.chau.' site.validate - assert_not site.errors.messages[:name].present? + assert site.errors.messages[:name].present? end test 'el nombre del sitio no puede contener wildcard' do From a1fde94378a6f19833ccef7a734cb223169bed1d Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 19:18:47 -0300 Subject: [PATCH 31/39] Poder validar localmente los .local --- config/initializers/validates_hostname.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 config/initializers/validates_hostname.rb diff --git a/config/initializers/validates_hostname.rb b/config/initializers/validates_hostname.rb new file mode 100644 index 00000000..43c47a63 --- /dev/null +++ b/config/initializers/validates_hostname.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# Agrega el subdominio .local a menos que estemos en producción. +# +# TODO: Permitir TLDs que no sean de ICANN aquí. +PAK::ValidatesHostname::ALLOWED_TLDS << 'local' unless Rails.env.production? From df29c4ca95213c3e2ee365f1ba672251ebbcfb5b Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:06:44 -0300 Subject: [PATCH 32/39] Al cambiar el nombre del sitio no puede haber un dominio alternativo igual --- app/models/site/deployment.rb | 14 ++++++++++++++ config/locales/en.yml | 1 + config/locales/es.yml | 1 + test/models/site/deployment_test.rb | 25 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 test/models/site/deployment_test.rb diff --git a/app/models/site/deployment.rb b/app/models/site/deployment.rb index f88d2a0c..b3b88987 100644 --- a/app/models/site/deployment.rb +++ b/app/models/site/deployment.rb @@ -17,6 +17,7 @@ class Site validates :name, hostname: true validates_presence_of :canonical_deploy validate :deploy_local_presence + validate :name_changed_is_unique_hostname, if: :name_changed? has_one :canonical_deploy, class_name: 'Deploy' has_many :deploys, dependent: :destroy @@ -68,6 +69,19 @@ class Site private + # Validar que al cambiar el nombre no estemos utilizando un + # hostname reservado por otro sitio. + # + # Al cambiar el nombre del DeployLocal se va a validar que el + # hostname nuevo sea único. + def name_changed_is_unique_hostname + deploy_local.hostname = nil + + return if deploy_local.valid? + + errors.add :name, I18n.t('activerecord.errors.models.site.attributes.name.duplicated_hostname') + end + # Si cambia el nombre queremos actualizarlo en el DeployLocal y # recargar el deploy canónico para tomar el nombre que # corresponda. diff --git a/config/locales/en.yml b/config/locales/en.yml index b59a5f21..27ffd43c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -147,6 +147,7 @@ en: attributes: name: no_subdomains: 'Name cannot contain dots' + duplicated_hostname: 'There already is a site with this address' deploys: deploy_local_presence: 'We need to be build the site!' design_id: diff --git a/config/locales/es.yml b/config/locales/es.yml index 68ee3669..f98fd7db 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -147,6 +147,7 @@ es: attributes: name: no_subdomains: 'El nombre no puede contener puntos' + duplicated_hostname: 'Ya existe un sitio con ese nombre' deploys: deploy_local_presence: '¡Necesitamos poder generar el sitio!' design_id: diff --git a/test/models/site/deployment_test.rb b/test/models/site/deployment_test.rb new file mode 100644 index 00000000..8ec61e8a --- /dev/null +++ b/test/models/site/deployment_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'test_helper' + +class Site::DeploymentTest < ActiveSupport::TestCase + def site + @site ||= create :site + end + + teardown do + @site&.destroy + end + + test 'al publicar el sitio se crea el directorio' do + assert site.deploy_local.deploy + assert site.deploy_local.exist? + end + + test 'al cambiar el nombre no puede pisar un dominio que ya existe' do + site_pre = create :site + dup_name = "test-#{SecureRandom.hex}" + assert site_pre.deploys.create(type: 'DeployAlternativeDomain', hostname: "#{dup_name}.#{Site.domain}") + assert_not site.update(name: dup_name) + end +end From e6ea1001cd272404bfea4f61367922a0e61d783e Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:12:30 -0300 Subject: [PATCH 33/39] La URL termina en / por defecto --- app/models/site/deployment.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/site/deployment.rb b/app/models/site/deployment.rb index b3b88987..abd82493 100644 --- a/app/models/site/deployment.rb +++ b/app/models/site/deployment.rb @@ -44,8 +44,8 @@ class Site # @param :slash [Boolean] # @return [String] def canonical_url(slash: true) - canonical_deploy.url.tap do |url| - "#{url}/" if slash + canonical_deploy.url.dup.tap do |url| + url << '/' if slash end end alias_method :url, :canonical_url From 341a693a359b6189095860640a77248b07fd3e39 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:21:30 -0300 Subject: [PATCH 34/39] Mantener los hostnames anteriores a medida que se cambia de nombre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Para no romper links preexistentes. Lo ideal sería que todos los Deploy se vinculen al directorio principal para no romper links, pero la idea es no romperlos (?) --- app/models/deploy.rb | 58 +++++++++++++++++++++++++++-- app/models/deploy_hidden_service.rb | 5 +++ app/models/deploy_local.rb | 9 +---- app/models/deploy_www.rb | 5 --- config/locales/en.yml | 4 ++ config/locales/es.yml | 4 ++ test/models/site/deployment_test.rb | 42 +++++++++++++++++++++ 7 files changed, 111 insertions(+), 16 deletions(-) diff --git a/app/models/deploy.rb b/app/models/deploy.rb index e7e0844c..7032f969 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -5,20 +5,27 @@ require 'open3' # Este modelo implementa los distintos tipos de alojamiento que provee # Sutty. # +# Cuando cambia el hostname de un Deploy, generamos un +# DeployAlternativeDomain en su lugar. Esto permite que no se rompan +# links preexistentes y que el nombre no pueda ser tomado por alguien +# más. +# # TODO: Cambiar el nombre a algo que no sea industrial/militar. class Deploy < ApplicationRecord # Un sitio puede tener muchas formas de publicarse. belongs_to :site # Puede tener muchos access logs a través del hostname has_many :access_logs, primary_key: 'hostname', foreign_key: 'host' + # Registro de las tareas ejecutadas + has_many :build_stats, dependent: :destroy # Siempre generar el hostname after_initialize :default_hostname! # Eliminar los archivos generados por el deploy. before_destroy :remove_destination! - - # Registro de las tareas ejecutadas - has_many :build_stats, dependent: :destroy + # Los hostnames alternativos se crean después de actualizar, cuando ya + # se modificó el hostname. + around_update :create_alternative_domain!, if: :destination_changed? # Siempre tienen que pertenecer a un sitio validates :site, presence: true @@ -34,6 +41,37 @@ class Deploy < ApplicationRecord where(hostname: hostname).includes(:site).pluck(:name).first end + # Detecta si el destino existe y si no es un symlink roto. + def exist? + File.exist? destination + end + + # Detecta si el link está roto + def broken? + File.symlink?(destination) && !File.exist?(File.readlink(destination)) + end + + # Ubicación del deploy + # + # @return [String] Una ruta en el sistema de archivos + def destination + File.join(Rails.root, '_deploy', hostname) + end + + # Ubicación anterior del deploy + # + # @return [String] Una ruta en el sistema de archivos + def destination_was + return destination unless will_save_change_to_hostname? + + File.join(Rails.root, '_deploy', hostname_was) + end + + # Determina si la ubicación cambió + def destination_changed? + persisted? && will_save_change_to_hostname? + end + # Genera el hostname # # @return [String] @@ -143,6 +181,20 @@ class Deploy < ApplicationRecord raise NotImplementedError end + # Cuando el deploy cambia de hostname, generamos un dominio + # alternativo para no romper links hacia este sitio. + def create_alternative_domain! + hw = hostname_was + + # Aplicar la actualización + yield + + # Crear el deploy alternativo con el nombre anterior una vez que + # lo cambiamos en la base de datos. + ad = site.deploys.create(type: 'DeployAlternativeDomain', hostname: hw) + ad.deploy if ad.persisted? + end + # Convierte el comando en una versión resumida. # # @param [String] diff --git a/app/models/deploy_hidden_service.rb b/app/models/deploy_hidden_service.rb index 184ca2c2..3f687bd1 100644 --- a/app/models/deploy_hidden_service.rb +++ b/app/models/deploy_hidden_service.rb @@ -37,6 +37,11 @@ class DeployHiddenService < DeployWww private + # No soportamos cambiar de onion + def destination_changed? + false + end + def implements_hostname_validation? true end diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index be8de366..655e11e9 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -3,7 +3,7 @@ # Alojamiento local, genera el sitio como si corriéramos `jekyll build`. class DeployLocal < Deploy # Asegurarse que el hostname es el permitido. - before_save :reset_hostname!, :default_hostname! + before_validation :reset_hostname!, :default_hostname! # Realizamos la construcción del sitio usando Jekyll y un entorno # limpio para no pasarle secretos @@ -30,13 +30,6 @@ class DeployLocal < Deploy end.inject(:+) end - # La ubicación del sitio luego de generarlo. - # - # @return [String] - def destination - File.join(Rails.root, '_deploy', hostname) - end - # El hostname es el nombre del sitio más el dominio principal. # # @return [String] diff --git a/app/models/deploy_www.rb b/app/models/deploy_www.rb index 9ef94df7..3d99a6c2 100644 --- a/app/models/deploy_www.rb +++ b/app/models/deploy_www.rb @@ -20,11 +20,6 @@ class DeployWww < Deploy File.size destination end - # @return [String] - def destination - File.join(Rails.root, '_deploy', hostname) - end - # El hostname por defecto incluye WWW # # @return [String] diff --git a/config/locales/en.yml b/config/locales/en.yml index 27ffd43c..24bd3d3d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -143,6 +143,10 @@ en: tienda_api_key: Store access key errors: models: + deploy: + attributes: + hostname: + destination_exist: 'There already is a file in the destination' site: attributes: name: diff --git a/config/locales/es.yml b/config/locales/es.yml index f98fd7db..851e33e5 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -143,6 +143,10 @@ es: tienda_api_key: Clave de acceso errors: models: + deploy: + attributes: + hostname: + destination_exist: 'Ya hay un archivo en esta ubicación' site: attributes: name: diff --git a/test/models/site/deployment_test.rb b/test/models/site/deployment_test.rb index 8ec61e8a..b8e8464b 100644 --- a/test/models/site/deployment_test.rb +++ b/test/models/site/deployment_test.rb @@ -22,4 +22,46 @@ class Site::DeploymentTest < ActiveSupport::TestCase assert site_pre.deploys.create(type: 'DeployAlternativeDomain', hostname: "#{dup_name}.#{Site.domain}") assert_not site.update(name: dup_name) end + + test 'al cambiar el nombre se crea un deploy alternativo' do + site_name = site.name + new_name = SecureRandom.hex + original_destination = site.deploy_local.destination + urls = [site.url] + + assert site.deploy_local.deploy + assert_not site.deploy_local.destination_changed? + assert site.update(name: new_name) + + urls << site.url + + assert_equal urls.sort, site.urls.sort + assert File.symlink?(original_destination) + assert File.exist?(site.deploy_local.destination) + assert_equal 2, site.deploys.count + end + + test 'al cambiar el nombre se renombra el directorio' do + site_name = site.name + new_name = "test-#{SecureRandom.hex}" + original_destination = site.deploy_local.destination + + assert site.deploy_local.deploy + assert_not site.deploy_local.destination_changed? + assert site.update(name: new_name) + assert site.deploy_local.hostname.start_with?(new_name) + assert File.symlink?(original_destination) + assert File.exist?(site.deploy_local.destination) + end + + test 'al cambiar el nombre varias veces se crean varios links' do + assert site.deploy_local.deploy + + q = rand(3..10) + q.times do + assert site.update(name: "test-#{SecureRandom.hex}") + end + + assert_equal q, site.deploys.count + end end From b20b15317b64505dd396e53eb925a57e4e092d3f Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:24:02 -0300 Subject: [PATCH 35/39] =?UTF-8?q?Cambiar=20la=20ubicaci=C3=B3n=20del=20sit?= =?UTF-8?q?io=20al=20cambiar=20su=20nombre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/deploy.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 7032f969..87f1e530 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -23,6 +23,9 @@ class Deploy < ApplicationRecord after_initialize :default_hostname! # Eliminar los archivos generados por el deploy. before_destroy :remove_destination! + # Cambiar el lugar del destino antes de guardar los cambios, para que + # el hostname anterior siga estando disponible. + before_update :rename_destination!, if: :destination_changed? # Los hostnames alternativos se crean después de actualizar, cuando ya # se modificó el hostname. around_update :create_alternative_domain!, if: :destination_changed? @@ -171,7 +174,13 @@ class Deploy < ApplicationRecord # @return [Boolean] def default_hostname! self.hostname ||= default_hostname - true + end + + # Cambia la ubicación de destino cuando cambia el hostname. + def rename_destination! + return unless File.exist? destination_was + + FileUtils.mv destination_was, destination end # Elimina los archivos generados por el deploy From 3e76ffe4c467ab2b427da195c7c4542231fd5205 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:26:42 -0300 Subject: [PATCH 36/39] =?UTF-8?q?Si=20ya=20exist=C3=ADa=20algo=20en=20el?= =?UTF-8?q?=20mismo=20lugar=20no=20se=20puede=20cambiar=20el=20nombre?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/models/deploy.rb | 16 ++++++++++++++++ test/models/site/deployment_test.rb | 9 +++++++++ 2 files changed, 25 insertions(+) diff --git a/app/models/deploy.rb b/app/models/deploy.rb index 87f1e530..ac95a5f4 100644 --- a/app/models/deploy.rb +++ b/app/models/deploy.rb @@ -36,6 +36,9 @@ class Deploy < ApplicationRecord validates :hostname, uniqueness: true # Cada deploy puede implementar su propia validación validates :hostname, hostname: true, unless: :implements_hostname_validation? + # Verificar que se puede cambiar de lugar el destino y no hay nada + # preexistente. + validate :destination_can_change?, if: :destination_changed? # Retrocompatibilidad: Encuentra el site_name a partir del hostname. # @@ -204,6 +207,19 @@ class Deploy < ApplicationRecord ad.deploy if ad.persisted? end + # Devuelve un error si el destino ya existe. No debería fallar si ya + # pasamos la validación de cambio de nombres, pero siempre puede haber + # directorios y links sueltos. + def destination_can_change? + return true unless persisted? + + remove_destination! if broken? + + return true unless exist? + + errors.add :hostname, I18n.t('activerecord.errors.models.deploy.attributes.hostname.destination_exist') + end + # Convierte el comando en una versión resumida. # # @param [String] diff --git a/test/models/site/deployment_test.rb b/test/models/site/deployment_test.rb index b8e8464b..f2e73c6d 100644 --- a/test/models/site/deployment_test.rb +++ b/test/models/site/deployment_test.rb @@ -64,4 +64,13 @@ class Site::DeploymentTest < ActiveSupport::TestCase assert_equal q, site.deploys.count end + + test 'no se puede cambiar el nombre si ya existía un archivo en el mismo lugar' do + assert site.deploy_local.deploy + + new_name = "test-#{SecureRandom.hex}" + FileUtils.mkdir File.join(Rails.root, '_deploy', "#{new_name}.#{Site.domain}") + + assert_not site.update(name: new_name) + end end From 5a53facaa75dede801c5dfa299b3492b660aca55 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:27:08 -0300 Subject: [PATCH 37/39] Al cambiar el nombre actualizamos el www --- app/models/deploy_local.rb | 9 +++++++++ test/models/site/deployment_test.rb | 12 ++++++++++++ 2 files changed, 21 insertions(+) diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index 655e11e9..5a6a87cd 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -4,6 +4,8 @@ class DeployLocal < Deploy # Asegurarse que el hostname es el permitido. before_validation :reset_hostname!, :default_hostname! + # Actualiza el hostname con www si cambiamos el hostname + before_update :update_deploy_www!, if: :hostname_changed? # Realizamos la construcción del sitio usando Jekyll y un entorno # limpio para no pasarle secretos @@ -43,6 +45,13 @@ class DeployLocal < Deploy self.hostname = nil end + # XXX: En realidad el DeployWww debería regenerar su propio hostname. + def update_deploy_www! + site.deploys.where(type: 'DeployWww').map do |www| + www.update hostname: www.default_hostname + end + end + # Crea el directorio destino si no existe. def mkdir FileUtils.mkdir_p destination diff --git a/test/models/site/deployment_test.rb b/test/models/site/deployment_test.rb index f2e73c6d..799a33e7 100644 --- a/test/models/site/deployment_test.rb +++ b/test/models/site/deployment_test.rb @@ -54,6 +54,18 @@ class Site::DeploymentTest < ActiveSupport::TestCase assert File.exist?(site.deploy_local.destination) end + test 'al cambiar el nombre se actualiza el www' do + site_name = site.name + new_name = "test-#{SecureRandom.hex}" + + assert (deploy_www = site.deploys.create(type: 'DeployWww')) + assert site.deploy_local.deploy + assert_not site.deploy_local.destination_changed? + assert site.update(name: new_name) + assert deploy_www.reload.hostname.include?(new_name) + assert_equal 4, site.deploys.count + end + test 'al cambiar el nombre varias veces se crean varios links' do assert site.deploy_local.deploy From 339c02c92bf5d465429a65c2d329e714059d33e4 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 14 Aug 2021 20:27:25 -0300 Subject: [PATCH 38/39] =?UTF-8?q?Siempre=20vincular=20con=20la=20ubicaci?= =?UTF-8?q?=C3=B3n=20relativa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Para que podamos cambiar la estructura de directorios sin romper los links. --- app/models/deploy_www.rb | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/app/models/deploy_www.rb b/app/models/deploy_www.rb index 3d99a6c2..2cfb1362 100644 --- a/app/models/deploy_www.rb +++ b/app/models/deploy_www.rb @@ -5,19 +5,24 @@ class DeployWww < Deploy # La forma de hacer este deploy es generar un link simbólico entre el # directorio canónico y el actual. # - # XXX: Asume que el origen y el destino se encuentran en el mismo - # directorio (¿por qué no estarían?) - # # @return [Boolean] def deploy - return true if File.symlink? destination + # Eliminar los links rotos + remove_destination! if broken? - File.symlink(site.deploy_local.hostname, destination).zero? + # No hacer nada si ya existe. + return true if exist? + + # Generar un link simbólico con la ruta relativa al destino + File.symlink(relative_path, destination).zero? end + # Siempre devuelve el espacio ocupado por el link simbólico, no el + # destino. + # # @return [Integer] def size - File.size destination + relative_path.size end # El hostname por defecto incluye WWW @@ -33,4 +38,12 @@ class DeployWww < Deploy def remove_destination! FileUtils.rm_f destination end + + # Obtiene la ubicación relativa del deploy local hacia la ubicación de + # este deploy + # + # @return [String] + def relative_path + Pathname.new(site.deploy_local.destination).relative_path_from(File.dirname(destination)).to_s + end end From 2eff3ba7163273f7a81a2450adfc305e77379142 Mon Sep 17 00:00:00 2001 From: f Date: Sat, 28 Aug 2021 12:20:40 -0300 Subject: [PATCH 39/39] Agregar las licencias solo si el sitio se pudo crear --- app/services/site_service.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/services/site_service.rb b/app/services/site_service.rb index 389549c3..73d50f97 100644 --- a/app/services/site_service.rb +++ b/app/services/site_service.rb @@ -13,11 +13,10 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do I18n.with_locale(usuarie&.lang&.to_sym || I18n.default_locale) do site.save && site.config.write && - commit_config(action: :create) + commit_config(action: :create) && + add_licencias end - add_licencias - site end