Files
ttstd-infinite-canvas/backend/internal/app/provider_protocol_auth.go

270 lines
9.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 声明式协议插件的鉴权:Bearer/Header/Query、AWS SigV4 与腾讯云 TC3 签名。
package app
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"time"
"github.com/volcengine/volc-sdk-golang/base"
"yingce/backend/internal/protocol"
)
func applyProtocolAuth(req *http.Request, config providerConfig, auth protocol.ManifestAuth) error {
typeName := strings.ToLower(strings.TrimSpace(auth.Type))
if typeName == "" {
applyProviderAuth(req, config)
return nil
}
credential := protocolCredentialField(config, auth.Field)
switch typeName {
case "none":
return nil
case "bearer":
header := defaultString(strings.TrimSpace(auth.Header), "Authorization")
prefix := auth.Prefix
if prefix == "" {
prefix = "Bearer "
}
req.Header.Set(header, prefix+credential)
return nil
case "header", "api-key", "apikey":
header := strings.TrimSpace(auth.Header)
if header == "" {
return errors.New("插件 header 鉴权缺少 header 名称")
}
req.Header.Set(header, auth.Prefix+credential)
return nil
case "query":
name := defaultString(strings.TrimSpace(auth.Query), strings.TrimSpace(auth.Field))
if name == "" {
return errors.New("插件 query 鉴权缺少参数名")
}
query := req.URL.Query()
query.Set(name, auth.Prefix+credential)
req.URL.RawQuery = query.Encode()
return nil
case "basic":
username := auth.Username
if username == "" {
username = credential
}
password := protocolCredentialField(config, auth.SecretField)
req.SetBasicAuth(username, password)
return nil
case "anthropic":
req.Header.Set(defaultString(auth.Header, "x-api-key"), credential)
if req.Header.Get("anthropic-version") == "" {
req.Header.Set("anthropic-version", "2023-06-01")
}
return nil
case "google-api-key", "gemini":
req.Header.Set(defaultString(auth.Header, "x-goog-api-key"), credential)
return nil
case "volcengine-v4":
secret := protocolCredentialField(config, auth.SecretField)
if credential == "" || secret == "" {
return errors.New("火山引擎 V4 鉴权需要 Access Key 和 Secret Key")
}
credentials := base.Credentials{
AccessKeyID: credential, SecretAccessKey: secret,
Region: defaultString(strings.TrimSpace(auth.Region), "cn-north-1"),
Service: strings.TrimSpace(auth.Service),
}
if credentials.Service == "" {
return errors.New("火山引擎 V4 鉴权缺少 service")
}
signed := credentials.Sign(req)
*req = *signed
return nil
case "aws-sigv4":
secret := protocolCredentialField(config, auth.SecretField)
return signProtocolAWSV4(req, credential, secret, auth)
case "tc3":
secret := protocolCredentialField(config, auth.SecretField)
return signProtocolTC3(req, credential, secret, auth)
default:
return fmt.Errorf("插件声明了尚未启用的鉴权驱动 %s", auth.Type)
}
}
func signProtocolAWSV4(req *http.Request, accessKey, secretKey string, auth protocol.ManifestAuth) error {
if strings.TrimSpace(accessKey) == "" || strings.TrimSpace(secretKey) == "" {
return errors.New("AWS SigV4 鉴权需要 Access Key ID 和 Secret Access Key")
}
serviceName := defaultString(strings.TrimSpace(auth.Service), "bedrock")
region := strings.TrimSpace(auth.Region)
if region == "" {
parts := strings.Split(strings.ToLower(req.URL.Hostname()), ".")
for index, part := range parts {
if strings.HasPrefix(part, serviceName) && index+1 < len(parts) {
region = parts[index+1]
break
}
}
}
if region == "" {
return errors.New("AWS SigV4 鉴权无法从 Base URL 推断 region,请使用包含区域的 Bedrock Runtime 地址")
}
payload, err := protocolRequestPayload(req)
if err != nil {
return err
}
now := time.Now().UTC()
amzDate := now.Format("20060102T150405Z")
dateStamp := now.Format("20060102")
payloadHash := sha256Hex(payload)
req.Header.Set("X-Amz-Date", amzDate)
req.Header.Set("X-Amz-Content-Sha256", payloadHash)
canonicalHeaders, signedHeaders := protocolCanonicalHeaders(req)
canonicalRequest := strings.Join([]string{
req.Method,
defaultString(req.URL.EscapedPath(), "/"),
req.URL.Query().Encode(),
canonicalHeaders,
signedHeaders,
payloadHash,
}, "\n")
scope := strings.Join([]string{dateStamp, region, serviceName, "aws4_request"}, "/")
stringToSign := strings.Join([]string{"AWS4-HMAC-SHA256", amzDate, scope, sha256Hex([]byte(canonicalRequest))}, "\n")
dateKey := protocolHMAC([]byte("AWS4"+secretKey), dateStamp)
regionKey := protocolHMAC(dateKey, region)
serviceKey := protocolHMAC(regionKey, serviceName)
signingKey := protocolHMAC(serviceKey, "aws4_request")
signature := hex.EncodeToString(protocolHMAC(signingKey, stringToSign))
req.Header.Set("Authorization", fmt.Sprintf("AWS4-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", accessKey, scope, signedHeaders, signature))
return nil
}
func signProtocolTC3(req *http.Request, secretID, secretKey string, auth protocol.ManifestAuth) error {
if strings.TrimSpace(secretID) == "" || strings.TrimSpace(secretKey) == "" {
return errors.New("腾讯云 TC3 鉴权需要 SecretId 和 SecretKey")
}
serviceName := defaultString(strings.TrimSpace(auth.Service), "hunyuan")
payload, err := protocolRequestPayload(req)
if err != nil {
return err
}
now := time.Now().UTC()
timestamp := now.Unix()
dateStamp := now.Format("2006-01-02")
contentType := defaultString(req.Header.Get("Content-Type"), "application/json")
req.Header.Set("Content-Type", contentType)
req.Header.Set("X-TC-Timestamp", strconv.FormatInt(timestamp, 10))
if region := strings.TrimSpace(auth.Region); region != "" {
req.Header.Set("X-TC-Region", region)
}
canonicalHeaders := "content-type:" + strings.ToLower(strings.TrimSpace(contentType)) + "\n" + "host:" + strings.ToLower(req.URL.Host) + "\n"
signedHeaders := "content-type;host"
canonicalRequest := strings.Join([]string{req.Method, defaultString(req.URL.EscapedPath(), "/"), req.URL.Query().Encode(), canonicalHeaders, signedHeaders, sha256Hex(payload)}, "\n")
scope := dateStamp + "/" + serviceName + "/tc3_request"
stringToSign := strings.Join([]string{"TC3-HMAC-SHA256", strconv.FormatInt(timestamp, 10), scope, sha256Hex([]byte(canonicalRequest))}, "\n")
secretDate := protocolHMAC([]byte("TC3"+secretKey), dateStamp)
secretService := protocolHMAC(secretDate, serviceName)
secretSigning := protocolHMAC(secretService, "tc3_request")
signature := hex.EncodeToString(protocolHMAC(secretSigning, stringToSign))
req.Header.Set("Authorization", fmt.Sprintf("TC3-HMAC-SHA256 Credential=%s/%s, SignedHeaders=%s, Signature=%s", secretID, scope, signedHeaders, signature))
return nil
}
func protocolRequestPayload(req *http.Request) ([]byte, error) {
if req.Body == nil {
return nil, nil
}
var reader io.ReadCloser
var err error
if req.GetBody != nil {
reader, err = req.GetBody()
} else {
reader = req.Body
}
if err != nil {
return nil, err
}
data, err := io.ReadAll(reader)
if req.GetBody != nil {
_ = reader.Close()
} else {
req.Body = io.NopCloser(bytes.NewReader(data))
}
return data, err
}
func protocolCanonicalHeaders(req *http.Request) (string, string) {
values := map[string]string{"host": strings.ToLower(req.URL.Host)}
for name, entries := range req.Header {
lower := strings.ToLower(strings.TrimSpace(name))
if lower == "authorization" || lower == "user-agent" || lower == "content-length" || lower == "expect" {
continue
}
cleaned := make([]string, 0, len(entries))
for _, entry := range entries {
cleaned = append(cleaned, strings.Join(strings.Fields(entry), " "))
}
values[lower] = strings.Join(cleaned, ",")
}
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
sort.Strings(keys)
var canonical strings.Builder
for _, key := range keys {
canonical.WriteString(key)
canonical.WriteByte(':')
canonical.WriteString(values[key])
canonical.WriteByte('\n')
}
return canonical.String(), strings.Join(keys, ";")
}
func protocolHMAC(key []byte, value string) []byte {
mac := hmac.New(sha256.New, key)
_, _ = mac.Write([]byte(value))
return mac.Sum(nil)
}
func sha256Hex(value []byte) string {
hash := sha256.Sum256(value)
return hex.EncodeToString(hash[:])
}
func protocolCredentialField(config providerConfig, field string) string {
switch strings.ToLower(strings.TrimSpace(field)) {
case "secretkey", "secret_key", "secret":
return strings.TrimSpace(config.SecretKey)
default:
return strings.TrimSpace(config.APIKey)
}
}
func protocolRequestURL(baseURL string, spec protocol.RequestSpec) (string, error) {
if !spec.OriginPath {
return appendProtocolQuery(apiURL(baseURL, spec.Path), spec.Query)
}
base, err := url.Parse(strings.TrimSpace(baseURL))
if err != nil || base.Scheme == "" || base.Host == "" {
return "", fmt.Errorf("协议根路径请求的 Base URL 无效")
}
requestPath, err := url.Parse(spec.Path)
if err != nil || !strings.HasPrefix(requestPath.Path, "/") {
return "", fmt.Errorf("协议根路径请求必须使用绝对路径")
}
base.Path = requestPath.Path
base.RawPath = requestPath.RawPath
base.RawQuery = requestPath.RawQuery
base.Fragment = ""
return appendProtocolQuery(base.String(), spec.Query)
}