This commit is contained in:
wangjianhong
2025-07-23 17:30:33 +08:00
commit 5e4e272b3a
875 changed files with 362355 additions and 0 deletions

View File

@@ -0,0 +1,61 @@
package config
import (
"ca-mini/pkg/utils"
"fmt"
"github.com/spf13/viper"
)
type AppConfig struct {
Name string `mapstructure:"name"`
Version string `mapstructure:"version"`
CopyrightYear int `mapstructure:"copyrightYear"`
}
type ServerConfig struct {
Port int `mapstructure:"port"`
ContextPath string `mapstructure:"context-path"`
}
type LoggingConfig struct {
Level string `mapstructure:"level"`
Path string `mapstructure:"path"`
}
type DatasourceConfig struct {
Url string `mapstructure:"url"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
}
type Config struct {
App AppConfig `mapstructure:"app"`
Server ServerConfig `mapstructure:"server"`
Datasource DatasourceConfig `mapstructure:"datasource"`
Logging LoggingConfig `mapstructure:"logging"`
ServerAddress string
}
func Load() (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(utils.WORK_PATH + "/conf")
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
var config Config
err = viper.Unmarshal(&config)
if err != nil {
return nil, err
}
// 设置ServerAddress
config.ServerAddress = fmt.Sprintf(":%d", config.Server.Port)
return &config, nil
}

View File

@@ -0,0 +1,317 @@
package handlers
import (
"ca-mini/db"
"ca-mini/internal/logger"
"ca-mini/internal/model"
"ca-mini/internal/repository"
"ca-mini/pkg/utils"
"crypto/x509"
"encoding/json"
"encoding/pem"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"github.com/gorilla/mux"
)
const (
// 错误码
ERROR_CODE_SUCCESS = 0
ERROR_CODE_FAIL = 1
// 错误信息
ERROR_MSG_SUCCESS = "success"
ERROR_MSG_FAIL = "fail"
WORK_PATH = "/opt/arrokoth/ca-mini"
CA_CERT_PATH = WORK_PATH + "/ca/CaRoot.crt"
CA_CONFIG_PATH = WORK_PATH + "/ca/ca.conf"
CRL_FILE_PATH = WORK_PATH + "/crl/CaRoot.crl"
)
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Date string `json:"date"`
}
type SuccessResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Date string `json:"date"`
}
type CertificateResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Date string `json:"date"`
Data model.CertInfo `json:"data"`
}
// IssueCertificate 处理证书签发请求
func IssueCertificate(w http.ResponseWriter, r *http.Request) {
// 解析CSR请求
var csrRequest struct {
Subject string `json:"subject"`
SubjectAltName string `json:"subjectAltName"`
Alg string `json:"alg"`
Len int `json:"len"`
}
if err := json.NewDecoder(r.Body).Decode(&csrRequest); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 检查Subject是否为空
if csrRequest.Subject == "" {
http.Error(w, "Subject is required", http.StatusBadRequest)
return
}
// 生成密钥
id, err := utils.GenerateKey(csrRequest.Alg, csrRequest.Len)
if err != nil {
http.Error(w, "Failed to generate key", http.StatusInternalServerError)
return
}
var sans []string
if csrRequest.SubjectAltName != "" {
sans = strings.Split(strings.TrimSpace(csrRequest.SubjectAltName), ",")
}
// 如果SubjectAltName为空则使用Subject作为SAN
if len(sans) == 0 {
sans = []string{csrRequest.Subject}
}
// 生成CSR文件
id, err = utils.GenerateCsr(id, csrRequest.Subject, sans)
if err != nil {
http.Error(w, "Failed to generate CSR file", http.StatusInternalServerError)
return
}
// 生成证书
id, err = utils.GenerateCert(id, 0)
if err != nil {
http.Error(w, "Failed to generate certificate", http.StatusInternalServerError)
return
}
// 解析证书信息
certInfo, err := ParseCert(id)
if err != nil {
http.Error(w, "Failed to parse certificate", http.StatusInternalServerError)
return
}
// 新增证书到数据库
repository := repository.NewCertificateRepository(db.DB)
if err := repository.AddCert(certInfo); err != nil {
logger.Error("Failed to save certificate to database %v", err)
http.Error(w, "Failed to save certificate", http.StatusInternalServerError)
return
}
// 返回证书信息
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := CertificateResponse{
Code: http.StatusOK,
Message: "Certificate issued successfully",
Date: time.Now().Format(time.RFC3339),
Data: *certInfo,
}
json.NewEncoder(w).Encode(response)
}
// 查询证书
func GetCertificate(w http.ResponseWriter, r *http.Request) {
// 从URL查询参数中获取id
//id := r.URL.Query().Get("id")
// 获取路径参数 id
vars := mux.Vars(r)
id := vars["id"]
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
// 新增证书到数据库
repository := repository.NewCertificateRepository(db.DB)
certInfo, err := repository.SelectCertById(id)
if err != nil {
logger.Error("Failed to query certificate to database %v", err)
http.Error(w, "Failed to query certificate", http.StatusInternalServerError)
return
}
// 读取CA证书文件内容
caData, err := utils.GetCaCert()
if err == nil {
certInfo.Ca = caData
}
// 返回证书信息
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := CertificateResponse{
Code: http.StatusOK,
Message: "Certificate retrieved successfully",
Date: time.Now().Format(time.RFC3339),
Data: *certInfo,
}
json.NewEncoder(w).Encode(response)
}
// CheckBlacklist 处理黑名单查询请求
func CheckBlacklist(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]bool{
"isBlacklisted": false,
})
}
// RevokeCertificate 处理证书撤销请求
func RevokeCertificate(w http.ResponseWriter, r *http.Request) {
// 从URL查询参数中获取id
// id := r.URL.Query().Get("id")
// 获取路径参数 id
vars := mux.Vars(r)
id := vars["id"]
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
// 检查管理员权限假设通过HTTP Basic Auth验证
username, password, ok := r.BasicAuth()
if !ok || username != "admin" || password != "admin" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 解析证书信息
id, err := utils.RevokeCert(id)
if err != nil {
http.Error(w, "Failed to revoke certificate", http.StatusInternalServerError)
return
}
// 返回证书信息
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := SuccessResponse{
Code: http.StatusOK,
Message: id,
Date: time.Now().Format(time.RFC3339),
}
json.NewEncoder(w).Encode(response)
}
// 解析证书
func ParseCert(id string) (*model.CertInfo, error) {
// 读取私钥文件内容
keyData, err := utils.GetKey(id)
if err != nil {
return nil, fmt.Errorf("获取私钥失败: %v", err)
}
// 读取证书请求文件内容
csrData, err := utils.GetCsr(id)
if err != nil {
return nil, fmt.Errorf("获取证书请求失败: %v", err)
}
// 读取CA证书文件内容
caData, err := utils.GetCaCert()
if err != nil {
return nil, fmt.Errorf("获取CA证书失败: %v", err)
}
// 读取证书文件内容
certData, err := utils.GetCert(id)
if err != nil {
return nil, fmt.Errorf("获取证书失败: %v", err)
}
// 解析PEM编码的证书
block, _ := pem.Decode([]byte(certData))
if block == nil {
return nil, fmt.Errorf("无法解析PEM块")
}
// 解析X.509证书
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("解析证书失败: %v", err)
}
version := fmt.Sprintf("%d", cert.Version)
certCn := cert.Subject.CommonName
certDn := cert.Subject.String()
issuerCn := cert.Issuer.CommonName
issuerDn := cert.Issuer.String()
publicKeyAlg := cert.PublicKeyAlgorithm.String()
signatureAlg := cert.SignatureAlgorithm.String()
certSubAltName := ""
for i, DNSName := range cert.DNSNames {
if i == 0 {
certSubAltName = DNSName
} else {
certSubAltName = fmt.Sprintf("%s,%s", certSubAltName, DNSName)
}
}
// beforeTime := cert.NotBefore.Format(time.RFC3339)
// afterTime := cert.NotAfter.Format(time.RFC3339)
beforeTime := cert.NotBefore
afterTime := cert.NotAfter
serialNumber := cert.SerialNumber.String()
return &model.CertInfo{
CertId: id,
Version: version,
CertCn: certCn,
CertDn: certDn,
PublicKeyAlg: publicKeyAlg,
SignatureAlg: signatureAlg,
IssuerCn: issuerCn,
IssuerDn: issuerDn,
SerialNumber: serialNumber,
CertSubAltName: certSubAltName,
BeforeTime: beforeTime,
AfterTime: afterTime,
PrivateKey: keyData,
Csr: csrData,
Cert: certData,
Ca: caData,
}, nil
}
// DownloadCRL 处理黑名单文件下载请求
func DownloadCRL(w http.ResponseWriter, r *http.Request) {
// 打开黑名单文件
filePath := CRL_FILE_PATH
file, err := os.Open(filePath)
if err != nil {
http.Error(w, "Failed to open blacklist file", http.StatusInternalServerError)
return
}
defer file.Close()
// 设置响应头,触发浏览器下载行为
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment; filename=CaRoot.crl")
// 将文件内容写入响应体
if _, err := io.Copy(w, file); err != nil {
http.Error(w, "Error streaming the file", http.StatusInternalServerError)
return
}
}

View File

@@ -0,0 +1,116 @@
package handlers
import (
"ca-mini/pkg/utils"
"encoding/json"
"fmt"
"net/http"
"time"
)
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type TokenInfo struct {
Token string `json:"token"`
}
type UserInfo struct {
Name string `json:"name"`
Introduction string `json:"introduction"`
Avatar string `json:"avatar"`
Roles []string `json:"roles"`
}
type LoginResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Date string `json:"date"`
Data TokenInfo `json:"data"`
}
type LogutResponse struct {
Code int `json:"code"`
Message string `json:"message"`
}
type UserInfoResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Date string `json:"date"`
Data UserInfo `json:"data"`
}
// IssueCertificate 处理证书签发请求
func Login(w http.ResponseWriter, r *http.Request) {
// 解析CSR请求
var loginRequest LoginRequest
if err := json.NewDecoder(r.Body).Decode(&loginRequest); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// 检查用户
if !CheckUser(loginRequest) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// 生成Token
token, err := utils.GenerateRandomString(32)
if err != nil {
http.Error(w, fmt.Sprintf("Failed to generate token: %v", err), http.StatusInternalServerError)
return
}
// 返回Token信息
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := LoginResponse{
Code: http.StatusOK,
Message: "Login successful",
Date: time.Now().Format(time.RFC3339),
Data: TokenInfo{Token: token},
}
json.NewEncoder(w).Encode(response)
}
func Logout(w http.ResponseWriter, r *http.Request) {
// 这里可以添加注销逻辑,例如清除用户会话等
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := LogutResponse{
Code: http.StatusOK,
Message: "Logout successful",
}
json.NewEncoder(w).Encode(response)
}
// CheckBlacklist 处理黑名单查询请求
func CheckUser(login LoginRequest) bool {
if login.Username == "admin" && login.Password == "111111" {
return true
}
return false
}
func GetUserInfo(w http.ResponseWriter, r *http.Request) {
// 假设用户信息存储在某个地方,这里直接返回一个示例用户信息
userInfo := UserInfo{
Name: "Admin User",
Introduction: "This is an admin",
Avatar: "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
Roles: []string{"admin"},
}
// 返回Token信息
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
response := UserInfoResponse{
Code: http.StatusOK,
Message: "Login successful",
Date: time.Now().Format(time.RFC3339),
Data: userInfo,
}
json.NewEncoder(w).Encode(response)
}

View File

@@ -0,0 +1,107 @@
package logger
import (
"ca-mini/internal/config"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/sirupsen/logrus"
)
var (
// Logger 日志记录器
Logger *logrus.Logger
// LogFile 日志文件
LogFile *os.File
// LogFilePath 日志文件路径
LogFilePath string
// LogFileName 日志文件名
LogFileName string
// LogFileExt 日志文件扩展名
LogFileExt string
)
// InitLogger 初始化日志记录器
func InitLogger() {
// 创建日志记录器
Logger = logrus.New()
// 显示行号
logrus.SetReportCaller(true)
// 设置日志格式
Logger.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: "2006-01-02 15:04:05",
})
cfg, err := config.Load()
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 设置日志级别
level, err := logrus.ParseLevel(cfg.Logging.Level)
if err != nil {
log.Fatalf("Invalid log level: %v", err)
}
Logger.SetLevel(level)
// 设置日志输出到文件
LogFilePath = cfg.Logging.Path
if LogFilePath == "" {
LogFilePath = "./logs"
}
if _, err := os.Stat(LogFilePath); os.IsNotExist(err) {
err := os.MkdirAll(LogFilePath, os.ModePerm)
if err != nil {
log.Fatalf("Failed to create log directory: %v", err)
}
}
LogFileName = fmt.Sprintf("ca-server-%s.log", time.Now().Format("2006-01-02"))
LogFileExt = ".log"
LogFile, err = os.OpenFile(filepath.Join(LogFilePath, LogFileName), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
if err != nil {
log.Fatalf("Failed to open log file: %v", err)
}
Logger.SetOutput(LogFile)
}
// CloseLogger 关闭日志记录器
func CloseLogger() {
if LogFile != nil {
err := LogFile.Close()
if err != nil {
if Logger != nil {
Logger.Errorf("Failed to close log file: %v", err)
} else {
log.Printf("Failed to close log file: %v", err)
}
}
}
}
// LogDebugf 记录调试日志(格式化)
func Debug(format string, args ...interface{}) {
if Logger != nil {
Logger.Debugf(format, args...)
}
}
// LogInfof 记录信息日志(格式化)
func Info(format string, args ...interface{}) {
if Logger != nil {
Logger.Infof(format, args...)
}
}
// LogErrorf 记录错误日志(格式化)
func Error(format string, args ...interface{}) {
if Logger != nil {
Logger.Errorf(format, args...)
} else {
log.Printf(format, args...)
}
}

View File

@@ -0,0 +1,29 @@
package middleware
import (
"net/http"
"github.com/dgrijalva/jwt-go"
)
// JWTMiddleware 实现JWT认证中间件
func JWTMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tokenString := r.Header.Get("Authorization")
if tokenString == "" {
http.Error(w, "Authorization header required", http.StatusUnauthorized)
return
}
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("your-secret-key"), nil
})
if err != nil || !token.Valid {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}

View File

@@ -0,0 +1,97 @@
package middleware
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
"github.com/sirupsen/logrus"
"ca-mini/internal/config"
"ca-mini/internal/logger"
)
type RequestInfo struct {
Method string `json:"method"`
Path string `json:"path"`
Header http.Header `json:"header"`
Body string `json:"body"`
}
type ResponseInfo struct {
Code int `json:"code"`
Header http.Header `json:"header"`
Body string `json:"body"`
}
// responseWriter 包装 http.ResponseWriter 以捕获状态码和响应体
type responseWriter struct {
http.ResponseWriter
statusCode int
body *bytes.Buffer
}
// WriteHeader 捕获状态码
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// Write 捕获响应体
func (rw *responseWriter) Write(b []byte) (int, error) {
rw.body.Write(b)
return rw.ResponseWriter.Write(b)
}
// LoggerMiddleware 日志中间件
func LoggerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cfg, err := config.Load()
if err != nil || cfg.Logging.Level == "debug" {
next.ServeHTTP(w, r)
return
}
// 开始计时
start := time.Now()
// 读取请求体并重置 r.Body
var requestBody bytes.Buffer
_, err = io.Copy(&requestBody, r.Body)
if err != nil {
logger.Error("Failed to read request body: %v", err)
}
r.Body = io.NopCloser(&requestBody)
// 创建一个响应包装器来捕获响应状态码和响应体
lrw := &responseWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
body: &bytes.Buffer{},
}
// 处理请求
next.ServeHTTP(lrw, r)
// 统计请求处理时间
duration := time.Since(start).Milliseconds()
// 记录日志
logger.Logger.WithFields(logrus.Fields{
"request": RequestInfo{
Method: r.Method,
Path: r.URL.Path,
Header: r.Header,
Body: requestBody.String(),
},
"response": ResponseInfo{
Code: lrw.statusCode,
Header: lrw.Header(),
Body: lrw.body.String(),
},
"duration": fmt.Sprintf("%dms", duration),
}).Debug("Request processed")
})
}

View File

@@ -0,0 +1,23 @@
package model
import "time"
type CertInfo struct {
CertId string `json:"certId"`
CertCn string `json:"certCn"`
CertDn string `json:"certDn"`
PublicKeyAlg string `json:"publicKeyAlg"`
SignatureAlg string `json:"signatureAlg"`
KeyLength int `json:"keyLength"`
IssuerCn string `json:"issuerCn"`
IssuerDn string `json:"issuerDn"`
SerialNumber string `json:"serialNumber"`
CertSubAltName string `json:"certSubAltName"`
BeforeTime time.Time `json:"beforeTime"`
AfterTime time.Time `json:"afterTime"`
Version string `json:"version"`
PrivateKey string `json:"privateKey"`
Csr string `json:"csr"`
Cert string `json:"cert"`
Ca string `json:"ca"`
}

View File

@@ -0,0 +1,56 @@
package repository
import (
"ca-mini/internal/model"
"database/sql"
"time"
)
type CertificateRepository struct {
db *sql.DB
}
func NewCertificateRepository(db *sql.DB) *CertificateRepository {
return &CertificateRepository{db: db}
}
func (r *CertificateRepository) AddCert(cert *model.CertInfo) error {
createdAt := time.Now()
updatedAt := time.Now()
_, err := r.db.Exec(
"INSERT INTO certificates (id, serial_number, cert_cn, cert_dn, cert_version, public_key_alg, signature_alg, issuer_cn, issuer_dn, cert_sub_alt_name, algorithm, key_length, csr, private_key, certificate, valid_from, valid_to, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
cert.CertId, cert.SerialNumber, cert.CertCn, cert.CertDn, cert.Version, cert.PublicKeyAlg, cert.SignatureAlg, cert.IssuerCn, cert.IssuerDn, cert.CertSubAltName, cert.PublicKeyAlg, 2048, cert.Csr, cert.PrivateKey, cert.Cert, cert.BeforeTime, cert.AfterTime, createdAt, updatedAt,
)
return err
}
func (r *CertificateRepository) SelectCertById(id string) (*model.CertInfo, error) {
var cert model.CertInfo
err := r.db.QueryRow(
"SELECT id, serial_number, cert_cn, cert_dn, cert_version, public_key_alg, signature_alg, issuer_cn, issuer_dn, cert_sub_alt_name, algorithm, key_length, csr, private_key, certificate, valid_from, valid_to FROM certificates WHERE id = ?",
id,
).Scan(
&cert.CertId, &cert.SerialNumber, &cert.CertCn, &cert.CertDn, &cert.Version, &cert.PublicKeyAlg, &cert.SignatureAlg, &cert.IssuerCn, &cert.IssuerDn, &cert.CertSubAltName, &cert.PublicKeyAlg, &cert.KeyLength, &cert.Csr, &cert.PrivateKey, &cert.Cert, &cert.BeforeTime, &cert.AfterTime,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, nil // 未找到记录
}
return nil, err
}
return &cert, nil
}
func (r *CertificateRepository) UpdateCert(cert *model.CertInfo) error {
updatedAt := time.Now()
_, err := r.db.Exec(
"UPDATE certificates SET serial_number = ?, cert_cn = ?, cert_dn = ?, cert_version = ?, public_key_alg = ?, signature_alg = ?, issuer_cn = ?, issuer_dn = ?, cert_sub_alt_name = ?, algorithm = ?, key_length = ?, csr = ?, private_key = ?, certificate = ?, valid_from = ?, valid_to = ?, updated_at = ? WHERE id = ?",
cert.SerialNumber, cert.CertCn, cert.CertDn, cert.Version, cert.PublicKeyAlg, cert.SignatureAlg, cert.IssuerCn, cert.IssuerDn, cert.CertSubAltName, cert.PublicKeyAlg, cert.KeyLength, cert.Csr, cert.PrivateKey, cert.Cert, cert.BeforeTime, cert.AfterTime, updatedAt, cert.CertId,
)
return err
}
func (r *CertificateRepository) DeleteCert(id string) error {
_, err := r.db.Exec("DELETE FROM certificates WHERE id = ?", id)
return err
}

View File

@@ -0,0 +1,27 @@
package routes
import (
"ca-mini/internal/handlers"
"ca-mini/internal/middleware"
"github.com/gorilla/mux"
)
// SetupAPIRoutes 初始化API路由
func SetupAPIRoutes(r *mux.Router) {
r.HandleFunc("/CaRoot.crl", handlers.DownloadCRL).Methods("GET")
r.HandleFunc("/user/login", handlers.Login).Methods("POST")
r.HandleFunc("/user/logout", handlers.Logout).Methods("POST")
r.HandleFunc("/user/info", handlers.GetUserInfo).Methods("GET")
api := r.PathPrefix("/api/v1").Subrouter()
// api.Use(middleware.JWTMiddleware)
api.Use(middleware.LoggerMiddleware)
api.HandleFunc("/certificates", handlers.IssueCertificate).Methods("POST")
api.HandleFunc("/blacklist", handlers.CheckBlacklist).Methods("GET")
api.HandleFunc("/certificates/{id}", handlers.GetCertificate).Methods("GET")
api.HandleFunc("/certificates/{id}", handlers.RevokeCertificate).Methods("DELETE")
}