Ð޸´ȫÏîĿÉó²éÎÊÌⲢ·¢²¼ 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

@@ -1,4 +1,4 @@
FROM golang:1.25 AS build
FROM golang:1.26.10 AS build
WORKDIR /src

View File

@@ -1,3 +1,16 @@
# 独立 Tab 补全服务
服务监听 18041,支持远程部署。配置文件必须包含两种不同的令牌:
```yaml
token: "你的上游 Cursor 访问令牌"
clientToken: "另行生成的随机客户端访问密钥"
```
调用方使用 `Authorization: Bearer <clientToken>`,Local-Cursor 中对应 `tabServerToken`。服务验证客户端后,用上游 token 转发请求。不要共享上游令牌作为客户端密钥;对外部署请通过 HTTPS 反向代理提供服务。
仅接受 POST 和已配置的 RPC 路由,单次请求最大 64 MiB。缺少或错误的客户端凭据返回 401。配置或令牌不会写入日志。
### 如何获取 token?
**macos**
```bash

View File

@@ -1 +1,3 @@
token: "eyJ..."
# Separate random caller secret; configure the same value as tabServerToken.
clientToken: ""

View File

@@ -1,5 +1,7 @@
module cursor-tab-server
go 1.25
go 1.26.0
toolchain go1.26.10
require gopkg.in/yaml.v3 v3.0.1

View File

@@ -2,7 +2,9 @@ package main
import (
"bytes"
"context"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
@@ -58,7 +60,8 @@ var defaultUpstreamTargets = map[string]string{
}
type appConfig struct {
Token string
Token string `yaml:"token"`
ClientToken string `yaml:"clientToken"`
}
type serverApp struct {
@@ -97,9 +100,65 @@ func newServerApp(cfg appConfig, client *http.Client, upstreamTargets map[string
return app
}
const maxRequestBytes int64 = 64 << 20
type trackingWriter struct {
http.ResponseWriter
started bool
}
func (w *trackingWriter) WriteHeader(code int) {
if !w.started {
w.started = true
w.ResponseWriter.WriteHeader(code)
}
}
func (w *trackingWriter) Write(p []byte) (int, error) {
if !w.started {
w.WriteHeader(http.StatusOK)
}
return w.ResponseWriter.Write(p)
}
func (w *trackingWriter) Flush() {
if !w.started {
w.WriteHeader(http.StatusOK)
}
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func (app *serverApp) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
if err := app.handleProxy(writer, request); err != nil {
http.Error(writer, err.Error(), http.StatusBadGateway)
w := &trackingWriter{ResponseWriter: writer}
expected := formatBearerAuthorization(app.config.ClientToken)
if app.config.ClientToken == "" || subtle.ConstantTimeCompare([]byte(request.Header.Get("Authorization")), []byte(expected)) != 1 {
w.Header().Set("WWW-Authenticate", "Bearer")
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if request.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if request.ContentLength > maxRequestBytes {
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
return
}
request.Body = http.MaxBytesReader(w, request.Body, maxRequestBytes)
if err := app.handleProxy(w, request); err != nil {
if !w.started {
status := http.StatusBadGateway
var limit *http.MaxBytesError
if errors.As(err, &limit) {
status = http.StatusRequestEntityTooLarge
}
if errors.Is(err, context.Canceled) {
status = 499
}
http.Error(w, http.StatusText(status), status)
} else {
log.Printf("流式转发中断 path=%s", request.URL.Path)
}
}
}
@@ -160,11 +219,19 @@ func loadConfig(path string) (appConfig, error) {
if err != nil {
return appConfig{}, err
}
token, err := parseTokenYAML(contents)
if err != nil {
var cfg appConfig
if err = yaml.Unmarshal(contents, &cfg); err != nil {
return appConfig{}, err
}
return appConfig{Token: token}, nil
cfg.Token = strings.TrimSpace(cfg.Token)
cfg.ClientToken = strings.TrimSpace(cfg.ClientToken)
if cfg.Token == "" || cfg.ClientToken == "" {
return appConfig{}, fmt.Errorf("token 和独立 clientToken 均不能为空")
}
if cfg.Token == cfg.ClientToken {
return appConfig{}, fmt.Errorf("clientToken 必须与上游 token 不同")
}
return cfg, nil
}
func parseTokenYAML(contents []byte) (string, error) {
@@ -277,7 +344,9 @@ func buildCursorChecksum(authorization string) string {
}
func newHTTPClient() *http.Client {
return &http.Client{}
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.ResponseHeaderTimeout = 30 * time.Second
return &http.Client{Transport: transport, CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }}
}
func cloneUpstreamTargets(input map[string]string) map[string]string {

View File

@@ -0,0 +1,131 @@
package main
import (
"context"
"errors"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestAuthorizationAndUpstreamTokenSeparation(t *testing.T) {
calls := 0
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if r.Header.Get("Authorization") != "Bearer upstream-secret" {
t.Error("wrong upstream token")
}
_, _ = w.Write([]byte("ok"))
}))
defer upstream.Close()
app := newServerApp(appConfig{Token: "upstream-secret", ClientToken: "client-secret"}, upstream.Client(), map[string]string{"/rpc": upstream.URL})
for _, token := range []string{"", "Bearer wrong", "Bearer upstream-secret", "Bearer client-secret"} {
r := httptest.NewRequest(http.MethodPost, "/rpc", strings.NewReader("request"))
r.Header.Set("Authorization", token)
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
want := 401
if token == "Bearer client-secret" {
want = 200
}
if w.Code != want {
t.Fatalf("status=%d want=%d", w.Code, want)
}
}
if calls != 1 {
t.Fatal("unauthorized request reached upstream")
}
}
func TestOversizeRejectedBeforeUpstream(t *testing.T) {
app := newServerApp(appConfig{Token: "upstream", ClientToken: "client"}, nil, map[string]string{"/rpc": "http://127.0.0.1:1"})
r := httptest.NewRequest(http.MethodPost, "/rpc", strings.NewReader(""))
r.ContentLength = maxRequestBytes + 1
r.Header.Set("Authorization", "Bearer client")
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
if w.Code != 413 {
t.Fatalf("status=%d", w.Code)
}
}
type testTransport func(*http.Request) (*http.Response, error)
func (t testTransport) RoundTrip(r *http.Request) (*http.Response, error) { return t(r) }
type brokenStream struct{ sent bool }
func (b *brokenStream) Read(p []byte) (int, error) {
if !b.sent {
b.sent = true
return copy(p, []byte("partial-event")), nil
}
return 0, errors.New("stream interrupted")
}
func (b *brokenStream) Close() error { return nil }
func TestStreamErrorNeverAppendsHTTPError(t *testing.T) {
client := &http.Client{Transport: testTransport(func(*http.Request) (*http.Response, error) {
return &http.Response{StatusCode: 200, Header: http.Header{}, Body: &brokenStream{}}, nil
})}
app := newServerApp(appConfig{Token: "upstream", ClientToken: "client"}, client, map[string]string{"/rpc": "https://upstream.invalid"})
r := httptest.NewRequest(http.MethodPost, "/rpc", nil)
r.Header.Set("Authorization", "Bearer client")
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
if w.Code != 200 || w.Body.String() != "partial-event" {
t.Fatalf("response corrupted: %d %q", w.Code, w.Body.String())
}
}
func TestCanceledRequestDoesNotCallUpstream(t *testing.T) {
client := &http.Client{Transport: testTransport(func(r *http.Request) (*http.Response, error) { return nil, r.Context().Err() })}
app := newServerApp(appConfig{Token: "upstream", ClientToken: "client"}, client, map[string]string{"/rpc": "https://upstream.invalid"})
ctx, cancel := context.WithCancel(context.Background())
cancel()
r := httptest.NewRequest(http.MethodPost, "/rpc", nil).WithContext(ctx)
r.Header.Set("Authorization", "Bearer client")
w := httptest.NewRecorder()
app.ServeHTTP(w, r)
if w.Code != 499 {
t.Fatalf("status=%d", w.Code)
}
}
func TestClientTokenRequiredAndDistinct(t *testing.T) {
p := filepath.Join(t.TempDir(), "config.yaml")
for _, content := range []string{"token: upstream\n", "token: same\nclientToken: same\n"} {
if err := os.WriteFile(p, []byte(content), 0600); err != nil {
t.Fatal(err)
}
if _, err := loadConfig(p); err == nil {
t.Fatal("insecure configuration accepted")
}
}
_ = os.WriteFile(p, []byte("token: upstream\nclientToken: client\n"), 0600)
if _, err := loadConfig(p); err != nil {
t.Fatal(err)
}
}
func TestUpstreamRedirectDoesNotForwardSecret(t *testing.T) {
leaked := false
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { leaked = true; _, _ = io.WriteString(w, "bad") }))
defer target.Close()
redirect := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, target.URL, 307) }))
defer redirect.Close()
r, _ := http.NewRequest(http.MethodPost, redirect.URL, strings.NewReader(""))
r.Header.Set("Authorization", "Bearer secret")
resp, err := newHTTPClient().Do(r)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 307 || leaked {
t.Fatal("redirect followed with credentials")
}
}