Initial commit

master
Alexey Berezhok 1 week ago
commit c2b717bea6

@ -0,0 +1,2 @@
---
BUNDLE_PATH: "vendor/bundle"

9
.gitignore vendored

@ -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

@ -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"]

@ -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"

@ -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

@ -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

1196
app.rb

File diff suppressed because it is too large Load Diff

@ -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

@ -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

@ -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

@ -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

@ -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.

@ -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
```

@ -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

@ -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,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

@ -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

@ -0,0 +1,3 @@
class User < Sequel::Model(:users)
# Модель для пользователей в приложении Sinatra с использованием Sequel
end

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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

@ -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"

@ -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

@ -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
}

@ -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
}

@ -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

@ -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
}

@ -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

@ -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>

@ -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>

@ -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>

@ -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>

@ -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 &copy; 2026
</div>
</body>
</html>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>

@ -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>
Loading…
Cancel
Save