TXT DNS entry to control refresh duration

stats
aeris 9 months ago
parent 089a3b98be
commit e6a36dc056
  1. 2
      Gemfile
  2. 4
      app/controllers/check_controller.rb
  3. 99
      app/models/analysis.rb
  4. 13
      app/views/check/show.html.erb
  5. 13
      app/views/ssh/show.html.erb
  6. 8
      config/locales/en.yml
  7. 3
      config/locales/fr.yml
  8. 16
      db/migrate/20220626133648_add_refresh_to_analysis.rb
  9. 3
      db/schema.rb
  10. 55
      spec/models/analysis_spec.rb

@ -17,6 +17,8 @@ gem 'sidekiq-workflow', git: 'https://git.imirhil.fr/aeris/sidekiq-workflow.git'
gem 'simpleidn'
gem 'http_accept_language'
gem 'recursive-open-struct'
gem 'ruby-progressbar'
gem 'public_suffix'
gem 'uglifier'
gem 'sass-rails'

@ -18,8 +18,8 @@ class CheckController < ApplicationController
def refresh
unless @analysis.pending
if Rails.env == 'production'
refresh_allowed = @analysis.updated_at + Rails.configuration.refresh_delay
if Time.now < refresh_allowed
refresh_at = @analysis.refresh_at
if refresh_at || Time.now < refresh_at
flash[:warning] = "Merci d’attendre au moins #{l refresh_allowed} pour rafraîchir"
return redirect_to action: :show, id: @host
end

@ -1,36 +1,67 @@
class Analysis < ApplicationRecord
enum service: %i[https smtp xmpp tls ssh].collect { |e| [e, e.to_s] }.to_h
validates :service, presence: true
validates :host, presence: true
def self.[](service, host, args)
key = self.key service, host, args
self.find_by key
end
def self.pending!(service, host, args)
key = self.key service, host, args
analysis = self.find_or_create_by! key
analysis.pending!
end
def pending!
self.update! pending: true
self
end
def self.post!(service, host, args, result)
analysis = self[service, host, args]
analysis.post! result
end
def post!(result)
self.update! pending: false, result: result
end
private
def self.key(service, host, args)
{ service: service, host: host, args: args }
end
enum service: %i[https smtp xmpp tls ssh].collect { |e| [e, e.to_s] }.to_h
validates :service, presence: true
validates :host, presence: true
def self.[](service, host, args)
key = self.key service, host, args
self.find_by key
end
def self.pending!(service, host, args)
key = self.key service, host, args
analysis = self.find_or_create_by! key
analysis.pending!
end
def pending!
self.update! pending: true
self
end
def self.post!(service, host, args, result)
analysis = self[service, host, args]
analysis.post! result
end
RESOLVER = ::Resolv::DNS.new
DNS_TXT_FORMAT = /^cryptcheck=(.*)/
DEBUG = 'debug'
DEFAULT_REFRESH_DELAY = Rails.configuration.refresh_delay
def find_refresh_delay
host = self.host
loop do
break unless host && PublicSuffix.valid?(host)
RESOLVER.getresources(host, ::Resolv::DNS::Resource::IN::TXT).each do |txt|
txt.strings.each do |value|
if match = DNS_TXT_FORMAT.match(value)
match = match[1]
return if match == DEBUG
begin
delay = ::ActiveSupport::Duration.parse match
delay = DEFAULT_REFRESH_DELAY if delay > DEFAULT_REFRESH_DELAY
return delay
rescue ActiveSupport::Duration::ISO8601Parser::ParsingError
end
end
end
end
_, host = host.split '.', 2
end
DEFAULT_REFRESH_DELAY
rescue
DEFAULT_REFRESH_DELAY
end
def post!(result)
refresh_at = self.find_refresh_delay&.since
self.update! pending: false, refresh_at: refresh_at, result: result
end
private
def self.key(service, host, args)
{ service: service, host: host, args: args }
end
end

@ -1,16 +1,19 @@
<div class="container">
<div class="row">
<div class="col-sm-11">
<div class="col-sm-10">
<h1>
[<%= self.type.to_s.upcase %>] <%= @host %>
<span class="small">(<%= l @analysis.updated_at %>)</span>
</h1>
</div>
<% if Time.now - @analysis.updated_at >= Rails.configuration.refresh_delay %>
<div class="col-sm-1">
<%= link_to t('Refresh'), { action: :refresh }, class: %i(btn btn-outline-secondary) %>
</div>
<div class="col-sm-2 text-end">
<% if @analysis.refresh_at.nil? || Time.now >= @analysis.refresh_at %>
<%= link_to t('Refresh'), { action: :refresh }, class: %i(btn btn-outline-secondary) %>
<% else %>
<%= button_tag t('Refresh not available', date: l(@analysis.refresh_at, format: :time)),
type: :button, class: %i(btn btn-outline-secondary), disabled: true %>
<% end %>
</div>
</div>
<% @result.each do |host|

@ -1,15 +1,18 @@
<div class="container">
<div class="row">
<div class="col-sm-11">
<div class="col-sm-10">
<h1>
[SSH] <%= @host %> <span class="small">(<%= l @analysis.updated_at %>)</span>
</h1>
</div>
<% if Time.now - @analysis.updated_at >= Rails.configuration.refresh_delay %>
<div class="col-sm-1">
<%= link_to t('Refresh'), {action: :refresh}, class: %i(btn btn-default) %>
<div class="col-sm-2 text-end">
<% if Time.now >= @analysis.refresh_at %>
<%= link_to t('Refresh'), { action: :refresh }, class: %i(btn btn-outline-secondary) %>
<% else %>
<%= button_tag t('Refresh not available', date: l(@analysis.refresh_at, format: :time)),
type: :button, class: %i(btn btn-outline-secondary), disabled: true %>
<% end %>
</div>
<% end %>
</div>
<%
@result.each do |host|

@ -18,6 +18,7 @@ en:
"Error during analysis:": "Error during analysis:"
Refresh: Refresh
Refresh not available: Refresh available at %{date}
Protocol: Protocol
Protocols: Protocols
@ -78,3 +79,10 @@ en:
great:
hsts: This server supports HSTS with long duration
time:
formats:
default: "%d/%m/%Y %H:%M:%S %:z"
long: "%d/%m/%Y %H:%M:%S"
short: "%d %b %Hh%M"
time: "%H:%M:%S"

@ -18,6 +18,7 @@ fr:
"Error during analysis:": "Erreur durant l’analyse :"
Refresh: Rafraîchir
Refresh not available: Rafraîchissement disponible à %{date}
Protocol: Protocole
Protocols: Protocoles
@ -262,5 +263,7 @@ fr:
am: am
formats:
default: "%d/%m/%Y %H:%M:%S %:z"
long: "%d/%m/%Y %H:%M:%S"
short: "%d %b %Hh%M"
time: "%H:%M:%S"
pm: pm

@ -0,0 +1,16 @@
class AddRefreshToAnalysis < ActiveRecord::Migration[7.0]
def up
add_column :analyses, :refresh_at, :timestamp
delay = Rails.configuration.refresh_delay
progress = ProgressBar.create total: Analysis.count, format: '%t (%c/%C) %W %e'
Analysis.all.each do |analysis|
analysis.update_column :refresh_at, analysis.updated_at + delay
progress.increment
end
end
def down
remove_column :analyses, :refresh_at
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2022_03_26_181216) do
ActiveRecord::Schema[7.0].define(version: 2022_06_26_133648) do
# These are extensions that must be enabled in order to support this database
enable_extension "pgcrypto"
enable_extension "plpgsql"
@ -23,6 +23,7 @@ ActiveRecord::Schema[7.0].define(version: 2022_03_26_181216) do
t.datetime "created_at", precision: nil, null: false
t.datetime "updated_at", precision: nil, null: false
t.jsonb "args"
t.datetime "refresh_at", precision: nil
t.index ["service", "host", "args"], name: "index_analyses_on_service_and_host_and_args", unique: true
end

@ -0,0 +1,55 @@
require 'rails_helper'
describe Analysis do
describe '::find_refresh_delay' do
it 'must return host value' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([::Resolv::DNS::Resource::IN::TXT.new('cryptcheck=PT1M')])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to eq 1.minute
end
it 'must recurse domain value' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([])
expect(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([::Resolv::DNS::Resource::IN::TXT.new('cryptcheck=PT1M')])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to eq 1.minute
end
it 'must not test TLD' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([])
expect(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to eq 1.hour
end
it 'must support debug mode' do
expect(Analysis::RESOLVER).to receive(:getresources)
.with('www.example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_return([::Resolv::DNS::Resource::IN::TXT.new('cryptcheck=debug')])
allow(Analysis::RESOLVER).to receive(:getresources)
.with('example.org', ::Resolv::DNS::Resource::IN::TXT)
.and_raise(Exception, :must_not_be_called)
host = Analysis.new host: 'www.example.org'
expect(host.find_refresh_delay).to be_nil
end
end
end
Loading…
Cancel
Save