Initial commit
This commit is contained in:
2
.bundle/config
Normal file
2
.bundle/config
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
BUNDLE_PATH: "vendor/bundle"
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
vendor
|
||||
db/*.sqlite
|
||||
.ruby-lsp
|
||||
logs/actions.log
|
||||
utils/custom_config.sh
|
||||
Steepfile
|
||||
caapp.private.key.pem
|
||||
caapp.public.key.pem
|
||||
!.gitignore
|
||||
62
Dockerfile
Normal file
62
Dockerfile
Normal file
@@ -0,0 +1,62 @@
|
||||
FROM almalinux:9
|
||||
|
||||
RUN yum -y install dnf-plugins-core && \
|
||||
dnf config-manager --set-enabled crb
|
||||
|
||||
# Install build tools and dependencies for RVM / Ruby
|
||||
RUN yum -y update && \
|
||||
yum -y --allowerasing install \
|
||||
curl git gnupg2 \
|
||||
gcc gcc-c++ patch \
|
||||
readline-devel zlib-devel libyaml-devel libffi-devel openssl-devel ruby ruby-devel which procps-ng && \
|
||||
yum clean all
|
||||
|
||||
# Install RVM and Ruby 3.3.0
|
||||
RUN gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys \
|
||||
409B6B1796C275462A1703113804BB82D39DC0E3 \
|
||||
7D2BAF1CF37B13E2069D6956105BD0E739499BDB && \
|
||||
curl -sSL https://get.rvm.io | bash -s stable --ruby=3.3.0 && \
|
||||
echo 'source /etc/profile.d/rvm.sh' >> /etc/profile && \
|
||||
/bin/bash -lc "source /etc/profile.d/rvm.sh && gem install bundler"
|
||||
|
||||
# Make RVM binaries available
|
||||
ENV PATH="/usr/local/rvm/bin:${PATH}"
|
||||
|
||||
# Set Ruby 3.3.0 as the default version
|
||||
RUN /bin/bash -lc "source /etc/profile.d/rvm.sh && rvm use 3.3.0 --default"
|
||||
|
||||
# Create application user
|
||||
RUN useradd -m certuser
|
||||
|
||||
# Create project directory and set working directory
|
||||
WORKDIR /opt/cert/certcenter
|
||||
|
||||
# Copy only the specified directories and files
|
||||
COPY ca ./ca/
|
||||
COPY classes ./classes/
|
||||
COPY db ./db/
|
||||
COPY docs ./docs/
|
||||
COPY locale ./locale/
|
||||
COPY logs ./logs/
|
||||
COPY locks ./locks/
|
||||
COPY migration ./migration/
|
||||
COPY models ./models/
|
||||
COPY public ./public/
|
||||
COPY utils ./utils/
|
||||
COPY views ./views/
|
||||
COPY .bundle ./.bundle/
|
||||
COPY app.rb Gemfile Gemfile.lock ./
|
||||
|
||||
# Make the CA directory a bind mount point
|
||||
VOLUME /opt/cert/certcenter/ca
|
||||
VOLUME /opt/cert/certcenter/logs
|
||||
|
||||
# Prepare the application
|
||||
RUN /bin/bash -lc "source /etc/profile.d/rvm.sh && chmod +x ./utils/make_app_keys.sh" && \
|
||||
/bin/bash -lc "source /etc/profile.d/rvm.sh && ./utils/make_app_keys.sh ." && \
|
||||
/bin/bash -lc "source /etc/profile.d/rvm.sh && bundle install" && \
|
||||
/bin/bash -lc "source /etc/profile.d/rvm.sh && bundle exec sequel -m migration sqlite://db/base.sqlite"
|
||||
|
||||
EXPOSE 4567
|
||||
|
||||
CMD ["bash", "-lc", "source /etc/profile.d/rvm.sh && bundle exec ruby app.rb"]
|
||||
25
Gemfile
Normal file
25
Gemfile
Normal file
@@ -0,0 +1,25 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
source 'https://rubygems.org'
|
||||
|
||||
git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
|
||||
|
||||
# gem "rails"
|
||||
|
||||
gem "sinatra", "~> 4.2"
|
||||
|
||||
gem "sqlite3", "~> 2.9"
|
||||
|
||||
gem "sequel", "~> 5.101"
|
||||
|
||||
gem "puma", "~> 7.2"
|
||||
|
||||
gem "rackup", "~> 2.3"
|
||||
|
||||
gem "rubyzip", "~> 3.2"
|
||||
|
||||
gem "jwt", "~> 3.1"
|
||||
|
||||
gem "openssl", "~> 4.0"
|
||||
|
||||
gem "i18n", "~> 1.14"
|
||||
61
Gemfile.lock
Normal file
61
Gemfile.lock
Normal file
@@ -0,0 +1,61 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
base64 (0.3.0)
|
||||
bigdecimal (4.0.1)
|
||||
concurrent-ruby (1.3.6)
|
||||
i18n (1.14.8)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jwt (3.1.2)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_portile2 (2.8.9)
|
||||
mustermann (3.0.4)
|
||||
ruby2_keywords (~> 0.0.1)
|
||||
nio4r (2.7.5)
|
||||
openssl (4.0.0)
|
||||
puma (7.2.0)
|
||||
nio4r (~> 2.0)
|
||||
rack (3.2.4)
|
||||
rack-protection (4.2.1)
|
||||
base64 (>= 0.1.0)
|
||||
logger (>= 1.6.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-session (2.1.1)
|
||||
base64 (>= 0.1.0)
|
||||
rack (>= 3.0.0)
|
||||
rackup (2.3.1)
|
||||
rack (>= 3)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (3.2.2)
|
||||
sequel (5.101.0)
|
||||
bigdecimal
|
||||
sinatra (4.2.1)
|
||||
logger (>= 1.6.0)
|
||||
mustermann (~> 3.0)
|
||||
rack (>= 3.0.0, < 4)
|
||||
rack-protection (= 4.2.1)
|
||||
rack-session (>= 2.0.0, < 3)
|
||||
tilt (~> 2.0)
|
||||
sqlite3 (2.9.0)
|
||||
mini_portile2 (~> 2.8.0)
|
||||
sqlite3 (2.9.0-x86_64-linux-gnu)
|
||||
tilt (2.7.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
x86_64-linux
|
||||
|
||||
DEPENDENCIES
|
||||
i18n (~> 1.14)
|
||||
jwt (~> 3.1)
|
||||
openssl (~> 4.0)
|
||||
puma (~> 7.2)
|
||||
rackup (~> 2.3)
|
||||
rubyzip (~> 3.2)
|
||||
sequel (~> 5.101)
|
||||
sinatra (~> 4.2)
|
||||
sqlite3 (~> 2.9)
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.22
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
bash utils/make_app_keys.sh .
|
||||
bundle install
|
||||
bundle exec sequel -m migration sqlite://db/base.sqlite
|
||||
bundle exec ruby app.rb
|
||||
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/login -H "Content-Type: application/x-www-form-urlencoded" -d 'login=admin&password=admin'
|
||||
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/servers -H "Content-Type: application/x-www-form-urlencoded" -d 'token=...'
|
||||
|
||||
# 1️⃣ Перейдите в корень проекта
|
||||
cd /home/alexey/projects/workspace-zed/certcenter
|
||||
|
||||
# 2️⃣ Соберите образ
|
||||
docker build -t certcenter:latest .
|
||||
|
||||
# 3️⃣ Запустите контейнер, пробросив порт 9090 наружу
|
||||
# и указав, что каталог /tmp/ca на хосте должен быть смонтирован в /tmp/ca внутри контейнера
|
||||
docker run -d \
|
||||
--name certcenter \
|
||||
-p 9090:4567 \
|
||||
-v /tmp/ca:/opt/cert/certcenter/ca \
|
||||
-v /tmp/logs:/opt/cert/certcenter/logs \
|
||||
certcenter:latest
|
||||
|
||||
|
||||
docker run -d --name certcenter -p 9090:4567 -v /opt/ca:/opt/cert/certcenter/ca -v /opt/logs:/opt/cert/certcenter/logs ertcenter:latest
|
||||
522
classes/cert.rb
Normal file
522
classes/cert.rb
Normal file
@@ -0,0 +1,522 @@
|
||||
require 'date'
|
||||
require 'zip'
|
||||
require 'i18n'
|
||||
require_relative 'runner'
|
||||
|
||||
class CertManager
|
||||
attr_accessor :error, :log, :root_ca
|
||||
|
||||
def initialize()
|
||||
@root_ca = gt_root_dir
|
||||
if @root_ca.nil?
|
||||
@error = I18n.t('errors.cannot_determine_root_dir')
|
||||
end
|
||||
end
|
||||
|
||||
def log?
|
||||
@log
|
||||
end
|
||||
|
||||
def rootca?
|
||||
@root_ca
|
||||
end
|
||||
|
||||
def error?
|
||||
!(@error.nil? || @error.strip == '')
|
||||
end
|
||||
|
||||
def add_cert(days, domains_ips_list)
|
||||
begin
|
||||
@error = nil
|
||||
@log = nil
|
||||
new_cert_info = nil
|
||||
days = begin
|
||||
Integer(days)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
raise ArgumentError, I18n.t('errors.argument_error_days') if days.nil? || days <= 0
|
||||
|
||||
domains_ips = domains_ips_list.split(/[\s,]+/).reject(&:empty?).uniq
|
||||
raise ArgumentError, I18n.t('errors.argument_error_domains_ips') if domains_ips.empty?
|
||||
|
||||
result = ''
|
||||
current_directory = Dir.pwd
|
||||
Dir.chdir('utils') do
|
||||
cmd_args = %Q(bash ./make_server_cert.sh -t #{days} #{domains_ips.join(' ')} 2>&1)
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
result = cmd.stdout
|
||||
raise StandardError, I18n.t('errors.command_execution_error', command: cmd_args) if cmd.exit_status != 0
|
||||
result.each_line do |line|
|
||||
if line =~ /\[OUTPUTDATA_CERT\]/
|
||||
match = line.match(/([^\/]*?)\.cert\.pem\.(\d+)/)
|
||||
if match
|
||||
new_cert_info = { name: match[1], seq: match[2] }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
raise StandardError, I18n.t('errors.no_result_file') if new_cert_info.nil?
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
@error = e.message
|
||||
rescue StandardError => e
|
||||
@error = e.message
|
||||
@log = result
|
||||
Dir.chdir(current_directory)
|
||||
ensure
|
||||
end
|
||||
@log = result
|
||||
if new_cert_info.nil?
|
||||
nil
|
||||
else
|
||||
get_cert_id_by_name(domains_ips[0], new_cert_info[:seq], new_cert_info[:name], 's')
|
||||
end
|
||||
end
|
||||
|
||||
def add_client_cert(server_domain, client_id, days)
|
||||
begin
|
||||
@error = nil
|
||||
@log = nil
|
||||
new_cert_info = nil
|
||||
raise ArgumentError, I18n.t('errors.argument_error_server_domain') if server_domain.strip.empty?
|
||||
raise ArgumentError, I18n.t('errors.argument_error_client_id') if client_id.strip.empty?
|
||||
|
||||
days = begin
|
||||
Integer(days)
|
||||
rescue StandardError
|
||||
nil
|
||||
end
|
||||
raise ArgumentError, I18n.t('errors.argument_error_days') if days.nil? || days <= 0
|
||||
|
||||
@error = nil
|
||||
result = ''
|
||||
current_directory = Dir.pwd
|
||||
Dir.chdir('utils') do
|
||||
cmd_args = %Q(bash ./make_client_cert.sh -s #{server_domain} -c #{client_id} -d #{days} 2>&1)
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
result = cmd.stdout
|
||||
raise StandardError, I18n.t('errors.command_execution_error', command: cmd_args) if cmd.exit_status != 0
|
||||
result.each_line do |line|
|
||||
if line =~ /\[OUTPUTDATA_CERT\]/
|
||||
match = line.match(/([^\/]*?)\.cert\.pem\.(\d+)/)
|
||||
if match
|
||||
new_cert_info = { name: match[1], seq: match[2] }
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
raise StandardError, I18n.t('errors.no_result_file') if new_cert_info.nil?
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
@error = e.message
|
||||
rescue StandardError => e
|
||||
@error = e.message
|
||||
Dir.chdir(current_directory)
|
||||
ensure
|
||||
end
|
||||
@log = result
|
||||
if new_cert_info.nil?
|
||||
nil
|
||||
else
|
||||
get_cert_id_by_name(server_domain, new_cert_info[:seq], new_cert_info[:name], 'c')
|
||||
end
|
||||
end
|
||||
|
||||
def get_server_certs
|
||||
get_list_certs('s')
|
||||
end
|
||||
|
||||
def get_clients_certs(server_domain)
|
||||
list = get_list_certs('c')
|
||||
if server_domain == ''
|
||||
list
|
||||
else
|
||||
filtered_list = list.select { |entry| entry[:ui][:CN] == server_domain }
|
||||
filtered_list.sort_by! { |entry| entry[:id] }
|
||||
end
|
||||
end
|
||||
|
||||
def get_cert_info(id)
|
||||
@log = ""
|
||||
@error = nil
|
||||
list_certs = get_list_certs('*')
|
||||
target_id = id
|
||||
found_entry = list_certs.find do |entry|
|
||||
entry[:id] == target_id
|
||||
end
|
||||
if found_entry
|
||||
return found_entry
|
||||
else
|
||||
@error = I18n.t('errors.record_not_found')
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
def get_detail_cert_info(id)
|
||||
@log = nil
|
||||
@error = nil
|
||||
cert_info = { common: nil, revoke: nil, is_client: nil, name: nil, id: id }
|
||||
|
||||
if @root_ca.nil?
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return cert_info
|
||||
end
|
||||
|
||||
cert_item_data = get_cert_info(id)
|
||||
|
||||
if !@error.nil? || cert_item_data.nil?
|
||||
return cert_info
|
||||
end
|
||||
|
||||
cert_item = get_cert_path(cert_item_data)
|
||||
cert_path = if cert_item[:is_client]
|
||||
cert_item[:client]
|
||||
else
|
||||
cert_item[:server]
|
||||
end
|
||||
|
||||
unless File.exist?(cert_path)
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return cert_info
|
||||
end
|
||||
|
||||
cmd_args = %Q(openssl x509 -in "#{cert_path}" -text -noout 2>&1)
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
if cmd.exit_status != 0
|
||||
@error = I18n.t('errors.cannot_get_certificate_info')
|
||||
@log = cmd.stdout
|
||||
return cert_info
|
||||
end
|
||||
|
||||
cert_info[:common] = cmd.stdout
|
||||
|
||||
cmd_args = %Q(openssl verify -crl_check_all -CAfile "#{@root_ca}/ca/intermediate/certs/ca-chain.cert.pem" -CRLfile "#{@root_ca}/ca/intermediate/crl/ca-full.crl.pem" "#{cert_path}" 2>&1)
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
cert_info[:revoke] = cmd.stdout
|
||||
cert_info[:name] = "/#{cert_item_data[:ui][:O]}/#{cert_item_data[:ui][:CN]}/"
|
||||
|
||||
cert_info
|
||||
end
|
||||
|
||||
def revoke_certificat(id)
|
||||
@error = nil
|
||||
@log = nil
|
||||
cert_info = get_cert_info(id)
|
||||
if cert_info.nil?
|
||||
nil
|
||||
else
|
||||
cert_data = get_cert_path(cert_info)
|
||||
if cert_data[:is_client]
|
||||
revoke_client_cert(cert_data[:server_name], cert_data[:client_id], cert_data[:seq])
|
||||
else
|
||||
revoke_cert(cert_data[:server_name], cert_data[:seq])
|
||||
end
|
||||
if @error.nil?
|
||||
cert_info = get_cert_info(id)
|
||||
if cert_info.nil?
|
||||
nil
|
||||
else
|
||||
cert_info[:is_client] = cert_data[:is_client]
|
||||
cert_info
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_cert_binary(id)
|
||||
@error = nil
|
||||
@log = nil
|
||||
files_list = []
|
||||
readme_txt = ""
|
||||
cert_data = get_cert_info(id)
|
||||
if cert_data.nil?
|
||||
nil
|
||||
else
|
||||
cert_path = get_cert_path(cert_data)
|
||||
if cert_path[:is_client]
|
||||
files_list << cert_path[:client]
|
||||
files_list << "#{@root_ca}/ca/client_certs/#{cert_path[:server_name]}/private/#{cert_path[:client_id]}_private.key.pem"
|
||||
files_list << "#{@root_ca}/ca/intermediate/certs/ca-chain.cert.pem"
|
||||
readme_txt = I18n.t('messages.client_readme', private_key: File.basename(files_list[1]), server_cert: File.basename(files_list[0]), ca_chain: File.basename(files_list[2]))
|
||||
else
|
||||
files_list << cert_path[:server]
|
||||
files_list << "#{@root_ca}/ca/intermediate/private/#{cert_path[:server_name]}.key.pem"
|
||||
files_list << "#{@root_ca}/ca/intermediate/certs/ca-chain.cert.pem"
|
||||
files_list << "#{@root_ca}/ca/intermediate/crl/ca-full.crl.pem"
|
||||
readme_txt = I18n.t('messages.server_readme', private_key: File.basename(files_list[1]), server_cert: File.basename(files_list[0]), ca_chain: File.basename(files_list[2]), crl: File.basename(files_list[3]))
|
||||
end
|
||||
if files_list.all? { |file| File.exist?(file) }
|
||||
zip_memory = Zip::OutputStream.write_buffer do |zos|
|
||||
files_list.each do |file|
|
||||
zos.put_next_entry(File.basename(file))
|
||||
File.open(file, 'rb') { |f| zos.write f.read }
|
||||
end
|
||||
text_entry_name = 'readme.txt'
|
||||
zos.put_next_entry(text_entry_name)
|
||||
zos.write readme_txt
|
||||
end
|
||||
{ zip: zip_memory.string, is_client: cert_path[:is_client] }
|
||||
else
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_root_info
|
||||
@log = nil
|
||||
@error = nil
|
||||
cert_info = { common: nil, revoke: nil, is_client: nil, name: nil, id: nil }
|
||||
|
||||
if @root_ca.nil?
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return cert_info
|
||||
end
|
||||
|
||||
org_nm = nil
|
||||
config_sh = File.read('utils/custom_config.sh')
|
||||
match = config_sh.match(/ORG_NAME="([^"]+)"/)
|
||||
org_nm = match[1] if match
|
||||
return nil if org_nm.nil?
|
||||
|
||||
cert_path = "#{root_ca}/ca/root/certs/ca.cert.pem"
|
||||
|
||||
unless File.exist?(cert_path)
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return cert_info
|
||||
end
|
||||
|
||||
cmd_args = %Q(openssl x509 -in "#{cert_path}" -text -noout 2>&1)
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
if cmd.exit_status != 0
|
||||
@error = I18n.t('errors.cannot_get_certificate_info')
|
||||
@log = cmd.stdout
|
||||
return cert_info
|
||||
end
|
||||
|
||||
cert_info[:common] = cmd.stdout
|
||||
|
||||
cmd_args = %Q(openssl verify -crl_check_all -CAfile "#{cert_path}" -CRLfile "#{@root_ca}/ca/root/crl/ca.crl.pem" "#{cert_path}" 2>&1)
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
cert_info[:revoke] = cmd.stdout
|
||||
cert_info[:name] = "/CN=#{org_nm}/"
|
||||
|
||||
cert_info
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def revoke_cert(server_domain, seq)
|
||||
current_dir = Dir.pwd
|
||||
begin
|
||||
@log = nil
|
||||
raise ArgumentError, I18n.t('errors.argument_error_server_domain') if server_domain.strip.empty?
|
||||
|
||||
@error = nil
|
||||
result = ''
|
||||
Dir.chdir('utils') do
|
||||
cmd_args = if seq.nil? || seq.empty?
|
||||
%Q(bash ./make_server_revoke.sh -s #{server_domain} 2>&1)
|
||||
else
|
||||
%Q(bash ./make_server_revoke.sh -n #{seq} -s #{server_domain} 2>&1)
|
||||
end
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
result = cmd.stdout
|
||||
raise StandardError, I18n.t('errors.command_execution_error', command: cmd_args) if cmd.exit_status != 0
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
@error = e.message
|
||||
rescue StandardError => e
|
||||
@error = e.message
|
||||
Dir.chdir(current_dir)
|
||||
ensure
|
||||
end
|
||||
@log = result
|
||||
end
|
||||
|
||||
def revoke_client_cert(server_domain, client_id, seq)
|
||||
current_dir = Dir.pwd
|
||||
begin
|
||||
@log = nil
|
||||
raise ArgumentError, I18n.t('errors.argument_error_server_domain') if server_domain.strip.empty?
|
||||
raise ArgumentError, I18n.t('errors.argument_error_client_id') if client_id.strip.empty?
|
||||
|
||||
@error = nil
|
||||
result = ''
|
||||
Dir.chdir('utils') do
|
||||
cmd_args = if seq.nil? || seq.empty?
|
||||
%Q(bash ./make_client_revoke.sh -s #{server_domain} -c #{client_id} 2>&1)
|
||||
else
|
||||
%Q(bash ./make_client_revoke.sh -s #{server_domain} -c #{client_id} -n #{seq} 2>&1)
|
||||
end
|
||||
cmd = Runner.new(cmd_args)
|
||||
cmd.run_clean
|
||||
result = cmd.stdout
|
||||
raise StandardError, I18n.t('errors.command_execution_error', command: cmd_args) if cmd.exit_status != 0
|
||||
end
|
||||
rescue ArgumentError => e
|
||||
@error = e.message
|
||||
rescue StandardError => e
|
||||
@error = e.message
|
||||
Dir.chdir(current_dir)
|
||||
ensure
|
||||
end
|
||||
@log = result
|
||||
end
|
||||
|
||||
def gt_root_dir
|
||||
root_ca = nil
|
||||
|
||||
config_sh = File.read('utils/custom_config.sh')
|
||||
match = config_sh.match(/ROOT_DIR="([^"]+)"/)
|
||||
root_ca = match[1] if match
|
||||
root_ca
|
||||
end
|
||||
|
||||
def get_cert_path(item)
|
||||
cl_name = item[:ui][:O].split(":")
|
||||
cert_file = if cl_name.length > 1
|
||||
"#{@root_ca}/ca/client_certs/#{item[:ui][:CN]}/#{cl_name[0]}.cert.pem.#{cl_name[1]}"
|
||||
else
|
||||
"#{@root_ca}/ca/client_certs/#{item[:ui][:CN]}/#{cl_name[0]}.cert.pem"
|
||||
end
|
||||
sr_name = item[:ui][:CN]
|
||||
serv_file = if cl_name.length > 1
|
||||
"#{@root_ca}/ca/intermediate/certs/#{sr_name}.cert.pem.#{cl_name[1]}"
|
||||
else
|
||||
"#{@root_ca}/ca/intermediate/certs/#{sr_name}.cert.pem"
|
||||
end
|
||||
is_client = File.exist?(cert_file)
|
||||
seq = if cl_name.length > 1
|
||||
cl_name[1]
|
||||
else
|
||||
nil
|
||||
end
|
||||
{ client: cert_file, server: serv_file, is_client: is_client, server_name: sr_name, seq: seq, client_id: cl_name[0] }
|
||||
end
|
||||
|
||||
def get_list_certs(type)
|
||||
|
||||
if @root_ca.nil?
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return []
|
||||
end
|
||||
|
||||
index_txt_path = "#{@root_ca}/ca/intermediate/index.txt"
|
||||
unless File.exist?(index_txt_path)
|
||||
@error = I18n.t('errors.root_ca_not_detected')
|
||||
return []
|
||||
end
|
||||
|
||||
ca_index_txt = File.read(index_txt_path, encoding: 'utf-8').split("\n").each_with_object([]) do |line, entries|
|
||||
match = line.split("\t")
|
||||
next if match.length != 6
|
||||
|
||||
exp = false
|
||||
date_tm = parse_time_string(match[1])
|
||||
if date_tm.nil?
|
||||
date_tm = "нет даты"
|
||||
else
|
||||
exp = date_tm[1] < DateTime.now
|
||||
date_tm = date_tm[0]
|
||||
end
|
||||
|
||||
date_tm_revoke = parse_time_string(match[2])
|
||||
if date_tm_revoke.nil?
|
||||
date_tm_revoke = "нет даты"
|
||||
else
|
||||
date_tm_revoke = date_tm_revoke[0]
|
||||
end
|
||||
|
||||
prep = { id: match[3], status: match[0], date: date_tm, fld: match[4], ui: nil, revoke_date: date_tm_revoke, expired: exp }
|
||||
|
||||
parts = match[5].split('/').reject(&:empty?).map(&:strip)
|
||||
cert_info = {}
|
||||
parts.each do |part|
|
||||
key, value = part.split('=', 2)
|
||||
key_downcased = key.upcase
|
||||
cert_info[key_downcased.to_sym] = value || 'default_value'
|
||||
end
|
||||
|
||||
prep[:ui] = cert_info
|
||||
|
||||
cl_name = prep[:ui][:O].split(":")
|
||||
cert_file = if cl_name.length > 1
|
||||
"#{@root_ca}/ca/client_certs/#{prep[:ui][:CN]}/#{cl_name[0]}.cert.pem.#{cl_name[1]}"
|
||||
else
|
||||
"#{@root_ca}/ca/client_certs/#{prep[:ui][:CN]}/#{cl_name[0]}.cert.pem"
|
||||
end
|
||||
|
||||
if type == '*'
|
||||
entries << prep
|
||||
else
|
||||
if File.exist?(cert_file)
|
||||
entries << prep if type == 'c'
|
||||
elsif type == 's'
|
||||
entries << prep
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ca_index_txt.sort_by! { |entry| entry[:id] }
|
||||
|
||||
ca_index_txt
|
||||
end
|
||||
|
||||
def parse_time_string(str)
|
||||
return nil if str.nil? || str.length != 13 || !str[10..11].match?(/[0-9]{2}/)
|
||||
year = str[0..1].to_i + 2000 # Первые два символа - год
|
||||
month = str[2..3].to_i # Следующие два символа - месяц
|
||||
day = str[4..5].to_i # Еще два символа - день
|
||||
hour = str[6..7].to_i # Следующие два символа - часы
|
||||
minute = str[8..9].to_i # Еще два символа - минуты
|
||||
second = str[10..11].to_i # Последние два символа - секунды
|
||||
utc_offset = 0 # По умолчанию считаем, что строка содержит Z, т.е. UTC
|
||||
[ DateTime.new(year, month, day, hour, minute, second, utc_offset).strftime('%d-%m-%y %H:%M:%S'),
|
||||
DateTime.new(year, month, day, hour, minute, second, utc_offset) ]
|
||||
end
|
||||
|
||||
def get_cert_id_by_name(server_name, seq, org_name, type)
|
||||
org_nm = nil
|
||||
config_sh = File.read('utils/custom_config.sh')
|
||||
match = config_sh.match(/ORG_NAME="([^"]+)"/)
|
||||
org_nm = match[1] if match
|
||||
return nil if org_nm.nil?
|
||||
list_certs = get_list_certs('*')
|
||||
|
||||
if type == 's'
|
||||
|
||||
found_entry = list_certs.find do |entry|
|
||||
if seq == ''
|
||||
"#{org_nm}" == entry[:ui][:O] && server_name == entry[:ui][:CN]
|
||||
else
|
||||
"#{org_nm}:#{seq}" == entry[:ui][:O] && server_name == entry[:ui][:CN]
|
||||
end
|
||||
end
|
||||
else
|
||||
found_entry = list_certs.find do |entry|
|
||||
if seq == ''
|
||||
"#{org_name}" == entry[:ui][:O] && server_name == entry[:ui][:CN]
|
||||
else
|
||||
"#{org_name}:#{seq}" == entry[:ui][:O] && server_name == entry[:ui][:CN]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if found_entry
|
||||
found_entry
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
18
classes/config.rb
Normal file
18
classes/config.rb
Normal file
@@ -0,0 +1,18 @@
|
||||
LOCK_PATH = 'locks/lock'.freeze
|
||||
GRANTED_UTILS = [
|
||||
'utils/config.sh',
|
||||
'utils/make_client_cert.sh',
|
||||
'utils/make_client_revoke.sh',
|
||||
'utils/make_server_cert.sh',
|
||||
'utils/make_server_revoke.sh',
|
||||
'utils/prepare.sh'
|
||||
].freeze
|
||||
PER_PAGE = 30
|
||||
LIFE_TOKEN = 300
|
||||
ALLOWED_IPS = [
|
||||
# Example: '192.168.1.10',
|
||||
# Add allowed IP addresses here
|
||||
'*'
|
||||
]
|
||||
PORT = 4567
|
||||
IPBIND = '0.0.0.0'.freeze
|
||||
20
classes/pagination.rb
Normal file
20
classes/pagination.rb
Normal file
@@ -0,0 +1,20 @@
|
||||
class Paginator
|
||||
attr_reader :page, :per_page
|
||||
|
||||
def initialize(params, per_page, custom_name = 'p')
|
||||
@current_page = params[custom_name].nil? || params[custom_name].to_i < 1 ? 1 : params[custom_name].to_i
|
||||
@per_page = per_page
|
||||
end
|
||||
|
||||
def get_page(items)
|
||||
start_index = (@current_page - 1) * @per_page
|
||||
items[start_index, @per_page]
|
||||
end
|
||||
|
||||
def pages_info(items)
|
||||
total_pages = (items.length / @per_page.to_f).ceil
|
||||
(1..total_pages).map do |page_number|
|
||||
{ page: page_number, is_current: page_number == @current_page }
|
||||
end
|
||||
end
|
||||
end
|
||||
137
classes/runner.rb
Normal file
137
classes/runner.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
require "open3"
|
||||
require "logger"
|
||||
|
||||
class Runner
|
||||
attr_reader :cmd, :exit_status, :stdout, :stderr, :pid, :log
|
||||
|
||||
# Run a command, return runner instance
|
||||
# @param cmd [String,Array<String>] command to execute
|
||||
def self.run(*cmd)
|
||||
Runner.new(*cmd).run
|
||||
end
|
||||
|
||||
# Run a command, raise Runner::Error if it fails
|
||||
# @param cmd [String,Array<String>] command to execute
|
||||
# @raise [Runner::Error]
|
||||
def self.run!(*cmd)
|
||||
Runner.new(*cmd).run!
|
||||
end
|
||||
|
||||
# Run a command, return true if it succeeds, false if not
|
||||
# @param cmd [String,Array<String>] command to execute
|
||||
# @return [Boolean]
|
||||
def self.run?(*cmd)
|
||||
Runner.new(*cmd).run?
|
||||
end
|
||||
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
# @param cmd [String,Array<String>] command to execute
|
||||
def initialize(cmd, log = nil)
|
||||
@cmd = cmd.is_a?(Array) ? cmd.join(" ") : cmd
|
||||
@stdout = +""
|
||||
@stderr = +""
|
||||
@exit_status = nil
|
||||
@log = log
|
||||
end
|
||||
|
||||
# @return [Boolean] success or failure?
|
||||
def success?
|
||||
exit_status.zero?
|
||||
end
|
||||
|
||||
# Run the command, return self
|
||||
# @return [Runner]
|
||||
def run
|
||||
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
||||
until [stdout, stderr].all?(&:eof?)
|
||||
readable = IO.select([stdout, stderr])
|
||||
next unless readable&.first
|
||||
|
||||
readable.first.each do |stream|
|
||||
data = +""
|
||||
# rubocop:disable Lint/HandleExceptions
|
||||
begin
|
||||
stream.read_nonblock(1024, data)
|
||||
rescue EOFError
|
||||
# ignore, it's expected for read_nonblock to raise EOFError
|
||||
# when all is read
|
||||
end
|
||||
|
||||
if stream == stdout
|
||||
@stdout << data
|
||||
if log.nil?
|
||||
$stdout.write(data)
|
||||
else
|
||||
log.info(data)
|
||||
end
|
||||
else
|
||||
@stderr << data
|
||||
if log.nil?
|
||||
$stderr.write(data)
|
||||
else
|
||||
log.error(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@exit_status = wait_thr.value.exitstatus
|
||||
@pid = wait_thr.pid
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Run the command no output, return self
|
||||
# @return [Runner]
|
||||
def run_clean
|
||||
Open3.popen3(cmd) do |stdin, stdout, stderr, wait_thr|
|
||||
until [stdout, stderr].all?(&:eof?)
|
||||
readable = IO.select([stdout, stderr])
|
||||
next unless readable&.first
|
||||
|
||||
readable.first.each do |stream|
|
||||
data = +""
|
||||
# rubocop:disable Lint/HandleExceptions
|
||||
begin
|
||||
stream.read_nonblock(1024, data)
|
||||
rescue EOFError
|
||||
# ignore, it's expected for read_nonblock to raise EOFError
|
||||
# when all is read
|
||||
end
|
||||
|
||||
if stream == stdout
|
||||
@stdout << data
|
||||
unless log.nil?
|
||||
log.info(data)
|
||||
end
|
||||
else
|
||||
@stderr << data
|
||||
unless log.nil?
|
||||
log.error(data)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@exit_status = wait_thr.value.exitstatus
|
||||
@pid = wait_thr.pid
|
||||
end
|
||||
|
||||
self
|
||||
end
|
||||
|
||||
# Run the command and return stdout, raise if fails
|
||||
# @return stdout [String]
|
||||
# @raise [Runner::Error]
|
||||
def run!
|
||||
return run.stdout if run.success?
|
||||
|
||||
raise(Error, "command failed, exit: %d - stdout: %s / stderr: %s" % [exit_status, stdout, stderr])
|
||||
end
|
||||
|
||||
# Run the command and return true if success, false if failure
|
||||
# @return success [Boolean]
|
||||
def run?
|
||||
run.success?
|
||||
end
|
||||
end
|
||||
157
docs/API.md
Normal file
157
docs/API.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# API Документация для CertCenter (POST-запросы, поддержка JSON)
|
||||
|
||||
Базовый адрес: `http://127.0.0.1:4567`
|
||||
|
||||
---
|
||||
|
||||
## 1. Получение токена
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"login":"admin","password":"admin"}'
|
||||
```
|
||||
|
||||
> **Ответ**
|
||||
> ```
|
||||
> { "error": null, "content": { "token": "<JWT>" } }
|
||||
> ```
|
||||
> Сохраняйте значение `token` для последующих запросов.
|
||||
|
||||
---
|
||||
|
||||
## 2. Список серверных сертификатов
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/servers \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Список клиентских сертификатов
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/clients \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Детальная информация о сертификате
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/certinfo/123 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Детальная информация о корневом сертификате центра сертификации
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/root \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Отзыв сертификата
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/revoke/123 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Добавление клиентского сертификата
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/addclient \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>","server_domain":"example.com","client":"client1"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Добавление серверного сертификата
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/addserver \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>","domains":"example.com,example.org","validity_days":365}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Список пользователей (admin)
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/ulist \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Удаление пользователя
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/deleteuser/42 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Создание пользователя
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/adduser \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>","login":"jane","password":"secret","email":"jane@example.com","role":1}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Редактирование пользователя
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/edituser/42 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"token":"<JWT>","login":"jane","password":"newpass","role":2}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. Установка и подготовка структуры центра сертификации
|
||||
|
||||
```
|
||||
curl -X POST http://127.0.0.1:4567/api/v1/install \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"cert-path":"/tmp","org-name":"neworg","common-name":"name","cert-password":"pass","country-name": "RU", "validity-days":"3650"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. Обработка ошибок
|
||||
|
||||
Если сервер возвращает статус **400**, то это ошибка синтаксиса JSON:
|
||||
|
||||
```
|
||||
{ "error": "Invalid JSON", "content": null }
|
||||
```
|
||||
|
||||
Если токен недействителен или истёк, будет:
|
||||
|
||||
```
|
||||
{ "error": "Токен устарел", "content": null }
|
||||
```
|
||||
|
||||
При других ошибках сервер выдаёт `{ "error": "...", "content": "..." }` в формате JSON.
|
||||
139
docs/UTILS_EXAMPLES.md
Normal file
139
docs/UTILS_EXAMPLES.md
Normal file
@@ -0,0 +1,139 @@
|
||||
Сводка утилит для создания сертификатов и управление ими в инфраструктуре центра сертификации
|
||||
|
||||
В данной статье представлены краткое описание и примеры использования нескольких полезных утилит, которые помогают автоматизировать процесс создания сертификатов и управления ими в инфраструктуре центра сертификации.
|
||||
|
||||
## Утилита `prepare.sh`
|
||||
### Описание:
|
||||
Скрипт автоматизирует процесс создания инфраструктуры центра сертификации (ЦС), включая создание директорий, генерацию ключей и сертификатов, а также настройку конфигурационных файлов для корневого и промежуточного ЦА.
|
||||
|
||||
### Примеры использования:
|
||||
1. Запуск скрипта с правами суперпользователя:
|
||||
```sh
|
||||
sudo bash prepare.sh
|
||||
```
|
||||
|
||||
## Утилита `make_server_cert.sh`
|
||||
### Описание:
|
||||
Генерирует серверные сертификаты для указанных доменов или IP-адресов. Скрипт создает приватный ключ, запрос на подпись сертификата (CSR) и сам сертификат.
|
||||
|
||||
### Примеры использования:
|
||||
1. Генерация серверного сертификата для домена `example1.com` и IP-адреса `192.168.3.145`:
|
||||
```sh
|
||||
bash make_server_cert.sh example1.com 192.168.3.145
|
||||
```
|
||||
|
||||
## Утилита `make_client_cert.sh`
|
||||
### Описание:
|
||||
Скрипт для создания клиентских сертификатов для указанного сервера и клиента. Генерирует приватный ключ, запрос на сертификат (CSR), подписывает его и выводит информацию о сгенерированном сертификате.
|
||||
|
||||
### Примеры использования:
|
||||
1. Генерация клиентского сертификата для домена `example1.com` и имени пользователя `user1@test.com`, действующего 365 дней:
|
||||
```sh
|
||||
bash make_client_cert.sh -s example1.com -c user1@test.com -d 365
|
||||
```
|
||||
|
||||
## Утилита `make_server_revoke.sh`
|
||||
### Описание:
|
||||
Позволяет отозвать серверный сертификат для указанного домена или IP-адреса.
|
||||
|
||||
### Примеры использования:
|
||||
1. Отозвать серверный сертификат для домена `brepo.ru`:
|
||||
```sh
|
||||
bash make_server_revoke.sh -n 1 brepo.ru
|
||||
```
|
||||
|
||||
## Утилита `make_client_revoke.sh`
|
||||
### Описание:
|
||||
Позволяет отозвать клиентский сертификат для указанного сервера и клиента.
|
||||
|
||||
### Примеры использования:
|
||||
1. Отозвать клиентский сертификат для домена `example1.com` и имени пользователя `user2@test.com`:
|
||||
```sh
|
||||
bash make_client_revoke.sh -n 1 -s example1.com -c user2@test.com
|
||||
```
|
||||
|
||||
## Утилита `make_app_keys.sh`
|
||||
### Описание:
|
||||
Скрипт генерирует беспарольный приватный и публичный ключ с помощью `openssl` и сохраняет их в указанной директории. Это удобно для создания ключей, которые будут использоваться приложениями без необходимости вводить пароль при каждом использовании.
|
||||
|
||||
### Примеры использования:
|
||||
1. Генерация ключей в директории `/etc/ssl/app_keys`:
|
||||
```sh
|
||||
bash make_app_keys.sh /etc/ssl/app_keys
|
||||
```
|
||||
После выполнения ключи будут доступны как:
|
||||
- `/etc/ssl/app_keys/caapp.private.key.pem`
|
||||
- `/etc/ssl/app_keys/caapp.public.key.pem`
|
||||
|
||||
## Еще примеры
|
||||
1. Подготовка инфраструктуры ЦС и генерация серверного и клиентского сертификатов:
|
||||
```sh
|
||||
bash prepare.sh
|
||||
bash make_server_cert.sh example1.com 192.168.5.145
|
||||
bash make_client_cert.sh -s example1.com -c user1@test.com -d 365
|
||||
```
|
||||
|
||||
2. Отозвать серверный и клиентский сертификаты (все версии):
|
||||
```sh
|
||||
bash make_server_revoke.sh brepo.ru
|
||||
bash make_client_revoke.sh -s example1.com -c user2@test.com
|
||||
```
|
||||
|
||||
Эти утилиты и примеры помогут вам автоматизировать процесс создания сертификатов и управления ими в инфраструктуре центра сертификации.
|
||||
|
||||
## Примеры настройки nginx
|
||||
|
||||
Как обеспечить доступ к сайту с помощью сертификатов:
|
||||
|
||||
|
||||
Примкр настройки домена, например с репозиторием пакетов `/etc/nginx/conf.d/example1.com.conf`:
|
||||
|
||||
```
|
||||
server {
|
||||
listen 8081 ssl;
|
||||
server_name example1.com www.example1.com;
|
||||
|
||||
root /var/www/example1.com/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/example1.com.access.log;
|
||||
error_log /var/log/nginx/example1.com.error.log debug;
|
||||
|
||||
ssl_certificate /database/ca/intermediate/certs/example1.com.cert.pem;
|
||||
ssl_certificate_key /database/ca/intermediate/private/example1.com.key.pem;
|
||||
ssl_client_certificate /database/ca/intermediate/certs/ca-chain.cert.pem;
|
||||
ssl_crl /database/ca/intermediate/crl/ca-full.crl.pem;
|
||||
ssl_verify_client on;
|
||||
|
||||
keepalive_timeout 70;
|
||||
fastcgi_param SSL_VERIFIED $ssl_client_verify;
|
||||
fastcgi_param SSL_CLIENT_SERIAL $ssl_client_serial;
|
||||
fastcgi_param SSL_CLIENT_CERT $ssl_client_cert;
|
||||
fastcgi_param SSL_DN $ssl_client_s_dn;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Вызов на строне клиента:
|
||||
|
||||
```
|
||||
curl -k --cert /database/ca/client_certs/example1.com/user2@test.com.cert.pem --key /database/ca/client_certs/example1.com/private/user2@test.com_private.key.pem https://example1.com:8081
|
||||
```
|
||||
|
||||
|
||||
Или настройка DNF репозитория для доступа к закрытому репозиторию:
|
||||
|
||||
```[test]
|
||||
name = test
|
||||
enabled = 1
|
||||
sslverify = 0
|
||||
gpgcheck = 1
|
||||
baseurl = https://example1.com:8081
|
||||
sslclientkey=/database/ca/client_certs/example1.com/private/user2@test.com_private.key.pem
|
||||
sslclientcert=/database/ca/client_certs/example1.com/user2@test.com.cert.pem
|
||||
sslcacert=/database/ca/intermediate/certs/ca-chain.cert.pem
|
||||
```
|
||||
141
locale/en.yml
Normal file
141
locale/en.yml
Normal file
@@ -0,0 +1,141 @@
|
||||
---
|
||||
en:
|
||||
errors:
|
||||
cannot_determine_root_dir: "Unable to determine root directory of the certificate center"
|
||||
no_result_file: "Result file not found"
|
||||
record_not_found: "Record with the specified ID not found"
|
||||
root_ca_not_detected: "ROOT CA not detected"
|
||||
cannot_get_certificate_info: "Unable to get certificate information"
|
||||
command_execution_error: "Command execution error: %{command}"
|
||||
argument_error_days: "Days must be a number greater than 0"
|
||||
argument_error_domains_ips: "Domains and IPs list cannot be empty"
|
||||
argument_error_server_domain: "Server domain cannot be empty"
|
||||
argument_error_client_id: "Client ID cannot be empty"
|
||||
no_permission: You do not have access rights to the given URL
|
||||
authorization_error: "Authorization error: cannot recognize session data"
|
||||
token_expired: Token has expired
|
||||
revocation_missing_id: Missing certificate ID for revocation
|
||||
revocation_failed: Certificate revocation finished with error
|
||||
server_domain_missing: Missing server domain
|
||||
client_id_missing: Missing client identifier
|
||||
validity_days_missing: Validity period not set
|
||||
cert_created_error: Certificate creation failed
|
||||
domains_missing: Missing domain list for certificate
|
||||
user_not_found: User does not exist
|
||||
all_fields_required: All fields are required
|
||||
invalid_json: Invalid JSON format
|
||||
invalid_username_password: Invalid username or password
|
||||
page_not_found: Page not found
|
||||
user_already_exists: User already exists
|
||||
search_parameters_not_set: Search parameters not set
|
||||
access_denied: Access denied
|
||||
pages:
|
||||
servers: Servers
|
||||
clients: Clients
|
||||
revocation: Revocation
|
||||
not_found: Page not found
|
||||
users: Users
|
||||
login: Login
|
||||
install_server: Install Server
|
||||
api: API
|
||||
root: Root
|
||||
messages:
|
||||
install_not_possible: Installation impossible
|
||||
install_detected: Installation already performed or manually detected
|
||||
missing_utilities: "Missing required utilities: %{missing}"
|
||||
install_success: Installation completed successfully
|
||||
install_incomplete: Please fill all required fields.
|
||||
install_failed: Installation failed, clear the directory %{cert_path}, delete utils/custom_config.h and restart the installation.
|
||||
install_success_descr: Go to / and log in as admin.
|
||||
client_readme: |
|
||||
Generated set of keys for installing on client machine for access:
|
||||
|
||||
- private key: `%{private_key}`;
|
||||
- server certificate: `%{server_cert}`;
|
||||
- CA chain: `%{ca_chain}`.
|
||||
server_readme: |
|
||||
Generated set of keys for installing on server:
|
||||
|
||||
- private key: `%{private_key}`;
|
||||
- server certificate: `%{server_cert}`;
|
||||
- CA chain: `%{ca_chain}`;
|
||||
- revoked certificates list: `%{crl}`.
|
||||
user_added: User saved
|
||||
user_deleted: User deleted
|
||||
views:
|
||||
cert_val_day: Number of days the certificate will be valid
|
||||
authorize: Login
|
||||
user_name: Username
|
||||
enter_login: Enter login
|
||||
password: Password
|
||||
enter_pasword: Enter password
|
||||
please_enter_all_required_fields: Please fill in all required fields.
|
||||
certificate_information: Certificate information
|
||||
revoke_information: Revocation information
|
||||
user: User
|
||||
logout: Logout
|
||||
install_server_title: Complete the installation and configuration of the certification server
|
||||
install_server_description: In case configuration files and initialized environment were not found, you need to perform a preliminary setup
|
||||
cert_path_label: Path to the directory where the certificate database will be stored
|
||||
country_name_label: Country code
|
||||
org_name_label: Organization name (Organization name)
|
||||
common_name_label: Division name (Common name)
|
||||
cert_password_label: Certificate password (root and intermediate)
|
||||
enter_cert_path_placeholder: Enter the directory path
|
||||
enter_country_name_placeholder: Enter country code
|
||||
enter_org_name_placeholder: Enter organization name
|
||||
enter_common_name_placeholder: Enter division name
|
||||
enter_cert_password_placeholder: Enter password
|
||||
save: Save
|
||||
create_user_button: Create user
|
||||
user_list_tab: User list
|
||||
edit_user_tab: Edit user
|
||||
user_name_header: Username
|
||||
role_header: Role
|
||||
email_header: Email
|
||||
created_at_header: Creation date
|
||||
actions_header: Actions
|
||||
card_header_edit_user: Edit user
|
||||
login_label: Login
|
||||
password_label: Password
|
||||
email_label: Email
|
||||
role_label: Role
|
||||
submit_create_user: Create user
|
||||
submit_update_user: Update user
|
||||
modal_title_confirm: Confirm action
|
||||
modal_body_confirm: Are you shure you want to delete user?
|
||||
modal_body_confirm_revoke: Are you sure you want to revoke the certificate?
|
||||
modal_btn_delete: Delete
|
||||
modal_btn_cancel: Cancel
|
||||
no_email: None
|
||||
role_user: user
|
||||
role_creator: creator
|
||||
role_admin: admin
|
||||
role_unknown: unknown
|
||||
create_request_cert_button: Request new certificate
|
||||
domains_label: Domains and IPs separated by commas
|
||||
validity_days_label: Number of days the certificate will be valid
|
||||
domains_placeholder: example.com, 192.168.1.1
|
||||
request_cert_submit: Request certificate
|
||||
request_cert_info: You can later download the certificate and additional information on the certificate list page or on the certificate information page. The first domain name will be the certificate identifier and must be unique.
|
||||
server_certs_tab: Server certificates list
|
||||
selected_cert_info_tab: Selected certificate information
|
||||
status_header: Status
|
||||
id_header: ID
|
||||
date_header: Date
|
||||
revoke_date_header: Revocation date
|
||||
info_header: Info
|
||||
revoke_cert_tooltip: Revoke certificate
|
||||
download_cert_tooltip: Download certificate
|
||||
view_cert_tooltip: View certificate
|
||||
view_clients_tooltip: View client certificates for server
|
||||
cert_info_card_title: Certificate information
|
||||
revoke_info_card_title: Revocation information
|
||||
additional_actions: Additional actions
|
||||
revoke_button: Revoke
|
||||
server_access_label: Server to which access will be granted
|
||||
client_name_email_label: Client name or its email (must be unique)
|
||||
client_placeholder: user@user.example
|
||||
list_clients_tab: Client certificates list
|
||||
filtered_certs_tab: Filtered certificates
|
||||
delete_user: Delete user
|
||||
146
locale/ru.yml
Normal file
146
locale/ru.yml
Normal file
@@ -0,0 +1,146 @@
|
||||
---
|
||||
ru:
|
||||
errors:
|
||||
cannot_determine_root_dir: "Не возможно определить корневую директорию центра сертификации"
|
||||
no_result_file: "Не найден результирующий файл"
|
||||
record_not_found: "Запись с указанным ID не найдена"
|
||||
root_ca_not_detected: "ROOT CA не обнаружена"
|
||||
cannot_get_certificate_info: "Не могу получить информацию о сертификате"
|
||||
command_execution_error: "Ошибка выполнения %{command}"
|
||||
no_permission: У вас нет прав доступа к данному URL
|
||||
authorization_error: "Ошибка авторизации: не могу распознать сессионные данные"
|
||||
token_expired: Токен устарел
|
||||
revocation_missing_id: Отсуствует идентификатор сертификата для отзыва
|
||||
revocation_failed: Отзыв сертификата завершился с ошибкой
|
||||
server_domain_missing: Отсуствует домен сервера
|
||||
client_id_missing: Отсуствует идентификатор клиента
|
||||
validity_days_missing: Не задан срок действия сертификата
|
||||
cert_created_error: Сертификат создан с ошибкой
|
||||
domains_missing: Не задан список доменов для сертификата
|
||||
user_not_found: Пользователь не существует
|
||||
all_fields_required: Все поля обязательны
|
||||
invalid_json: Неверный формат JSON
|
||||
invalid_username_password: Неверное имя пользователя или пароль
|
||||
page_not_found: Страница не найдена
|
||||
argument_error_days: "Дни должны быть числом больше 0"
|
||||
argument_error_domains_ips: "Список доменов и IP-адресов не может быть пустым"
|
||||
argument_error_server_domain: "Домен сервера не может быть пустым"
|
||||
argument_error_client_id: "Идентификатор клиента не может быть пустым"
|
||||
user_already_exists: Пользователь уже существует
|
||||
search_parameters_not_set: Не заданы параметры поиска
|
||||
access_denied: Доступ запрещен
|
||||
pages:
|
||||
servers: Сервера
|
||||
clients: Клиенты
|
||||
revocation: Отзыв сертификата
|
||||
not_found: Страница не найдена
|
||||
users: Пользователи
|
||||
login: Авторизация
|
||||
install_server: Установка сервера сертификатов
|
||||
api: API
|
||||
root: Root
|
||||
messages:
|
||||
install_not_possible: Установка невозможна
|
||||
install_detected:
|
||||
Обнаружены признаки, что установка базы сертификатов уже была
|
||||
произведена ранее или вручную
|
||||
missing_utilities: "Отсутствуют необходимые утилиты: %{missing}"
|
||||
install_success: Установка успешно завершена
|
||||
install_incomplete: Пожалуйста, заполните все обязательные поля.
|
||||
install_failed:
|
||||
Установка провалилась, очистите каталог %{cert_path}, удалите
|
||||
utils/custom_config.h и перезапустите установку.
|
||||
install_success_descr: Перейдите по адресу / и авторизуйтесь
|
||||
как пользователь admin.
|
||||
client_readme: |
|
||||
Сгенерированный набор ключей для установки на клиентскую машину для доступа:
|
||||
|
||||
- приватный ключ: `%{private_key}`;
|
||||
- сертификат сервера: `%{server_cert}`;
|
||||
- цепочка CA: `%{ca_chain}`.
|
||||
server_readme: |
|
||||
Сгенерированный набор ключей для установки на сервер:
|
||||
|
||||
- приватный ключ: `%{private_key}`;
|
||||
- сертификат сервера: `%{server_cert}`;
|
||||
- цепочка CA: `%{ca_chain}`;
|
||||
- список отмененных сертификатов: `%{crl}`.
|
||||
user_added: Пользователь сохранен
|
||||
user_deleted: Пользователь удален
|
||||
views:
|
||||
cert_val_day: Число дней действия сертификата
|
||||
authorize: Авторизируйтесь
|
||||
user_name: Имя пользователя
|
||||
enter_login: Введите логин
|
||||
password: Пароль
|
||||
enter_pasword: Введите пароль
|
||||
please_enter_all_required_fields: Пожалуйста, заполните все обязательные поля.
|
||||
certificate_information: Информация о сертификате
|
||||
revoke_information: Информация об отзыве
|
||||
user: Пользователь
|
||||
logout: Выход
|
||||
install_server_title: Завершите установку и настройку сервера сертификации
|
||||
install_server_description: В связи с тем, что не были обнаружены конфигурационные файлы и инициализованная среда, необходимо произвести предварительную настройку
|
||||
cert_path_label: Путь к каталогу, где будет хранится база с сертификатами
|
||||
country_name_label: Код страны
|
||||
org_name_label: Название организации (Organization name)
|
||||
common_name_label: Название подразделения (Common name)
|
||||
cert_password_label: Пароль сертификатов (корневого и промежуточного)
|
||||
enter_cert_path_placeholder: Введите путь к каталогу
|
||||
enter_country_name_placeholder: Введите код страны
|
||||
enter_org_name_placeholder: Введите название организации
|
||||
enter_common_name_placeholder: Введите название подразделения
|
||||
enter_cert_password_placeholder: Введите пароль
|
||||
save: Сохранить
|
||||
create_user_button: Создать пользователя
|
||||
user_list_tab: Список пользователей
|
||||
edit_user_tab: Редактировать пользователя
|
||||
user_name_header: Имя пользователя
|
||||
role_header: Роль
|
||||
email_header: email
|
||||
created_at_header: Дата создания
|
||||
actions_header: Действия
|
||||
card_header_edit_user: Редактировать пользователя
|
||||
login_label: Логин
|
||||
password_label: Пароль
|
||||
email_label: Email
|
||||
role_label: Роль
|
||||
submit_create_user: Создать пользователя
|
||||
submit_update_user: Обновить пользователя
|
||||
modal_title_confirm: Подтвердите действие
|
||||
modal_body_confirm: Вы уверены, что хотите удалить пользователя?
|
||||
modal_body_confirm_revoke: Вы уверены, что хотите отозвать сертификат?
|
||||
modal_btn_delete: Удалить
|
||||
modal_btn_cancel: Отмена
|
||||
no_email: нет
|
||||
role_user: user
|
||||
role_creator: creator
|
||||
role_admin: admin
|
||||
role_unknown: unknown
|
||||
create_request_cert_button: "Запросить новый сертификат"
|
||||
domains_label: "Список доменов и IP через запятую"
|
||||
validity_days_label: "Число дней в течение которых сертификат будет действительным"
|
||||
domains_placeholder: "example.com, 192.168.1.1"
|
||||
request_cert_submit: "Запросить сертификат"
|
||||
request_cert_info: "Вы можете позже скачать сам сертификат и дополнительные сведения на странице списка сертификатов или на странице информации о сертификате. Первое доменное имя будет идентификатором сертификата и должно быть уникальным."
|
||||
server_certs_tab: "Список серверных сертификатов"
|
||||
selected_cert_info_tab: "Информация о выбранном сертификате"
|
||||
status_header: "Статус"
|
||||
id_header: "ID"
|
||||
date_header: "Дата"
|
||||
revoke_date_header: "Дата отзыва"
|
||||
info_header: "Сведения"
|
||||
revoke_cert_tooltip: "отозвать сертификат"
|
||||
download_cert_tooltip: "Скачать сертификат"
|
||||
view_cert_tooltip: "Посмотреть сертификат"
|
||||
view_clients_tooltip: "Посмотреть сертификаты клиентов сервера"
|
||||
cert_info_card_title: "информация о сертификате"
|
||||
revoke_info_card_title: "информация об отзыве"
|
||||
additional_actions: "Дополнительные действия"
|
||||
revoke_button: "Отозвать"
|
||||
server_access_label: "Сервер, к которому будет осуществляться доступ"
|
||||
client_name_email_label: "Имя клиента или его email (он должен быть уникальным)"
|
||||
client_placeholder: "user@user.example"
|
||||
list_clients_tab: "Список клиентских сертификатов"
|
||||
filtered_certs_tab: "Отфильтрованные сертификаты"
|
||||
delete_user: Удалить пользователя
|
||||
0
locks/lock
Normal file
0
locks/lock
Normal file
0
logs/empty
Normal file
0
logs/empty
Normal file
17
migration/202602050000000_create.rb
Normal file
17
migration/202602050000000_create.rb
Normal file
@@ -0,0 +1,17 @@
|
||||
require 'sequel'
|
||||
require 'digest'
|
||||
|
||||
Sequel.migration do
|
||||
change do
|
||||
create_table(:users) do
|
||||
primary_key :id
|
||||
String :login, null: false, unique: true
|
||||
String :password, null: false
|
||||
String :email
|
||||
Integer :role, null: false
|
||||
DateTime :create_at, default: Sequel.lit('CURRENT_TIMESTAMP')
|
||||
end
|
||||
|
||||
self[:users].insert(login: 'admin', password: Digest::SHA256.hexdigest('admin'), role: 2, email: 'admin@admin')
|
||||
end
|
||||
end
|
||||
152
models/userdata.rb
Normal file
152
models/userdata.rb
Normal file
@@ -0,0 +1,152 @@
|
||||
require 'i18n'
|
||||
require_relative 'users'
|
||||
|
||||
class UserSessionData
|
||||
attr_accessor :user_info, :error
|
||||
|
||||
def initialize(name, password, init = nil)
|
||||
@user_info = nil
|
||||
@error = nil
|
||||
if init.nil?
|
||||
user = get_user(name, password)
|
||||
if user
|
||||
@user_info = user
|
||||
else
|
||||
@error = I18n.t('errors.invalid_username_password')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def auth?
|
||||
!@user_info.nil?
|
||||
end
|
||||
|
||||
def role
|
||||
if auth?
|
||||
@user_info[:role]
|
||||
else
|
||||
-1
|
||||
end
|
||||
end
|
||||
|
||||
def user_info
|
||||
@user_info
|
||||
end
|
||||
|
||||
def add_user(name, password, email, role)
|
||||
@error = nil
|
||||
user = User.where(login: name).first
|
||||
if user
|
||||
@error = I18n.t('errors.user_already_exists')
|
||||
else
|
||||
User.create(login: name, password: Digest::SHA256.hexdigest(password.strip), email: email, role: role)
|
||||
end
|
||||
end
|
||||
|
||||
def list_users()
|
||||
User.order(Sequel.asc(:login)).all
|
||||
end
|
||||
|
||||
def user_info(name, id = nil)
|
||||
@error = nil
|
||||
if name.nil? && id.nil?
|
||||
@error = I18n.t('errors.search_parameters_not_set')
|
||||
retrun nil
|
||||
end
|
||||
user = nil
|
||||
if id.nil?
|
||||
user = User.where(login: name).first
|
||||
else
|
||||
user = User.where(id: id).first
|
||||
end
|
||||
if user
|
||||
user
|
||||
else
|
||||
@error = I18n.t('errors.user_not_found')
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def del_user(name, id = nil)
|
||||
@error = nil
|
||||
if name.nil? && id.nil?
|
||||
@error = I18n.t('errors.search_parameters_not_set')
|
||||
return
|
||||
end
|
||||
user = nil
|
||||
if id.nil?
|
||||
user = User.where(login: name).first
|
||||
else
|
||||
user = User.where(id: id).first
|
||||
end
|
||||
if user
|
||||
user.delete
|
||||
else
|
||||
@error = I18n.t('errors.user_not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def update_user(name, password, email, role, id = nil)
|
||||
@error = nil
|
||||
if name.nil? && id.nil?
|
||||
@error = I18n.t('errors.search_parameters_not_set')
|
||||
return
|
||||
end
|
||||
user = nil
|
||||
if id.nil?
|
||||
user = User.where(login: name).first
|
||||
else
|
||||
user = User.where(id: id).first
|
||||
end
|
||||
if user
|
||||
changes = {}
|
||||
changes[:password] = Digest::SHA256.hexdigest(password) unless password.nil? || password.empty?
|
||||
changes[:email] = email unless email.nil? || email.empty?
|
||||
changes[:role] = case role.to_i
|
||||
when 0 then 0
|
||||
when 1 then 1
|
||||
when 2 then 2
|
||||
else user[:role]
|
||||
end
|
||||
user.update(changes) unless changes.empty?
|
||||
else
|
||||
@error = I18n.t('errors.user_not_found')
|
||||
end
|
||||
end
|
||||
|
||||
def err
|
||||
@error
|
||||
end
|
||||
|
||||
def tok
|
||||
@user_info.nil?
|
||||
end
|
||||
|
||||
def login
|
||||
@user_info[:login]
|
||||
end
|
||||
|
||||
# Методы для сериализации и десериализации объекта в JWT токене
|
||||
def serialize
|
||||
{ user_info: @user_info.to_hash, error: @error }.to_json
|
||||
end
|
||||
|
||||
def self.deserialize(token)
|
||||
data = JSON.parse(token, symbolize_names: true)
|
||||
instance = new(nil, nil, true)
|
||||
user_data = data[:user_info]
|
||||
|
||||
instance.instance_variable_set(:@user_info, user_data)
|
||||
instance.instance_variable_set(:@error, data[:error])
|
||||
instance
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def get_user(name, password)
|
||||
user = User.where(login: name).first
|
||||
return unless user && user[:password] == Digest::SHA256.hexdigest(password)
|
||||
|
||||
user
|
||||
end
|
||||
end
|
||||
3
models/users.rb
Normal file
3
models/users.rb
Normal file
@@ -0,0 +1,3 @@
|
||||
class User < Sequel::Model(:users)
|
||||
# Модель для пользователей в приложении Sinatra с использованием Sequel
|
||||
end
|
||||
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
7
public/js/bootstrap.bundle.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
6
public/js/bootstrap.min.css
vendored
Normal file
6
public/js/bootstrap.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/js/jquery-4.0.0.min.js
vendored
Normal file
2
public/js/jquery-4.0.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
230
utils/api_call.sh
Executable file
230
utils/api_call.sh
Executable file
@@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") -s SERVER -u USER -p PASS <command> [args]
|
||||
|
||||
Commands:
|
||||
install <login> <password> <email> # Create initial admin user (no auth needed)
|
||||
listserv # List server certificates
|
||||
listclient # List client certificates
|
||||
addserv <domains> <validity_days> # Add server certificate
|
||||
addclient <server_domain> <client> <validity_days> # Add client certificate
|
||||
listuser # List users
|
||||
adduser <login> <password> <email> <role> # Add user (role numeric)
|
||||
revokecert <id> # Revoke certificate
|
||||
deleteuser <id> # Delete user
|
||||
edituser <id> <login> <password> <role> # Edit user
|
||||
certdetail <id> # Cert detail
|
||||
rootdetail # Root cert detail
|
||||
help # Show this help
|
||||
|
||||
Options:
|
||||
-s SERVER Base URL of the API (default: http://127.0.0.1:4567)
|
||||
-u USER Username for authentication
|
||||
-p PASS Password for authentication
|
||||
EOF
|
||||
}
|
||||
|
||||
# Parse global options
|
||||
SERVER="http://127.0.0.1:4567"
|
||||
USERNAME=""
|
||||
PASSWORD=""
|
||||
|
||||
while getopts ":s:u:p:h" opt; do
|
||||
case $opt in
|
||||
s) SERVER="$OPTARG" ;;
|
||||
u) USERNAME="$OPTARG" ;;
|
||||
p) PASSWORD="$OPTARG" ;;
|
||||
h)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option -$OPTARG"
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
shift $((OPTIND - 1))
|
||||
|
||||
COMMAND="$1"
|
||||
shift
|
||||
|
||||
# Helper: get token
|
||||
get_token() {
|
||||
local login="$1"
|
||||
local pass="$2"
|
||||
local resp
|
||||
resp=$(curl -s -X POST "$SERVER/api/v1/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"login\":\"$login\",\"password\":\"$pass\"}")
|
||||
local err
|
||||
err=$(echo "$resp" | jq -r '.error')
|
||||
if [[ "$err" != "null" && -n "$err" ]]; then
|
||||
echo "Login error: $err" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$resp" | jq -r '.content.token'
|
||||
}
|
||||
|
||||
# Helper: perform request
|
||||
do_req() {
|
||||
local method="$1"
|
||||
local url="$2"
|
||||
local data="$3"
|
||||
local token="$4"
|
||||
local json_data
|
||||
if [[ -n "$token" ]]; then
|
||||
json_data=$(echo "$data" | jq --arg token "$token" '. + {token: $token}')
|
||||
else
|
||||
json_data="$data"
|
||||
fi
|
||||
local resp
|
||||
resp=$(curl -s -X "$method" "$url" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$json_data")
|
||||
local err
|
||||
err=$(echo "$resp" | jq -r '.error')
|
||||
if [[ "$err" != "null" && -n "$err" ]]; then
|
||||
echo "Error: $err" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "$resp" | jq -r '.content'
|
||||
}
|
||||
|
||||
# Commands requiring authentication
|
||||
auth_required_commands=("listserv" "listclient" "addserv" "addclient" "listuser" "adduser" "revokecert" "deleteuser" "edituser" "certdetail" "rootdetail")
|
||||
needs_auth=false
|
||||
for cmd in "${auth_required_commands[@]}"; do
|
||||
if [[ "$COMMAND" == "$cmd" ]]; then
|
||||
needs_auth=true
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if $needs_auth; then
|
||||
if [[ -z "$USERNAME" || -z "$PASSWORD" ]]; then
|
||||
echo "Username and password required for command '$COMMAND'" >&2
|
||||
exit 1
|
||||
fi
|
||||
TOKEN=$(get_token "$USERNAME" "$PASSWORD")
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "Failed to obtain token" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$COMMAND" in
|
||||
install)
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "install requires <login> <password> <email>" >&2
|
||||
exit 1
|
||||
fi
|
||||
login="$1"
|
||||
pw="$2"
|
||||
email="$3"
|
||||
do_req POST "$SERVER/api/v1/adduser" "{\"login\":\"$login\",\"password\":\"$pw\",\"email\":\"$email\",\"role\":1}" ""
|
||||
;;
|
||||
listserv)
|
||||
do_req POST "$SERVER/api/v1/servers" "{}" "$TOKEN"
|
||||
;;
|
||||
listclient)
|
||||
do_req POST "$SERVER/api/v1/clients" "{}" "$TOKEN"
|
||||
;;
|
||||
addserv)
|
||||
if [[ $# -ne 2 ]]; then
|
||||
echo "addserv requires <domains> <validity_days>" >&2
|
||||
exit 1
|
||||
fi
|
||||
domains="$1"
|
||||
days="$2"
|
||||
do_req POST "$SERVER/api/v1/addserver" "{\"domains\":\"$domains\",\"validity_days\":$days}" "$TOKEN"
|
||||
;;
|
||||
addclient)
|
||||
if [[ $# -ne 3 ]]; then
|
||||
echo "addclient requires <server_domain> <client> <validity_days>" >&2
|
||||
exit 1
|
||||
fi
|
||||
server_domain="$1"
|
||||
client="$2"
|
||||
validity_days="$3"
|
||||
do_req POST "$SERVER/api/v1/addclient" "{\"server_domain\":\"$server_domain\",\"client\":\"$client\",\"validity_days\":\"$validity_days\"}" "$TOKEN"
|
||||
;;
|
||||
listuser)
|
||||
do_req POST "$SERVER/api/v1/ulist" "{}" "$TOKEN"
|
||||
;;
|
||||
adduser)
|
||||
if [[ $# -ne 4 ]]; then
|
||||
echo "adduser requires <login> <password> <email> <role:user|creator|admin>" >&2
|
||||
exit 1
|
||||
fi
|
||||
login="$1"
|
||||
pw="$2"
|
||||
email="$3"
|
||||
role="$4"
|
||||
case "$role" in
|
||||
user) role=0 ;;
|
||||
creator) role=1 ;;
|
||||
admin) role=2 ;;
|
||||
*) role=0 ;;
|
||||
esac
|
||||
do_req POST "$SERVER/api/v1/adduser" "{\"login\":\"$login\",\"password\":\"$pw\",\"email\":\"$email\",\"role\":$role}" "$TOKEN"
|
||||
;;
|
||||
revokecert)
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "revokecert requires <id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
id="$1"
|
||||
do_req POST "$SERVER/api/v1/revoke/$id" "{}" "$TOKEN"
|
||||
;;
|
||||
deleteuser)
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "deleteuser requires <id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
id="$1"
|
||||
do_req POST "$SERVER/api/v1/deleteuser/$id" "{}" "$TOKEN"
|
||||
;;
|
||||
edituser)
|
||||
if [[ $# -ne 5 ]]; then
|
||||
echo "edituser requires <id> <login> <password> <email> <role: user|creator|admin>" >&2
|
||||
exit 1
|
||||
fi
|
||||
id="$1"
|
||||
login="$2"
|
||||
pw="$3"
|
||||
role="$5"
|
||||
email="$4"
|
||||
case "$role" in
|
||||
user) role=0 ;;
|
||||
creator) role=1 ;;
|
||||
admin) role=2 ;;
|
||||
*) role=0 ;;
|
||||
esac
|
||||
do_req POST "$SERVER/api/v1/edituser/$id" "{\"login\":\"$login\",\"password\":\"$pw\",\"role\":$role,\"email\":\"$email\"}" "$TOKEN"
|
||||
;;
|
||||
certdetail)
|
||||
if [[ $# -ne 1 ]]; then
|
||||
echo "certdetail requires <id>" >&2
|
||||
exit 1
|
||||
fi
|
||||
id="$1"
|
||||
do_req POST "$SERVER/api/v1/certinfo/$id" "{}" "$TOKEN"
|
||||
;;
|
||||
rootdetail)
|
||||
do_req POST "$SERVER/api/v1/root" "{}" "$TOKEN"
|
||||
;;
|
||||
help | --help | -h)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
echo "Unknown command: $COMMAND" >&2
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
28
utils/config.sh
Normal file
28
utils/config.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ -e custom_config.sh ]; then
|
||||
source custom_config.sh
|
||||
else
|
||||
|
||||
ROOT_DIR="."
|
||||
|
||||
COUNTRY_NAME="RU"
|
||||
ORG_NAME="Regenal Organization"
|
||||
COMM_NAME="General Name"
|
||||
SERT_PASS=""
|
||||
|
||||
fi
|
||||
|
||||
if [ -z "$SERT_PASS" ]; then
|
||||
if [[ "$LANG" =~ ^ru ]]; then
|
||||
echo "Установите пароль для корневого сертификата и промежуточного"
|
||||
else
|
||||
echo "Please set a password for the root certificate and intermediate"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PATH_TO_CA="$ROOT_DIR/ca"
|
||||
ROOT_CA="$PATH_TO_CA/root"
|
||||
IMM_CA="$PATH_TO_CA/intermediate"
|
||||
CLI_CA="$PATH_TO_CA/client_certs"
|
||||
41
utils/make_app_keys.sh
Normal file
41
utils/make_app_keys.sh
Normal file
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Описание: Этот скрипт генерирует беспарольный публичный и приватный ключ с помощью openssl.
|
||||
# Путь к файлу указывается в первом обязательном параметре, если он не указан,
|
||||
# то показывается help или usage. Если путь не существует, то сообщает об ошибке и завершает работу.
|
||||
|
||||
# Определяем язык вывода
|
||||
if [[ "$LANG" =~ ^ru|RU ]]; then
|
||||
USE_RU=1
|
||||
else
|
||||
USE_RU=0
|
||||
fi
|
||||
|
||||
if [ -z "$1" ] || [ ! -e "$1" ]; then
|
||||
if [ "$USE_RU" -eq 1 ]; then
|
||||
echo "Использование: $0 <путь>"
|
||||
else
|
||||
echo "Usage: $0 <path>"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PATH_TO_KEYS=$1
|
||||
|
||||
if [ ! -e "$PATH_TO_KEYS" ]; then
|
||||
if [ "$USE_RU" -eq 1 ]; then
|
||||
echo "Такого пути $PATH_TO_KEYS не существует"
|
||||
else
|
||||
echo "Path $PATH_TO_KEYS does not exist"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
openssl genpkey -algorithm RSA -out "$PATH_TO_KEYS/caapp.private.key.pem" -pkeyopt rsa_keygen_bits:2048
|
||||
openssl rsa -in "$PATH_TO_KEYS/caapp.private.key.pem" -pubout -out "$PATH_TO_KEYS/caapp.public.key.pem"
|
||||
|
||||
if [ "$USE_RU" -eq 1 ]; then
|
||||
echo "Беспарольный публичный $PATH_TO_KEYS/caapp.public.key.pem и приватный $PATH_TO_KEYS/caapp.private.key.pem ключи созданы по пути"
|
||||
else
|
||||
echo "Passwordless public key $PATH_TO_KEYS/caapp.public.key.pem and private key $PATH_TO_KEYS/caapp.private.key.pem created at path"
|
||||
fi
|
||||
182
utils/make_client_cert.sh
Normal file
182
utils/make_client_cert.sh
Normal file
@@ -0,0 +1,182 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Скрипт для создания клиентских сертификатов для указанного сервера и клиента.
|
||||
# Генерирует приватный ключ, запрос на сертификат (CSR), подписывает его и выводит информацию о сгенерированном сертификате.
|
||||
#
|
||||
|
||||
source config.sh
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
# Determine language based on locale
|
||||
if [[ "$LANG" =~ ^ru ]]; then
|
||||
USE_RU=true
|
||||
else
|
||||
USE_RU=false
|
||||
fi
|
||||
|
||||
msg() {
|
||||
if $USE_RU; then
|
||||
printf '%s\n' "$1"
|
||||
else
|
||||
printf '%s\n' "$2"
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверка наличия и валидации параметров командной строки
|
||||
while getopts "s:c:d:h" opt; do
|
||||
case $opt in
|
||||
s) server=$OPTARG ;;
|
||||
c) client=$OPTARG ;;
|
||||
d) days=$OPTARG ;;
|
||||
h)
|
||||
msg "Использование: $0 -s <доменное имя сервера> -c <строка имени клиента> -d <число дней>" "Usage: $0 -s <server domain> -c <client name string> -d <number of days>"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
msg "Неверный аргумент или пустой параметр" "Invalid argument or empty parameter" >&2
|
||||
msg "Использование: $0 -s <доменное имя сервера> -c <строка имени клиента> -d <число дней>" "Usage: $0 -s <server domain> -c <client name string> -d <number of days>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Если параметры -d и -c не заданы или пусты, вывести сообщение об ошибке и справку
|
||||
if [[ -z "$server" || -z "$client" ]]; then
|
||||
msg "Неверные аргументы или пустые параметры" "Invalid arguments or empty parameters" >&2
|
||||
msg "Использование: $0 -s <доменное имя сервера> -c <строка имени клиента> -d <число дней>" "Usage: $0 -s <server domain> -c <client name string> -d <number of days>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Если параметр -d не задан, принять число дней равным 30
|
||||
if [ -z "$days" ]; then
|
||||
days=30
|
||||
fi
|
||||
|
||||
if [ ! -e "$PATH_TO_CA/server_certs/$server" ]; then
|
||||
msg "Данного ресурса не существует $server" "Resource $server does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd $PATH_TO_CA || {
|
||||
msg "Ошибка: Не удалось перейти в каталог $PATH_TO_CA" "Error: Could not change directory to $PATH_TO_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
mkdir -p client_certs
|
||||
|
||||
pushd client_certs || {
|
||||
msg "Ошибка: Не удалось перейти в каталог client_certs" "Error: Could not change directory to client_certs" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
SEQ="1"
|
||||
|
||||
if [ ! -e "$server/${client}_csr_req.cnf" ]; then
|
||||
|
||||
mkdir -p "$server" "$server/private"
|
||||
|
||||
chmod 0700 "$server/private"
|
||||
|
||||
echo -n "2" >"$server/${client}_seq.seq"
|
||||
|
||||
cat <<EOF >"$server/${client}_csr_req.cnf"
|
||||
[req]
|
||||
default_bits = 2048
|
||||
default_md = sha256
|
||||
prompt = no
|
||||
distinguished_name = dn
|
||||
req_extensions = req_ext
|
||||
|
||||
[dn]
|
||||
CN = $server
|
||||
O = $client:1
|
||||
|
||||
[req_ext]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
email.1 = $client
|
||||
EOF
|
||||
|
||||
# Создание запроса на сертификат (CSR) для указанного сервера и клиента
|
||||
openssl req -new -sha256 -nodes -keyout "$CLI_CA/$server/private/${client}_private.key.pem" -out "$CLI_CA/$server/${client}.csr.pem.$SEQ" -config "$CLI_CA/$server/${client}_csr_req.cnf" || {
|
||||
msg "Error: Failed to create CSR for server certificate" "Error: Failed to create CSR for server certificate" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
chmod 0400 "$CLI_CA/$server/private/${client}_private.key.pem"
|
||||
else
|
||||
# Чтение файла "$server/${client}_seq.seq" и его значение сохраняется в переменной SEQ, а в файл без переноса строки записывается новое значение SEQ+1
|
||||
SEQ=$(cat "$server/${client}_seq.seq")
|
||||
echo $((SEQ + 1)) >"$server/${client}_seq.seq"
|
||||
|
||||
# Парсинг файла "$server/${client}_csr_req.cnf" и замена значения ключа O на $client:$SEQ
|
||||
sed -i "s/^O = .*/O = $client:$SEQ/" "$server/${client}_csr_req.cnf"
|
||||
|
||||
openssl req -new -sha256 -key "$CLI_CA/$server/private/${client}_private.key.pem" -out "$CLI_CA/$server/${client}.csr.pem.$SEQ" -config "$CLI_CA/$server/${client}_csr_req.cnf" || {
|
||||
msg "Error: Failed to create CSR for server certificate" "Error: Failed to create CSR for server certificate" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
fi
|
||||
|
||||
popd || {
|
||||
msg "Ошибка: Не удалось вернуться из каталога client_certs" "Error: Could not popd from client_certs" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
pushd "$IMM_CA" || {
|
||||
msg "Ошибка: Не удалось перейти в каталог $IMM_CA" "Error: Could not change directory to $IMM_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Подпись CSR сертификатом CA
|
||||
openssl ca -batch -config "immissuer.conf" -extensions client_cert -days "$days" -notext -md sha256 -in "$CLI_CA/$server/${client}.csr.pem.$SEQ" -out "$CLI_CA/$server/${client}.cert.pem.$SEQ" -passin "pass:$SERT_PASS" || {
|
||||
msg "Ошибка: Не удалось подписать сертификат сервера" "Error: Failed to sign the server certificate" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
chmod 644 "$CLI_CA/$server/${client}.cert.pem.$SEQ"
|
||||
|
||||
# Вывод информации о сгенерированном сертификате
|
||||
openssl x509 -noout -text -in "$CLI_CA/$server/${client}.cert.pem.$SEQ" || {
|
||||
msg "Ошибка: Не удалось отобразить информацию о сертификате сервера" "Error: Failed to display the server certificate information" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
if $USE_RU; then
|
||||
cat <<EOF
|
||||
Сгенерированный набор ключей для установки на машину клиента для доступа:
|
||||
|
||||
- [OUTPUTDATA] приватный ключ: \`$CLI_CA/$server/private/${client}_private.key.pem\`;
|
||||
- [OUTPUTDATA_CERT] сертификат сервера: \`$CLI_CA/$server/${client}.cert.pem.$SEQ\`;
|
||||
- [OUTPUTDATA] цепочка CA: \`$IMM_CA/certs/ca-chain.cert.pem\`.
|
||||
EOF
|
||||
else
|
||||
cat <<EOF
|
||||
Generated key set for client machine:
|
||||
|
||||
- [OUTPUTDATA] private key: \`$CLI_CA/$server/private/${client}_private.key.pem\`;
|
||||
- [OUTPUTDATA_CERT] server certificate: \`$CLI_CA/$server/${client}.cert.pem.$SEQ\`;
|
||||
- [OUTPUTDATA] CA chain: \`$IMM_CA/certs/ca-chain.cert.pem\`.
|
||||
EOF
|
||||
fi
|
||||
|
||||
popd || {
|
||||
msg "Ошибка: Не удалось вернуться из каталога $IMM_CA" "Error: Could not popd from $IMM_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
|
||||
popd || {
|
||||
msg "Ошибка: Не удалось вернуться из каталога $PATH_TO_CA" "Error: Could not popd from $PATH_TO_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit
|
||||
exit 1
|
||||
}
|
||||
96
utils/make_client_revoke.sh
Normal file
96
utils/make_client_revoke.sh
Normal file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
|
||||
source config.sh
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
CERT_REV=""
|
||||
|
||||
# Language detection
|
||||
if [[ "$LANG" == ru_* || "$LANG" == *ru* ]]; then
|
||||
IS_RU=1
|
||||
else
|
||||
IS_RU=0
|
||||
fi
|
||||
|
||||
msg() {
|
||||
local ru=$1
|
||||
local en=$2
|
||||
if [[ $IS_RU -eq 1 ]]; then
|
||||
echo "$ru"
|
||||
else
|
||||
echo "$en"
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверка наличия и валидации параметров командной строки
|
||||
while getopts "s:c:n:h" opt; do
|
||||
case $opt in
|
||||
s) server=$OPTARG ;;
|
||||
c) client=$OPTARG ;;
|
||||
n) CERT_REV=$OPTARG ;;
|
||||
h)
|
||||
msg "Использование: $0 -s <доменное имя сервера> -c <строка имени клиента> -n <номер версии>" "Usage: $0 -s <server domain> -c <client name> -n <version number>"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
msg "Неверный аргумент или пустой параметр" "Invalid argument or empty parameter" >&2
|
||||
msg "Использование: $0 -s <доменное имя сервера> -c <строка имени клиента> -n <номер версии>" "Usage: $0 -s <server domain> -c <client name> -n <version number>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Если параметры -d и -c не заданы или пусты, вывести сообщение об ошибке и справку
|
||||
if [[ -z "$server" || -z "$client" ]]; then
|
||||
msg "Неверные аргументы или пустые параметры" "Invalid arguments or empty parameters" >&2
|
||||
msg "Использование: $0 -s <доменное имя сервера> -c <строка имени клиента> -n <номер версии>" "Usage: $0 -s <server domain> -c <client name> -n <version number>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e "$PATH_TO_CA/server_certs/$server" ]; then
|
||||
msg "Данного ресурса не существует $server" "Resource does not exist: $server" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e "$CLI_CA/$server/${client}_csr_req.cnf" ]; then
|
||||
msg "Данного клиента не существует $client" "Client does not exist: $client" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd "$IMM_CA" || {
|
||||
msg "Ошибка: не удалось перейти в каталог $IMM_CA" "Error: Could not change directory to $IMM_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
}
|
||||
|
||||
if [ -z "$CERT_REV" ]; then
|
||||
files=("$CLI_CA/$server/${client}.cert.pem".*)
|
||||
else
|
||||
files=("$CLI_CA/$server/${client}.cert.pem.$CERT_REV")
|
||||
fi
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ -e "$file" ]; then
|
||||
if ! openssl ca -config "immissuer.conf" -revoke "$file" -passin "pass:$SERT_PASS"; then
|
||||
msg "Ошибка при выполнении команды openssl ca -revoke" "Error executing openssl ca -revoke" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if ! openssl ca -config "immissuer.conf" -gencrl -out crl/intermediate.crl.pem -passin "pass:$SERT_PASS"; then
|
||||
msg "Ошибка при выполнении команды openssl ca -gencrl" "Error executing openssl ca -gencrl" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
fi
|
||||
|
||||
if ! openssl crl -in crl/intermediate.crl.pem -noout -text; then
|
||||
msg "Ошибка при выполнении команды openssl crl" "Error executing openssl crl" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
fi
|
||||
|
||||
cat $ROOT_CA/crl/ca.crl.pem $IMM_CA/crl/intermediate.crl.pem >$IMM_CA/crl/ca-full.crl.pem
|
||||
|
||||
popd || {
|
||||
msg "Ошибка: не удалось выйти из каталога $IMM_CA" "Error: Could not popd from $IMM_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
}
|
||||
209
utils/make_server_cert.sh
Normal file
209
utils/make_server_cert.sh
Normal file
@@ -0,0 +1,209 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Этот скрипт предназначен для генерации серверных сертификатов для указанных доменов или IP-адресов.
|
||||
# Он создает приватный ключ, запрос на подпись сертификата (CSR) и сам сертификат,
|
||||
# используя конфигурационные файлы и инфраструктуру центра сертификации (CA).
|
||||
# Скрипт также выводит информацию о сгенерированных ключах и сертификатах.
|
||||
|
||||
source ./config.sh
|
||||
|
||||
# Определяем, установлена ли русская локаль
|
||||
if [[ "${LANG,,}" == ru* ]] || [[ "${LC_MESSAGES,,}" == ru* ]]; then
|
||||
LANG_RU=1
|
||||
else
|
||||
LANG_RU=0
|
||||
fi
|
||||
|
||||
# Handle -h option for help message
|
||||
if [ "$1" == "-h" ]; then
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
echo "Использование: $0 [-h] [-t|--days DAYS] [domain1, domain2, ...]"
|
||||
echo "Создает сертификаты сервера для указанных доменов или IP."
|
||||
echo
|
||||
echo "Опции:"
|
||||
echo " -h Показать это сообщение"
|
||||
echo " -t|--days DAYS Установить срок действия сертификата (по умолчанию: 3650 дней)"
|
||||
else
|
||||
echo "Usage: $0 [-h] [-t|--days DAYS] [domain1, domain2, ...]"
|
||||
echo "Generate server certificates for the given domains or IPs."
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " -h Display this help message"
|
||||
echo " -t|--days DAYS Set the number of days the certificate is valid for (default: 3650)"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Чтение параметра -t через getopt для установки числа дней действия сертификата
|
||||
while true; do
|
||||
case "$1" in
|
||||
-t | --days)
|
||||
CERT_DAYS="$2"
|
||||
shift 2
|
||||
break
|
||||
;;
|
||||
-h | --help)
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
echo "Использование: $0 [-h] [-t|--days DAYS] [domain1, domain2, ...]"
|
||||
echo "Создает сертификаты сервера для указанных доменов или IP."
|
||||
echo
|
||||
echo "Опции:"
|
||||
echo " -h Показать это сообщение"
|
||||
echo " -t|--days DAYS Установить срок действия сертификата (по умолчанию: 3650 дней)"
|
||||
else
|
||||
echo "Usage: $0 [-h] [-t|--days DAYS] [domain1, domain2, ...]"
|
||||
echo "Generate server certificates for the given domains or IPs."
|
||||
echo
|
||||
echo "Options:"
|
||||
echo " -h Display this help message"
|
||||
echo " -t|--days DAYS Set the number of days the certificate is valid for (default: 3650)"
|
||||
fi
|
||||
exit 0
|
||||
;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Если параметр не указан, используем значение по умолчанию
|
||||
CERT_DAYS=${CERT_DAYS:-3650}
|
||||
|
||||
pushd $PATH_TO_CA || exit
|
||||
|
||||
mkdir -p server_certs
|
||||
|
||||
pushd server_certs || exit
|
||||
|
||||
# Проверка, предоставлен ли первый параметр и не пуст ли он
|
||||
if [ -z "$1" ]; then
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
echo "Нет входных данных"
|
||||
else
|
||||
echo "No input provided"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Разделение входной строки в массив с использованием запятых или пробелов как разделителей
|
||||
IFS=', ' read -r -a items <<<"$1"
|
||||
|
||||
# Проверка, есть ли хотя бы один элемент в списке
|
||||
if [ "${#items[@]}" -eq 0 ]; then
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
echo "Входные данные пусты"
|
||||
else
|
||||
echo "No elements found in the input"
|
||||
fi
|
||||
popd || exit
|
||||
popd || exit
|
||||
exit 0
|
||||
fi
|
||||
|
||||
SEQ="1"
|
||||
|
||||
# Извлечение первого элемента списка
|
||||
fst_elem="${items[0]}"
|
||||
|
||||
# Создание директории с именем первого элемента, если она не существует
|
||||
mkdir -p "$fst_elem" || true
|
||||
|
||||
if [ ! -e "$fst_elem/csr_req.cnf" ]; then
|
||||
|
||||
echo -n "2" >"$fst_elem/${fst_elem}_seq.seq"
|
||||
|
||||
# Создание файла csr_api.cnf с необходимым содержимым
|
||||
cat <<EOF >"$fst_elem/csr_req.cnf"
|
||||
[req]
|
||||
default_bits = 2048
|
||||
default_md = sha256
|
||||
prompt = no
|
||||
distinguished_name = dn
|
||||
req_extensions = req_ext
|
||||
|
||||
[dn]
|
||||
CN = $fst_elem
|
||||
O = ${ORG_NAME}:$SEQ
|
||||
|
||||
[req_ext]
|
||||
subjectAltName = @alt_names
|
||||
|
||||
[alt_names]
|
||||
EOF
|
||||
|
||||
# Добавление записей DNS для доменных имен и записей IP для IP-адресов
|
||||
dns_count=1
|
||||
ip_count=1
|
||||
|
||||
for item in "${items[@]}"; do
|
||||
if [[ "$item" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "IP.$ip_count = $item" >>"$fst_elem/csr_req.cnf"
|
||||
((ip_count++))
|
||||
else
|
||||
echo "DNS.$dns_count = $item" >>"$fst_elem/csr_req.cnf"
|
||||
((dns_count++))
|
||||
fi
|
||||
done
|
||||
|
||||
popd || exit
|
||||
|
||||
pushd "$IMM_CA" || {
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
echo "Ошибка: не удалось перейти в каталог $IMM_CA"
|
||||
else
|
||||
echo "Error: Could not change directory to $IMM_CA"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
openssl genrsa -out "private/$fst_elem.key.pem" -passout "pass:$SERT_PASS" 2048
|
||||
chmod 400 "private/$fst_elem.key.pem"
|
||||
else
|
||||
SEQ=$(cat "$fst_elem/${fst_elem}_seq.seq")
|
||||
echo $((SEQ + 1)) >"$fst_elem/${fst_elem}_seq.seq"
|
||||
|
||||
sed -i "s/^O = .*/O = ${ORG_NAME}:${SEQ}/" "${fst_elem}/csr_req.cnf"
|
||||
|
||||
pushd "$IMM_CA" || {
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
echo "Ошибка: не удалось перейти в каталог $IMM_CA"
|
||||
else
|
||||
echo "Error: Could not change directory to $IMM_CA"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
fi
|
||||
|
||||
# Генерация запроса на подпись сертификата (CSR)
|
||||
openssl req -new -sha256 -key "private/$fst_elem.key.pem" -out "csr/$fst_elem.csr.pem" -config "../server_certs/$fst_elem/csr_req.cnf"
|
||||
|
||||
# Подписание CSR и создание сертификата
|
||||
openssl ca -batch -config "immissuer.conf" -extensions server_cert -days "$CERT_DAYS" -notext -md sha256 -in "csr/$fst_elem.csr.pem" -out "certs/$fst_elem.cert.pem.$SEQ" -passin "pass:$SERT_PASS"
|
||||
chmod 444 "certs/$fst_elem.cert.pem.$SEQ"
|
||||
|
||||
# Вывод информации о сертификате
|
||||
openssl x509 -noout -text -in "certs/$fst_elem.cert.pem.$SEQ"
|
||||
|
||||
# Информирование пользователя о сгенерированных ключах и сертификатах
|
||||
if [ "$LANG_RU" -eq 1 ]; then
|
||||
cat <<EOF
|
||||
Сгенерированный набор ключей для установки на сервер:
|
||||
|
||||
- [OUTPUTDATA] приватный ключ: \`$IMM_CA/private/$fst_elem.key.pem\`;
|
||||
- [OUTPUTDATA_CERT] сертификат сервера: \`$IMM_CA/certs/$fst_elem.cert.pem.$SEQ\`;
|
||||
- [OUTPUTDATA] цепочка CA: \`$IMM_CA/certs/ca-chain.cert.pem\`;
|
||||
- [OUTPUTDATA] список отмененных сертификатов: \`$IMM_CA/crl/ca-full.crl.pem\`
|
||||
EOF
|
||||
else
|
||||
cat <<EOF
|
||||
Generated key set for server installation:
|
||||
|
||||
- [OUTPUTDATA] private key: \`$IMM_CA/private/$fst_elem.key.pem\`;
|
||||
- [OUTPUTDATA_CERT] server certificate: \`$IMM_CA/certs/$fst_elem.cert.pem.$SEQ\`;
|
||||
- [OUTPUTDATA] CA chain: \`$IMM_CA/certs/ca-chain.cert.pem\`;
|
||||
- [OUTPUTDATA] revoked certificates list: \`$IMM_CA/crl/ca-full.crl.pem\`
|
||||
EOF
|
||||
fi
|
||||
|
||||
popd || exit
|
||||
|
||||
popd || exit
|
||||
90
utils/make_server_revoke.sh
Normal file
90
utils/make_server_revoke.sh
Normal file
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
|
||||
source config.sh
|
||||
|
||||
CURRENT_DIR=$(pwd)
|
||||
|
||||
CERT_REV=""
|
||||
|
||||
# Detect language: Russian if LANG or LC_ALL starts with 'ru'
|
||||
if [[ "${LANG:-$LC_ALL}" =~ ^ru|^ru_RU ]]; then
|
||||
IS_RUSSIAN=1
|
||||
else
|
||||
IS_RUSSIAN=0
|
||||
fi
|
||||
|
||||
# Helper to print messages in the appropriate language
|
||||
msg() {
|
||||
local en="$1"
|
||||
local ru="$2"
|
||||
if [ "$IS_RUSSIAN" -eq 1 ]; then
|
||||
echo "$ru"
|
||||
else
|
||||
echo "$en"
|
||||
fi
|
||||
}
|
||||
|
||||
# Проверка наличия и валидации параметров командной строки
|
||||
while getopts "s:n:h" opt; do
|
||||
case $opt in
|
||||
s) server=$OPTARG ;;
|
||||
n) CERT_REV=$OPTARG ;;
|
||||
h)
|
||||
msg "Usage: $0 -s <server domain name> -n <certificate reverse number>" "Использование: $0 -s <доменное имя сервера> -n <реверс-номер сертификата>"
|
||||
exit 0
|
||||
;;
|
||||
\?)
|
||||
msg "Invalid argument or missing parameter" "Неверный аргумент или пустой параметр" >&2
|
||||
msg "Usage: $0 -s <server domain name> -n <certificate reverse number>" "Использование: $0 -s <доменное имя сервера> -n <реверс-номер сертификата>" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -z "$server" ]; then
|
||||
msg "Invalid arguments or missing parameters" "Неверные аргументы или пустые параметры" >&2
|
||||
msg "Usage: $0 -s <server domain name> -n <certificate reverse number>" "Использование: $0 -s <доменное имя сервера> -n <реверс-номер сертификата>" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -e "$PATH_TO_CA/server_certs/$server" ]; then
|
||||
msg "Resource does not exist $server" "Данного ресурса не существует $server" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd "$IMM_CA" || {
|
||||
msg "Error: Could not change directory to $IMM_CA" "Ошибка: Не удалось перейти в каталог $IMM_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
}
|
||||
|
||||
if [ -z "$CERT_REV" ]; then
|
||||
files=("certs/$server.cert.pem".*)
|
||||
else
|
||||
files=("certs/$server.cert.pem.$CERT_REV")
|
||||
fi
|
||||
|
||||
for file in "${files[@]}"; do
|
||||
if [ -e "$file" ]; then
|
||||
if ! openssl ca -config "immissuer.conf" -revoke "$file" -passin "pass:$SERT_PASS"; then
|
||||
msg "Error executing openssl ca -revoke ($file)" "Ошибка при выполнении команды openssl ca -revoke ($file)" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if ! openssl ca -config "immissuer.conf" -gencrl -out crl/intermediate.crl.pem -passin "pass:$SERT_PASS"; then
|
||||
msg "Error executing openssl ca -gencrl" "Ошибка при выполнении команды openssl ca -gencrl" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
fi
|
||||
|
||||
if ! openssl crl -in crl/intermediate.crl.pem -noout -text; then
|
||||
msg "Error executing openssl crl" "Ошибка при выполнении команды openssl crl" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
fi
|
||||
|
||||
cat $ROOT_CA/crl/ca.crl.pem $IMM_CA/crl/intermediate.crl.pem >$IMM_CA/crl/ca-full.crl.pem
|
||||
|
||||
popd || {
|
||||
msg "Error: Could not popd from $IMM_CA" "Ошибка: Не удалось выполнить popd из $IMM_CA" >&2
|
||||
cd "$CURRENT_DIR" || exit 1
|
||||
}
|
||||
279
utils/prepare.sh
Normal file
279
utils/prepare.sh
Normal file
@@ -0,0 +1,279 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Этот скрипт автоматизирует процесс создания инфраструктуры центра сертификации (ЦС),
|
||||
# включая создание директорий, генерацию ключей и сертификатов,
|
||||
# а также настройку конфигурационных файлов для корневого и промежуточного ЦА.
|
||||
#
|
||||
# Скрипт выполняет следующие основные действия:
|
||||
# 1. Создает необходимые директории для хранения сертификатов, ключей и других файлов ЦС.
|
||||
# 2. Генерирует RSA ключи для корневого и промежуточного ЦА с шифрованием AES-256.
|
||||
# 3. Создает самоподписанный корневой сертификат и CSR (Certificate Signing Request)
|
||||
# для промежуточного ЦА.
|
||||
# 4. Подписывает сертификат промежуточного ЦА корневым ЦА.
|
||||
# 5. Создает цепочку сертификатов, включающую корневой и промежуточный сертификаты.
|
||||
# 6. Проверяет целостность созданного сертификата промежуточного ЦА с помощью корневого ЦА.
|
||||
#
|
||||
# Для использования скрипта рекомендуется запускать его с правами суперпользователя (root),
|
||||
# так как он создает файлы и директории в защищенных системных папках.
|
||||
|
||||
#set -e
|
||||
#trap 'echo "Error: Script execution failed"; exit 1' ERR
|
||||
|
||||
source ./config.sh
|
||||
|
||||
# Detect language and define error function
|
||||
LANGUAGE="en"
|
||||
if [[ "$LANG" == ru* || "$LANG" == *ru_* || "$LC_ALL" == ru* || "$LC_ALL" == *ru_* ]]; then
|
||||
LANGUAGE="ru"
|
||||
fi
|
||||
|
||||
msg() {
|
||||
local en_msg="$1"
|
||||
local ru_msg="$2"
|
||||
if [[ "$LANGUAGE" == "ru" ]]; then
|
||||
echo "$ru_msg"
|
||||
else
|
||||
echo "$en_msg"
|
||||
fi
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Проверка переменной VAL_DAYS
|
||||
if [[ -z "$VAL_DAYS" || ! "$VAL_DAYS" =~ ^[0-9]+$ || "$VAL_DAYS" -le 0 ]]; then
|
||||
msg "Error: VAL_DAYS must be a positive integer" "Ошибка: Переменная VAL_DAYS должна быть положительным целым числом"
|
||||
fi
|
||||
|
||||
if ! mkdir -m 700 "$PATH_TO_CA"; then
|
||||
msg "Error: Failed to create directory $PATH_TO_CA" "Ошибка: Не удалось создать директорию $PATH_TO_CA"
|
||||
fi
|
||||
|
||||
# Перейти в директорию CA или выйти с ошибкой, если это не удалось
|
||||
cd "$PATH_TO_CA" || { msg "Error: Failed to change directory to $PATH_TO_CA" "Ошибка: Не удалось перейти в каталог $PATH_TO_CA"; }
|
||||
|
||||
# Список каталогов, для которых создается структура
|
||||
DIRECTORIES=("root" "intermediate")
|
||||
|
||||
# Создание необходимых директорий и настройка их прав доступа
|
||||
for dir in "${DIRECTORIES[@]}"; do
|
||||
# Создание поддиректорий в каждой директории из списка
|
||||
mkdir -p "$dir/certs" "$dir/crl" "$dir/newcerts" "$dir/private" "$dir/csr"
|
||||
chmod 700 "$dir/private"
|
||||
|
||||
# Создание файлов базы CA
|
||||
touch "$dir/index.txt"
|
||||
echo -n 100000 >"$dir/serial"
|
||||
|
||||
# Настройка файла для CRL (список отозванных сертификатов)
|
||||
echo -n 100000 >"$dir/crlnumber"
|
||||
done
|
||||
|
||||
# Создание конфигурационного файла для корневого ЦА
|
||||
cat >root/sertissuer.conf <<EOL
|
||||
[ca]
|
||||
default_ca=CA_default
|
||||
|
||||
[CA_default]
|
||||
dir = $ROOT_CA
|
||||
certs = \$dir/certs
|
||||
crl_dir = \$dir/crl
|
||||
database = \$dir/index.txt
|
||||
new_certs_dir = \$dir/newcerts
|
||||
serial = \$dir/serial
|
||||
|
||||
certificate = \$dir/certs/ca.cert.pem
|
||||
private_key = \$dir/private/ca.key.pem
|
||||
crlnumber = \$dir/crlnumber
|
||||
crl = \$dir/crl/ca.crl.pem
|
||||
crl_extensions = crl_ext
|
||||
default_crl_days = 30
|
||||
|
||||
default_md = sha256
|
||||
name_opt = ca_default
|
||||
cert_opt = ca_default
|
||||
default_days = $VAL_DAYS
|
||||
preserve = no
|
||||
policy = policy_strict
|
||||
|
||||
[policy_strict]
|
||||
countryName = match
|
||||
stateOrProvinceName = optional
|
||||
organizationName = match
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[req]
|
||||
default_bits = 4096
|
||||
default_md = sha256
|
||||
default_keyfile = privkey.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
string_mask = utf8only
|
||||
x509_extensions = v3_ca
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
countryName = $COUNTRY_NAME
|
||||
organizationName = $ORG_NAME
|
||||
commonName = $ORG_NAME
|
||||
|
||||
[v3_ca]
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
basicConstraints = critical, CA:true
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
|
||||
[v3_inter]
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
basicConstraints = critical, CA:true
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
|
||||
[crl_ext]
|
||||
authorityKeyIdentifier = keyid:always
|
||||
EOL
|
||||
|
||||
# Перейти в директорию корневого ЦА или выйти с ошибкой, если это не удалось
|
||||
pushd "$ROOT_CA" || { msg "Error: Failed to change directory to $ROOT_CA" "Ошибка: Не удалось перейти в каталог $ROOT_CA"; }
|
||||
|
||||
# Генерация RSA ключа для корневого ЦА с шифрованием AES-256
|
||||
openssl genrsa -aes256 -out private/ca.key.pem -passout "pass:$SERT_PASS" 4096 || { msg "Error: Failed to generate RSA key for root CA" "Ошибка: Не удалось создать RSA‑ключ для корневого ЦА"; }
|
||||
|
||||
# Установка прав доступа для ключа корневого ЦА
|
||||
chmod 400 private/ca.key.pem
|
||||
|
||||
# Создание самоподписанного сертификата корневого ЦА
|
||||
openssl req -config sertissuer.conf -key private/ca.key.pem -new -x509 -days "$VAL_DAYS" -sha256 -extensions v3_ca -out certs/ca.cert.pem -passin "pass:$SERT_PASS" || { msg "Error: Failed to create root CA certificate" "Ошибка: Не удалось создать сертификат корневого ЦА"; }
|
||||
|
||||
# Установка прав доступа для сертификата корневого ЦА
|
||||
chmod 444 certs/ca.cert.pem
|
||||
|
||||
# Отображение деталей сертификата корневого ЦА
|
||||
openssl x509 -noout -text -in certs/ca.cert.pem || { msg "Error: Failed to display root CA certificate details" "Ошибка: Не удалось вывести детали сертификата корневого ЦА"; }
|
||||
|
||||
# Вернуться в исходную директорию или выйти с ошибкой, если это не удалось
|
||||
popd || { msg "Can't return to old directory" "Невозможно вернуться к старому каталогу"; }
|
||||
|
||||
# Создание конфигурационного файла для промежуточного ЦА
|
||||
cat >intermediate/immissuer.conf <<EOL
|
||||
[ca]
|
||||
default_ca=CA_default
|
||||
|
||||
[CA_default]
|
||||
dir = $IMM_CA
|
||||
certs = \$dir/certs
|
||||
crl_dir = \$dir/crl
|
||||
database = \$dir/index.txt
|
||||
new_certs_dir = \$dir/newcerts
|
||||
serial = \$dir/serial
|
||||
|
||||
certificate = \$dir/certs/intermediate.cert.pem
|
||||
private_key = \$dir/private/intermediate.key.pem
|
||||
crlnumber = \$dir/crlnumber
|
||||
crl = \$dir/crl/intermediate.crl.pem
|
||||
crl_extensions = crl_ext
|
||||
default_crl_days = 7
|
||||
|
||||
default_md = sha256
|
||||
name_opt = ca_default
|
||||
cert_opt = ca_default
|
||||
default_days = 825
|
||||
preserve = no
|
||||
policy = policy_loose
|
||||
|
||||
[policy_loose]
|
||||
countryName = optional
|
||||
stateOrProvinceName = optional
|
||||
localityName = optional
|
||||
organizationName = optional
|
||||
organizationalUnitName = optional
|
||||
commonName = supplied
|
||||
emailAddress = optional
|
||||
|
||||
[req]
|
||||
default_bits = 4096
|
||||
default_md = sha256
|
||||
default_keyfile = privkey.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
string_mask = utf8only
|
||||
x509_extensions = v3_intermediate_ca
|
||||
prompt = no
|
||||
|
||||
[req_distinguished_name]
|
||||
countryName = $COUNTRY_NAME
|
||||
organizationName = $ORG_NAME
|
||||
commonName = $ORG_NAME
|
||||
|
||||
[v3_intermediate_ca]
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid:always,issuer
|
||||
basicConstraints = critical, CA:true, pathlen:0
|
||||
keyUsage = critical, keyCertSign, cRLSign
|
||||
|
||||
[server_cert]
|
||||
basicConstraints = CA:false
|
||||
nsCertType = server
|
||||
nsComment = "$COMM_NAME TLS server cert"
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid,issuer
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = serverAuth
|
||||
|
||||
[client_cert]
|
||||
basicConstraints = CA:false
|
||||
nsCertType = client
|
||||
nsComment = "Brepo client cert"
|
||||
subjectKeyIdentifier = hash
|
||||
authorityKeyIdentifier = keyid,issuer
|
||||
keyUsage = critical, digitalSignature, keyEncipherment
|
||||
extendedKeyUsage = clientAuth
|
||||
|
||||
[crl_ext]
|
||||
authorityKeyIdentifier = keyid:always
|
||||
EOL
|
||||
|
||||
# Перейти в директорию промежуточного ЦА или выйти с ошибкой, если это не удалось
|
||||
pushd "$IMM_CA" || { msg "Error: Failed to change directory to $IMM_CA" "Ошибка: Не удалось перейти в каталог $IMM_CA"; }
|
||||
|
||||
# Генерация RSA ключа для промежуточного ЦА с шифрованием AES-256
|
||||
openssl genrsa -aes256 -out private/intermediate.key.pem -passout "pass:$SERT_PASS" 4096 || { msg "Error: Failed to generate RSA key for intermediate CA" "Ошибка: Не удалось создать RSA‑ключ для промежуточного ЦА"; }
|
||||
|
||||
# Установка прав доступа для ключа промежуточного ЦА
|
||||
chmod 400 private/intermediate.key.pem
|
||||
|
||||
# Создание CSR для промежуточного ЦА
|
||||
openssl req -config immissuer.conf -new -sha256 -key private/intermediate.key.pem -out csr/intermediate.csr.pem -passin "pass:$SERT_PASS" || { msg "Error: Failed to create CSR for intermediate CA" "Ошибка: Не удалось создать запрос на сертификат для промежуточного ЦА"; }
|
||||
|
||||
# Вернуться в исходную директорию или выйти с ошибкой, если это не удалось
|
||||
popd || { msg "Can't return to old directory" "Невозможно вернуться к старому каталогу"; }
|
||||
|
||||
# Перейти в директорию корневого ЦА или выйти с ошибкой, если это не удалось
|
||||
pushd "$ROOT_CA" || { msg "Error: Failed to change directory to $ROOT_CA" "Ошибка: Не удалось перейти в каталог $ROOT_CA"; }
|
||||
|
||||
# Подпись сертификата промежуточного ЦА корневым ЦА
|
||||
openssl ca -batch -config sertissuer.conf -extensions v3_inter -days 3550 -notext -md sha256 -in $IMM_CA/csr/intermediate.csr.pem -out $IMM_CA/certs/intermediate.cert.pem -passin "pass:$SERT_PASS" || { msg "Error: Failed to sign intermediate CA certificate" "Ошибка: Не удалось подписать сертификат промежуточного ЦА корневым ЦА"; }
|
||||
|
||||
# Установка прав доступа для сертификата промежуточного ЦА
|
||||
chmod 444 $IMM_CA/certs/intermediate.cert.pem
|
||||
|
||||
openssl ca -config "sertissuer.conf" -gencrl -out crl/ca.crl.pem -passin "pass:$SERT_PASS"
|
||||
|
||||
# Вернуться в исходную директорию или выйти с ошибкой, если это не удалось
|
||||
popd || { msg "Can't return to old directory" "Невозможно вернуться к старому каталогу"; }
|
||||
|
||||
# Перейти в директорию промежуточного ЦА или выйти с ошибкой, если это не удалось
|
||||
pushd "$IMM_CA" || { msg "Error: Failed to change directory to $IMM_CA" "Ошибка: Не удалось перейти в каталог $IMM_CA"; }
|
||||
|
||||
openssl ca -config "immissuer.conf" -gencrl -out crl/intermediate.crl.pem -passin "pass:$SERT_PASS"
|
||||
|
||||
cat $ROOT_CA/crl/ca.crl.pem $IMM_CA/crl/intermediate.crl.pem >$IMM_CA/crl/ca-full.crl.pem
|
||||
|
||||
# Вернуться в исходную директорию или выйти с ошибкой, если это не удалось
|
||||
popd || { msg "Can't return to old directory" "Невозможно вернуться к старому каталогу"; }
|
||||
|
||||
# Создание цепочки сертификатов
|
||||
cat "$IMM_CA/certs/intermediate.cert.pem" "$ROOT_CA/certs/ca.cert.pem" >"$IMM_CA/certs/ca-chain.cert.pem" || { msg "Error: Failed to create CA chain certificate" "Ошибка: Не удалось создать цепочку сертификатов ЦА"; }
|
||||
|
||||
# Проверка сертификата промежуточного ЦА с использованием корневого центра сертификации
|
||||
openssl verify -CAfile $ROOT_CA/certs/ca.cert.pem $IMM_CA/certs/intermediate.cert.pem || { msg "Error: Failed to verify intermediate CA certificate" "Ошибка: Не удалось проверить сертификат промежуточного ЦА"; }
|
||||
|
||||
exit 0
|
||||
10
views/api.erb
Normal file
10
views/api.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= I18n.t('pages.api') %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<pre><%= ERB::Util.html_escape(File.read('docs/API.md')) %></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
69
views/config.erb
Normal file
69
views/config.erb
Normal file
@@ -0,0 +1,69 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= I18n.t('views.install_server_title') %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text text-center"><%= I18n.t('views.install_server_description') %></p>
|
||||
<div class="container">
|
||||
<form id="cert-form" action="/install" method="POST" class="d-flex flex-column align-items-center">
|
||||
<div class="mb-3 w-100">
|
||||
<label for="cert-path" class="form-label"><%= I18n.t('views.cert_path_label') %></label>
|
||||
<input type="text" class="form-control w-100 required" id="cert-path" name="cert-path"
|
||||
placeholder="<%= I18n.t('views.enter_cert_path_placeholder') %>">
|
||||
</div>
|
||||
<div class="mb-3 w-100">
|
||||
<label for="validity-days" class="form-label"><%= I18n.t('views.cert_val_day') %></label>
|
||||
<input type="text" class="form-control w-100 required" id="validity-days" name="validity-days"
|
||||
placeholder="3650">
|
||||
</div>
|
||||
<div class="mb-3 w-100">
|
||||
<label for="country-name" class="form-label"><%= I18n.t('views.country_name_label') %></label>
|
||||
<input type="text" class="form-control w-100 required" id="country-name" name="country-name"
|
||||
placeholder="<%= I18n.t('views.enter_country_name_placeholder') %>">
|
||||
</div>
|
||||
<div class="mb-3 w-100">
|
||||
<label for="org-name" class="form-label"><%= I18n.t('views.org_name_label') %></label>
|
||||
<input type="text" class="form-control w-100 required" id="org-name" name="org-name"
|
||||
placeholder="<%= I18n.t('views.enter_org_name_placeholder') %>">
|
||||
</div>
|
||||
<div class="mb-3 w-100">
|
||||
<label for="common-name" class="form-label"><%= I18n.t('views.common_name_label') %></label>
|
||||
<input type="text" class="form-control w-100 required" id="common-name" name="common-name"
|
||||
placeholder="<%= I18n.t('views.enter_common_name_placeholder') %>">
|
||||
</div>
|
||||
<div class="mb-3 w-100">
|
||||
<label for="cert-password" class="form-label"><%= I18n.t('views.cert_password_label') %></label>
|
||||
<input type="password" class="form-control w-100 required" id="cert-password" name="cert-password"
|
||||
placeholder="<%= I18n.t('views.enter_cert_password_placeholder') %>">
|
||||
</div>
|
||||
<div class="d-flex justify-content-center w-100">
|
||||
<button type="button" class="btn btn-primary" id="submit-btn"><%= I18n.t('views.save') %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#submit-btn').click(function () {
|
||||
var isValid = true;
|
||||
$('.required').each(function() {
|
||||
if ($(this).val() === '') {
|
||||
isValid = false;
|
||||
$(this).addClass('is-invalid');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
$('#cert-form').submit();
|
||||
} else {
|
||||
alert('<%= I18n.t('views.please_enter_all_required_fields') %>');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
6
views/errinfo.erb
Normal file
6
views/errinfo.erb
Normal file
@@ -0,0 +1,6 @@
|
||||
<div class="card text-white mb-3 p-3">
|
||||
<div class="card-header bg-danger text-center"><%= ERB::Util.html_escape(@error) %></div>
|
||||
<div class="card-body text-black">
|
||||
<pre><%= ERB::Util.html_escape(@log) %></pre>
|
||||
</div>
|
||||
</div>
|
||||
10
views/info.erb
Normal file
10
views/info.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= @reason %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text text-center"><%= @info_descr %></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
61
views/layout.erb
Normal file
61
views/layout.erb
Normal file
@@ -0,0 +1,61 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title><%= @page_name %></title>
|
||||
<link rel="stylesheet" href="/js/bootstrap.min.css" />
|
||||
<script src="/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="/js/jquery-4.0.0.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<% if @menu %>
|
||||
<nav class="navbar navbar-expand-lg bg-body-tertiary">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="https://brepo.ru">brepo.ru</a>
|
||||
<span class="navbar-text pe-2 me-2 border-end"><%= I18n.t('views.user') %>: <%= session[:user].login %></span>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
|
||||
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
|
||||
<div class="navbar-nav">
|
||||
<a class="nav-link
|
||||
<% if @page_name == I18n.t('pages.servers') %>
|
||||
active
|
||||
<% end %>" aria-current="page" href="/"><%= I18n.t('pages.servers') %></a>
|
||||
<a class="nav-link
|
||||
<% if @page_name == I18n.t('pages.clients') %>
|
||||
active
|
||||
<% end %>" aria-current="page" href="/clients"><%= I18n.t('pages.clients') %></a>
|
||||
<% if hasperms?('admin') %>
|
||||
<a class="nav-link
|
||||
<% if @page_name == I18n.t('pages.users') %>
|
||||
active
|
||||
<% end %>
|
||||
" href="/ulist"><%= I18n.t('pages.users') %></a>
|
||||
<% end %>
|
||||
<a class="nav-link <%= 'active' if @page_name == I18n.t('pages.api') %>" href="/apiinfo"><%= I18n.t('pages.api') %></a>
|
||||
<a class="nav-link <%= 'active' if @page_name == I18n.t('pages.root') %>" href="/root"><%= I18n.t('pages.root') %></a>
|
||||
<a class="nav-link" href="/logout"><%= I18n.t('views.logout') %></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<% end %>
|
||||
|
||||
<div class="container pb-5"></div>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
<div class="container pb-5"></div>
|
||||
|
||||
<div class="container-fluid text-center bg-light p-2">
|
||||
Made by BayRepo © 2026
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
245
views/list.erb
Normal file
245
views/list.erb
Normal file
@@ -0,0 +1,245 @@
|
||||
<% if hasperms?('creator') %>
|
||||
<div class="container p-3">
|
||||
<p class="d-inline-flex gap-1">
|
||||
<button class="btn btn-outline-success" type="button" data-bs-toggle="collapse" data-bs-target="#collapseForm" aria-expanded="false" aria-controls="collapseForm">
|
||||
<%= I18n.t('views.create_request_cert_button') %>
|
||||
</button>
|
||||
</p>
|
||||
<div class="collapse" id="collapseForm">
|
||||
<div class="card card-body">
|
||||
<form action="/addserver" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="domains" class="form-label"><%= I18n.t('views.domains_label') %></label>
|
||||
<input type="text" class="form-control" id="domains" name="domains" required placeholder="<%= I18n.t('views.domains_placeholder') %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="validityDays" class="form-label"><%= I18n.t('views.validity_days_label') %></label>
|
||||
<input type="number" class="form-control" id="validityDays" name="validity_days" required value="365">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><%= I18n.t('views.request_cert_submit') %></button>
|
||||
<p class="mt-2"><%= I18n.t('views.request_cert_info') %></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="container">
|
||||
<ul class="nav nav-pills mb-3" id="liclist-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 0 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-slist-tab" data-bs-toggle="pill" data-bs-target="#pills-slist"
|
||||
type="button" role="tab" aria-controls="pills-slist"
|
||||
<% if @tab == 0 %>
|
||||
aria-selected="true"
|
||||
<% else %>
|
||||
aria-selected="false"
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.server_certs_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 1 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-clist-tab" data-bs-toggle="pill" data-bs-target="#pills-clist"
|
||||
type="button" role="tab" aria-controls="pills-clist"
|
||||
<% if @tab == 0 %>
|
||||
aria-selected="false" disabled
|
||||
<% else %>
|
||||
aria-selected="true"
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.selected_cert_info_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="liclist-tabContent">
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 0 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-slist" role="tabpanel" aria-labelledby="pills-slist-tab" tabindex="0">
|
||||
<div class="container text-center">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><%= I18n.t('views.status_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.id_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.date_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.revoke_date_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.info_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.actions_header') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @list_serv.each do |item| %>
|
||||
<tr
|
||||
<% if item[:status] != 'V' %>
|
||||
class="table-danger"
|
||||
<% elsif item[:expired] %>
|
||||
class="table-warning"
|
||||
<% end %>
|
||||
>
|
||||
<td><%= item[:status] %></td>
|
||||
<td><%= item[:id] %></td>
|
||||
<td><%= item[:date] %></td>
|
||||
<td><%= item[:revoke_date] %></td>
|
||||
<td>CN=<%= item[:ui][:CN] %>, O=<%= item[:ui][:O] %></td>
|
||||
<td>
|
||||
<% if hasperms?('creator') %>
|
||||
<a href="/revoke/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.revoke_cert_tooltip') %>" class="icon-link revoke-cert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-sign-stop" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M3.16 10.08c-.931 0-1.447-.493-1.494-1.132h.653c.065.346.396.583.891.583.524 0 .83-.246.83-.62 0-.303-.203-.467-.637-.572l-.656-.164c-.61-.147-.978-.51-.978-1.078 0-.706.597-1.184 1.444-1.184.853 0 1.386.475 1.436 1.087h-.645c-.064-.32-.352-.542-.797-.542-.472 0-.77.246-.77.6 0 .261.196.437.553.522l.654.161c.673.164 1.06.487 1.06 1.11 0 .736-.574 1.228-1.544 1.228Zm3.427-3.51V10h-.665V6.57H4.753V6h3.006v.568H6.587Z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.045 7.73v.544c0 1.131-.636 1.805-1.661 1.805-1.026 0-1.664-.674-1.664-1.805V7.73c0-1.136.638-1.807 1.664-1.807s1.66.674 1.66 1.807Zm-.674.547v-.553c0-.827-.422-1.234-.987-1.234-.572 0-.99.407-.99 1.234v.553c0 .83.418 1.237.99 1.237.565 0 .987-.408.987-1.237m1.15-2.276h1.535c.82 0 1.316.55 1.316 1.292 0 .747-.501 1.289-1.321 1.289h-.865V10h-.665zm1.436 2.036c.463 0 .735-.272.735-.744s-.272-.741-.735-.741h-.774v1.485z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.893 0a.5.5 0 0 0-.353.146L.146 4.54A.5.5 0 0 0 0 4.893v6.214a.5.5 0 0 0 .146.353l4.394 4.394a.5.5 0 0 0 .353.146h6.214a.5.5 0 0 0 .353-.146l4.394-4.394a.5.5 0 0 0 .146-.353V4.893a.5.5 0 0 0-.146-.353L11.46.146A.5.5 0 0 0 11.107 0zM1 5.1 5.1 1h5.8L15 5.1v5.8L10.9 15H5.1L1 10.9z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/download/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.download_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<% end %>
|
||||
<a href="/shows/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.view_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z" />
|
||||
<path
|
||||
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/clients/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.view_clients_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="container text-centered">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm">
|
||||
<% @pages.each do |item| %>
|
||||
<li class="page-item
|
||||
<% if item[:is_current] %>
|
||||
active
|
||||
<% end %>
|
||||
">
|
||||
<a class="page-link" aria-current="page"
|
||||
<% unless item[:is_current] %>
|
||||
href="/?p=<%= item[:page] %>"
|
||||
<% end %>><%= item[:page] %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 1 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-clist" role="tabpanel" aria-labelledby="pills-clist-tab" tabindex="0">
|
||||
<% if @tab == 1 %>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= @cert_info[:name] %> id: <%= @cert_info[:id] %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><%= I18n.t('views.cert_info_card_title') %></h5>
|
||||
<div class="card">
|
||||
<div class="card-body overflow-x-auto">
|
||||
<pre>
|
||||
<%= @cert_info[:common] %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="card-title"><%= I18n.t('views.revoke_info_card_title') %></h5>
|
||||
<div class="card">
|
||||
<div class="card-body overflow-x-auto">
|
||||
<pre>
|
||||
<%= @cert_info[:revoke] %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<% if hasperms?('creator') %>
|
||||
<h5 class="card-title"><%= I18n.t('views.additional_actions') %></h5>
|
||||
<div class="card">
|
||||
<p><%= I18n.t('views.download_cert_tooltip') %>
|
||||
<a href="/download/<%= ERB::Util.url_encode(@cert_info[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.download_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5 1.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
Nothing
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirmModal" class="modal fade" tabindex="-1" aria-labelledby="confirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmModalLabel"><%= I18n.t('views.modal_title_confirm') %></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><%= I18n.t('views.modal_body_confirm_revoke') %></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="revokeButton" class="btn btn-danger"><%= I18n.t('views.revoke_button') %></button>
|
||||
<button type="button" id="cancelButton" class="btn btn-secondary" data-bs-dismiss="modal"><%= I18n.t('views.modal_btn_cancel') %></button>
|
||||
<input type="hidden" value="" id="modal-redirect-url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
$('.revoke-cert').click(function(event) {
|
||||
event.preventDefault(); // Prevent the default action (navigation)
|
||||
var href = $(this).attr('href'); // Get the value of data-href
|
||||
if (href) {
|
||||
$('#modal-redirect-url').val(href);
|
||||
$('#confirmModal').modal('show'); // Show the modal
|
||||
}
|
||||
});
|
||||
|
||||
$('#cancelButton').click(function() {
|
||||
$('#confirmModal').modal('hide'); // Hide the modal when "Cancel" is clicked
|
||||
});
|
||||
|
||||
$('#revokeButton').click(function() {
|
||||
var redirectUrl = $('#modal-redirect-url').val(); // Get the saved URL
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl; // Redirect to the saved URL
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
371
views/listc.erb
Normal file
371
views/listc.erb
Normal file
@@ -0,0 +1,371 @@
|
||||
<% if hasperms?('creator') %>
|
||||
<div class="container p-3">
|
||||
<p class="d-inline-flex gap-1">
|
||||
<button class="btn btn-outline-warning" type="button" data-bs-toggle="collapse" data-bs-target="#collapseForm" aria-expanded="false" aria-controls="collapseForm">
|
||||
<%= I18n.t('views.create_request_cert_button') %>
|
||||
</button>
|
||||
</p>
|
||||
<div class="collapse" id="collapseForm">
|
||||
<div class="card card-body">
|
||||
<form action="/addclient" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="server_domain" class="form-label"><%= I18n.t('views.server_access_label') %></label>
|
||||
<select class="form-select" aria-label="<%= I18n.t('views.server_access_label') %>" id="server_domain" name="server_domain" required>
|
||||
<% fst = true %>
|
||||
<% @list_servers_full.each do |item| %>
|
||||
<% if fst == true %>
|
||||
<% fst = false %>
|
||||
<option value="<%= item %>"selected><%= item %></option>
|
||||
<% else %>
|
||||
<option value="<%= item %>"><%= item %></option>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="client" class="form-label"><%= I18n.t('views.client_name_email_label') %></label>
|
||||
<input type="text" class="form-control" id="client" name="client" required placeholder="<%= I18n.t('views.client_placeholder') %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="validityDays" class="form-label"><%= I18n.t('views.validity_days_label') %></label>
|
||||
<input type="number" class="form-control" id="validityDays" name="validity_days" required value="365">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><%= I18n.t('views.request_cert_submit') %></button>
|
||||
<p class="mt-2"><%= I18n.t('views.request_cert_info') %></p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="container">
|
||||
<ul class="nav nav-pills mb-3" id="liclist-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 0 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-slist-tab" data-bs-toggle="pill" data-bs-target="#pills-slist"
|
||||
type="button" role="tab" aria-controls="pills-slist"
|
||||
<% if @tab == 0 %>
|
||||
aria-selected="true"
|
||||
<% else %>
|
||||
aria-selected="false"
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.list_clients_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 1 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-clist-tab" data-bs-toggle="pill" data-bs-target="#pills-clist"
|
||||
type="button" role="tab" aria-controls="pills-clist"
|
||||
<% if @tab == 1 %>
|
||||
aria-selected="true"
|
||||
<% else %>
|
||||
aria-selected="false" disabled
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.filtered_certs_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 2 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-ilist-tab" data-bs-toggle="pill" data-bs-target="#pills-ilist"
|
||||
type="button" role="tab" aria-controls="pills-ilist"
|
||||
<% if @tab == 2 %>
|
||||
aria-selected="true"
|
||||
<% else %>
|
||||
aria-selected="false" disabled
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.selected_cert_info_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="liclist-tabContent">
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 0 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-slist" role="tabpanel" aria-labelledby="pills-slist-tab"
|
||||
tabindex="0">
|
||||
<div class="container text-center">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><%= I18n.t('views.status_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.id_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.date_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.revoke_date_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.info_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.actions_header') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @list_clients.each do |item| %>
|
||||
<tr
|
||||
<% if item[:status] != 'V' %>
|
||||
class="table-danger"
|
||||
<% elsif item[:expired] %>
|
||||
class="table-warning"
|
||||
<% end %>
|
||||
>
|
||||
<td><%= item[:status] %></td>
|
||||
<td><%= item[:id] %></td>
|
||||
<td><%= item[:date] %></td>
|
||||
<td><%= item[:revoke_date] %></td>
|
||||
<td>CN=<%= item[:ui][:CN] %>, O=<%= item[:ui][:O] %></td>
|
||||
<td>
|
||||
<% if hasperms?('creator') %>
|
||||
<a href="/revoke/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.revoke_cert_tooltip') %>" class="icon-link revoke-cert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-sign-stop" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M3.16 10.08c-.931 0-1.447-.493-1.494-1.132h.653c.065.346.396.583.891.583.524 0 .83-.246.83-.62 0-.303-.203-.467-.637-.572l-.656-.164c-.61-.147-.978-.51-.978-1.078 0-.706.597-1.184 1.444-1.184.853 0 1.386.475 1.436 1.087h-.645c-.064-.32-.352-.542-.797-.542-.472 0-.77.246-.77.6 0 .261.196.437.553.522l.654.161c.673.164 1.06.487 1.06 1.11 0 .736-.574 1.228-1.544 1.228Zm3.427-3.51V10h-.665V6.57H4.753V6h3.006v.568H6.587Z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.045 7.73v.544c0 1.131-.636 1.805-1.661 1.805-1.026 0-1.664-.674-1.664-1.805V7.73c0-1.136.638-1.807 1.664-1.807s1.66.674 1.66 1.807Zm-.674.547v-.553c0-.827-.422-1.234-.987-1.234-.572 0-.99.407-.99 1.234v.553c0 .83.418 1.237.99 1.237.565 0 .987-.408.987-1.237m1.15-2.276h1.535c.82 0 1.316.55 1.316 1.292 0 .747-.501 1.289-1.321 1.289h-.865V10h-.665zm1.436 2.036c.463 0 .735-.272.735-.744s-.272-.741-.735-.741h-.774v1.485z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.893 0a.5.5 0 0 0-.353.146L.146 4.54A.5.5 0 0 0 0 4.893v6.214a.5.5 0 0 0 .146.353l4.394 4.394a.5.5 0 0 0 .353.146h6.214a.5.5 0 0 0 .353-.146l4.394-4.394a.5.5 0 0 0 .146-.353V4.893a.5.5 0 0 0-.146-.353L11.46.146A.5.5 0 0 0 11.107 0zM1 5.1 5.1 1h5.8L15 5.1v5.8L10.9 15H5.1L1 10.9z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/download/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.download_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<% end %>
|
||||
<a href="/showc/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.view_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z" />
|
||||
<path
|
||||
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/clients/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.view_clients_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="container text-centered">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm">
|
||||
<% @pages.each do |item| %>
|
||||
<li class="page-item
|
||||
<% if item[:is_current] %>
|
||||
active
|
||||
<% end %>
|
||||
">
|
||||
<a class="page-link" aria-current="page"
|
||||
<% unless item[:is_current] %>
|
||||
href="/clients?p=<%= item[:page] %>"
|
||||
<% end %>><%= item[:page] %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 1 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-clist" role="tabpanel" aria-labelledby="pills-clist-tab" tabindex="0">
|
||||
<% if @tab == 1 %>
|
||||
<div class="container text-center">
|
||||
<p><%= @server_name %>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><%= I18n.t('views.status_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.id_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.date_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.revoke_date_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.info_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.actions_header') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @list_clients_short.each do |item| %>
|
||||
<tr
|
||||
<% if item[:status] != 'V' %>
|
||||
class="table-danger"
|
||||
<% elsif item[:expired] %>
|
||||
class="table-warning"
|
||||
<% end %>
|
||||
>
|
||||
<td><%= item[:status] %></td>
|
||||
<td><%= item[:id] %></td>
|
||||
<td><%= item[:date] %></td>
|
||||
<td><%= item[:revoke_date] %></td>
|
||||
<td>CN=<%= item[:ui][:CN] %>, O=<%= item[:ui][:O] %></td>
|
||||
<td>
|
||||
<% if hasperms?('creator') %>
|
||||
<a href="/revoke/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.revoke_cert_tooltip') %>" class="icon-link revoke-cert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-sign-stop" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M3.16 10.08c-.931 0-1.447-.493-1.494-1.132h.653c.065.346.396.583.891.583.524 0 .83-.246.83-.62 0-.303-.203-.467-.637-.572l-.656-.164c-.61-.147-.978-.51-.978-1.078 0-.706.597-1.184 1.444-1.184.853 0 1.386.475 1.436 1.087h-.645c-.064-.32-.352-.542-.797-.542-.472 0-.77.246-.77.6 0 .261.196.437.553.522l.654.161c.673.164 1.06.487 1.06 1.11 0 .736-.574 1.228-1.544 1.228Zm3.427-3.51V10h-.665V6.57H4.753V6h3.006v.568H6.587Z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M11.045 7.73v.544c0 1.131-.636 1.805-1.661 1.805-1.026 0-1.664-.674-1.664-1.805V7.73c0-1.136.638-1.807 1.664-1.807s1.66.674 1.66 1.807Zm-.674.547v-.553c0-.827-.422-1.234-.987-1.234-.572 0-.99.407-.99 1.234v.553c0 .83.418 1.237.99 1.237.565 0 .987-.408.987-1.237m1.15-2.276h1.535c.82 0 1.316.55 1.316 1.292 0 .747-.501 1.289-1.321 1.289h-.865V10h-.665zm1.436 2.036c.463 0 .735-.272.735-.744s-.272-.741-.735-.741h-.774v1.485z" />
|
||||
<path fill-rule="evenodd"
|
||||
d="M4.893 0a.5.5 0 0 0-.353.146L.146 4.54A.5.5 0 0 0 0 4.893v6.214a.5.5 0 0 0 .146.353l4.394 4.394a.5.5 0 0 0 .353.146h6.214a.5.5 0 0 0 .353-.146l4.394-4.394a.5.5 0 0 0 .146-.353V4.893a.5.5 0 0 0-.146-.353L11.46.146A.5.5 0 0 0 11.107 0zM1 5.1 5.1 1h5.8L15 5.1v5.8L10.9 15H5.1L1 10.9z" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/download/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.download_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<% end %>
|
||||
<a href="/showc/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.view_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"
|
||||
fill="currentColor" class="bi bi-eye" viewBox="0 0 16 16">
|
||||
<path
|
||||
d="M16 8s-3-5.5-8-5.5S0 8 0 8s3 5.5 8 5.5S16 8 16 8M1.173 8a13 13 0 0 1 1.66-2.043C4.12 4.668 5.88 3.5 8 3.5s3.879 1.168 5.168 2.457A13 13 0 0 1 14.828 8q-.086.13-.195.288c-.335.48-.83 1.12-1.465 1.755C11.879 11.332 10.119 12.5 8 12.5s-3.879-1.168-5.168-2.457A13 13 0 0 1 1.172 8z" />
|
||||
<path
|
||||
d="M8 5.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M4.5 8a3.5 3.5 0 1 1 7 0 3.5 3.5 0 0 1-7 0" />
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/clients/<%= ERB::Util.url_encode(item[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.view_clients_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-funnel" viewBox="0 0 16 16">
|
||||
<path d="M1.5 1.5A.5.5 0 0 1 2 1h12a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-.128.334L10 8.692V13.5a.5.5 0 0 1-.342.474l-3 1A.5.5 0 0 1 6 14.5V8.692L1.628 3.834A.5.5 0 0 1 1.5 3.5zm1 .5v1.308l4.372 4.858A.5.5 0 0 1 7 8.5v5.306l2-.666V8.5a.5.5 0 0 1 .128-.334L13.5 3.308V2z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="container text-centered">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm">
|
||||
<% @pages_short.each do |item| %>
|
||||
<li class="page-item
|
||||
<% if item[:is_current] %>
|
||||
active
|
||||
<% end %>
|
||||
">
|
||||
<a class="page-link" aria-current="page"
|
||||
<% unless item[:is_current] %>
|
||||
href="/clients/<%= @id %>?fp=<%= item[:page] %>"
|
||||
<% end %>><%= item[:page] %></a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
Nothing
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 2 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-ilist" role="tabpanel" aria-labelledby="pills-ilist-tab" tabindex="0">
|
||||
<% if @tab == 2 %>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= @cert_info[:name] %> id: <%= @cert_info[:id] %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><%= I18n.t('views.cert_info_card_title') %></h5>
|
||||
<div class="card">
|
||||
<div class="card-body overflow-x-auto">
|
||||
<pre>
|
||||
<%= @cert_info[:common] %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="card-title"><%= I18n.t('views.revoke_info_card_title') %></h5>
|
||||
<div class="card">
|
||||
<div class="card-body overflow-x-auto">
|
||||
<pre>
|
||||
<%= @cert_info[:revoke] %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<% if hasperms?('creator') %>
|
||||
<h5 class="card-title"><%= I18n.t('views.additional_actions') %></h5>
|
||||
<div class="card">
|
||||
<p><%= I18n.t('views.download_cert_tooltip') %>
|
||||
<a href="/download/<%= ERB::Util.url_encode(@cert_info[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.download_cert_tooltip') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-file-earmark-arrow-down" viewBox="0 0 16 16">
|
||||
<path d="M8.5 6.5a.5.5 0 0 0-1 0v3.793L6.354 9.146a.5.5 0 1 0-.708.708l2 2a.5.5 0 0 0 .708 0l2-2a.5.5 0 0 0-.708-.708L8.5 10.293z"/>
|
||||
<path d="M14 14V4.5L9.5 0H4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2M9.5 3A1.5.5 0 0 0 11 4.5h2V14a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1h5.5z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
Nothing
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirmModal" class="modal fade" tabindex="-1" aria-labelledby="confirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmModalLabel"><%= I18n.t('views.modal_title_confirm') %></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><%= I18n.t('views.modal_body_confirm_revoke') %></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="revokeButton" class="btn btn-danger"><%= I18n.t('views.revoke_button') %></button>
|
||||
<button type="button" id="cancelButton" class="btn btn-secondary" data-bs-dismiss="modal"><%= I18n.t('views.modal_btn_cancel') %></button>
|
||||
<input type="hidden" value="" id="modal-redirect-url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
$('.revoke-cert').click(function(event) {
|
||||
event.preventDefault(); // Prevent the default action (navigation)
|
||||
var href = $(this).attr('href'); // Get the value of data-href
|
||||
if (href) {
|
||||
$('#modal-redirect-url').val(href);
|
||||
$('#confirmModal').modal('show'); // Show the modal
|
||||
}
|
||||
});
|
||||
|
||||
$('#cancelButton').click(function() {
|
||||
$('#confirmModal').modal('hide'); // Hide the modal when "Cancel" is clicked
|
||||
});
|
||||
|
||||
$('#revokeButton').click(function() {
|
||||
var redirectUrl = $('#modal-redirect-url').val(); // Get the saved URL
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl; // Redirect to the saved URL
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
51
views/login.erb
Normal file
51
views/login.erb
Normal file
@@ -0,0 +1,51 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= I18n.t('views.authorize') %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<% unless @error.nil? %>
|
||||
<div class="alert alert-danger" role="alert"><%= @error %></div>
|
||||
<% end %>
|
||||
<div class="container">
|
||||
<form id="login-form" action="/login" method="POST" class="d-flex flex-column align-items-center">
|
||||
<div class="mb-3 w-100">
|
||||
<label for="login" class="form-label"><%= I18n.t('views.user_name') %></label>
|
||||
<input type="text" class="form-control w-100 required" id="login" name="login"
|
||||
placeholder="<%= I18n.t('views.enter_login') %>">
|
||||
</div>
|
||||
<div class="mb-3 w-100">
|
||||
<label for="password" class="form-label"><%= I18n.t('views.password') %></label>
|
||||
<input type="password" class="form-control w-100 required" id="password" name="password"
|
||||
placeholder="<%= I18n.t('views.enter_password') %>">
|
||||
</div>
|
||||
<div class="d-flex justify-content-center w-100">
|
||||
<button type="button" class="btn btn-primary" id="submit-btn"><%= I18n.t('views.authorize') %></button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#submit-btn').click(function () {
|
||||
var isValid = true;
|
||||
$('.required').each(function() {
|
||||
if ($(this).val() === '') {
|
||||
isValid = false;
|
||||
$(this).addClass('is-invalid');
|
||||
} else {
|
||||
$(this).removeClass('is-invalid');
|
||||
}
|
||||
});
|
||||
|
||||
if (isValid) {
|
||||
$('#login-form').submit();
|
||||
} else {
|
||||
alert('<%= I18n.t('views.please_enter_all_required_fields') %>');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
10
views/perms.erb
Normal file
10
views/perms.erb
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= @reason %>!!!
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="alert alert-danger text-center" role="alert"><%= @info_descr %></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
23
views/root.erb
Normal file
23
views/root.erb
Normal file
@@ -0,0 +1,23 @@
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= @cert_info[:name] %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5 class="card-title"><%= I18n.t('views.certificate_information') %></h5>
|
||||
<div class="card">
|
||||
<div class="card-body overflow-x-auto">
|
||||
<pre>
|
||||
<%= @cert_info[:common] %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="card-title"><%= I18n.t('views.revoke_information') %></h5>
|
||||
<div class="card">
|
||||
<div class="card-body overflow-x-auto">
|
||||
<pre>
|
||||
<%= @cert_info[:revoke] %>
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
240
views/ulist.erb
Normal file
240
views/ulist.erb
Normal file
@@ -0,0 +1,240 @@
|
||||
<div class="container p-3">
|
||||
<p class="d-inline-flex gap-1">
|
||||
<button class="btn btn-outline-success" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFormUser" aria-expanded="false" aria-controls="collapseFormUser">
|
||||
<%= I18n.t('views.create_user_button') %>
|
||||
</button>
|
||||
</p>
|
||||
<div class="collapse" id="collapseFormUser">
|
||||
<div class="card card-body">
|
||||
<form action="/adduser" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="login" class="form-label"><%= I18n.t('views.login_label') %></label>
|
||||
<input type="text" class="form-control" id="login" name="login" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label"><%= I18n.t('views.password_label') %></label>
|
||||
<input type="password" class="form-control" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label"><%= I18n.t('views.email_label') %></label>
|
||||
<input type="email" class="form-control" id="email" name="email">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="role" class="form-label"><%= I18n.t('views.role_label') %></label>
|
||||
<select class="form-select" id="role" name="role">
|
||||
<option value="0" selected><%= I18n.t('views.role_user') %></option>
|
||||
<option value="1"><%= I18n.t('views.role_creator') %></option>
|
||||
<option value="2"><%= I18n.t('views.role_admin') %></option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><%= I18n.t('views.submit_create_user') %></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<ul class="nav nav-pills mb-3" id="userlist-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 0 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-ulist-tab" data-bs-toggle="pill" data-bs-target="#pills-ulist" type="button" role="tab" aria-controls="pills-ulist"
|
||||
<% if @tab == 0 %>
|
||||
aria-selected="true"
|
||||
<% else %>
|
||||
aria-selected="false"
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.user_list_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link
|
||||
<% if @tab == 1 %>
|
||||
active
|
||||
<% end %>
|
||||
" id="pills-edit-tab" data-bs-toggle="pill" data-bs-target="#pills-edit" type="button" role="tab" aria-controls="pills-edit"
|
||||
<% if @tab == 1 %>
|
||||
aria-selected="true"
|
||||
<% else %>
|
||||
aria-selected="false" disabled
|
||||
<% end %>
|
||||
>
|
||||
<%= I18n.t('views.edit_user_tab') %>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content" id="userlist-tabContent">
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 0 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-ulist" role="tabpanel" aria-labelledby="pills-ulist-tab" tabindex="0">
|
||||
<div class="container text-center">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col"><%= I18n.t('views.user_name_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.role_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.email_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.created_at_header') %></th>
|
||||
<th scope="col"><%= I18n.t('views.actions_header') %></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @users.each do |user| %>
|
||||
<tr>
|
||||
<td><%= user[:login] %></td>
|
||||
<td>
|
||||
<%= case user[:role]
|
||||
when 0 then I18n.t('views.role_user')
|
||||
when 1 then I18n.t('views.role_creator')
|
||||
when 2 then I18n.t('views.role_admin')
|
||||
else I18n.t('views.role_unknown')
|
||||
end %>
|
||||
</td>
|
||||
<td>
|
||||
<% if user[:email].nil? || user[:email].strip == '' %>
|
||||
<%= I18n.t('views.no_email') %>
|
||||
<% else %>
|
||||
<%= user[:email] %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td><%= user[:create_at].strftime('%Y-%m-%d') %></td>
|
||||
<td>
|
||||
<a href="/edituser/<%= ERB::Util.url_encode(user[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.edit_user_tab') %>" class="icon-link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pen" viewBox="0 0 16 16">
|
||||
<path d="m13.498.795.149-.149a1.207 1.207 0 1 1 1.707 1.708l-.149.148a1.5 1.5 0 0 1-.059 2.059L4.854 14.854a.5.5 0 0 1-.233.131l-4 1a.5.5 0 0 1-.606-.606l1-4a.5.5 0 0 1 .131-.232l9.642-9.642a.5.5 0 0 0-.642.056L6.854 4.854a.5.5 0 1 1-.708-.708L9.44.854A1.5 1.5 0 0 1 11.5.796a1.5 1.5 0 0 1 1.998-.001m-.644.766a.5.5 0 0 0-.707 0L1.95 11.756l-.764 3.057 3.057-.764L14.44 3.854a.5.5 0 0 0 0-.708z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/deleteuser/<%= ERB::Util.url_encode(user[:id]) %>" data-bs-toggle="tooltip" data-bs-title="<%= I18n.t('views.delete_user') %>" class="icon-link revoke-cert">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-trash3" viewBox="0 0 16 16">
|
||||
<path d="M6.5 1h3a.5.5 0 0 1 .5.5v1H6v-1a.5.5 0 0 1 .5-.5M11 2.5v-1A1.5 1.5 0 0 0 9.5 0h-3A1.5 1.5 0 0 0 5 1.5v1H1.5a.5.5 0 0 0 0 1h.538l.853 10.66A2 2 0 0 0 4.885 16h6.23a2 2 0 0 0 1.994-1.84l.853-10.66h.538a.5.5 0 0 0 0-1zm1.958 1-.846 10.58a1 1 0 0 1-.997.92h-6.23a1 1 0 0 1-.997-.92L3.042 3.5zm-7.487 1a.5.5 0 0 1 .528.47l.5 8.5a.5.5 0 0 1-.998.06L5 5.03a.5.5 0 0 1 .47-.53Zm5.058 0a.5.5 0 0 1 .47.53l-.5 8.5a.5.5 0 1 1-.998-.06l.5-8.5a.5.5 0 0 1 .528-.47M8 4.5a.5.5 0 0 1 .5.5v8.5a.5.5 0 0 1-1 0V5a.5.5 0 0 1 .5-.5"/>
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="container text-centered">
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm">
|
||||
<% @pages.each do |item| %>
|
||||
<li class="page-item
|
||||
<% if item[:is_current] %>
|
||||
active
|
||||
<% end %>"
|
||||
>
|
||||
<a class="page-link" aria-current="page" href="/users?p=<%= item[:page] %>">
|
||||
<%= item[:page] %>
|
||||
</a>
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane fade
|
||||
<% if @tab == 1 %>
|
||||
show active
|
||||
<% end %>
|
||||
" id="pills-edit" role="tabpanel" aria-labelledby="pills-edit-tab" tabindex="0">
|
||||
<% if @tab == 1 %>
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<%= I18n.t('views.card_header_edit_user') %>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="/edituser/<%= @selected_user[:id] %>" method="post" id="editUserForm">
|
||||
<input type="hidden" name="id" value="<%= @selected_user[:id] %>">
|
||||
<div class="mb-3">
|
||||
<label for="login" class="form-label"><%= I18n.t('views.login_label') %></label>
|
||||
<input type="text" class="form-control" id="login" name="login" value="<%= @selected_user[:login] %>" readonly>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label"><%= I18n.t('views.password_label') %></label>
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label"><%= I18n.t('views.email_label') %></label>
|
||||
<input type="email" class="form-control" id="email" name="email" value="<%= @selected_user[:email] %>">
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="role" class="form-label"><%= I18n.t('views.role_label') %></label>
|
||||
<select class="form-select" id="role" name="role">
|
||||
<% if @selected_user[:role] == 0 %>
|
||||
<option value="0" selected><%= I18n.t('views.role_user') %></option>
|
||||
<% else %>
|
||||
<option value="0"><%= I18n.t('views.role_user') %></option>
|
||||
<% end %>
|
||||
<% if @selected_user[:role] == 1 %>
|
||||
<option value="1" selected><%= I18n.t('views.role_creator') %></option>
|
||||
<% else %>
|
||||
<option value="1"><%= I18n.t('views.role_creator') %></option>
|
||||
<% end %>
|
||||
<% if @selected_user[:role] == 2 %>
|
||||
<option value="2" selected><%= I18n.t('views.role_admin') %></option>
|
||||
<% else %>
|
||||
<option value="2"><%= I18n.t('views.role_admin') %></option>
|
||||
<% end %>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><%= I18n.t('views.submit_update_user') %></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
Nothing
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="confirmModal" class="modal fade" tabindex="-1" aria-labelledby="confirmModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="confirmModalLabel"><%= I18n.t('views.modal_title_confirm') %></h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p><%= I18n.t('views.modal_body_confirm') %></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" id="revokeButton" class="btn btn-danger"><%= I18n.t('views.modal_btn_delete') %></button>
|
||||
<button type="button" id="cancelButton" class="btn btn-secondary" data-bs-dismiss="modal"><%= I18n.t('views.modal_btn_cancel') %></button>
|
||||
<input type="hidden" value="" id="modal-redirect-url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]')
|
||||
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl))
|
||||
$('.revoke-cert').click(function(event) {
|
||||
event.preventDefault(); // Prevent the default action (navigation)
|
||||
var href = $(this).attr('href'); // Get the value of data-href
|
||||
if (href) {
|
||||
$('#modal-redirect-url').val(href);
|
||||
$('#confirmModal').modal('show'); // Show the modal
|
||||
}
|
||||
});
|
||||
|
||||
$('#cancelButton').click(function() {
|
||||
$('#confirmModal').modal('hide'); // Hide the modal when "Cancel" is clicked
|
||||
});
|
||||
|
||||
$('#revokeButton').click(function() {
|
||||
var redirectUrl = $('#modal-redirect-url').val(); // Get the saved URL
|
||||
if (redirectUrl) {
|
||||
window.location.href = redirectUrl; // Redirect to the saved URL
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user