- DCAuth 协议升级到 v2,签名原文只含设备码;客户端 2.0.0,旧版本返回 426 - 删除客户端机器码采集、后端 machine_code_* 字段与日志、后台机器码展示 - 102 迁移在生产执行前直接改为无机器码结构;SQL 守卫仅放行该文件 - Mock 短信不启用每日限额;修复慢电脑首次启动的协议转发竞态 - 新增 macOS DCAuth.app 打包脚本,注册 dcauth:// 协议,浏览器可直接唤起
329 lines
8.3 KiB
Go
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()
|
|
}
|