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