Smail/backend/services/adminreset/password_linux.go
smail 97c815deb0
Some checks are pending
Security Checks / security (push) Waiting to run
限制管理员重置为显式启动并隔离终端输入
2026-09-19 23:43:25 +08:00

116 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build linux
package adminreset
import (
"context"
"errors"
"io"
"os/exec"
"strings"
"syscall"
"time"
"git.pcux.cn/xuans/Smail/utils/password"
)
func ReadProtectedPassword() (string, error) {
return ReadProtectedPasswordContext(context.Background())
}
func ReadProtectedPasswordContext(ctx context.Context) (string, error) {
first, err := askPasswordContext(ctx, "请输入新的超级管理员密码")
if err != nil {
return "", err
}
second, err := askPasswordContext(ctx, "请再次输入新的超级管理员密码")
if err != nil {
clearPassword(&first)
return "", err
}
if first == "" || first != second {
clearPassword(&first)
clearPassword(&second)
return "", errors.New("密码为空或两次输入不一致")
}
clearPassword(&second)
return first, nil
}
func askPassword(prompt string) (string, error) {
return askPasswordContext(context.Background(), prompt)
}
func askPasswordContext(ctx context.Context, prompt string) (string, error) {
value, err := readSystemdInputContext(ctx, prompt, "no")
if err != nil {
return "", err
}
if password.ValidateUserPassword(value) != nil {
return "", errors.New("密码长度无效")
}
return value, nil
}
func ReadVisibleInput(prompt string) (string, error) {
return ReadVisibleInputContext(context.Background(), prompt)
}
func ReadVisibleInputContext(ctx context.Context, prompt string) (string, error) {
return readSystemdInputContext(ctx, prompt, "yes")
}
// ShowResult 保留隔离密码代理的测试入口,正式流程使用绑定终端的上下文。
func ShowResult(message string) {
ShowResultContext(context.Background(), message)
}
func ShowResultContext(ctx context.Context, message string) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
_, _ = readSystemdInputContext(ctx, message+"\n按回车结束", "yes", "--timeout=30")
}
func readSystemdInput(prompt, echo string) (string, error) {
return readSystemdInputContext(context.Background(), prompt, echo)
}
func readSystemdInputContext(ctx context.Context, prompt, echo string, options ...string) (string, error) {
if terminal, ok := ctx.Value(invocationTerminalKey{}).(*invocationTerminal); ok {
return terminal.read(ctx, prompt, echo == "no")
}
prompt = strings.TrimRight(prompt, ":: ") + ":"
// systemd 代理把提示保存在单行配置中,用终端换行控制显示列表,避免配置被拆行。
prompt = strings.ReplaceAll(strings.ReplaceAll(prompt, "\r\n", "\n"), "\n", "\x1bE")
args := append([]string{"--no-tty", "--echo=" + echo}, options...)
command := exec.CommandContext(ctx, "systemd-ask-password", append(args, prompt)...)
// 先允许代理清理请求文件,避免取消后继续弹出已经失效的输入提示。
command.Cancel = func() error { return command.Process.Signal(syscall.SIGTERM) }
command.WaitDelay = time.Second
command.Stderr = io.Discard
output, err := command.Output()
if err != nil {
return "", errors.New("受保护密码代理不可用")
}
output = stripPasswordLineEnding(output)
value := string(output)
defer clearBytes(output)
return value, nil
}
func stripPasswordLineEnding(value []byte) []byte {
if len(value) > 0 && value[len(value)-1] == '\n' {
value = value[:len(value)-1]
}
if len(value) > 0 && value[len(value)-1] == '\r' {
value = value[:len(value)-1]
}
return value
}
func clearBytes(value []byte) {
for index := range value {
value[index] = 0
}
}