返回 DeepSeek-Reasonix
auth.go
根目录 / internal / mcpdiag / auth.go
1 package mcpdiag
2
3 import (
4 "net"
5 "net/url"
6 "strings"
7 )
8
9 const (
10 AuthNone = "none"
11 AuthPossible = "possible"
12 AuthRequired = "required"
13 )
14
15 type AuthDiagnosis struct {
16 Status string
17 URL string
18 }
19
20 func DiagnoseAuth(transport, status, errText, url string, authConfigured bool) AuthDiagnosis {
21 eligible := CanUseHTTPMCPOAuth(transport, url, authConfigured)
22 if IsAuthFailure(errText) {
23 if eligible {
24 return AuthDiagnosis{Status: AuthRequired, URL: strings.TrimSpace(url)}
25 }
26 return AuthDiagnosis{Status: AuthNone}
27 }
28 if !eligible || strings.TrimSpace(errText) != "" {
29 return AuthDiagnosis{Status: AuthNone}
30 }
31 switch strings.ToLower(strings.TrimSpace(status)) {
32 case "connected", "failed":
33 return AuthDiagnosis{Status: AuthNone}
34 case "deferred", "initializing", "disabled":
35 return AuthDiagnosis{Status: AuthPossible, URL: strings.TrimSpace(url)}
36 default:
37 return AuthDiagnosis{Status: AuthNone}
38 }
39 }
40
41 // CanUseHTTPMCPOAuth reports whether Reasonix's native authorization-code flow
42 // can own authentication for this server. Legacy SSE, stdio, malformed URLs,
43 // and configurations with explicit credentials must keep their normal retry
44 // or credential-management path.
45 func CanUseHTTPMCPOAuth(transport, url string, authConfigured bool) bool {
46 if authConfigured || HasAuthConfig(nil, nil, url) || !looksLikeNativeOAuthURL(url) {
47 return false
48 }
49 switch strings.ToLower(strings.TrimSpace(transport)) {
50 case "http", "streamable-http", "streamable_http":
51 return true
52 default:
53 return false
54 }
55 }
56
57 // HTTPMCPOAuthResource returns the resource URL that native OAuth may own, or
58 // an empty string when the configured transport/auth boundary belongs elsewhere.
59 func HTTPMCPOAuthResource(transport, url string, authConfigured bool) string {
60 if !CanUseHTTPMCPOAuth(transport, url, authConfigured) {
61 return ""
62 }
63 return strings.TrimSpace(url)
64 }
65
66 func IsAuthFailure(errText string) bool {
67 lower := strings.ToLower(errText)
68 for _, needle := range []string{
69 "401",
70 "403",
71 "unauthorized",
72 "forbidden",
73 "invalid token",
74 "login required",
75 "authentication",
76 "not authenticated",
77 } {
78 if strings.Contains(lower, needle) {
79 return true
80 }
81 }
82 return false
83 }
84
85 func HasAuthConfig(headers, env map[string]string, url string) bool {
86 for k, v := range headers {
87 if strings.TrimSpace(k) == "" {
88 continue
89 }
90 if strings.TrimSpace(v) != "" && (isAuthish(k) || containsExplicitAuthMaterial(v)) {
91 return true
92 }
93 }
94 if urlHasAuthConfig(url) {
95 return true
96 }
97 for k, v := range env {
98 if strings.TrimSpace(v) == "" {
99 continue
100 }
101 if isAuthish(k) || containsAuthMaterial(v) {
102 return true
103 }
104 }
105 return false
106 }
107
108 func ClearAuthConfig(headers, env map[string]string, rawURL string) (map[string]string, map[string]string, string, bool) {
109 cleanHeaders, changedHeaders := clearAuthMap(headers)
110 cleanEnv, changedEnv := clearAuthMap(env)
111 cleanURL, changedURL := clearAuthURL(rawURL)
112 return cleanHeaders, cleanEnv, cleanURL, changedHeaders || changedEnv || changedURL
113 }
114
115 func IsRemoteTransport(transport string) bool {
116 return isRemoteTransport(transport)
117 }
118
119 func isRemoteTransport(transport string) bool {
120 switch strings.ToLower(strings.TrimSpace(transport)) {
121 case "http", "streamable-http", "sse":
122 return true
123 default:
124 return false
125 }
126 }
127
128 func looksLikeHTTPURL(rawURL string) bool {
129 u, err := url.Parse(strings.TrimSpace(rawURL))
130 if err != nil || u == nil || strings.TrimSpace(u.Host) == "" {
131 return false
132 }
133 return strings.EqualFold(u.Scheme, "https") || strings.EqualFold(u.Scheme, "http")
134 }
135
136 func looksLikeNativeOAuthURL(rawURL string) bool {
137 u, err := url.Parse(strings.TrimSpace(rawURL))
138 if err != nil || u == nil || strings.TrimSpace(u.Host) == "" || u.User != nil || u.Fragment != "" {
139 return false
140 }
141 if strings.EqualFold(u.Scheme, "https") {
142 return true
143 }
144 return strings.EqualFold(u.Scheme, "http") && isLoopbackHost(u.Hostname())
145 }
146
147 func isLoopbackHost(host string) bool {
148 host = strings.Trim(strings.ToLower(strings.TrimSpace(host)), "[]")
149 return host == "localhost" || (net.ParseIP(host) != nil && net.ParseIP(host).IsLoopback())
150 }
151
152 func containsAuthMaterial(s string) bool {
153 lower := strings.ToLower(s)
154 return strings.Contains(lower, "${") || containsExplicitAuthMaterial(lower)
155 }
156
157 func containsExplicitAuthMaterial(s string) bool {
158 lower := strings.ToLower(s)
159 return strings.Contains(lower, "access_token") ||
160 strings.Contains(lower, "id_token") ||
161 strings.Contains(lower, "refresh_token") ||
162 strings.Contains(lower, "api_key") ||
163 strings.Contains(lower, "api-key") ||
164 strings.Contains(lower, "apikey") ||
165 strings.Contains(lower, "bearer ")
166 }
167
168 func isAuthish(key string) bool {
169 lower := strings.ToLower(strings.TrimSpace(key))
170 return strings.Contains(lower, "auth") ||
171 strings.Contains(lower, "token") ||
172 strings.Contains(lower, "secret") ||
173 strings.Contains(lower, "credential") ||
174 strings.Contains(lower, "api_key") ||
175 strings.Contains(lower, "api-key") ||
176 strings.Contains(lower, "apikey") ||
177 strings.Contains(lower, "subscription-key") ||
178 strings.Contains(lower, "subscription_key") ||
179 strings.Contains(lower, "signature") ||
180 strings.Contains(lower, "hmac") ||
181 strings.Contains(lower, "cookie")
182 }
183
184 func clearAuthMap(in map[string]string) (map[string]string, bool) {
185 if len(in) == 0 {
186 return nil, false
187 }
188 out := make(map[string]string, len(in))
189 changed := false
190 for k, v := range in {
191 if isAuthish(k) || containsExplicitAuthMaterial(v) {
192 changed = true
193 continue
194 }
195 out[k] = v
196 }
197 if len(out) == 0 {
198 out = nil
199 }
200 return out, changed
201 }
202
203 func clearAuthURL(raw string) (string, bool) {
204 trimmed := strings.TrimSpace(raw)
205 if !looksLikeHTTPURL(trimmed) {
206 return raw, false
207 }
208 u, err := url.Parse(trimmed)
209 if err != nil || u == nil {
210 return raw, false
211 }
212 q := u.Query()
213 changed := u.User != nil
214 u.User = nil
215 for key := range q {
216 if isAuthQueryKey(key) {
217 q.Del(key)
218 changed = true
219 }
220 }
221 if !changed {
222 return raw, false
223 }
224 u.RawQuery = q.Encode()
225 return u.String(), true
226 }
227
228 func isAuthQueryKey(key string) bool {
229 normalized := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(strings.TrimSpace(key)))
230 switch normalized {
231 case "auth", "authorization", "bearer", "credential", "credentials", "key", "sig", "signature", "hmac", "token", "accesstoken", "idtoken", "refreshtoken", "apikey", "accesskey", "secretkey", "subscriptionkey", "clientsecret", "password", "passwd":
232 return true
233 }
234 for _, suffix := range []string{"token", "secret", "password", "passwd", "apikey", "signature"} {
235 if strings.HasSuffix(normalized, suffix) {
236 return true
237 }
238 }
239 return false
240 }
241
242 func urlHasAuthConfig(raw string) bool {
243 trimmed := strings.TrimSpace(raw)
244 u, err := url.Parse(trimmed)
245 if err == nil && u != nil {
246 if u.User != nil {
247 return true
248 }
249 for key := range u.Query() {
250 if isAuthQueryKey(key) {
251 return true
252 }
253 }
254 }
255 return containsAuthMaterial(trimmed)
256 }
257
257 lines GO