Files
yml2213 ef36453694 feat(dcauth): 移除机器码,改用纯设备码认证
- DCAuth 协议升级到 v2,签名原文只含设备码;客户端 2.0.0,旧版本返回 426
- 删除客户端机器码采集、后端 machine_code_* 字段与日志、后台机器码展示
- 102 迁移在生产执行前直接改为无机器码结构;SQL 守卫仅放行该文件
- Mock 短信不启用每日限额;修复慢电脑首次启动的协议转发竞态
- 新增 macOS DCAuth.app 打包脚本,注册 dcauth:// 协议,浏览器可直接唤起
2026-10-21 20:46:25 +08:00

329 lines
8.3 KiB
Go

//go:build darwin
package main
import (
"bufio"
"errors"
"fmt"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"time"
)
const (
daemonShow = "__show__"
daemonExit = "__exit__"
)
func runClient(protocolArg string) {
socketPath, err := darwinSocketPath()
if err != nil {
showError("DCAuth 启动失败", err.Error())
return
}
if hasCommandLineArg("--exit") {
if forwardToDarwinDaemon(socketPath, daemonExit) {
return
}
showError("DCAuth 未运行", "当前没有正在运行的 DCAuth 后台进程。")
return
}
message := protocolArg
if message == "" {
message = daemonShow
}
if forwardToDarwinDaemon(socketPath, message) {
return
}
if os.Getenv("DCAUTH_BACKGROUND") == "1" {
runDarwinDaemon(protocolArg)
return
}
if err := startDarwinDaemon(protocolArg); err != nil {
showError("DCAuth 启动失败", err.Error())
}
}
func runDarwinDaemon(protocolArg string) {
lockPath, socketPath, err := darwinDaemonPaths()
if err != nil {
showError("DCAuth 启动失败", err.Error())
return
}
lockFile, acquired, err := acquireDaemonLock(lockPath)
if err != nil {
showError("DCAuth 启动失败", err.Error())
return
}
if !acquired {
message := protocolArg
if message == "" {
message = daemonShow
}
if !waitForDarwinDaemon(socketPath, message, 10*time.Second) {
showError("DCAuth 启动失败", "已有后台进程正在启动,但暂时无法连接。")
}
return
}
defer lockFile.Close()
identity, err := loadOrCreateIdentity()
if err != nil {
showError("DCAuth 初始化失败", err.Error())
return
}
_ = os.Remove(socketPath)
listener, err := net.Listen("unix", socketPath)
if err != nil {
showError("DCAuth 启动失败", fmt.Sprintf("无法创建后台通信通道:%v", err))
return
}
defer listener.Close()
defer os.Remove(socketPath)
_ = os.Chmod(socketPath, 0o600)
requests := make(chan string, 16)
go acceptDarwinRequests(listener, requests)
if protocolArg != "" {
handleDarwinAuthorization(identity, protocolArg)
} else {
notifyDarwin("DCAuth v"+clientVersion, "DCAuth 已在后台运行。首次打开会更新协议绑定。")
}
for request := range requests {
switch request {
case daemonExit:
return
case daemonShow:
showDarwinDeviceInfo(identity)
default:
handleDarwinAuthorization(identity, request)
}
}
}
func acceptDarwinRequests(listener net.Listener, requests chan<- string) {
for {
connection, err := listener.Accept()
if err != nil {
return
}
go func(conn net.Conn) {
defer conn.Close()
scanner := bufio.NewScanner(conn)
scanner.Buffer(make([]byte, 1024), 1024*1024)
for scanner.Scan() {
request := strings.TrimSpace(scanner.Text())
if request != "" {
requests <- request
}
}
}(connection)
}
}
func handleDarwinAuthorization(identity identity, raw string) {
target, err := parseAuthorizeURL(raw)
if err != nil {
showError("DCAuth 协议无效", err.Error())
return
}
app := appRegistry[target.App]
if !confirmDarwin(
"DCAuth v"+clientVersion+" · "+app.DisplayName+" 本机登录确认",
fmt.Sprintf(
"是否允许使用本机设备登录「%s」?\n\n客户端版本:v%s\n设备码:%s\n\n点击“允许登录”完成验证,点击“取消”拒绝。",
app.DisplayName,
clientVersion,
identity.deviceID,
),
) {
return
}
if err := submitAuthorization(identity, target); err != nil {
showError("本机验证失败", err.Error())
return
}
notifyDarwin("DCAuth v"+clientVersion+" 验证成功", "已确认本机设备,请返回浏览器继续登录。")
}
func showDarwinDeviceInfo(identity identity) {
copyToClipboard(identity.deviceID)
showInfo(
"DCAuth v"+clientVersion+" 设备信息",
fmt.Sprintf(
"版本:v%s\n\n设备码:%s\n\nDCAuth 正在后台运行。浏览器登录时只会打开一个确认窗口。\n设备码已复制到剪贴板。",
clientVersion,
identity.deviceID,
),
)
}
func startDarwinDaemon(protocolArg string) error {
executable, err := os.Executable()
if err != nil {
return fmt.Errorf("无法定位 DCAuth 程序:%w", err)
}
args := make([]string, 0, 1)
if protocolArg != "" {
args = append(args, protocolArg)
}
command := exec.Command(executable, args...)
command.Env = append(os.Environ(), "DCAUTH_BACKGROUND=1")
command.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
logPath, err := darwinLogPath()
if err == nil {
if logFile, openErr := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o600); openErr == nil {
defer logFile.Close()
command.Stdout = logFile
command.Stderr = logFile
}
}
if err := command.Start(); err != nil {
return fmt.Errorf("无法启动后台进程:%w", err)
}
return command.Process.Release()
}
func acquireDaemonLock(path string) (*os.File, bool, error) {
file, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0o600)
if err != nil {
return nil, false, fmt.Errorf("无法创建后台锁文件:%w", err)
}
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
_ = file.Close()
if errors.Is(err, syscall.EWOULDBLOCK) || errors.Is(err, syscall.EAGAIN) {
return nil, false, nil
}
return nil, false, fmt.Errorf("无法锁定后台进程:%w", err)
}
return file, true, nil
}
func forwardToDarwinDaemon(socketPath, message string) bool {
connection, err := net.DialTimeout("unix", socketPath, 250*time.Millisecond)
if err != nil {
return false
}
defer connection.Close()
_ = connection.SetWriteDeadline(time.Now().Add(time.Second))
_, err = io.WriteString(connection, message+"\n")
return err == nil
}
func waitForDarwinDaemon(socketPath, message string, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if forwardToDarwinDaemon(socketPath, message) {
return true
}
time.Sleep(100 * time.Millisecond)
}
return false
}
func darwinDaemonPaths() (string, string, error) {
directory, err := darwinDataDir()
if err != nil {
return "", "", err
}
return filepath.Join(directory, "daemon.lock"), filepath.Join(directory, "dcauth.sock"), nil
}
func darwinSocketPath() (string, error) {
_, socketPath, err := darwinDaemonPaths()
return socketPath, err
}
func darwinLogPath() (string, error) {
directory, err := darwinDataDir()
if err != nil {
return "", err
}
return filepath.Join(directory, "dcauth.log"), nil
}
func darwinDataDir() (string, error) {
base, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("无法定位用户配置目录:%w", err)
}
directory := filepath.Join(base, "DCAuth")
if err := os.MkdirAll(directory, 0o700); err != nil {
return "", fmt.Errorf("无法创建 DCAuth 配置目录:%w", err)
}
return directory, nil
}
func hasCommandLineArg(target string) bool {
for _, arg := range os.Args[1:] {
if strings.EqualFold(strings.TrimSpace(arg), target) {
return true
}
}
return false
}
func showInfo(title, text string) {
_, _ = runAppleScript(
"display dialog " + appleScriptString(text) +
" with title " + appleScriptString(title) +
" buttons {\"好\"} default button \"好\" with icon note",
)
}
func showError(title, text string) {
_, _ = runAppleScript(
"display alert " + appleScriptString(title) +
" message " + appleScriptString(text) +
" as critical buttons {\"好\"} default button \"好\"",
)
}
func confirmDarwin(title, text string) bool {
output, err := runAppleScript(
"display dialog " + appleScriptString(text) +
" with title " + appleScriptString(title) +
" buttons {\"取消\", \"允许登录\"} default button \"允许登录\" cancel button \"取消\" with icon caution",
)
return err == nil && strings.Contains(output, "允许登录")
}
func notifyDarwin(title, text string) {
_, _ = runAppleScript(
"display notification " + appleScriptString(text) +
" with title " + appleScriptString(title),
)
}
func runAppleScript(statement string) (string, error) {
output, err := exec.Command("osascript", "-e", statement).CombinedOutput()
return strings.TrimSpace(string(output)), err
}
func appleScriptString(text string) string {
replacer := strings.NewReplacer(
"\\", "\\\\",
"\"", "\\\"",
"\r", "",
"\n", "\\n",
)
return `"` + replacer.Replace(text) + `"`
}
func copyToClipboard(text string) {
command := exec.Command("pbcopy")
command.Stdin = strings.NewReader(text)
_ = command.Run()
}