Add batch install cert feature.
This commit is contained in:
BoHung Chiu 2021-12-09 17:59:15 +08:00
parent 10b6a4ec25
commit 2f1888f568
11 changed files with 178 additions and 37 deletions

View File

@ -246,19 +246,31 @@ class Admin::SitePanelController < OrbitAdminController
SiteConstruct.find(params[:id]).destroy
redirect_to :back and return
elsif params[:type] == 'select_cert'
if !params[:is_server]
@site_construct = SiteConstruct.find(params[:id])
@site_construct.update(:redirect_to_https=>params[:redirect_to_https])
is_certbot = true
if params[:site_cert_id] != "certbot"
is_certbot = false
@site_construct.update(:site_cert_id=>BSON::ObjectId(params[:site_cert_id]))
is_certbot = true
if params[:server_names]
is_certbot = false
Thread.new do
last_idx = params[:server_names].count
params[:server_names].each_with_index do |server_name, i|
ss = SiteServer.where(:server_name=>server_name).first
next if ss.nil?
system("bundle exec rake create_site:change_site_cert[#{ss.id.to_s},#{is_certbot},true,#{params[:site_cert_id]},#{params[:redirect_to_https]}#{i == (last_idx -1) ? ',true' : ''}}]")
end
end
else
is_certbot = true
end
Thread.new do
system("bundle exec rake create_site:change_site_cert[#{params[:id]},#{is_certbot},#{params[:is_server]}]")
if !params[:is_server]
@site_construct = SiteConstruct.find(params[:id])
@site_construct.update(:redirect_to_https=>params[:redirect_to_https])
if params[:site_cert_id] != "certbot"
is_certbot = false
@site_construct.update(:site_cert_id=>BSON::ObjectId(params[:site_cert_id]))
end
else
is_certbot = true
end
Thread.new do
system("bundle exec rake create_site:change_site_cert[#{params[:id]},#{is_certbot},#{params[:is_server]}]")
end
end
else
Thread.new do

View File

@ -82,12 +82,12 @@ class ClientManagementsController < CPanelController
def site_tickets
if params[:category].present? && params[:keyword].present?
regex = Regexp.new(".*" + params[:keyword] + ".*", "i")
regex = ::Regexp.new(".*" + params[:keyword] + ".*", "i")
@tickets = @site.tickets.where(:category_id => params[:category], :subject => regex).order_by([:status, :desc],[:created_at, :desc]).page(params[:page]).per(10)
elsif params[:category].present?
@tickets = @site.tickets.where(:category_id => params[:category]).order_by([:status, :desc],[:created_at, :desc]).page(params[:page]).per(10)
elsif params[:keyword].present?
regex = Regexp.new(".*" + params[:keyword] + ".*", "i")
regex = ::Regexp.new(".*" + params[:keyword] + ".*", "i")
@tickets = @site.tickets.where(:subject => regex).order_by([:status, :desc],[:created_at, :desc]).page(params[:page]).per(10)
else
@tickets = @site.tickets.order_by([:status, :desc],[:created_at, :desc]).page(params[:page]).per(10)

View File

@ -34,7 +34,7 @@ class SiteCert
cert_file_md5 = `openssl x509 -noout -modulus -in #{self.cert_file.file.file} | openssl md5`
private_key_md5 = `openssl rsa -noout -modulus -in #{self.private_key.file.file} | openssl md5`
is_valid = (cert_file_md5 == private_key_md5)
domain_names = `openssl x509 -text < #{self.cert_file.file.file} | grep 'DNS:' | sed 's/\s*DNS:\([a-z0-9.\-]*\)[,\s]\?/\1 /g'`.split('DNS:').map{|s| s.strip}.select{|s| s.present?} rescue []
domain_names = `openssl x509 -text < #{self.cert_file.file.file} | grep 'DNS:' | sed 's/\s*DNS:\([a-z0-9.\-]*\)[,\s]\?/\1 /g'`.split('DNS:').map{|s| s.sub(',','').strip}.select{|s| s.present?} rescue []
if domain_names.blank?
self.is_valid = false
@skip_callback = true
@ -53,4 +53,13 @@ class SiteCert
false
end
end
def valid_domain_names(site_names)
site_names = site_names.split(" ").map{|s| s.strip}
valid_site_names = []
self.domain_names.each do |d|
regx = ::Regexp.new("\\A"+d.gsub('.',"\\.").gsub('*','[^\\.]+').sub(',','').strip)
valid_site_names += site_names.select{|s| !(s.match(regx).nil?)}
end
return valid_site_names
end
end

View File

@ -83,7 +83,7 @@ class SiteConstruct
}.join('\n')
end
def match_exact_index(text,match_character,level=1)
text.enum_for(:scan,/(?:[^#{match_character}])#{match_character}{#{level}}(?!#{match_character})/m).map { offset_index=Regexp.last_match.to_s.index(match_character);Regexp.last_match.offset(0).first + offset_index}
text.enum_for(:scan,/(?:[^#{match_character}])#{match_character}{#{level}}(?!#{match_character})/m).map { offset_index=::Regexp.last_match.to_s.index(match_character);::Regexp.last_match.offset(0).first + offset_index}
end
def parse_nginx_text_to_server_blocks(nginx_text,get_all_blocks=false,level=1)
num = 1
@ -231,7 +231,7 @@ class SiteConstruct
domain_names = domain_name.strip().split(" ")
if default_domain_idx == -1
site_server.default_domain_names.each do |default_domain_name|
default_domain_name = Regexp.new("\\A"+default_domain_name.gsub(".","\\.").gsub("*","[^.]*"))
default_domain_name = ::Regexp.new("\\A"+default_domain_name.gsub(".","\\.").gsub("*","[^.]*"))
custom_default_domain_name = domain_names.select{|n| n.match(default_domain_name) }.first
break if custom_default_domain_name.present?
end

View File

@ -42,12 +42,12 @@ class SiteServer
add_default_domains = self.default_domain_names - self.default_domain_names_was.to_a
check_domains += removed_default_domains
check_domains += add_default_domains
check_domains = check_domains.map{|domain| Regexp.new("(\\A|\\s)"+domain.gsub(".","\\.").gsub("*","[^.]*") + "($|\\s)")}
check_domains = check_domains.map{|domain| ::Regexp.new("(\\A|\\s)"+domain.gsub(".","\\.").gsub("*","[^.]*") + "($|\\s)")}
tmp_site_constructs = tmp_site_constructs.where(:domain_name.in=>check_domains)
need_change = true
elsif need_change
check_domains = self.default_domain_names
check_domains = check_domains.map{|domain| Regexp.new("(\\A|\\s)"+domain.gsub(".","\\.").gsub("*","[^.]*") + "($|\\s)")}
check_domains = check_domains.map{|domain| ::Regexp.new("(\\A|\\s)"+domain.gsub(".","\\.").gsub("*","[^.]*") + "($|\\s)")}
tmp_site_constructs = tmp_site_constructs.where(:domain_name.in=>check_domains)
end
end

View File

@ -24,6 +24,7 @@
<td><%=site_cert.generate_file_link('ca_bundle')%></td>
<td><%=site_cert.generate_file_link('private_key')%></td>
<td>
<%= link_to t("client_management.batch_install"),"#",:class=>"btn btn-success btn-small batch_install_cert",:data=>{:id=>site_cert.id.to_s} %>
<%= link_to t(:edit),admin_site_panel_edit_cert_path(site_cert.id),:class=>"btn btn-primary btn-small" %>
<%= link_to t(:delete_),admin_site_panel_destroy_cert_path(site_cert.id),:class=>"btn btn-danger btn-small",:data=>{:method=>"delete",:confirm=>t("client_management.confirm_delete")} %>
</td>
@ -48,4 +49,108 @@
color: red;
font-size: 2em;
}
</style>
</style>
<script>
var close_info = false;
var timeout_id;
var cert_id;
var status_relation = {"starting":"<span style=\"color: skyblue;\">starting</span>","execing":"<span style=\"color: skyblue;\">execing</span>","detecting":"<span style=\"color: skyblue;\">detecting</span>","error":"<span style=\"color: red;\">error</span>","finish": "<span style=\"color: darkseagreen;\">finish</span>","closed":"<span style=\"color: red;\">closed</span>"};
$(".batch_install_cert").off('click').on('click',function(){
cert_id = $(this).data("id");
$.post("<%=admin_site_panel_edit_server_info_path%>",{"type":"get_server_names"}).done(function(server_names){
var server_names = Array.isArray(server_names) ? server_names : [];
show_exec_commands_block(server_names);
})
});
function show_exec_commands_block(server_names){
if($("#batch_install_cert-dialog-confirm").length == 0){
$("#main-wrap").before("<div id='batch_install_cert-dialog-confirm' title='Select servers need to install this cert'>"+
"<div style='clear: both;'></div><div class='control-group'><label style='font-size: 1.2em; font-weight: bold;'><%= check_box_tag("redirect_to_https",1,(@site_construct.redirect_to_https rescue false)).gsub('"',"'").html_safe %><%=t("client_management.redirect_to_https")%></label></div><div id='server_names_block'></div></div>");
};
$( "#batch_install_cert-dialog-confirm" ).dialog({
resizable: true,
minHeight: 300,
maxHeight: 400,
modal: true,
width: '80%',
open: function(){
$(this).parent().css("top",$(document).height() - $(window).height() + "px");
},
close: function(){$( this ).dialog( "close" );},
buttons: {
"<%= t(:submit) %>": function(){$( this ).dialog( "close" );exec_commands()},
}
});
$("#server_names_block").html($.map(server_names,function(server_name){
return "<input style='float: left;' class='server_name_checkbox' type='checkbox' id='server_name_"+server_name+"'><label style='float: left;' for='server_name_"+server_name+"'>"+server_name+"</label>"
}))
}
function exec_commands(){
var server_names = [];
var $els = $(".server_name_checkbox");
for(var i = 0; i < $els.length; i++){
if($els.eq(i).prop("checked")){
console.log($els.eq(i));
server_names.push($("label[for='"+$els.eq(i).attr('id')+"']").text().trim());
}
}
var redirect_to_https = ($('#batch_install_cert-dialog-confirm [name="redirect_to_https"]:checked').length != 0);
$.post("<%=admin_site_panel_edit_site_path%>",{'server_names':server_names,'type':'select_cert', 'is_server': true, 'site_cert_id': cert_id, 'redirect_to_https': redirect_to_https}).done(function(){
show_infos_dialog("batch_install_certs");
});
}
function see_infos(key){
key = key || "";
if(!close_info){
var request = $.post("<%=admin_site_panel_edit_server_info_path%>",{"type":'see_infos',"key":key});
request.done(function(data){
var infos = request.responseJSON.infos;
var status = request.responseJSON.status;
if($("#info_texts").length == 0)
return infos.join("<br>")
else{
if(status == "")
var status_text = "not yet create";
else
var status_text = status_relation[status];
if(!status_text){
status_text = "<span style=\"color: skyblue;\">"+status+"</span>";
}
$("#info_texts").html(status_text+"<div style='clear:both;'></div>"+infos.join("<br>"));
msg_end.scrollIntoView();
timeout_id = window.setTimeout(see_infos(key),1000);
}
})
}else{
window.clearTimeout(timeout_id);
}
};
function show_infos_dialog(key){
key = key || "";
close_info = true;
window.clearTimeout(timeout_id);
close_info = false;
if($("#dialog-confirm").length == 0){
$("#main-wrap").before("<div id='dialog-confirm' title='site infos'>"+
"<div style='clear:both;'></div><div id='info_texts'>"+see_infos(key)+"</div><div id='msg_end' style='height:0px; overflow:hidden'></div>"+
"</div>");
}else{
see_infos(key);
};
$( "#dialog-confirm" ).dialog({
resizable: true,
minHeight: 100,
maxHeight: 400,
width: '80%',
modal: true,
open: function(){
$(this).parent().css("top",$(document).height() - $(window).height() + "px");
},
close: function(){close_info = true;},
buttons: {
"<%= t('client_management.confirm') %>": function(){$( this ).dialog( "close" );close_info = true;},
"stop update": function(){close_info = true;}
}
});
}
</script>

View File

@ -210,7 +210,7 @@
$(".install_certbot_certs").off("click").on("click",function(){
var id = $(this).data("id");
$.post("<%=admin_site_panel_edit_site_path%>",{'id': id,'type':'select_cert', 'is_server': true}).done(function(response){
show_infos_dialog("install_certbot_certs");
show_infos_dialog("batch_install_certs");
});
})
})

View File

@ -7,6 +7,7 @@ en:
upload_cert: Upload Cert
cert_management: Cert Management
client_management:
batch_install: Batch Install
install_certbot_certificates_in_batches: Install certbot certificates in batches
update_nginx_settings: "Update nginx settings"
set_to_default_domain_name: "Set to default domain name"

View File

@ -7,6 +7,7 @@ zh_tw:
upload_cert: 上傳憑證
cert_management: 憑證管理
client_management:
batch_install: 批次安裝
install_certbot_certificates_in_batches: 批次安裝certbot憑證
update_nginx_settings: "修改nginx設定"
set_to_default_domain_name: "設定為預設的domain name"

View File

@ -1,7 +1,7 @@
Rails.application.routes.draw do
locales = Site.find_by(site_active: true).in_use_locales rescue I18n.available_locales
scope "(:locale)", locale: Regexp.new(locales.join("|")) do
scope "(:locale)", locale: ::Regexp.new(locales.join("|")) do
namespace :admin do
# get "client_managements/completed_requests", to: 'client_managements#completed_requests'
# get "client_managements/contracts", to: 'client_managements#contracts'

View File

@ -3,23 +3,24 @@ require 'pathname'
require 'json'
namespace :create_site do
desc "Change Site Cert"
task :change_site_cert,[:id,:is_certbot,:id_is_server] => :environment do |task,args|
task :change_site_cert,[:id,:is_certbot,:id_is_server,:cert_id,:redirect_to_https,:exec_finish] => :environment do |task,args|
begin
site_server = nil
site_constructs = []
@is_multithread = false
@thread = nil
@default_cert = args.cert_id ? SiteCert.find(args.cert_id) : nil
if args.id_is_server
site_server = SiteServer.find(args.id)
site_constructs = site_server.site_constructs.to_a
@is_multithread = true
Multithread.where(:key=>'install_certbot_certs').each{|thread| thread.destroy if (thread.status["status"] == "error" || thread.status["status"] == "finish")}
Multithread.where(:key=>'install_certbot_certs').destroy
@thread = Multithread.where(:key=>'install_certbot_certs').first
Multithread.where(:key=>'batch_install_certs').each{|thread| thread.destroy if (thread.status["status"] == "error" || thread.status["status"] == "finish")}
Multithread.where(:key=>'batch_install_certs').destroy
@thread = Multithread.where(:key=>'batch_install_certs').first
if @thread.nil?
@thread = Multithread.create(:key=>'install_certbot_certs',:status=>{"infos"=>[],"status"=>"execing"})
@thread = Multithread.create(:key=>'batch_install_certs',:status=>{"infos"=>[],"status"=>"execing"})
else
return false
return false if @default_cert.nil?
end
else
@site_construct = SiteConstruct.find(args.id)
@ -31,6 +32,7 @@ namespace :create_site do
if !site_server.nil?
@password = site_server.password
Net::SSH.start(site_server.ip , site_server.account , password: site_server.password) do |ssh|
redirect_to_https = args.redirect_to_https ? true : (@site_construct.redirect_to_https rescue false)
if tmp_is_certbot || (@site_cert.is_certbot rescue false)
domain_names = site_constructs.flat_map{|site_construct| site_construct.domain_name.strip.split(" ")}.select{|d| d.present? && d.match(/^[\d\.]+$/).nil?}
domain_names = domain_names.select{|d| `dig #{d} +short`.split("\n").select{|s| s.strip == site_server.ip}.count != 0}
@ -41,7 +43,6 @@ namespace :create_site do
if site_constructs.count > 1
auto_update_infos("Generating single cert for multiple domain names...")
end
redirect_to_https = (@site_construct.redirect_to_https rescue false)
exec_ssh_command_by_sudo_and_see_output(ssh,"sudo -p 'sudo password:' #{certbot_path} --nginx #{domain_names.map{|d| "-d " + d}.join(" ")} -n --#{redirect_to_https ? 'redirect' : 'no-redirect'}",true,false)
site_constructs.each do |site_construct|
@site_construct = site_construct
@ -73,7 +74,7 @@ namespace :create_site do
site_construct.update(:site_cert=>site_cert)
end
all_ports = (site_construct.port + ["443"]).uniq
site_construct.update(:port=> all_ports )
site_construct.update(:port=> all_ports, :status=>'finish' )
auto_update_infos("Finish installing cert with certbot in #{site_construct.site_name} !")
else
auto_update_infos("Certbot generate cert failed!")
@ -89,13 +90,19 @@ namespace :create_site do
end
else
site_constructs.each do |site_construct|
@site_cert = @site_construct.site_cert
@site_cert = @default_cert || @site_construct.site_cert
next if @site_cert.nil?
is_certbot = tmp_is_certbot || (@site_cert.is_certbot rescue false)
@site_construct = site_construct
@site_cert.change_data
if @site_cert.is_valid
valid_domain_names = @site_cert.valid_domain_names(@site_construct.domain_name)
next if valid_domain_names.count == 0
if @default_cert
@site_construct.update(:site_cert=>@default_cert,:redirect_to_https=>redirect_to_https)
end
auto_update_infos("Copying Cert to #{@site_construct.server_type}...")
auto_update_infos("Installing Cert on #{valid_domain_names.join(" , ")}")
cert_file_content = [(@site_cert.cert_file.file.read.strip rescue ""),(@site_cert.ca_bundle.file.read.strip rescue "")].join("\n").strip
private_key_content = @site_cert.private_key.file.read
cert_file_store_path = @site_construct.cert_file_remote_store_path
@ -108,7 +115,7 @@ namespace :create_site do
auto_update_infos("Setting Cert...")
nginx_file_content = exec_command_by_user(ssh,"cat #{@site_construct.nginx_file}")
all_ports = (@site_construct.port + ["443"]).uniq
@site_construct.update(:port=> all_ports )
@site_construct.update(:port=> all_ports,:status=>'finish' )
nginx_file_content = @site_construct.generate_nginx_text(nginx_file_content)
cmd = "x='#{nginx_file_content}'; echo '#{@password}' | sudo -S sh -c \"echo '$x' > #{@site_construct.nginx_file}\""
exec_command_by_user(ssh,cmd)
@ -116,11 +123,13 @@ namespace :create_site do
end
end
exec_ssh_command_by_sudo(ssh,"service nginx restart")
auto_update_infos("Finish!")
if @is_multithread
@thread.update(:status=>@thread.status.merge({"status"=>"finish"}))
else
@site_construct.update(:status=>"finish")
if @default_cert.nil? || args.exec_finish
auto_update_infos("Finish!")
if @is_multithread
@thread.update(:status=>@thread.status.merge({"status"=>"finish"}))
else
@site_construct.update(:status=>"finish")
end
end
end
else
@ -130,7 +139,11 @@ namespace :create_site do
rescue => e
puts [e,e.backtrace]
auto_update_infos(e.to_s)
@site_construct.update(:status=>"error")
if @is_multithread
@thread.update(:status=>@thread.status.merge({"status"=>"error"}))
else
@site_construct.update(:status=>"error")
end
end
end
def exec_command_by_user(session,command)