138 lines
4.1 KiB
Go
138 lines
4.1 KiB
Go
package runtimepath
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
|
|
"git.pcux.cn/xuans/Smail/utils/atomicfile"
|
|
)
|
|
|
|
// BusinessKeyNames 返回固定业务密钥清单,不能由外部输入扩展迁移范围。
|
|
func BusinessKeyNames() []string {
|
|
return []string{"credential.key", "external-email-encryption.key", "external-email-fingerprint.key"}
|
|
}
|
|
|
|
func businessKeyFolder(name string) string {
|
|
switch name {
|
|
case "credential.key":
|
|
return "credentials"
|
|
case "external-email-encryption.key":
|
|
return "external-email-encryption"
|
|
case "external-email-fingerprint.key":
|
|
return "external-email-fingerprint"
|
|
default:
|
|
panic("unknown business key")
|
|
}
|
|
}
|
|
|
|
func groupedKeyPath(root, name string) string {
|
|
return filepath.Join(root, "ssl", businessKeyFolder(name), name)
|
|
}
|
|
|
|
// KeyPath 兼容尚未升级的部署;正式启动完成归档后只使用业务子目录。
|
|
func KeyPath(root, name string) string {
|
|
folder := businessKeyFolder(name)
|
|
if !DeploymentLayout(root) {
|
|
return filepath.Join(root, "data", name)
|
|
}
|
|
if _, err := os.Lstat(filepath.Join(root, "ssl", name)); err == nil {
|
|
return filepath.Join(root, "ssl", name)
|
|
}
|
|
if _, err := os.Lstat(filepath.Join(root, "ssl", folder)); !os.IsNotExist(err) {
|
|
return groupedKeyPath(root, name)
|
|
}
|
|
return filepath.Join(root, "ssl", name)
|
|
}
|
|
|
|
// SecureKeyDirectory 逐层验证目录,不能通过父目录链接写入外部位置。
|
|
func SecureKeyDirectory(root, name string) (string, error) {
|
|
if err := CheckKeyLayout(root); err != nil {
|
|
return "", err
|
|
}
|
|
directory := filepath.Dir(KeyPath(root, name))
|
|
base := "data"
|
|
if DeploymentLayout(root) {
|
|
base = "ssl"
|
|
}
|
|
parent, err := SecureDirectory(root, base)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if filepath.Clean(directory) == filepath.Clean(parent) {
|
|
return parent, nil
|
|
}
|
|
return SecureDirectory(parent, businessKeyFolder(name))
|
|
}
|
|
|
|
// PrepareDeploymentKeys 由主程序启动入口在独占状态锁内调用,不依赖安装脚本。
|
|
// 每次重命名保留原字节;中断后的混合位置可在下次启动继续迁移。
|
|
func PrepareDeploymentKeys(root string) error {
|
|
if !DeploymentLayout(root) {
|
|
return nil
|
|
}
|
|
if err := CheckReady(root); err != nil {
|
|
return err
|
|
}
|
|
var moves []Move
|
|
for _, name := range BusinessKeyNames() {
|
|
source := KeyPath(root, name)
|
|
info, err := os.Lstat(source)
|
|
if os.IsNotExist(err) {
|
|
continue
|
|
}
|
|
if err != nil || !info.Mode().IsRegular() || info.Size() != 32 {
|
|
return fmt.Errorf("业务密钥无效,拒绝迁移:%s", name)
|
|
}
|
|
if err := safeDirectory(filepath.Join(root, "ssl")); err != nil {
|
|
return err
|
|
}
|
|
if err := safeDirectory(filepath.Dir(source)); err != nil {
|
|
return err
|
|
}
|
|
if runtime.GOOS != "windows" && info.Mode().Perm()&0077 != 0 {
|
|
return errors.New("业务密钥权限过于宽松,拒绝迁移")
|
|
}
|
|
if source != groupedKeyPath(root, name) {
|
|
moves = append(moves, Move{Source: source, Target: groupedKeyPath(root, name)})
|
|
}
|
|
}
|
|
ssl, err := SecureDirectory(root, "ssl")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 先检查所有目标,不能移动第一把密钥后才发现另一业务目录不安全。
|
|
for _, folder := range []string{"credentials", "external-email-encryption", "external-email-fingerprint", "dkim"} {
|
|
if err := safeDirectory(filepath.Join(ssl, folder)); err != nil && !os.IsNotExist(err) {
|
|
return err
|
|
}
|
|
}
|
|
for _, folder := range []string{"config", "log"} {
|
|
if _, err := SecureDirectory(root, folder); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, folder := range []string{"credentials", "external-email-encryption", "external-email-fingerprint", "dkim"} {
|
|
if _, err := SecureDirectory(ssl, folder); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, move := range moves {
|
|
if _, err := os.Lstat(move.Target); !os.IsNotExist(err) {
|
|
return errors.New("密钥迁移目标已存在,拒绝覆盖")
|
|
}
|
|
if err := os.Rename(move.Source, move.Target); err != nil {
|
|
return fmt.Errorf("业务密钥归档未完成,下次启动将继续:%w", err)
|
|
}
|
|
if err := atomicfile.SyncDirectory(filepath.Dir(move.Target)); err != nil {
|
|
return err
|
|
}
|
|
if err := atomicfile.SyncDirectory(ssl); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return CheckKeyLayout(root)
|
|
}
|