mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 11:37:20 +08:00
v0.3.8
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
//go:build darwin
|
||||
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
darwinSecurityExe = "security"
|
||||
darwinLoginKeychainName = "login.keychain-db"
|
||||
)
|
||||
|
||||
func getCertSHA1Fingerprint(certPEM []byte) (string, error) {
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("无法解析证书 PEM")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析证书失败: %w", err)
|
||||
}
|
||||
fingerprint := fmt.Sprintf("%X", sha1.Sum(cert.Raw))
|
||||
return fingerprint, nil
|
||||
}
|
||||
|
||||
func isCACertInstalled(certPEM []byte) (bool, error) {
|
||||
fingerprint, err := getCertSHA1Fingerprint(certPEM)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取证书指纹失败: %w", err)
|
||||
}
|
||||
|
||||
out, err := exec.Command(darwinSecurityExe, "find-certificate", "-a", "-Z", darwinLoginKeychainName).CombinedOutput()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("检查 macOS 登录钥匙串失败: %w: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
installed := strings.Contains(strings.ToUpper(string(out)), fingerprint)
|
||||
if installed {
|
||||
logger.Infof("isCACertInstalled: cert found in macOS login keychain, fingerprint=%s", fingerprint)
|
||||
} else {
|
||||
logger.Infof("isCACertInstalled: cert not found in macOS login keychain, fingerprint=%s", fingerprint)
|
||||
}
|
||||
return installed, nil
|
||||
}
|
||||
|
||||
func installCACertToDarwinKeychain(certPEM []byte, certPath string) error {
|
||||
fingerprint, err := getCertSHA1Fingerprint(certPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取证书指纹失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("installCACertToDarwinKeychain: installing cert into login keychain, path=%s fingerprint=%s", certPath, fingerprint)
|
||||
out, err := exec.Command(
|
||||
darwinSecurityExe,
|
||||
"add-trusted-cert",
|
||||
"-d",
|
||||
"-r", "trustRoot",
|
||||
"-p", "ssl",
|
||||
"-k", darwinLoginKeychainName,
|
||||
certPath,
|
||||
).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("安装 CA 到 macOS 登录钥匙串失败: %w: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
|
||||
installed, err := isCACertInstalled(certPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证 macOS 证书安装状态失败: %w", err)
|
||||
}
|
||||
if !installed {
|
||||
return fmt.Errorf("证书导入命令已执行,但 macOS 登录钥匙串中未找到证书")
|
||||
}
|
||||
|
||||
logger.Infof("installCACertToDarwinKeychain: cert installed successfully, fingerprint=%s", fingerprint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureCACertInstalled 确保 CA 证书已安装到 macOS 登录钥匙串。
|
||||
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
||||
installed, err := isCACertInstalled(certPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查 macOS 证书安装状态失败: %w", err)
|
||||
}
|
||||
if installed {
|
||||
logger.Infof("ensureCACertInstalled: cert already installed in macOS login keychain, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("ensureCACertInstalled: cert not installed in macOS login keychain, installing...")
|
||||
return installCACertToDarwinKeychain(certPEM, certPath)
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
)
|
||||
|
||||
// GetDeviceID 用于处理与 GetDeviceID 相关的逻辑。
|
||||
func GetDeviceID() (string, error) {
|
||||
deviceID, err := machineid.ProtectedID("cursor")
|
||||
if err != nil || strings.TrimSpace(deviceID) == "" {
|
||||
rawID, rawErr := machineid.ID()
|
||||
if rawErr != nil {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取设备码失败: %w", err)
|
||||
}
|
||||
return "", fmt.Errorf("获取设备码失败: %w", rawErr)
|
||||
}
|
||||
deviceID = rawID
|
||||
}
|
||||
deviceID = strings.TrimSpace(deviceID)
|
||||
if deviceID == "" {
|
||||
return "", errors.New("获取设备码失败: 设备码为空")
|
||||
}
|
||||
return deviceID, nil
|
||||
}
|
||||
|
||||
// defaultDeviceMeta 用于处理与 defaultDeviceMeta 相关的逻辑。
|
||||
func defaultDeviceMeta() string {
|
||||
return fmt.Sprintf("%s / %s", displayOSName(runtime.GOOS), runtime.GOARCH)
|
||||
}
|
||||
|
||||
// displayOSName 用于处理与 displayOSName 相关的逻辑。
|
||||
func displayOSName(goos string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(goos)) {
|
||||
case "darwin":
|
||||
return "macOS"
|
||||
case "windows":
|
||||
return "Windows"
|
||||
case "linux":
|
||||
return "Linux"
|
||||
default:
|
||||
return goos
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package cursor 负责与 Cursor 宿主环境交互,包括设置、状态库、设备与证书适配。
|
||||
package cursor
|
||||
@@ -0,0 +1,437 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/appdata"
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
// injectedCursorSettingsKeys 表示当前模块中的 injectedCursorSettingsKeys 状态值。
|
||||
var injectedCursorSettingsKeys = []string{
|
||||
"http.proxy",
|
||||
"http.proxyKerberosServicePrincipal",
|
||||
"http.proxySupport",
|
||||
"cursor.general.disableHttp2",
|
||||
"http.experimental.systemCertificatesV2",
|
||||
}
|
||||
|
||||
// EnsureCACertFile 用于处理与 EnsureCACertFile 相关的逻辑。
|
||||
func EnsureCACertFile(certPEM []byte, currentPath string) (string, error) {
|
||||
certPath := appdata.CACertFilePath()
|
||||
if samePath(strings.TrimSpace(currentPath), certPath) {
|
||||
if _, err := os.Stat(certPath); err == nil {
|
||||
logger.Infof("ensureCACertFile: reusing path=%s", certPath)
|
||||
return certPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(certPath), 0o755); err != nil {
|
||||
return "", fmt.Errorf("创建证书配置目录失败: %w", err)
|
||||
}
|
||||
|
||||
if existing, err := os.ReadFile(certPath); err == nil && bytes.Equal(existing, certPEM) {
|
||||
logger.Infof("ensureCACertFile: unchanged path=%s", certPath)
|
||||
return certPath, nil
|
||||
} else if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return "", fmt.Errorf("读取内置 CA 证书失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(certPath, certPEM, 0o644); err != nil {
|
||||
return "", fmt.Errorf("写入内置 CA 证书失败: %w", err)
|
||||
}
|
||||
sum := sha256.Sum256(certPEM)
|
||||
logger.Infof(
|
||||
"ensureCACertFile: wrote path=%s sha256=%s size=%d",
|
||||
certPath,
|
||||
strings.ToUpper(hex.EncodeToString(sum[:])),
|
||||
len(certPEM),
|
||||
)
|
||||
return certPath, nil
|
||||
}
|
||||
|
||||
func samePath(left string, right string) bool {
|
||||
if strings.TrimSpace(left) == "" || strings.TrimSpace(right) == "" {
|
||||
return false
|
||||
}
|
||||
return filepath.Clean(left) == filepath.Clean(right)
|
||||
}
|
||||
|
||||
// SetSystemNodeExtraCACerts 用于处理与 SetSystemNodeExtraCACerts 相关的逻辑。
|
||||
func SetSystemNodeExtraCACerts(caCertPath string) error {
|
||||
caCertPath = strings.TrimSpace(caCertPath)
|
||||
if caCertPath == "" {
|
||||
return errors.New("CA 证书路径为空")
|
||||
}
|
||||
if err := os.Setenv("NODE_EXTRA_CA_CERTS", caCertPath); err != nil {
|
||||
return fmt.Errorf("设置进程环境变量失败: %w", err)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
out, err := exec.Command("launchctl", "setenv", "NODE_EXTRA_CA_CERTS", caCertPath).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入 macOS 用户环境变量失败: %v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
case "linux":
|
||||
// Linux 发行版环境变量持久化方式差异较大,这里先确保当前进程生效。
|
||||
logger.Infof("setSystemNodeExtraCACerts: linux detected, applied to current process only")
|
||||
default:
|
||||
return fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
logger.Infof("setSystemNodeExtraCACerts: NODE_EXTRA_CA_CERTS=%s", caCertPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearSystemNodeExtraCACerts 用于处理与 ClearSystemNodeExtraCACerts 相关的逻辑。
|
||||
func ClearSystemNodeExtraCACerts() error {
|
||||
if err := os.Unsetenv("NODE_EXTRA_CA_CERTS"); err != nil {
|
||||
return fmt.Errorf("清理进程环境变量失败: %w", err)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
out, err := exec.Command("launchctl", "unsetenv", "NODE_EXTRA_CA_CERTS").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理 macOS 用户环境变量失败: %v: %s", err, strings.TrimSpace(string(out)))
|
||||
}
|
||||
case "linux":
|
||||
logger.Infof("clearSystemNodeExtraCACerts: linux detected, cleared in current process only")
|
||||
default:
|
||||
return fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
||||
}
|
||||
|
||||
logger.Infof("clearSystemNodeExtraCACerts: NODE_EXTRA_CA_CERTS cleared")
|
||||
return nil
|
||||
}
|
||||
|
||||
// WriteUserProxySettings 用于处理与 WriteUserProxySettings 相关的逻辑。
|
||||
func WriteUserProxySettings(proxyURL string) error {
|
||||
proxyURL = strings.TrimSpace(proxyURL)
|
||||
if proxyURL == "" {
|
||||
return errors.New("代理地址为空")
|
||||
}
|
||||
|
||||
settingsPath, err := resolveCursorSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(settingsPath), 0o755); err != nil {
|
||||
return fmt.Errorf("创建 Cursor 配置目录失败: %w", err)
|
||||
}
|
||||
|
||||
settings := make(map[string]any)
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return fmt.Errorf("读取 Cursor 配置失败: %w", err)
|
||||
}
|
||||
} else if len(bytes.TrimSpace(data)) > 0 {
|
||||
parsed, err := decodeCursorSettingsJSONC(data)
|
||||
if err != nil {
|
||||
if removeErr := os.Remove(settingsPath); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
|
||||
return fmt.Errorf("解析 Cursor 配置失败,且删除损坏配置失败: %w", removeErr)
|
||||
}
|
||||
logger.Infof("writeCursorUserProxySettings: removed invalid settings path=%s err=%v", settingsPath, err)
|
||||
data = nil
|
||||
} else {
|
||||
settings = parsed
|
||||
}
|
||||
}
|
||||
|
||||
settings["http.proxy"] = proxyURL
|
||||
settings["http.proxyKerberosServicePrincipal"] = proxyURL
|
||||
settings["http.proxySupport"] = "on"
|
||||
settings["cursor.general.disableHttp2"] = true
|
||||
settings["http.experimental.systemCertificatesV2"] = true
|
||||
|
||||
encoded, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化 Cursor 配置失败: %w", err)
|
||||
}
|
||||
encoded = append(encoded, '\n')
|
||||
|
||||
if len(bytes.TrimSpace(data)) > 0 && bytes.Equal(data, encoded) {
|
||||
logger.Infof("writeCursorUserProxySettings: unchanged path=%s proxy=%s", settingsPath, proxyURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
tempPath := settingsPath + ".tmp"
|
||||
if err := os.WriteFile(tempPath, encoded, 0o644); err != nil {
|
||||
return fmt.Errorf("写入 Cursor 配置临时文件失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(tempPath, settingsPath); err != nil {
|
||||
return fmt.Errorf("保存 Cursor 配置失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("writeCursorUserProxySettings: path=%s proxy=%s", settingsPath, proxyURL)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClearUserProxySettings 用于处理与 ClearUserProxySettings 相关的逻辑。
|
||||
func ClearUserProxySettings() error {
|
||||
settingsPath, err := resolveCursorSettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(settingsPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("读取 Cursor 配置失败: %w", err)
|
||||
}
|
||||
if len(bytes.TrimSpace(data)) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
settings := make(map[string]any)
|
||||
parsed, err := decodeCursorSettingsJSONC(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("解析 Cursor 配置失败: %w", err)
|
||||
}
|
||||
settings = parsed
|
||||
|
||||
changed := false
|
||||
for _, key := range injectedCursorSettingsKeys {
|
||||
if _, exists := settings[key]; exists {
|
||||
delete(settings, key)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
|
||||
encoded, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化 Cursor 配置失败: %w", err)
|
||||
}
|
||||
encoded = append(encoded, '\n')
|
||||
|
||||
tempPath := settingsPath + ".tmp"
|
||||
if err := os.WriteFile(tempPath, encoded, 0o644); err != nil {
|
||||
return fmt.Errorf("写入 Cursor 配置临时文件失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(tempPath, settingsPath); err != nil {
|
||||
return fmt.Errorf("保存 Cursor 配置失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("clearCursorUserProxySettings: path=%s", settingsPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveCursorSettingsPath 用于处理与 resolveCursorSettingsPath 相关的逻辑。
|
||||
func resolveCursorSettingsPath() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取用户目录失败: %w", err)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(homeDir, "Library", "Application Support", "Cursor", "User", "settings.json"), nil
|
||||
case "windows":
|
||||
appData := os.Getenv("APPDATA")
|
||||
if strings.TrimSpace(appData) == "" {
|
||||
appData = filepath.Join(homeDir, "AppData", "Roaming")
|
||||
}
|
||||
return filepath.Join(appData, "Cursor", "User", "settings.json"), nil
|
||||
case "linux":
|
||||
configDir := os.Getenv("XDG_CONFIG_HOME")
|
||||
if strings.TrimSpace(configDir) == "" {
|
||||
configDir = filepath.Join(homeDir, ".config")
|
||||
}
|
||||
return filepath.Join(configDir, "Cursor", "User", "settings.json"), nil
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeCursorSettingsJSONC 用于处理与 decodeCursorSettingsJSONC 相关的逻辑。
|
||||
func decodeCursorSettingsJSONC(data []byte) (map[string]any, error) {
|
||||
result := make(map[string]any)
|
||||
normalized, err := normalizeJSONC(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
normalized = bytes.TrimSpace(normalized)
|
||||
if len(normalized) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
if err := json.Unmarshal(normalized, &result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// normalizeJSONC 用于处理与 normalizeJSONC 相关的逻辑。
|
||||
func normalizeJSONC(data []byte) ([]byte, error) {
|
||||
if len(data) >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF {
|
||||
data = data[3:]
|
||||
}
|
||||
withoutComments, err := stripJSONCComments(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stripJSONCTrailingCommas(withoutComments), nil
|
||||
}
|
||||
|
||||
// stripJSONCComments 用于处理与 stripJSONCComments 相关的逻辑。
|
||||
func stripJSONCComments(data []byte) ([]byte, error) {
|
||||
out := make([]byte, 0, len(data))
|
||||
inString := false
|
||||
inLineComment := false
|
||||
inBlockComment := false
|
||||
escaped := false
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
ch := data[i]
|
||||
|
||||
if inLineComment {
|
||||
if ch == '\n' {
|
||||
inLineComment = false
|
||||
out = append(out, ch)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if inBlockComment {
|
||||
if ch == '*' && i+1 < len(data) && data[i+1] == '/' {
|
||||
inBlockComment = false
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if ch == '\n' {
|
||||
out = append(out, ch)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if inString {
|
||||
out = append(out, ch)
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if ch == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
inString = true
|
||||
out = append(out, ch)
|
||||
continue
|
||||
}
|
||||
if ch == '/' && i+1 < len(data) {
|
||||
next := data[i+1]
|
||||
if next == '/' {
|
||||
inLineComment = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if next == '*' {
|
||||
inBlockComment = true
|
||||
i++
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, ch)
|
||||
}
|
||||
|
||||
if inBlockComment {
|
||||
return nil, errors.New("JSONC 块注释未闭合")
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// stripJSONCTrailingCommas 用于处理与 stripJSONCTrailingCommas 相关的逻辑。
|
||||
func stripJSONCTrailingCommas(data []byte) []byte {
|
||||
out := make([]byte, 0, len(data))
|
||||
inString := false
|
||||
escaped := false
|
||||
|
||||
for i := 0; i < len(data); i++ {
|
||||
ch := data[i]
|
||||
if inString {
|
||||
out = append(out, ch)
|
||||
if escaped {
|
||||
escaped = false
|
||||
continue
|
||||
}
|
||||
if ch == '\\' {
|
||||
escaped = true
|
||||
continue
|
||||
}
|
||||
if ch == '"' {
|
||||
inString = false
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == '"' {
|
||||
inString = true
|
||||
out = append(out, ch)
|
||||
continue
|
||||
}
|
||||
|
||||
if ch == ',' {
|
||||
j := i + 1
|
||||
for j < len(data) && isJSONWhitespace(data[j]) {
|
||||
j++
|
||||
}
|
||||
if j < len(data) && (data[j] == '}' || data[j] == ']') {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
out = append(out, ch)
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// isJSONWhitespace 用于处理与 isJSONWhitespace 相关的逻辑。
|
||||
func isJSONWhitespace(ch byte) bool {
|
||||
return ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n'
|
||||
}
|
||||
|
||||
// ProxyURLFromListenAddr 用于处理与 ProxyURLFromListenAddr 相关的逻辑。
|
||||
func ProxyURLFromListenAddr(listenAddr string) string {
|
||||
addr := strings.TrimSpace(listenAddr)
|
||||
if addr == "" {
|
||||
return "http://127.0.0.1:8080"
|
||||
}
|
||||
|
||||
// :8189 -> 127.0.0.1:8189
|
||||
if strings.HasPrefix(addr, ":") {
|
||||
return "http://127.0.0.1" + addr
|
||||
}
|
||||
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err == nil {
|
||||
if host == "" || host == "0.0.0.0" || host == "::" || host == "[::]" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return "http://" + net.JoinHostPort(host, port)
|
||||
}
|
||||
|
||||
return "http://" + addr
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/logger"
|
||||
|
||||
_ "modernc.org/sqlite"
|
||||
)
|
||||
|
||||
const (
|
||||
cursorStateMembershipType = "ultra"
|
||||
cursorStateSubscriptionStatus = "active"
|
||||
cursorStateDefaultSignUpType = "Google"
|
||||
cursorStateSQLiteBusyTimeoutMS = 2000
|
||||
cursorStateDBRelativePath = "Cursor/User/globalStorage/state.vscdb"
|
||||
cursorStateDarwinRelativePath = "Library/Application Support/Cursor/User/globalStorage/state.vscdb"
|
||||
cursorStateLinuxRelativePath = ".config/Cursor/User/globalStorage/state.vscdb"
|
||||
)
|
||||
|
||||
// InjectCursorUserInfo synchronizes the Cursor user-level auth cache used by the
|
||||
// Settings page. It does not modify the installed Cursor app bundle.
|
||||
func InjectCursorUserInfo(email, token string) error {
|
||||
stateDBPath, err := resolveCursorStateDBPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(stateDBPath), 0o755); err != nil {
|
||||
return fmt.Errorf("创建 Cursor 状态目录失败: %w", err)
|
||||
}
|
||||
|
||||
values := buildCursorAuthStateValues(email, token)
|
||||
if err := syncCursorAuthStateDB(stateDBPath, values); err != nil {
|
||||
return fmt.Errorf("同步 Cursor 状态库失败 path=%s: %w", stateDBPath, err)
|
||||
}
|
||||
|
||||
logger.Infof(
|
||||
"injectCursorUserInfo synced path=%s email=%s membership=%s subscription=%s",
|
||||
stateDBPath,
|
||||
values["cursorAuth/cachedEmail"],
|
||||
values["cursorAuth/stripeMembershipType"],
|
||||
values["cursorAuth/stripeSubscriptionStatus"],
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildCursorAuthStateValues(email, token string) map[string]string {
|
||||
email = strings.TrimSpace(email)
|
||||
token = strings.TrimSpace(token)
|
||||
|
||||
return map[string]string{
|
||||
"cursorAuth/accessToken": token,
|
||||
"cursorAuth/cachedEmail": email,
|
||||
"cursorAuth/cachedSignUpType": cursorStateDefaultSignUpType,
|
||||
"cursorAuth/refreshToken": token,
|
||||
"cursorAuth/stripeMembershipType": cursorStateMembershipType,
|
||||
"cursorAuth/stripeSubscriptionStatus": cursorStateSubscriptionStatus,
|
||||
}
|
||||
}
|
||||
|
||||
func syncCursorAuthStateDB(path string, values map[string]string) error {
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer db.Close()
|
||||
db.SetMaxOpenConns(1)
|
||||
db.SetMaxIdleConns(1)
|
||||
|
||||
ctx := context.Background()
|
||||
if _, err := db.ExecContext(ctx, fmt.Sprintf("PRAGMA busy_timeout = %d", cursorStateSQLiteBusyTimeoutMS)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := db.ExecContext(ctx, "CREATE TABLE IF NOT EXISTS ItemTable (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB)"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tx, err := db.BeginTx(ctx, &sql.TxOptions{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
_ = tx.Rollback()
|
||||
}
|
||||
}()
|
||||
|
||||
keys := make([]string, 0, len(values))
|
||||
for key := range values {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
stmt, err := tx.PrepareContext(ctx, "INSERT OR REPLACE INTO ItemTable(key, value) VALUES(?, ?)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
|
||||
for _, key := range keys {
|
||||
if _, err := stmt.ExecContext(ctx, key, values[key]); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return err
|
||||
}
|
||||
committed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveCursorStateDBPath() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("获取用户目录失败: %w", err)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
return filepath.Join(homeDir, filepath.FromSlash(cursorStateDarwinRelativePath)), nil
|
||||
case "windows":
|
||||
appData := strings.TrimSpace(os.Getenv("APPDATA"))
|
||||
if appData == "" {
|
||||
appData = filepath.Join(homeDir, "AppData", "Roaming")
|
||||
}
|
||||
return filepath.Join(appData, "Cursor", "User", "globalStorage", "state.vscdb"), nil
|
||||
case "linux":
|
||||
configDir := strings.TrimSpace(os.Getenv("XDG_CONFIG_HOME"))
|
||||
if configDir == "" {
|
||||
return filepath.Join(homeDir, filepath.FromSlash(cursorStateLinuxRelativePath)), nil
|
||||
}
|
||||
return filepath.Join(configDir, filepath.FromSlash(cursorStateDBRelativePath)), nil
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的系统: %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
//go:build windows
|
||||
|
||||
package cursor
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
windowsRootStoreName = "Root"
|
||||
windowsCertutilExe = "certutil.exe"
|
||||
windowsPowerShellExe = "powershell.exe"
|
||||
windowsUserCancelCode = 1223
|
||||
)
|
||||
|
||||
// getCertThumbprint 获取证书的SHA1指纹,用于唯一标识证书
|
||||
func getCertThumbprint(certPEM []byte) (string, error) {
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("无法解析证书 PEM")
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("解析证书失败: %w", err)
|
||||
}
|
||||
// SHA1 指纹,certutil 使用此格式
|
||||
thumbprint := fmt.Sprintf("%X", sha1.Sum(cert.Raw))
|
||||
return thumbprint, nil
|
||||
}
|
||||
|
||||
// hideWindow 返回隐藏命令行窗口的 SysProcAttr
|
||||
func hideWindow() *syscall.SysProcAttr {
|
||||
return &syscall.SysProcAttr{
|
||||
HideWindow: true,
|
||||
}
|
||||
}
|
||||
|
||||
// isCACertInstalled 检查 CA 证书是否已安装到 Windows 系统根证书存储。
|
||||
// 默认不带 -user,表示 LocalMachine\Root。
|
||||
func isCACertInstalled(certPEM []byte) (bool, error) {
|
||||
thumbprint, err := getCertThumbprint(certPEM)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("获取证书指纹失败: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(windowsCertutilExe, "-verifystore", windowsRootStoreName, thumbprint)
|
||||
cmd.SysProcAttr = hideWindow()
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) {
|
||||
// certutil 在找不到证书时返回非零退出码。
|
||||
logger.Infof("isCACertInstalled: cert not found in system store, thumbprint=%s exitCode=%d", thumbprint, exitErr.ExitCode())
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("执行 certutil 检查系统证书存储失败: %w", err)
|
||||
}
|
||||
|
||||
outStr := strings.ToUpper(string(output))
|
||||
if strings.Contains(outStr, thumbprint) {
|
||||
logger.Infof("isCACertInstalled: cert found in system store, thumbprint=%s", thumbprint)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// 某些 Windows 语言环境下 certutil 的文本不同,这里仍然按未找到处理。
|
||||
logger.Infof("isCACertInstalled: cert not found in certutil output, thumbprint=%s", thumbprint)
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func quotePowerShellLiteral(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||
}
|
||||
|
||||
func runElevatedCertutil(args ...string) error {
|
||||
quotedArgs := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
quotedArgs = append(quotedArgs, quotePowerShellLiteral(arg))
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(
|
||||
"$process = Start-Process -FilePath %s -ArgumentList @(%s) -Verb RunAs -WindowStyle Hidden -Wait -PassThru; exit $process.ExitCode",
|
||||
quotePowerShellLiteral(windowsCertutilExe),
|
||||
strings.Join(quotedArgs, ","),
|
||||
)
|
||||
|
||||
cmd := exec.Command(
|
||||
windowsPowerShellExe,
|
||||
"-NoProfile",
|
||||
"-NonInteractive",
|
||||
"-ExecutionPolicy",
|
||||
"Bypass",
|
||||
"-Command",
|
||||
script,
|
||||
)
|
||||
cmd.SysProcAttr = hideWindow()
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var exitErr *exec.ExitError
|
||||
if errors.As(err, &exitErr) && exitErr.ExitCode() == windowsUserCancelCode {
|
||||
return fmt.Errorf("用户取消了管理员权限授予")
|
||||
}
|
||||
|
||||
trimmedOutput := strings.TrimSpace(string(output))
|
||||
if trimmedOutput == "" {
|
||||
return fmt.Errorf("通过管理员权限执行 certutil 失败: %w", err)
|
||||
}
|
||||
return fmt.Errorf("通过管理员权限执行 certutil 失败: %w, output: %s", err, trimmedOutput)
|
||||
}
|
||||
|
||||
// installCACertToWindowsStore 将 CA 证书安装到 Windows 系统根证书存储。
|
||||
// LocalMachine\Root 需要管理员权限,因此这里会触发 UAC 提权。
|
||||
func installCACertToWindowsStore(certPEM []byte, certPath string) error {
|
||||
thumbprint, err := getCertThumbprint(certPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取证书指纹失败: %w", err)
|
||||
}
|
||||
|
||||
logger.Infof("installCACertToWindowsStore: installing cert into system store, path=%s thumbprint=%s", certPath, thumbprint)
|
||||
|
||||
if err := runElevatedCertutil("-addstore", windowsRootStoreName, certPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installed, err := isCACertInstalled(certPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证系统证书安装状态失败: %w", err)
|
||||
}
|
||||
if !installed {
|
||||
return fmt.Errorf("证书导入命令已执行,但系统信任存储中未找到证书")
|
||||
}
|
||||
|
||||
logger.Infof("installCACertToWindowsStore: cert installed successfully into system store, thumbprint=%s", thumbprint)
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsureCACertInstalled 确保证书已安装到 Windows 系统信任存储。
|
||||
func EnsureCACertInstalled(certPEM []byte, certPath string) error {
|
||||
installed, err := isCACertInstalled(certPEM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("检查系统证书安装状态失败: %w", err)
|
||||
}
|
||||
|
||||
if installed {
|
||||
logger.Infof("ensureCACertInstalled: cert already installed in system store, skipping")
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Infof("ensureCACertInstalled: cert not installed in system store, installing...")
|
||||
return installCACertToWindowsStore(certPEM, certPath)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
//go:build !windows && !darwin
|
||||
|
||||
package cursor
|
||||
|
||||
import "fmt"
|
||||
|
||||
// EnsureCACertInstalled 非 Windows/macOS 平台的存根实现
|
||||
func EnsureCACertInstalled(_ []byte, certPath string) error {
|
||||
return fmt.Errorf("ensureCACertInstalled: 当前平台暂不支持,certPath=%s", certPath)
|
||||
}
|
||||
Reference in New Issue
Block a user