返回 DeepSeek-Reasonix
oauth_state.go
根目录 / internal / plugin / oauth_state.go
1 package plugin
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "os"
9 "path/filepath"
10 "strings"
11 "sync"
12
13 "reasonix/internal/fileutil"
14 filelock "reasonix/internal/identitylock"
15 )
16
17 // oauthRefreshGates prevent duplicate refresh requests from transports in the
18 // same Reasonix process. The file lock below remains the cross-process source
19 // of truth, but it must not be held across the token endpoint network request.
20 var oauthRefreshGates sync.Map // map[string]chan struct{}
21
22 func mcpOAuthStatePath(stateDir string) string {
23 if strings.TrimSpace(stateDir) == "" {
24 return ""
25 }
26 return filepath.Join(stateDir, mcpOAuthStateFile)
27 }
28
29 func mcpOAuthGenerationPath(stateDir string) string {
30 if strings.TrimSpace(stateDir) == "" {
31 return ""
32 }
33 return filepath.Join(stateDir, mcpOAuthGenerationFile)
34 }
35
36 func acquireMCPOAuthStateLock(ctx context.Context, stateDir string) (func(), error) {
37 path := mcpOAuthStatePath(stateDir)
38 if path == "" {
39 return nil, fmt.Errorf("private state directory is unavailable")
40 }
41 return filelock.Acquire(ctx, path+".lock")
42 }
43
44 func acquireMCPOAuthRefreshGate(ctx context.Context, stateDir string) (func(), error) {
45 key := filepath.Clean(strings.TrimSpace(stateDir))
46 if key == "." || key == "" {
47 return nil, fmt.Errorf("private state directory is unavailable")
48 }
49 gate, _ := oauthRefreshGates.LoadOrStore(key, make(chan struct{}, 1))
50 select {
51 case gate.(chan struct{}) <- struct{}{}:
52 return func() { <-gate.(chan struct{}) }, nil
53 case <-ctx.Done():
54 return nil, ctx.Err()
55 }
56 }
57
58 func loadMCPOAuthState(stateDir string) (mcpOAuthState, error) {
59 path := mcpOAuthStatePath(stateDir)
60 if path == "" {
61 return mcpOAuthState{}, nil
62 }
63 info, err := os.Lstat(path)
64 if err != nil {
65 if errors.Is(err, os.ErrNotExist) {
66 return mcpOAuthState{}, nil
67 }
68 return mcpOAuthState{}, fmt.Errorf("read MCP OAuth state: %w", err)
69 }
70 if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
71 return mcpOAuthState{}, fmt.Errorf("read MCP OAuth state: refusing non-regular file")
72 }
73 if info.Size() > maxOAuthBody {
74 return mcpOAuthState{}, fmt.Errorf("read MCP OAuth state: file is too large")
75 }
76 b, err := os.ReadFile(path)
77 if err != nil {
78 return mcpOAuthState{}, fmt.Errorf("read MCP OAuth state: %w", err)
79 }
80 var state mcpOAuthState
81 if err := json.Unmarshal(b, &state); err != nil {
82 return mcpOAuthState{}, fmt.Errorf("decode MCP OAuth state: %w", err)
83 }
84 if state.Version != 1 {
85 return mcpOAuthState{}, fmt.Errorf("decode MCP OAuth state: unsupported version %d", state.Version)
86 }
87 return state, nil
88 }
89
90 func saveMCPOAuthState(stateDir string, state mcpOAuthState) error {
91 path := mcpOAuthStatePath(stateDir)
92 if path == "" {
93 return fmt.Errorf("save MCP OAuth state: private state directory is unavailable")
94 }
95 if info, err := os.Lstat(path); err == nil && (info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular()) {
96 return fmt.Errorf("save MCP OAuth state: refusing non-regular file")
97 } else if err != nil && !errors.Is(err, os.ErrNotExist) {
98 return fmt.Errorf("save MCP OAuth state: %w", err)
99 }
100 state.Version = 1
101 b, err := json.MarshalIndent(state, "", " ")
102 if err != nil {
103 return fmt.Errorf("encode MCP OAuth state: %w", err)
104 }
105 if err := fileutil.AtomicWriteFileStrict(path, append(b, '\n'), 0o600); err != nil {
106 return fmt.Errorf("save MCP OAuth state: %w", err)
107 }
108 return nil
109 }
110
111 func loadMCPOAuthGeneration(stateDir string) (string, error) {
112 path := mcpOAuthGenerationPath(stateDir)
113 if path == "" {
114 return "", nil
115 }
116 info, err := os.Lstat(path)
117 if err != nil {
118 if errors.Is(err, os.ErrNotExist) {
119 return "", nil
120 }
121 return "", fmt.Errorf("read MCP OAuth generation: %w", err)
122 }
123 if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
124 return "", fmt.Errorf("read MCP OAuth generation: refusing non-regular file")
125 }
126 if info.Size() > 256 {
127 return "", fmt.Errorf("read MCP OAuth generation: file is too large")
128 }
129 b, err := os.ReadFile(path)
130 if err != nil {
131 return "", fmt.Errorf("read MCP OAuth generation: %w", err)
132 }
133 generation := strings.TrimSpace(string(b))
134 if generation == "" {
135 return "", fmt.Errorf("read MCP OAuth generation: empty generation")
136 }
137 return generation, nil
138 }
139
140 func saveMCPOAuthGeneration(stateDir, generation string) error {
141 path := mcpOAuthGenerationPath(stateDir)
142 if path == "" {
143 return fmt.Errorf("save MCP OAuth generation: private state directory is unavailable")
144 }
145 if strings.TrimSpace(generation) == "" {
146 return fmt.Errorf("save MCP OAuth generation: generation is empty")
147 }
148 if info, err := os.Lstat(path); err == nil && (info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular()) {
149 return fmt.Errorf("save MCP OAuth generation: refusing non-regular file")
150 } else if err != nil && !errors.Is(err, os.ErrNotExist) {
151 return fmt.Errorf("save MCP OAuth generation: %w", err)
152 }
153 if err := fileutil.AtomicWriteFileStrict(path, []byte(strings.TrimSpace(generation)+"\n"), 0o600); err != nil {
154 return fmt.Errorf("save MCP OAuth generation: %w", err)
155 }
156 return nil
157 }
158
159 func bumpMCPOAuthGeneration(stateDir string) error {
160 generation, err := randomBase64URL(24)
161 if err != nil {
162 return fmt.Errorf("create MCP OAuth generation: %w", err)
163 }
164 return saveMCPOAuthGeneration(stateDir, generation)
165 }
166
167 func captureMCPOAuthGeneration(ctx context.Context, stateDir string) (string, error) {
168 release, err := acquireMCPOAuthStateLock(ctx, stateDir)
169 if err != nil {
170 return "", fmt.Errorf("lock MCP OAuth generation: %w", err)
171 }
172 defer release()
173 return loadMCPOAuthGeneration(stateDir)
174 }
175
176 func saveMCPOAuthStateIfGenerationUnchanged(ctx context.Context, stateDir, generation string, state mcpOAuthState) error {
177 release, err := acquireMCPOAuthStateLock(ctx, stateDir)
178 if err != nil {
179 return fmt.Errorf("lock MCP OAuth state: %w", err)
180 }
181 defer release()
182 current, err := loadMCPOAuthGeneration(stateDir)
183 if err != nil {
184 return err
185 }
186 if current != generation {
187 return fmt.Errorf("MCP OAuth authorization was invalidated while waiting for the browser; authorize again")
188 }
189 return saveMCPOAuthState(stateDir, state)
190 }
191
191 lines GO