mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 03:07:20 +08:00
feat: 面板ip证书
This commit is contained in:
@@ -151,7 +151,7 @@ func initWeb() (*app.Web, error) {
|
||||
return nil, err
|
||||
}
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
jobs := job.NewJobs(db, logger, settingRepo, certRepo, backupRepo, cacheRepo, taskRepo)
|
||||
jobs := job.NewJobs(config, db, logger, settingRepo, certRepo, certAccountRepo, backupRepo, cacheRepo, taskRepo)
|
||||
cron, err := bootstrap.NewCron(config, logger, jobs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -68,7 +68,7 @@ func initCli() (*app.Cli, error) {
|
||||
certAccountRepo := data.NewCertAccountRepo(locale, db, userRepo, logger)
|
||||
websiteRepo := data.NewWebsiteRepo(locale, db, cacheRepo, databaseRepo, databaseServerRepo, databaseUserRepo, certRepo, certAccountRepo, settingRepo)
|
||||
backupRepo := data.NewBackupRepo(locale, db, settingRepo, websiteRepo)
|
||||
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo)
|
||||
cliService := service.NewCliService(locale, config, db, appRepo, cacheRepo, userRepo, settingRepo, backupRepo, websiteRepo, databaseServerRepo, certRepo, certAccountRepo)
|
||||
cli := route.NewCli(locale, cliService)
|
||||
command := bootstrap.NewCli(locale, cli)
|
||||
gormigrate := bootstrap.NewMigrate(db)
|
||||
|
||||
@@ -38,6 +38,7 @@ type CertRepo interface {
|
||||
Delete(id uint) error
|
||||
ObtainAuto(id uint) (*acme.Certificate, error)
|
||||
ObtainManual(id uint) (*acme.Certificate, error)
|
||||
ObtainPanel(account *CertAccount, ips []string) ([]byte, []byte, error)
|
||||
ObtainSelfSigned(id uint) error
|
||||
Renew(id uint) (*acme.Certificate, error)
|
||||
ManualDNS(id uint) ([]acme.DNSRecord, error)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
type SettingKey string
|
||||
|
||||
const (
|
||||
SettingKeyIP SettingKey = "ip"
|
||||
SettingKeyName SettingKey = "name"
|
||||
SettingKeyVersion SettingKey = "version"
|
||||
SettingKeyChannel SettingKey = "channel"
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -233,6 +234,21 @@ func (r *certRepo) ObtainManual(id uint) (*acme.Certificate, error) {
|
||||
return &ssl, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainPanel(account *biz.CertAccount, ips []string) ([]byte, []byte, error) {
|
||||
client, err := acme.NewPrivateKeyAccount(account.Email, account.PrivateKey, acme.CALetsEncrypt, nil, r.log)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
client.UsePanel(ips, filepath.Join(app.Root, "server/nginx/conf/acme.conf"))
|
||||
|
||||
ssl, err := client.ObtainCertificate(context.Background(), ips, acme.KeyEC256)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return ssl.ChainPEM, ssl.PrivateKey, nil
|
||||
}
|
||||
|
||||
func (r *certRepo) ObtainSelfSigned(id uint) error {
|
||||
cert, err := r.Get(id)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,8 +2,12 @@ package job
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/pkg/config"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/acepanel/panel/internal/app"
|
||||
@@ -13,16 +17,22 @@ import (
|
||||
|
||||
// CertRenew 证书续签
|
||||
type CertRenew struct {
|
||||
db *gorm.DB
|
||||
log *slog.Logger
|
||||
certRepo biz.CertRepo
|
||||
conf *config.Config
|
||||
db *gorm.DB
|
||||
log *slog.Logger
|
||||
settingRepo biz.SettingRepo
|
||||
certRepo biz.CertRepo
|
||||
certAccountRepo biz.CertAccountRepo
|
||||
}
|
||||
|
||||
func NewCertRenew(db *gorm.DB, log *slog.Logger, cert biz.CertRepo) *CertRenew {
|
||||
func NewCertRenew(conf *config.Config, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) *CertRenew {
|
||||
return &CertRenew{
|
||||
db: db,
|
||||
log: log,
|
||||
certRepo: cert,
|
||||
conf: conf,
|
||||
db: db,
|
||||
log: log,
|
||||
settingRepo: setting,
|
||||
certRepo: cert,
|
||||
certAccountRepo: certAccount,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +67,52 @@ func (r *CertRenew) Run() {
|
||||
r.log.Warn("[CertRenew] failed to renew cert", slog.Any("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
// 面板证书续签
|
||||
if r.conf.HTTP.ACME {
|
||||
decode, err := pkgcert.ParseCert(filepath.Join(app.Root, "panel/storage/cert.pem"))
|
||||
if err != nil {
|
||||
r.log.Warn("[CertRenew] failed to parse panel cert", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
// 结束时间大于 2 天不续签
|
||||
if time.Until(decode.NotAfter) > 24*2*time.Hour {
|
||||
return
|
||||
}
|
||||
|
||||
ip, err := r.settingRepo.Get(biz.SettingKeyIP)
|
||||
if err != nil || ip == "" {
|
||||
r.log.Warn("[CertRenew] failed to get panel IP", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
var user biz.User
|
||||
if err = r.db.First(&user).Error; err != nil {
|
||||
r.log.Warn("[CertRenew] failed to get a panel user", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
account, err := r.certAccountRepo.GetDefault(user.ID)
|
||||
if err != nil {
|
||||
r.log.Warn("[CertRenew] failed to get panel ACME account", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
crt, key, err := r.certRepo.ObtainPanel(account, []string{ip})
|
||||
if err != nil {
|
||||
r.log.Warn("[CertRenew] failed to obtain ACME cert", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0644); err != nil {
|
||||
r.log.Warn("[CertRenew] failed to write panel cert", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.key"), string(key), 0644); err != nil {
|
||||
r.log.Warn("[CertRenew] failed to write panel cert key", slog.Any("err", err))
|
||||
return
|
||||
}
|
||||
|
||||
r.log.Info("[CertRenew] panel cert renewed successfully")
|
||||
_ = systemctl.Restart("panel")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package job
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/acepanel/panel/pkg/config"
|
||||
"github.com/google/wire"
|
||||
"github.com/robfig/cron/v3"
|
||||
"gorm.io/gorm"
|
||||
@@ -13,24 +14,28 @@ import (
|
||||
var ProviderSet = wire.NewSet(NewJobs)
|
||||
|
||||
type Jobs struct {
|
||||
db *gorm.DB
|
||||
log *slog.Logger
|
||||
setting biz.SettingRepo
|
||||
cert biz.CertRepo
|
||||
backup biz.BackupRepo
|
||||
cache biz.CacheRepo
|
||||
task biz.TaskRepo
|
||||
conf *config.Config
|
||||
db *gorm.DB
|
||||
log *slog.Logger
|
||||
setting biz.SettingRepo
|
||||
cert biz.CertRepo
|
||||
certAccount biz.CertAccountRepo
|
||||
backup biz.BackupRepo
|
||||
cache biz.CacheRepo
|
||||
task biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewJobs(db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, backup biz.BackupRepo, cache biz.CacheRepo, task biz.TaskRepo) *Jobs {
|
||||
func NewJobs(conf *config.Config, db *gorm.DB, log *slog.Logger, setting biz.SettingRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo, backup biz.BackupRepo, cache biz.CacheRepo, task biz.TaskRepo) *Jobs {
|
||||
return &Jobs{
|
||||
db: db,
|
||||
log: log,
|
||||
setting: setting,
|
||||
cert: cert,
|
||||
backup: backup,
|
||||
cache: cache,
|
||||
task: task,
|
||||
conf: conf,
|
||||
db: db,
|
||||
log: log,
|
||||
setting: setting,
|
||||
cert: cert,
|
||||
certAccount: certAccount,
|
||||
backup: backup,
|
||||
cache: cache,
|
||||
task: task,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +43,9 @@ func (r *Jobs) Register(c *cron.Cron) error {
|
||||
if _, err := c.AddJob("* * * * *", NewMonitoring(r.db, r.log, r.setting)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.AddJob("0 4 * * *", NewCertRenew(r.db, r.log, r.cert)); err != nil {
|
||||
if _, err := c.AddJob("0 4 * * *", NewCertRenew(r.conf, r.db, r.log, r.setting, r.cert, r.certAccount)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := c.AddJob("0 2 * * *", NewPanelTask(r.db, r.log, r.backup, r.cache, r.task, r.setting)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/pkg/cert"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/libtnb/utils/collect"
|
||||
"github.com/libtnb/utils/hash"
|
||||
@@ -23,7 +24,6 @@ import (
|
||||
"github.com/acepanel/panel/internal/biz"
|
||||
"github.com/acepanel/panel/internal/http/request"
|
||||
"github.com/acepanel/panel/pkg/api"
|
||||
"github.com/acepanel/panel/pkg/cert"
|
||||
"github.com/acepanel/panel/pkg/config"
|
||||
"github.com/acepanel/panel/pkg/firewall"
|
||||
"github.com/acepanel/panel/pkg/io"
|
||||
@@ -46,10 +46,12 @@ type CliService struct {
|
||||
backupRepo biz.BackupRepo
|
||||
websiteRepo biz.WebsiteRepo
|
||||
databaseServerRepo biz.DatabaseServerRepo
|
||||
certRepo biz.CertRepo
|
||||
certAccountRepo biz.CertAccountRepo
|
||||
hash hash.Hasher
|
||||
}
|
||||
|
||||
func NewCliService(t *gotext.Locale, conf *config.Config, db *gorm.DB, appRepo biz.AppRepo, cache biz.CacheRepo, user biz.UserRepo, setting biz.SettingRepo, backup biz.BackupRepo, website biz.WebsiteRepo, databaseServer biz.DatabaseServerRepo) *CliService {
|
||||
func NewCliService(t *gotext.Locale, conf *config.Config, db *gorm.DB, appRepo biz.AppRepo, cache biz.CacheRepo, user biz.UserRepo, setting biz.SettingRepo, backup biz.BackupRepo, website biz.WebsiteRepo, databaseServer biz.DatabaseServerRepo, cert biz.CertRepo, certAccount biz.CertAccountRepo) *CliService {
|
||||
return &CliService{
|
||||
hr: `+----------------------------------------------------`,
|
||||
api: api.NewAPI(app.Version, app.Locale),
|
||||
@@ -63,6 +65,8 @@ func NewCliService(t *gotext.Locale, conf *config.Config, db *gorm.DB, appRepo b
|
||||
backupRepo: backup,
|
||||
websiteRepo: website,
|
||||
databaseServerRepo: databaseServer,
|
||||
certRepo: cert,
|
||||
certAccountRepo: certAccount,
|
||||
hash: hash.NewArgon2id(),
|
||||
}
|
||||
}
|
||||
@@ -218,9 +222,8 @@ func (s *CliService) UserName(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := s.db.Where("username", oldUsername).First(user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(s.t.Get("User not exists"))
|
||||
} else {
|
||||
return errors.New(s.t.Get("Failed to get user: %v", err))
|
||||
}
|
||||
return errors.New(s.t.Get("Failed to get user: %v", err))
|
||||
}
|
||||
|
||||
user.Username = newUsername
|
||||
@@ -246,9 +249,8 @@ func (s *CliService) UserPassword(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := s.db.Where("username", username).First(user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(s.t.Get("User not exists"))
|
||||
} else {
|
||||
return errors.New(s.t.Get("Failed to get user: %v", err))
|
||||
}
|
||||
return errors.New(s.t.Get("Failed to get user: %v", err))
|
||||
}
|
||||
|
||||
hashed, err := s.hash.Make(password)
|
||||
@@ -274,9 +276,8 @@ func (s *CliService) UserTwoFA(ctx context.Context, cmd *cli.Command) error {
|
||||
if err := s.db.Where("username", username).First(user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New(s.t.Get("User not exists"))
|
||||
} else {
|
||||
return errors.New(s.t.Get("Failed to get user: %v", err))
|
||||
}
|
||||
return errors.New(s.t.Get("Failed to get user: %v", err))
|
||||
}
|
||||
|
||||
// 已开启,关闭2FA
|
||||
@@ -359,6 +360,26 @@ func (s *CliService) HTTPSGenerate(ctx context.Context, cmd *cli.Command) error
|
||||
return err
|
||||
}
|
||||
|
||||
if s.conf.HTTP.ACME {
|
||||
ip, err := s.settingRepo.Get(biz.SettingKeyIP)
|
||||
if err != nil || ip == "" {
|
||||
return errors.New(s.t.Get("Please set the panel IP in settings first for ACME certificate generation: %v", err))
|
||||
}
|
||||
|
||||
var user biz.User
|
||||
if err := s.db.First(&user).Error; err != nil {
|
||||
return errors.New(s.t.Get("Failed to get a panel user: %v", err))
|
||||
}
|
||||
account, err := s.certAccountRepo.GetDefault(user.ID)
|
||||
if err != nil {
|
||||
return errors.New(s.t.Get("Failed to get ACME account: %v", err))
|
||||
}
|
||||
crt, key, err = s.certRepo.ObtainPanel(account, names)
|
||||
if err != nil {
|
||||
return errors.New(s.t.Get("Failed to obtain ACME certificate: %v", err))
|
||||
}
|
||||
}
|
||||
|
||||
if err = io.Write(filepath.Join(app.Root, "panel/storage/cert.pem"), string(crt), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -558,7 +579,7 @@ func (s *CliService) DatabaseAddServer(ctx context.Context, cmd *cli.Command) er
|
||||
Type: cmd.String("type"),
|
||||
Name: cmd.String("name"),
|
||||
Host: cmd.String("host"),
|
||||
Port: uint(cmd.Uint("port")),
|
||||
Port: cmd.Uint("port"),
|
||||
Username: cmd.String("username"),
|
||||
Password: cmd.String("password"),
|
||||
Remark: cmd.String("remark"),
|
||||
@@ -646,7 +667,7 @@ func (s *CliService) BackupClear(ctx context.Context, cmd *cli.Command) error {
|
||||
fmt.Println(s.t.Get("|-Cleaning type: %s", cmd.String("type")))
|
||||
fmt.Println(s.t.Get("|-Cleaning target: %s", cmd.String("file")))
|
||||
fmt.Println(s.t.Get("|-Keep count: %d", cmd.Int("save")))
|
||||
if err = s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
|
||||
if err = s.backupRepo.ClearExpired(path, cmd.String("file"), cmd.Int("save")); err != nil {
|
||||
return errors.New(s.t.Get("Cleaning failed: %v", err))
|
||||
}
|
||||
fmt.Println(s.hr)
|
||||
@@ -694,7 +715,7 @@ func (s *CliService) CutoffClear(ctx context.Context, cmd *cli.Command) error {
|
||||
fmt.Println(s.t.Get("|-Cleaning type: %s", cmd.String("type")))
|
||||
fmt.Println(s.t.Get("|-Cleaning target: %s", cmd.String("file")))
|
||||
fmt.Println(s.t.Get("|-Keep count: %d", cmd.Int("save")))
|
||||
if err := s.backupRepo.ClearExpired(path, cmd.String("file"), int(cmd.Int("save"))); err != nil {
|
||||
if err := s.backupRepo.ClearExpired(path, cmd.String("file"), cmd.Int("save")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(s.hr)
|
||||
@@ -868,19 +889,33 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return errors.New(s.t.Get("Already initialized"))
|
||||
}
|
||||
|
||||
ip := ""
|
||||
acme := false
|
||||
rv6, err := tools.GetPublicIPv6()
|
||||
if err == nil {
|
||||
ip = rv6
|
||||
acme = true
|
||||
}
|
||||
rv4, err := tools.GetPublicIPv4()
|
||||
if err == nil {
|
||||
ip = rv4
|
||||
acme = true
|
||||
}
|
||||
|
||||
settings := []biz.Setting{
|
||||
{Key: biz.SettingKeyIP, Value: ip},
|
||||
{Key: biz.SettingKeyName, Value: "AcePanel"},
|
||||
{Key: biz.SettingKeyChannel, Value: "stable"},
|
||||
{Key: biz.SettingKeyVersion, Value: app.Version},
|
||||
{Key: biz.SettingKeyMonitor, Value: "true"},
|
||||
{Key: biz.SettingKeyMonitorDays, Value: "30"},
|
||||
{Key: biz.SettingKeyBackupPath, Value: filepath.Join(app.Root, "backup")},
|
||||
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "wwwroot")},
|
||||
{Key: biz.SettingKeyWebsitePath, Value: filepath.Join(app.Root, "sites")},
|
||||
{Key: biz.SettingKeyOfflineMode, Value: "false"},
|
||||
{Key: biz.SettingKeyAutoUpdate, Value: "true"},
|
||||
{Key: biz.SettingHiddenMenu, Value: "[]"},
|
||||
}
|
||||
if err := s.db.Create(&settings).Error; err != nil {
|
||||
if err = s.db.Create(&settings).Error; err != nil {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
}
|
||||
|
||||
@@ -894,10 +929,6 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
}
|
||||
|
||||
if err = s.HTTPSGenerate(ctx, cmd); err != nil {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
}
|
||||
|
||||
conf, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -907,6 +938,7 @@ func (s *CliService) Init(ctx context.Context, cmd *cli.Command) error {
|
||||
conf.App.APIEndpoint = "api.acepanel.net"
|
||||
conf.App.DownloadEndpoint = "dl.acepanel.net"
|
||||
conf.HTTP.Entrance = "/" + str.Random(6)
|
||||
conf.HTTP.ACME = acme
|
||||
|
||||
// 随机默认端口
|
||||
checkPort:
|
||||
@@ -930,5 +962,11 @@ checkPort:
|
||||
return err
|
||||
}
|
||||
|
||||
s.conf = conf // 更新配置,否则后续签发证书不会使用ACME
|
||||
|
||||
if err = s.HTTPSGenerate(ctx, cmd); err != nil {
|
||||
return errors.New(s.t.Get("Initialization failed: %v", err))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ func (c *Client) UseManualDns(check ...bool) {
|
||||
|
||||
// UseHTTP 使用 HTTP 验证
|
||||
// conf nginx 配置文件路径
|
||||
// path 验证文件存放路径
|
||||
func (c *Client) UseHTTP(conf string) {
|
||||
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
|
||||
acme.ChallengeTypeHTTP01: httpSolver{
|
||||
@@ -61,6 +60,18 @@ func (c *Client) UseHTTP(conf string) {
|
||||
}
|
||||
}
|
||||
|
||||
// UsePanel 使用面板 HTTP 验证
|
||||
// ip 外网访问 IP 地址
|
||||
// conf nginx 配置文件路径
|
||||
func (c *Client) UsePanel(ip []string, conf string) {
|
||||
c.zClient.ChallengeSolvers = map[string]acmez.Solver{
|
||||
acme.ChallengeTypeHTTP01: &panelSolver{
|
||||
ip: ip,
|
||||
conf: conf,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ObtainCertificate 签发 SSL 证书
|
||||
func (c *Client) ObtainCertificate(ctx context.Context, domains []string, keyType KeyType) (Certificate, error) {
|
||||
certPrivateKey, err := generatePrivateKey(keyType)
|
||||
|
||||
@@ -2,7 +2,9 @@ package acme
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -20,10 +22,106 @@ import (
|
||||
"github.com/mholt/acmez/v3/acme"
|
||||
"golang.org/x/net/publicsuffix"
|
||||
|
||||
pkgos "github.com/acepanel/panel/pkg/os"
|
||||
"github.com/acepanel/panel/pkg/shell"
|
||||
"github.com/acepanel/panel/pkg/systemctl"
|
||||
)
|
||||
|
||||
type panelSolver struct {
|
||||
ip []string
|
||||
conf string
|
||||
server *http.Server
|
||||
}
|
||||
|
||||
func (s *panelSolver) Present(_ context.Context, challenge acme.Challenge) error {
|
||||
// 如果 80 端口没有被占用,则直接起一个内置的 HTTP 服务器验证
|
||||
if !pkgos.TCPPortInUse(80) {
|
||||
return s.presentPanel(challenge)
|
||||
}
|
||||
|
||||
conf := fmt.Sprintf(`server {
|
||||
listen 80;
|
||||
server_name %s;
|
||||
location = %s {
|
||||
default_type text/plain;
|
||||
return 200 %q;
|
||||
}
|
||||
}
|
||||
`, s.ip, challenge.HTTP01ResourcePath(), challenge.KeyAuthorization)
|
||||
|
||||
f, err := os.OpenFile(s.conf, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open nginx config %q: %w", s.conf, err)
|
||||
}
|
||||
defer func(f *os.File) { _ = f.Close() }(f)
|
||||
|
||||
if _, err = f.Write([]byte(conf)); err != nil {
|
||||
return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err)
|
||||
}
|
||||
|
||||
if err = systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return fmt.Errorf("failed to reload nginx: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *panelSolver) presentPanel(challenge acme.Challenge) error {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(challenge.HTTP01ResourcePath(), func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte(challenge.KeyAuthorization))
|
||||
})
|
||||
|
||||
s.server = &http.Server{
|
||||
Addr: ":80",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
errChan := make(chan error, 1)
|
||||
go func() {
|
||||
if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
errChan <- err
|
||||
}
|
||||
close(errChan)
|
||||
}()
|
||||
|
||||
// 等待一小段时间确保服务器启动成功
|
||||
select {
|
||||
case err := <-errChan:
|
||||
return fmt.Errorf("failed to start HTTP server: %w", err)
|
||||
case <-time.After(100 * time.Millisecond):
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// CleanUp cleans up the HTTP server if it is the last one to finish.
|
||||
func (s *panelSolver) CleanUp(ctx context.Context, _ acme.Challenge) error {
|
||||
// 如果启动了内置 HTTP 服务器,则关闭它
|
||||
if s.server != nil {
|
||||
shutdownCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer cancel()
|
||||
if err := s.server.Shutdown(shutdownCtx); err != nil {
|
||||
return fmt.Errorf("failed to shutdown HTTP server: %w", err)
|
||||
}
|
||||
s.server = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
// 否则清理 nginx 配置
|
||||
if err := os.WriteFile(s.conf, []byte(""), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write to nginx config %q: %w", s.conf, err)
|
||||
}
|
||||
|
||||
if err := systemctl.Reload("nginx"); err != nil {
|
||||
_, err = shell.Execf("nginx -t")
|
||||
return fmt.Errorf("failed to reload nginx: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type httpSolver struct {
|
||||
conf string
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ type HTTPConfig struct {
|
||||
Entrance string `yaml:"entrance"`
|
||||
EntranceError string `yaml:"entrance_error"`
|
||||
TLS bool `yaml:"tls"`
|
||||
ACME bool `yaml:"acme"`
|
||||
LoginCaptcha bool `yaml:"login_captcha"`
|
||||
IPHeader string `yaml:"ip_header"`
|
||||
BindDomain []string `yaml:"bind_domain"`
|
||||
|
||||
@@ -496,8 +496,6 @@ func (v *baseVhost) SetSSLConfig(cfg *types.SSLConfig) error {
|
||||
}
|
||||
|
||||
// 设置 OCSP
|
||||
_ = v.parser.Clear("server.ssl_stapling")
|
||||
_ = v.parser.Clear("server.ssl_stapling_verify")
|
||||
if cfg.OCSP {
|
||||
if err = v.parser.Set("server", []*config.Directive{
|
||||
{
|
||||
@@ -535,6 +533,11 @@ func (v *baseVhost) ClearSSL() error {
|
||||
_ = v.parser.Clear("server.ssl_ciphers")
|
||||
_ = v.parser.Clear("server.ssl_prefer_server_ciphers")
|
||||
_ = v.parser.Clear("server.ssl_early_data")
|
||||
_ = v.parser.Clear("server.ssl_stapling")
|
||||
_ = v.parser.Clear("server.ssl_stapling_verify")
|
||||
_ = v.setHSTS(false)
|
||||
_ = v.setHTTPSRedirect(false)
|
||||
_ = v.setAltSvc("")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
14
web/src/api/apps/openresty/index.ts
Normal file
14
web/src/api/apps/openresty/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 负载状态
|
||||
load: (): any => http.Get('/apps/openresty/load'),
|
||||
// 获取配置
|
||||
config: (): any => http.Get('/apps/openresty/config'),
|
||||
// 保存配置
|
||||
saveConfig: (config: string): any => http.Post('/apps/openresty/config', { config }),
|
||||
// 获取错误日志
|
||||
errorLog: (): any => http.Get('/apps/openresty/error_log'),
|
||||
// 清空错误日志
|
||||
clearErrorLog: (): any => http.Post('/apps/openresty/clear_error_log')
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface Task {
|
||||
id: number
|
||||
name: string
|
||||
status: string
|
||||
shell: string
|
||||
log: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
102
web/src/views/apps/openresty/IndexView.vue
Normal file
102
web/src/views/apps/openresty/IndexView.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'apps-openresty-index'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import openresty from '@/api/apps/openresty'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
|
||||
const { data: config } = useRequest(openresty.config, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: errorLog } = useRequest(openresty.errorLog, {
|
||||
initialData: ''
|
||||
})
|
||||
const { data: load } = useRequest(openresty.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
useRequest(openresty.saveConfig(config.value)).onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
}
|
||||
|
||||
const handleClearErrorLog = () => {
|
||||
useRequest(openresty.clearErrorLog()).onSuccess(() => {
|
||||
window.$message.success($gettext('Cleared successfully'))
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status service="nginx" show-reload />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Modify Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the OpenResty main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" lang="nginx" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="400"
|
||||
:loading="false"
|
||||
:columns="columns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log service="nginx" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="error-log" :tab="$gettext('Error Logs')">
|
||||
<n-flex vertical>
|
||||
<n-flex>
|
||||
<n-button type="primary" @click="handleClearErrorLog">
|
||||
{{ $gettext('Clear Log') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<realtime-log :path="errorLog" />
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
{
|
||||
name: 'apps-openresty-index',
|
||||
path: '',
|
||||
component: () => import('../nginx/IndexView.vue'),
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'OpenResty',
|
||||
role: ['admin'],
|
||||
|
||||
@@ -50,10 +50,10 @@ const channels = [
|
||||
<n-input-number v-model:value="model.port" :placeholder="$gettext('8888')" w-full />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Default Website Directory')">
|
||||
<n-input v-model:value="model.website_path" :placeholder="$gettext('/www/wwwroot')" />
|
||||
<n-input v-model:value="model.website_path" :placeholder="$gettext('/opt/ace/sites')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Default Backup Directory')">
|
||||
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/www/backup')" />
|
||||
<n-input v-model:value="model.backup_path" :placeholder="$gettext('/opt/ace/backup')" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-flex>
|
||||
|
||||
Reference in New Issue
Block a user