完善tls配置
This commit is contained in:
@@ -2,6 +2,7 @@ package pop3_server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -66,7 +67,36 @@ func (s *POP3Server) StartTLS() error {
|
||||
if s.cfg.TLSCert == "" || s.cfg.TLSKey == "" {
|
||||
return fmt.Errorf("POP3 TLS certificate or key not configured")
|
||||
}
|
||||
return fmt.Errorf("POP3 TLS not yet implemented")
|
||||
|
||||
cert, err := tls.LoadX509KeyPair(s.cfg.TLSCert, s.cfg.TLSKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load POP3 TLS certificate failed: %w", err)
|
||||
}
|
||||
|
||||
listener, err := tls.Listen("tcp", s.cfg.TLSAddr, &tls.Config{Certificates: []tls.Certificate{cert}})
|
||||
if err != nil {
|
||||
return fmt.Errorf("POP3 TLS listen failed: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("POP3 TLS server listening on %s", s.cfg.TLSAddr)
|
||||
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
s.wg.Add(1)
|
||||
go func() {
|
||||
defer s.wg.Done()
|
||||
s.handleConn(conn)
|
||||
}()
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleConn handles a single POP3 client connection.
|
||||
|
||||
@@ -11,6 +11,7 @@ type DomainStore interface {
|
||||
Create(domain *db.Domain) error
|
||||
GetByID(id uint) (*db.Domain, error)
|
||||
GetByName(name string) (*db.Domain, error)
|
||||
GetFirstTLSEnabledWithCert() (*db.Domain, error)
|
||||
Update(domain *db.Domain) error
|
||||
Delete(id uint) error
|
||||
List(page, size int) ([]db.Domain, int64, error)
|
||||
@@ -49,6 +50,15 @@ func (s *domainStoreGorm) GetByName(name string) (*db.Domain, error) {
|
||||
return &domain, nil
|
||||
}
|
||||
|
||||
// GetFirstTLSEnabledWithCert retrieves the first TLS-enabled domain with certificate paths.
|
||||
func (s *domainStoreGorm) GetFirstTLSEnabledWithCert() (*db.Domain, error) {
|
||||
var domain db.Domain
|
||||
if err := s.db.Where("tls_enabled = ? AND tls_cert_path <> ? AND tls_key_path <> ?", true, "", "").Order("id ASC").First(&domain).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain, nil
|
||||
}
|
||||
|
||||
// Update saves changes to an existing domain record.
|
||||
func (s *domainStoreGorm) Update(domain *db.Domain) error {
|
||||
return s.db.Save(domain).Error
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"mail_go/internal/db"
|
||||
@@ -20,11 +24,12 @@ import (
|
||||
type AdminHandler struct {
|
||||
stores *store.Stores
|
||||
storage *storage.AttachmentStorage
|
||||
tlsDir string
|
||||
}
|
||||
|
||||
// NewAdminHandler creates a new AdminHandler with the given stores and attachment storage.
|
||||
func NewAdminHandler(stores *store.Stores, attStorage *storage.AttachmentStorage) *AdminHandler {
|
||||
return &AdminHandler{stores: stores, storage: attStorage}
|
||||
func NewAdminHandler(stores *store.Stores, attStorage *storage.AttachmentStorage, tlsDir string) *AdminHandler {
|
||||
return &AdminHandler{stores: stores, storage: attStorage, tlsDir: tlsDir}
|
||||
}
|
||||
|
||||
// Dashboard renders the admin dashboard with summary statistics.
|
||||
@@ -207,6 +212,8 @@ func (h *AdminHandler) EditDomain(c *gin.Context) {
|
||||
"error": "",
|
||||
"isEdit": true,
|
||||
"domain": domain,
|
||||
"tlsPublicCert": readTLSCert(domain.TlsCertPath),
|
||||
"tlsCertConfigured": domain.TlsCertPath != "" && domain.TlsKeyPath != "",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -229,6 +236,13 @@ func (h *AdminHandler) UpdateDomain(c *gin.Context) {
|
||||
domain.Pop3Port = formIntOrDefault(c, "pop3_port", domain.Pop3Port)
|
||||
domain.TlsEnabled = c.PostForm("tls_enabled") == "on"
|
||||
|
||||
tlsPrivateKey := strings.TrimSpace(c.PostForm("tls_private_key"))
|
||||
tlsPublicCert := strings.TrimSpace(c.PostForm("tls_public_cert"))
|
||||
if err := h.handleDomainTLSUpdate(domain, tlsPublicCert, tlsPrivateKey); err != nil {
|
||||
h.renderDomainFormError(c, domain, err.Error(), tlsPublicCert)
|
||||
return
|
||||
}
|
||||
|
||||
// 重新生成DKIM
|
||||
if c.PostForm("regenerate_dkim") == "on" {
|
||||
privKey, pubKey, err := dkim.GenerateKeyPair()
|
||||
@@ -240,20 +254,84 @@ func (h *AdminHandler) UpdateDomain(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := h.stores.Domains.Update(domain); err != nil {
|
||||
currentUser, _ := c.Get("currentUser")
|
||||
c.HTML(http.StatusBadRequest, "admin_domain_form", gin.H{
|
||||
"currentUser": currentUser,
|
||||
"activeFolder": "domains",
|
||||
"error": fmt.Sprintf("更新域名失败: %v", err),
|
||||
"isEdit": true,
|
||||
"domain": domain,
|
||||
})
|
||||
h.renderDomainFormError(c, domain, fmt.Sprintf("更新域名失败: %v", err), tlsPublicCert)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/admin/domains")
|
||||
}
|
||||
|
||||
func readTLSCert(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) handleDomainTLSUpdate(domain *db.Domain, publicCert, privateKey string) error {
|
||||
if !domain.TlsEnabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
hasExistingCert := domain.TlsCertPath != "" && domain.TlsKeyPath != ""
|
||||
if publicCert == "" && privateKey == "" {
|
||||
if hasExistingCert {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("启用 TLS 时必须填写 TLS 私钥和公钥证书")
|
||||
}
|
||||
if hasExistingCert && privateKey == "" && strings.TrimSpace(readTLSCert(domain.TlsCertPath)) == publicCert {
|
||||
return nil
|
||||
}
|
||||
if publicCert == "" || privateKey == "" {
|
||||
return fmt.Errorf("TLS 私钥和公钥证书必须同时填写")
|
||||
}
|
||||
|
||||
if _, err := tls.X509KeyPair([]byte(publicCert), []byte(privateKey)); err != nil {
|
||||
return fmt.Errorf("TLS 证书或私钥无效: %v", err)
|
||||
}
|
||||
|
||||
domainTLSDir := filepath.Join(h.tlsDir, strconv.FormatUint(uint64(domain.ID), 10))
|
||||
if err := os.MkdirAll(domainTLSDir, 0700); err != nil {
|
||||
return fmt.Errorf("创建 TLS 证书目录失败: %v", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(domainTLSDir, "cert.pem")
|
||||
keyPath := filepath.Join(domainTLSDir, "key.pem")
|
||||
if err := os.WriteFile(certPath, []byte(publicCert+"\n"), 0644); err != nil {
|
||||
return fmt.Errorf("保存 TLS 公钥证书失败: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(keyPath, []byte(privateKey+"\n"), 0600); err != nil {
|
||||
return fmt.Errorf("保存 TLS 私钥失败: %v", err)
|
||||
}
|
||||
|
||||
domain.TlsCertPath = certPath
|
||||
domain.TlsKeyPath = keyPath
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *AdminHandler) renderDomainFormError(c *gin.Context, domain *db.Domain, message, tlsPublicCert string) {
|
||||
currentUser, _ := c.Get("currentUser")
|
||||
if tlsPublicCert == "" {
|
||||
tlsPublicCert = readTLSCert(domain.TlsCertPath)
|
||||
}
|
||||
|
||||
c.HTML(http.StatusBadRequest, "admin_domain_form", gin.H{
|
||||
"currentUser": currentUser,
|
||||
"activeFolder": "domains",
|
||||
"error": message,
|
||||
"isEdit": true,
|
||||
"domain": domain,
|
||||
"tlsPublicCert": tlsPublicCert,
|
||||
"tlsCertConfigured": domain.TlsCertPath != "" && domain.TlsKeyPath != "",
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteDomain removes a domain by ID.
|
||||
func (h *AdminHandler) DeleteDomain(c *gin.Context) {
|
||||
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"mail_go/config"
|
||||
@@ -39,6 +40,7 @@ type WebServer struct {
|
||||
stores *store.Stores
|
||||
storage *storage.AttachmentStorage
|
||||
cfg config.WebConfig
|
||||
storageCfg config.StorageConfig
|
||||
authCfg config.AuthConfig
|
||||
banCfg config.BanConfig
|
||||
}
|
||||
@@ -76,7 +78,7 @@ func templateFuncs() template.FuncMap {
|
||||
|
||||
// NewWebServer creates a new WebServer, initializes the Gin engine,
|
||||
// configures sessions, middleware, and registers all routes.
|
||||
func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storage.AttachmentStorage, authCfg config.AuthConfig, banCfg config.BanConfig) *WebServer {
|
||||
func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storage.AttachmentStorage, storageCfg config.StorageConfig, authCfg config.AuthConfig, banCfg config.BanConfig) *WebServer {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(gin.Logger())
|
||||
@@ -103,6 +105,7 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
|
||||
stores: stores,
|
||||
storage: attStorage,
|
||||
cfg: cfg,
|
||||
storageCfg: storageCfg,
|
||||
authCfg: authCfg,
|
||||
banCfg: banCfg,
|
||||
}
|
||||
@@ -115,7 +118,7 @@ func NewWebServer(cfg config.WebConfig, stores *store.Stores, attStorage *storag
|
||||
func (ws *WebServer) registerRoutes() {
|
||||
authHandler := handlers.NewAuthHandler(ws.stores, ws.authCfg, ws.banCfg)
|
||||
mailHandler := handlers.NewMailHandler(ws.stores, ws.storage)
|
||||
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage)
|
||||
adminHandler := handlers.NewAdminHandler(ws.stores, ws.storage, filepath.Join(ws.storageCfg.BaseDir, "tls", "domains"))
|
||||
|
||||
// Apply BanMiddleware globally before public routes
|
||||
ws.engine.Use(middleware.BanMiddleware(ws.stores))
|
||||
|
||||
@@ -51,13 +51,36 @@
|
||||
</label>
|
||||
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">勾选后端口将自动切换为 SSL/TLS 标准端口</p>
|
||||
</div>
|
||||
{{if .isEdit}}
|
||||
<div id="tls_cert_fields" style="display:none;">
|
||||
<div class="form-group">
|
||||
<label>TLS 私钥 PEM</label>
|
||||
<textarea name="tls_private_key" rows="8" placeholder="-----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY-----" style="font-family:monospace;"></textarea>
|
||||
<p style="color:#7f8c8d;font-size:12px;margin-top:4px;">私钥不会回显;留空则保留现有私钥。</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>TLS 公钥证书 PEM</label>
|
||||
<textarea name="tls_public_cert" rows="8" placeholder="-----BEGIN CERTIFICATE----- ... -----END CERTIFICATE-----" style="font-family:monospace;">{{.tlsPublicCert}}</textarea>
|
||||
{{if .tlsCertConfigured}}
|
||||
<p style="color:#27ae60;font-size:12px;margin-top:4px;">✅ TLS 证书已配置;上传新证书后需重启服务生效。</p>
|
||||
{{else}}
|
||||
<p style="color:#e67e22;font-size:12px;margin-top:4px;">⚠️ TLS 证书未配置,启用 TLS 时必须同时填写私钥和证书。</p>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<script>
|
||||
function togglePorts() {
|
||||
var tls = document.getElementById('tls_enabled').checked;
|
||||
document.querySelector('input[name="smtp_port"]').value = tls ? 465 : 25;
|
||||
document.querySelector('input[name="imap_port"]').value = tls ? 993 : 143;
|
||||
document.querySelector('input[name="pop3_port"]').value = tls ? 995 : 110;
|
||||
var certFields = document.getElementById('tls_cert_fields');
|
||||
if (certFields) {
|
||||
certFields.style.display = tls ? 'block' : 'none';
|
||||
}
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', togglePorts);
|
||||
</script>
|
||||
{{if .isEdit}}
|
||||
<div class="form-group">
|
||||
|
||||
@@ -16,6 +16,34 @@ import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
func applyDomainTLSConfig(stores *store.Stores, cfg *config.Config) {
|
||||
domain, err := stores.Domains.GetFirstTLSEnabledWithCert()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
applied := false
|
||||
if cfg.SMTP.TLSCert == "" && cfg.SMTP.TLSKey == "" {
|
||||
cfg.SMTP.TLSCert = domain.TlsCertPath
|
||||
cfg.SMTP.TLSKey = domain.TlsKeyPath
|
||||
applied = true
|
||||
}
|
||||
if cfg.IMAP.TLSCert == "" && cfg.IMAP.TLSKey == "" {
|
||||
cfg.IMAP.TLSCert = domain.TlsCertPath
|
||||
cfg.IMAP.TLSKey = domain.TlsKeyPath
|
||||
applied = true
|
||||
}
|
||||
if cfg.POP3.TLSCert == "" && cfg.POP3.TLSKey == "" {
|
||||
cfg.POP3.TLSCert = domain.TlsCertPath
|
||||
cfg.POP3.TLSKey = domain.TlsKeyPath
|
||||
applied = true
|
||||
}
|
||||
|
||||
if applied {
|
||||
log.Printf("使用域名 %s 的 TLS 证书;更新证书后需重启服务生效", domain.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 1. Load configuration
|
||||
cfg, err := config.LoadConfig()
|
||||
@@ -39,6 +67,7 @@ func main() {
|
||||
|
||||
// 5. Initialize attachment storage
|
||||
attStorage := storage.NewAttachmentStorage(cfg.Storage.AttachDir)
|
||||
applyDomainTLSConfig(stores, cfg)
|
||||
|
||||
// 6. Start SMTP server
|
||||
smtpSrv := smtp_server.NewSMTPServer(cfg.SMTP, stores, attStorage)
|
||||
@@ -89,7 +118,7 @@ func main() {
|
||||
}
|
||||
|
||||
// 9. Start Web server
|
||||
webServer := web.NewWebServer(cfg.Web, stores, attStorage, cfg.Auth, cfg.Ban)
|
||||
webServer := web.NewWebServer(cfg.Web, stores, attStorage, cfg.Storage, cfg.Auth, cfg.Ban)
|
||||
fmt.Printf("Web 服务启动在 %s\n", cfg.Web.Addr)
|
||||
go func() {
|
||||
if err := webServer.Start(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user