commits
This commit is contained in:
+63
-63
@@ -1,63 +1,63 @@
|
||||
package jwtutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// 密钥(后续可从配置中读取)
|
||||
var secret = []byte("yunzer_jwt_secret_key")
|
||||
|
||||
// Claims 定义JWT的claims结构
|
||||
type Claims struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
TenantId int `json:"tenant_id"` // 租户ID
|
||||
UserType string `json:"user_type"` // 用户类型:"user" / "employee" / "platform" 等
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func GenerateToken(userID int, username string, tenantId int, userType string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
TenantId: tenantId,
|
||||
UserType: userType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(secret)
|
||||
return tokenString, err
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT token
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
package jwtutil
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// 密钥(后续可从配置中读取)
|
||||
var secret = []byte("yunzer_jwt_secret_key")
|
||||
|
||||
// Claims 定义JWT的claims结构
|
||||
type Claims struct {
|
||||
UserID int `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
TenantId int `json:"tenant_id"` // 租户ID
|
||||
UserType string `json:"user_type"` // 用户类型:"user" / "employee" / "platform" 等
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// GenerateToken 生成JWT token
|
||||
func GenerateToken(userID int, username string, tenantId int, userType string) (string, error) {
|
||||
expirationTime := time.Now().Add(24 * time.Hour)
|
||||
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
TenantId: tenantId,
|
||||
UserType: userType,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(expirationTime),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
NotBefore: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(secret)
|
||||
return tokenString, err
|
||||
}
|
||||
|
||||
// ParseToken 解析JWT token
|
||||
func ParseToken(tokenString string) (*Claims, error) {
|
||||
claims := &Claims{}
|
||||
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, errors.New("unexpected signing method")
|
||||
}
|
||||
return secret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
package passwordutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
saltBytes = 16
|
||||
separator = "$"
|
||||
hashLength = 64 // sha256 hex length
|
||||
)
|
||||
|
||||
// Hash 生成 salt+hash 的存储串,格式:salt$hash(均为 hex)
|
||||
func Hash(plain string) (string, error) {
|
||||
plain = strings.TrimSpace(plain)
|
||||
if plain == "" {
|
||||
return "", errors.New("password 不能为空")
|
||||
}
|
||||
salt := make([]byte, saltBytes)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
saltHex := hex.EncodeToString(salt)
|
||||
hashHex := hashHex(saltHex, plain)
|
||||
return saltHex + separator + hashHex, nil
|
||||
}
|
||||
|
||||
// Verify 校验存储串(salt$hash)是否匹配输入明文密码。
|
||||
func Verify(stored, plain string) bool {
|
||||
stored = strings.TrimSpace(stored)
|
||||
plain = strings.TrimSpace(plain)
|
||||
if stored == "" || plain == "" {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(stored, separator)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
saltHex := strings.TrimSpace(parts[0])
|
||||
hashHexStored := strings.TrimSpace(parts[1])
|
||||
if saltHex == "" || len(hashHexStored) != hashLength {
|
||||
return false
|
||||
}
|
||||
return hashHex(saltHex, plain) == strings.ToLower(hashHexStored)
|
||||
}
|
||||
|
||||
func hashHex(saltHex, plain string) string {
|
||||
sum := sha256.Sum256([]byte(saltHex + plain))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
package passwordutil
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
saltBytes = 16
|
||||
separator = "$"
|
||||
hashLength = 64 // sha256 hex length
|
||||
)
|
||||
|
||||
// Hash 生成 salt+hash 的存储串,格式:salt$hash(均为 hex)
|
||||
func Hash(plain string) (string, error) {
|
||||
plain = strings.TrimSpace(plain)
|
||||
if plain == "" {
|
||||
return "", errors.New("password 不能为空")
|
||||
}
|
||||
salt := make([]byte, saltBytes)
|
||||
if _, err := rand.Read(salt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
saltHex := hex.EncodeToString(salt)
|
||||
hashHex := hashHex(saltHex, plain)
|
||||
return saltHex + separator + hashHex, nil
|
||||
}
|
||||
|
||||
// Verify 校验存储串(salt$hash)是否匹配输入明文密码。
|
||||
func Verify(stored, plain string) bool {
|
||||
stored = strings.TrimSpace(stored)
|
||||
plain = strings.TrimSpace(plain)
|
||||
if stored == "" || plain == "" {
|
||||
return false
|
||||
}
|
||||
parts := strings.Split(stored, separator)
|
||||
if len(parts) != 2 {
|
||||
return false
|
||||
}
|
||||
saltHex := strings.TrimSpace(parts[0])
|
||||
hashHexStored := strings.TrimSpace(parts[1])
|
||||
if saltHex == "" || len(hashHexStored) != hashLength {
|
||||
return false
|
||||
}
|
||||
return hashHex(saltHex, plain) == strings.ToLower(hashHexStored)
|
||||
}
|
||||
|
||||
func hashHex(saltHex, plain string) string {
|
||||
sum := sha256.Sum256([]byte(saltHex + plain))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
|
||||
+560
-560
File diff suppressed because it is too large
Load Diff
+217
-217
@@ -1,217 +1,217 @@
|
||||
package tokenprobe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: 12 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
OK bool `json:"ok"`
|
||||
Detail string `json:"detail"`
|
||||
HTTPStatus int `json:"httpStatus"`
|
||||
ProbeMessage string `json:"probeMessage,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
BytesRead int `json:"bytesRead,omitempty"`
|
||||
RawPreview string `json:"rawPreview,omitempty"`
|
||||
RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"`
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
}
|
||||
|
||||
func ProbeOfficial(module, rawToken string) Result {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(rawToken))
|
||||
if tok == "" {
|
||||
return Result{OK: false, Detail: "Token 为空"}
|
||||
}
|
||||
switch module {
|
||||
case "cursor":
|
||||
return probeCursor(tok)
|
||||
case "windsurf":
|
||||
return probeWindsurf(tok)
|
||||
case "krio":
|
||||
return probeKiro(tok)
|
||||
default:
|
||||
return Result{OK: false, Detail: "未知模块"}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBearerToken(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if i := strings.LastIndex(s, "::"); i >= 0 {
|
||||
return strings.TrimSpace(s[i+2:])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func probeCursor(token string) Result {
|
||||
return probeCursorHiAgent(token)
|
||||
}
|
||||
|
||||
func probeWindsurf(apiKey string) Result {
|
||||
payload := map[string]interface{}{
|
||||
"metadata": map[string]string{
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
},
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus",
|
||||
bytes.NewReader(raw),
|
||||
)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Connect-Protocol-Version", "1")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: "请求失败: " + err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var wrap map[string]interface{}
|
||||
if json.Unmarshal(body, &wrap) == nil {
|
||||
if _, ok := wrap["userStatus"]; ok {
|
||||
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
if bytes.Contains(body, []byte(`"planStatus"`)) || bytes.Contains(body, []byte(`"userStatus"`)) {
|
||||
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
return Result{OK: true, Detail: fmt.Sprintf("HTTP %d,已收到响应", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return Result{OK: false, Detail: fmt.Sprintf("API Key 无效或已失效(HTTP %d)", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
default:
|
||||
snip := strings.TrimSpace(string(body))
|
||||
if len(snip) > 220 {
|
||||
snip = snip[:220] + "…"
|
||||
}
|
||||
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
|
||||
func probeKiro(accessToken string) Result {
|
||||
arn := findProfileArnInJWT(accessToken)
|
||||
if arn == "" {
|
||||
return Result{
|
||||
OK: false,
|
||||
Detail: "无法从 Token 中解析 profileArn,Kiro 暂无法自动探测",
|
||||
}
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("origin", "AI_EDITOR")
|
||||
q.Set("profileArn", arn)
|
||||
q.Set("resourceType", "AGENTIC_REQUEST")
|
||||
u := "https://q.us-east-1.amazonaws.com/getUsageLimits?" + q.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+normalizeBearerToken(accessToken))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: "请求失败: " + err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return Result{OK: true, Detail: "Kiro(AWS Q)用量接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return Result{OK: false, Detail: fmt.Sprintf("Token 无效或已过期(HTTP %d)", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
default:
|
||||
snip := strings.TrimSpace(string(body))
|
||||
if len(snip) > 220 {
|
||||
snip = snip[:220] + "…"
|
||||
}
|
||||
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(raw))
|
||||
parts := strings.Split(tok, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("not a JWT")
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func findProfileArnInJWT(raw string) string {
|
||||
m, err := decodeJWTPayloadMap(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return findProfileArnValue(m)
|
||||
}
|
||||
|
||||
func findProfileArnValue(v interface{}) string {
|
||||
switch x := v.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range x {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "profilearn" || lk == "profile_arn" {
|
||||
if s, ok := val.(string); ok && strings.Contains(s, "arn:") {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, val := range x {
|
||||
if s := findProfileArnValue(val); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, el := range x {
|
||||
if s := findProfileArnValue(el); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case string:
|
||||
if strings.Contains(x, "arn:aws:codewhisperer") && strings.Contains(x, ":profile/") {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
package tokenprobe
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var httpClient = &http.Client{
|
||||
Timeout: 12 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
},
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
OK bool `json:"ok"`
|
||||
Detail string `json:"detail"`
|
||||
HTTPStatus int `json:"httpStatus"`
|
||||
ProbeMessage string `json:"probeMessage,omitempty"`
|
||||
Endpoint string `json:"endpoint,omitempty"`
|
||||
BytesRead int `json:"bytesRead,omitempty"`
|
||||
RawPreview string `json:"rawPreview,omitempty"`
|
||||
RequestBodyPrefixHex string `json:"requestBodyPrefixHex,omitempty"`
|
||||
StreamProtocol string `json:"streamProtocol,omitempty"`
|
||||
StreamNote string `json:"streamNote,omitempty"`
|
||||
}
|
||||
|
||||
func ProbeOfficial(module, rawToken string) Result {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(rawToken))
|
||||
if tok == "" {
|
||||
return Result{OK: false, Detail: "Token 为空"}
|
||||
}
|
||||
switch module {
|
||||
case "cursor":
|
||||
return probeCursor(tok)
|
||||
case "windsurf":
|
||||
return probeWindsurf(tok)
|
||||
case "krio":
|
||||
return probeKiro(tok)
|
||||
default:
|
||||
return Result{OK: false, Detail: "未知模块"}
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeBearerToken(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if i := strings.LastIndex(s, "::"); i >= 0 {
|
||||
return strings.TrimSpace(s[i+2:])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func probeCursor(token string) Result {
|
||||
return probeCursorHiAgent(token)
|
||||
}
|
||||
|
||||
func probeWindsurf(apiKey string) Result {
|
||||
payload := map[string]interface{}{
|
||||
"metadata": map[string]string{
|
||||
"apiKey": apiKey,
|
||||
"ideName": "windsurf",
|
||||
"ideVersion": "0.0.0",
|
||||
"extensionName": "windsurf",
|
||||
"extensionVersion": "0.0.0",
|
||||
"locale": "zh",
|
||||
},
|
||||
}
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req, err := http.NewRequest(
|
||||
http.MethodPost,
|
||||
"https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus",
|
||||
bytes.NewReader(raw),
|
||||
)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Connect-Protocol-Version", "1")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: "请求失败: " + err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
var wrap map[string]interface{}
|
||||
if json.Unmarshal(body, &wrap) == nil {
|
||||
if _, ok := wrap["userStatus"]; ok {
|
||||
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
if bytes.Contains(body, []byte(`"planStatus"`)) || bytes.Contains(body, []byte(`"userStatus"`)) {
|
||||
return Result{OK: true, Detail: "Codeium 云端接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
return Result{OK: true, Detail: fmt.Sprintf("HTTP %d,已收到响应", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return Result{OK: false, Detail: fmt.Sprintf("API Key 无效或已失效(HTTP %d)", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
default:
|
||||
snip := strings.TrimSpace(string(body))
|
||||
if len(snip) > 220 {
|
||||
snip = snip[:220] + "…"
|
||||
}
|
||||
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
|
||||
func probeKiro(accessToken string) Result {
|
||||
arn := findProfileArnInJWT(accessToken)
|
||||
if arn == "" {
|
||||
return Result{
|
||||
OK: false,
|
||||
Detail: "无法从 Token 中解析 profileArn,Kiro 暂无法自动探测",
|
||||
}
|
||||
}
|
||||
|
||||
q := url.Values{}
|
||||
q.Set("origin", "AI_EDITOR")
|
||||
q.Set("profileArn", arn)
|
||||
q.Set("resourceType", "AGENTIC_REQUEST")
|
||||
u := "https://q.us-east-1.amazonaws.com/getUsageLimits?" + q.Encode()
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: err.Error()}
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+normalizeBearerToken(accessToken))
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return Result{OK: false, Detail: "请求失败: " + err.Error()}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusOK:
|
||||
return Result{OK: true, Detail: "Kiro(AWS Q)用量接口响应正常", HTTPStatus: resp.StatusCode}
|
||||
case http.StatusUnauthorized, http.StatusForbidden:
|
||||
return Result{OK: false, Detail: fmt.Sprintf("Token 无效或已过期(HTTP %d)", resp.StatusCode), HTTPStatus: resp.StatusCode}
|
||||
default:
|
||||
snip := strings.TrimSpace(string(body))
|
||||
if len(snip) > 220 {
|
||||
snip = snip[:220] + "…"
|
||||
}
|
||||
return Result{OK: false, Detail: fmt.Sprintf("HTTP %d %s", resp.StatusCode, snip), HTTPStatus: resp.StatusCode}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJWTPayloadMap(raw string) (map[string]interface{}, error) {
|
||||
tok := normalizeBearerToken(strings.TrimSpace(raw))
|
||||
parts := strings.Split(tok, ".")
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("not a JWT")
|
||||
}
|
||||
b, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(b, &m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func findProfileArnInJWT(raw string) string {
|
||||
m, err := decodeJWTPayloadMap(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return findProfileArnValue(m)
|
||||
}
|
||||
|
||||
func findProfileArnValue(v interface{}) string {
|
||||
switch x := v.(type) {
|
||||
case map[string]interface{}:
|
||||
for k, val := range x {
|
||||
lk := strings.ToLower(k)
|
||||
if lk == "profilearn" || lk == "profile_arn" {
|
||||
if s, ok := val.(string); ok && strings.Contains(s, "arn:") {
|
||||
return s
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, val := range x {
|
||||
if s := findProfileArnValue(val); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case []interface{}:
|
||||
for _, el := range x {
|
||||
if s := findProfileArnValue(el); s != "" {
|
||||
return s
|
||||
}
|
||||
}
|
||||
case string:
|
||||
if strings.Contains(x, "arn:aws:codewhisperer") && strings.Contains(x, ":profile/") {
|
||||
return x
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -1,57 +1,57 @@
|
||||
package versionutil
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Compare 比较语义化版本号(按段数字比较,如 1.10.0 > 1.9.0)。不支持复杂 pre-release 规则。
|
||||
// 返回 -1 表示 a < b,0 表示相等,1 表示 a > b。
|
||||
func Compare(a, b string) int {
|
||||
pa := parseParts(a)
|
||||
pb := parseParts(b)
|
||||
maxLen := len(pa)
|
||||
if len(pb) > maxLen {
|
||||
maxLen = len(pb)
|
||||
}
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var xa, xb int64
|
||||
if i < len(pa) {
|
||||
xa = pa[i]
|
||||
}
|
||||
if i < len(pb) {
|
||||
xb = pb[i]
|
||||
}
|
||||
if xa < xb {
|
||||
return -1
|
||||
}
|
||||
if xa > xb {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseParts(s string) []int64 {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return []int64{0}
|
||||
}
|
||||
if i := strings.IndexByte(s, '-'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
parts := strings.Split(s, ".")
|
||||
out := make([]int64, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
n, err := strconv.ParseInt(p, 10, 64)
|
||||
if err != nil {
|
||||
n = 0
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []int64{0}
|
||||
}
|
||||
return out
|
||||
}
|
||||
package versionutil
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Compare 比较语义化版本号(按段数字比较,如 1.10.0 > 1.9.0)。不支持复杂 pre-release 规则。
|
||||
// 返回 -1 表示 a < b,0 表示相等,1 表示 a > b。
|
||||
func Compare(a, b string) int {
|
||||
pa := parseParts(a)
|
||||
pb := parseParts(b)
|
||||
maxLen := len(pa)
|
||||
if len(pb) > maxLen {
|
||||
maxLen = len(pb)
|
||||
}
|
||||
for i := 0; i < maxLen; i++ {
|
||||
var xa, xb int64
|
||||
if i < len(pa) {
|
||||
xa = pa[i]
|
||||
}
|
||||
if i < len(pb) {
|
||||
xb = pb[i]
|
||||
}
|
||||
if xa < xb {
|
||||
return -1
|
||||
}
|
||||
if xa > xb {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseParts(s string) []int64 {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return []int64{0}
|
||||
}
|
||||
if i := strings.IndexByte(s, '-'); i >= 0 {
|
||||
s = s[:i]
|
||||
}
|
||||
parts := strings.Split(s, ".")
|
||||
out := make([]int64, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
n, err := strconv.ParseInt(p, 10, 64)
|
||||
if err != nil {
|
||||
n = 0
|
||||
}
|
||||
out = append(out, n)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return []int64{0}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user