Ð޸´ȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9

¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档
Ð޸´ Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£
Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£

Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢
Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£
Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
This commit is contained in:
FuDie0915
2026-11-10 09:59:34 +08:00
parent db73cf5a39
commit 188225fae6
98 changed files with 3324 additions and 930 deletions

View File

@@ -4,12 +4,15 @@
package cursor
import (
"cursor/internal/securefile"
_ "embed"
"encoding/json"
"fmt"
"net"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"cursor/internal/appdata"
@@ -68,88 +71,10 @@ func DetectCLIInstallPath() string {
return ""
}
// cliPreloadFilename 是 preload 脚本的文件名。
const cliPreloadFilename = "tls-bypass.cjs"
const cliPreloadFilename = "tls-redirect.cjs"
// cliPreloadScript 是注入到 Node.js 的 preload 脚本。
//
// 工作原理:
// 1. patch net.connect — 将 cursor.sh 域名的 TCP 连接重定向到本地后端 HTTPS 端口
// 2. patch tls.connect — 强制 rejectUnauthorized=false,使后端签发的 CA 证书被接受
//
// CLI 的 connect-node 使用 HTTP/2 (http2.connect),goproxy 无法 MITM HTTP/2 帧。
// 因此跳过 MITM 代理,直接在 TCP 层重定向到后端的 HTTPS+HTTP/2 监听器。
// 后端 Go http.Server.ServeTLS 原生支持 HTTP/2 ALPN。
const cliPreloadScript = `// Generated by Local-Curosr: redirect cursor.sh TCP to local HTTPS backend
'use strict';
const tls = require('tls');
const net = require('net');
const fs = require('fs');
const path = require('path');
const os = require('os');
const logPath = path.join(os.homedir(), '.cursor-local-assistant-v2', 'logs', 'tls-bypass.log');
function dbg(msg) {
try { fs.appendFileSync(logPath, new Date().toISOString() + ' ' + msg + '\n'); } catch(e) {}
}
var REDIRECT_PORT = parseInt(process.env.CLI_HTTPS_PORT || '39092', 10);
var REDIRECT_HOST = '127.0.0.1';
var REDIRECT_DOMAINS = ['api2.cursor.sh', 'api3.cursor.sh'];
function shouldRedirect(host) {
if (!host) return false;
host = String(host).toLowerCase().trim();
for (var i = 0; i < REDIRECT_DOMAINS.length; i++) {
var d = REDIRECT_DOMAINS[i];
if (host === d || host.endsWith('.' + d)) return true;
}
return false;
}
// --- patch net.connect: TCP 层重定向 ---
var origNetConnect = net.connect;
function patchedNetConnect() {
if (typeof arguments[0] === 'object' && arguments[0] !== null) {
var opts = arguments[0];
if (shouldRedirect(opts.host)) {
dbg('net.connect redirect: ' + opts.host + ':' + opts.port + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
opts.host = REDIRECT_HOST;
opts.port = REDIRECT_PORT;
}
} else if (typeof arguments[0] === 'number' && typeof arguments[1] === 'string') {
if (shouldRedirect(arguments[1])) {
dbg('net.connect redirect (positional): ' + arguments[1] + ':' + arguments[0] + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
arguments[1] = REDIRECT_HOST;
arguments[0] = REDIRECT_PORT;
}
}
return origNetConnect.apply(this, arguments);
}
net.connect = patchedNetConnect;
net.createConnection = patchedNetConnect;
// --- patch tls.connect: 强制 rejectUnauthorized=false + 重定向 ---
var origTLSConnect = tls.connect;
tls.connect = function() {
for (var i = 0; i < arguments.length && i < 3; i++) {
if (arguments[i] && typeof arguments[i] === 'object' && !Array.isArray(arguments[i])) {
var o = arguments[i];
o.rejectUnauthorized = false;
if (o.host && shouldRedirect(o.host)) {
dbg('tls.connect redirect: ' + o.host + ':' + o.port + ' -> ' + REDIRECT_HOST + ':' + REDIRECT_PORT);
o.host = REDIRECT_HOST;
o.port = REDIRECT_PORT;
if (!o.servername) o.servername = o.host;
}
break;
}
}
return origTLSConnect.apply(this, arguments);
};
dbg('tls-bypass preload loaded, redirect=' + REDIRECT_HOST + ':' + REDIRECT_PORT);
`
//go:embed cli_preload.cjs
var cliPreloadScript string
// writeCLIPreloadScript 将 preload 脚本写入 wrapper 目录,返回文件路径。
func writeCLIPreloadScript() (string, error) {
@@ -158,7 +83,7 @@ func writeCLIPreloadScript() (string, error) {
return "", fmt.Errorf("创建 preload 目录失败: %w", err)
}
preloadPath := filepath.Join(wrapperDir, cliPreloadFilename)
if err := os.WriteFile(preloadPath, []byte(cliPreloadScript), 0o644); err != nil {
if err := securefile.Write(preloadPath, []byte(cliPreloadScript)); err != nil {
return "", fmt.Errorf("写入 preload 脚本失败: %w", err)
}
return preloadPath, nil
@@ -207,15 +132,21 @@ func WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath string) error {
var content string
if runtime.GOOS == "windows" {
content = buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath)
if err := securefile.Write(filepath.Join(wrapperDir, "launch.ps1"), append([]byte{0xef, 0xbb, 0xbf}, []byte(buildWindowsLauncher(port, caCertPath, authToken, cliPath, preloadPath))...)); err != nil {
return err
}
} else {
content = buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath)
}
wrapperPath := CLIWrapperPath()
if err := os.WriteFile(wrapperPath, []byte(content), 0o755); err != nil {
if err := securefile.Write(wrapperPath, []byte(content)); err != nil {
return fmt.Errorf("写入 wrapper 脚本失败: %w", err)
}
if err := os.Chmod(wrapperPath, 0700); err != nil {
return err
}
logger.Infof(
"writeCLIProxyWrapper: path=%s https_port=%s ca=%s cli=%s preload=%s",
wrapperPath, port, caCertPath, cliPath, preloadPath,
@@ -225,6 +156,11 @@ func WriteCLIProxyWrapper(cliHTTPSAddr, caCertPath string) error {
// ClearCLIProxyWrapper 移除 wrapper 脚本、preload 脚本和终端初始化脚本。
func ClearCLIProxyWrapper() error {
for _, ownedName := range []string{"launch.ps1", "tls-bypass.cjs"} {
if err := os.Remove(filepath.Join(CLIWrapperDir(), ownedName)); err != nil && !os.IsNotExist(err) {
return err
}
}
wrapperPath := CLIWrapperPath()
if _, err := os.Stat(wrapperPath); err != nil {
if !os.IsNotExist(err) {
@@ -387,31 +323,41 @@ func ClearCLIAuthFile() error {
}
func buildWindowsWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
cliDir := filepath.Dir(cliPath)
psScript := filepath.Join(cliDir, "cursor-agent.ps1")
return fmt.Sprintf(`@echo off
setlocal
set "HTTP_PROXY="
set "HTTPS_PROXY="
set "CLI_HTTPS_PORT=%s"
set "NODE_EXTRA_CA_CERTS=%s"
set "NODE_TLS_REJECT_UNAUTHORIZED=0"
set "NODE_OPTIONS=--require=%s"
set "CURSOR_AUTH_TOKEN=%s"
set "CURSOR_INVOKED_AS=agent.cmd"
%%SystemRoot%%\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%s" %%*
`, port, caCertPath, preloadPath, authToken, psScript)
return "@echo off\r\nsetlocal DisableDelayedExpansion\r\n\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -ExecutionPolicy Bypass -File \"%~dp0launch.ps1\" %*\r\n"
}
func buildWindowsLauncher(port, caCertPath, authToken, cliPath, preloadPath string) string {
psScript := filepath.Join(filepath.Dir(cliPath), "cursor-agent.ps1")
return fmt.Sprintf(`$ErrorActionPreference='Stop'
$env:HTTP_PROXY=''
$env:HTTPS_PROXY=''
$env:CLI_HTTPS_PORT=%s
$env:LOCAL_CURSOR_ORIGINAL_CA_FILE=$env:NODE_EXTRA_CA_CERTS
$env:NODE_EXTRA_CA_CERTS=%s
if($env:LOCAL_CURSOR_ORIGINAL_CA_FILE -eq $env:NODE_EXTRA_CA_CERTS){$env:LOCAL_CURSOR_ORIGINAL_CA_FILE=''}
Remove-Item Env:NODE_TLS_REJECT_UNAUTHORIZED -ErrorAction SilentlyContinue
if($env:NODE_OPTIONS -match 'tls-bypass[.]cjs'){$env:NODE_OPTIONS=''}
$env:NODE_OPTIONS=($env:NODE_OPTIONS + ' ' + %s).Trim()
$env:CURSOR_AUTH_TOKEN=%s
$env:CURSOR_INVOKED_AS='agent.cmd'
& %s @args
exit $LASTEXITCODE
`, psLiteral(port), psLiteral(caCertPath), psLiteral("--require="+strconv.Quote(preloadPath)), psLiteral(authToken), psLiteral(psScript))
}
func psLiteral(s string) string { return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
func shellLiteral(s string) string { return "'" + strings.ReplaceAll(s, "'", `'"'"'`) + "'" }
func buildUnixWrapper(port, caCertPath, authToken, cliPath, preloadPath string) string {
return fmt.Sprintf(`#!/bin/sh
unset HTTP_PROXY
unset HTTPS_PROXY
export CLI_HTTPS_PORT="%s"
export NODE_EXTRA_CA_CERTS="%s"
export NODE_TLS_REJECT_UNAUTHORIZED=0
export NODE_OPTIONS="--require=%s"
export CURSOR_AUTH_TOKEN="%s"
exec "%s" "$@"
`, port, caCertPath, preloadPath, authToken, cliPath)
unset HTTP_PROXY HTTPS_PROXY NODE_TLS_REJECT_UNAUTHORIZED
export CLI_HTTPS_PORT=%s
export LOCAL_CURSOR_ORIGINAL_CA_FILE="${NODE_EXTRA_CA_CERTS-}"
export NODE_EXTRA_CA_CERTS=%s
if [ "$LOCAL_CURSOR_ORIGINAL_CA_FILE" = "$NODE_EXTRA_CA_CERTS" ]; then unset LOCAL_CURSOR_ORIGINAL_CA_FILE; fi
case "${NODE_OPTIONS-}" in *tls-bypass.cjs*) unset NODE_OPTIONS;; esac
export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }"%s
export CURSOR_AUTH_TOKEN=%s
exec %s "$@"
`, shellLiteral(port), shellLiteral(caCertPath), shellLiteral("--require="+strconv.Quote(preloadPath)), shellLiteral(authToken), shellLiteral(cliPath))
}