дȫÏîĿÉó²éÎÊÌⲢ·¢²¼ v1.1.9
¸ÄÓñ¾»ú¶ÀÁ¢ CA£¬»ָ´ CLI TLS УÑ飬±£»¤ƾ¾ÝÓëÅäÖò¢·¢±£´档 д Cursor Êý¾ݡ¢»Ự·¾¶¡¢´úÀíÉúÃüÖÜÆڡ¢ģÐÍÇëÇóÓë Skills Îļþ²Ù×÷¡£ Ϊ¶ÀÁ¢ Tab ·þÎñÔö¼ӿͻ§¶˼øȨ£¬Éý¼¶ÒÀÀµÓ빤¾ßÁ´£¬ÍêÉƷ¢²¼ԪÐÅϢͬ²½¡£ Ñé֤£ºÁ½¸ö Go ģ¿éµIJâÊÔ/vet¡¢¹ؼüģ¿é race¡¢ǰ¶˲âÊÔ/Éú²ú¹¹½¨/Éó¼ơ¢ Go ¿ɴï©¶´ɨÃèÓë macOS/Linux ¿ÉÐÐģ¿齻²æ±àÒë¾ùͨ¹ý¡£ Windows ¸ôÀëÆô¶¯ͨ¹ý£»δÍê³ɵĵã»÷ðÑ̺ÍԭÉúƽ̨Ñé֤¼ûÉó²鱨¸档
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user