mirror of
https://github.com/alibaba/higress.git
synced 2026-05-28 22:57:31 +08:00
fix: bedrock canonical URI sig (#3590)
This commit is contained in:
1
.github/workflows/wasm-plugin-unit-test.yml
vendored
1
.github/workflows/wasm-plugin-unit-test.yml
vendored
@@ -207,7 +207,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Install required tools
|
- name: Install required tools
|
||||||
run: |
|
run: |
|
||||||
go install github.com/wadey/gocovmerge@latest
|
|
||||||
sudo apt-get update && sudo apt-get install -y bc
|
sudo apt-get update && sudo apt-get install -y bc
|
||||||
|
|
||||||
- name: Download all test results
|
- name: Download all test results
|
||||||
|
|||||||
@@ -1372,44 +1372,11 @@ func encodeSigV4Path(path string) string {
|
|||||||
if seg == "" {
|
if seg == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Normalize to "single-encoded" form:
|
segments[i] = url.PathEscape(seg)
|
||||||
// - raw ":" -> %3A
|
|
||||||
// - already encoded "%3A" -> still %3A (not %253A)
|
|
||||||
decoded, err := url.PathUnescape(seg)
|
|
||||||
if err == nil {
|
|
||||||
segments[i] = sigV4EscapePathSegment(decoded)
|
|
||||||
} else {
|
|
||||||
// If segment has invalid escape sequence, fall back to escaping raw segment.
|
|
||||||
segments[i] = sigV4EscapePathSegment(seg)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return strings.Join(segments, "/")
|
return strings.Join(segments, "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
func sigV4EscapePathSegment(segment string) string {
|
|
||||||
const upperHex = "0123456789ABCDEF"
|
|
||||||
var b strings.Builder
|
|
||||||
b.Grow(len(segment) * 3)
|
|
||||||
for i := 0; i < len(segment); i++ {
|
|
||||||
c := segment[i]
|
|
||||||
if isSigV4Unreserved(c) {
|
|
||||||
b.WriteByte(c)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
b.WriteByte('%')
|
|
||||||
b.WriteByte(upperHex[c>>4])
|
|
||||||
b.WriteByte(upperHex[c&0x0F])
|
|
||||||
}
|
|
||||||
return b.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func isSigV4Unreserved(c byte) bool {
|
|
||||||
return (c >= 'A' && c <= 'Z') ||
|
|
||||||
(c >= 'a' && c <= 'z') ||
|
|
||||||
(c >= '0' && c <= '9') ||
|
|
||||||
c == '-' || c == '_' || c == '.' || c == '~'
|
|
||||||
}
|
|
||||||
|
|
||||||
func getSignatureKey(key, dateStamp, region, service string) []byte {
|
func getSignatureKey(key, dateStamp, region, service string) []byte {
|
||||||
kDate := hmacSha256([]byte("AWS4"+key), dateStamp)
|
kDate := hmacSha256([]byte("AWS4"+key), dateStamp)
|
||||||
kRegion := hmacSha256(kDate, region)
|
kRegion := hmacSha256(kDate, region)
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeSigV4Path(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
path string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "raw model id keeps colon",
|
||||||
|
path: "/model/global.amazon.nova-2-lite-v1:0/converse-stream",
|
||||||
|
want: "/model/global.amazon.nova-2-lite-v1:0/converse-stream",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pre-encoded model id escapes percent to avoid mismatch",
|
||||||
|
path: "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream",
|
||||||
|
want: "/model/global.amazon.nova-2-lite-v1%253A0/converse-stream",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "raw inference profile arn keeps colon and slash delimiters",
|
||||||
|
path: "/model/arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.anthropic.claude-sonnet-4-20250514-v1:0/converse",
|
||||||
|
want: "/model/arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.anthropic.claude-sonnet-4-20250514-v1:0/converse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "encoded inference profile arn preserves escaped slash as double-escaped percent",
|
||||||
|
path: "/model/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Ainference-profile%2Fglobal.anthropic.claude-sonnet-4-20250514-v1%3A0/converse",
|
||||||
|
want: "/model/arn%253Aaws%253Abedrock%253Aus-east-1%253A123456789012%253Ainference-profile%252Fglobal.anthropic.claude-sonnet-4-20250514-v1%253A0/converse",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query string is stripped before canonical encoding",
|
||||||
|
path: "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream?trace=1&foo=bar",
|
||||||
|
want: "/model/global.amazon.nova-2-lite-v1%253A0/converse-stream",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid percent sequence falls back to escaped percent",
|
||||||
|
path: "/model/abc%ZZxyz/converse",
|
||||||
|
want: "/model/abc%25ZZxyz/converse",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, tt.want, encodeSigV4Path(tt.path))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverwriteRequestPathHeaderPreservesSingleEncodedRequestPath(t *testing.T) {
|
||||||
|
p := &bedrockProvider{}
|
||||||
|
plainModel := "arn:aws:bedrock:us-east-1:123456789012:inference-profile/global.amazon.nova-2-lite-v1:0"
|
||||||
|
preEncodedModel := url.QueryEscape(plainModel)
|
||||||
|
|
||||||
|
t.Run("plain model is encoded once", func(t *testing.T) {
|
||||||
|
headers := http.Header{}
|
||||||
|
p.overwriteRequestPathHeader(headers, bedrockChatCompletionPath, plainModel)
|
||||||
|
assert.Equal(t, "/model/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Ainference-profile%2Fglobal.amazon.nova-2-lite-v1%3A0/converse", headers.Get(":path"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pre-encoded model is not double encoded", func(t *testing.T) {
|
||||||
|
headers := http.Header{}
|
||||||
|
p.overwriteRequestPathHeader(headers, bedrockChatCompletionPath, preEncodedModel)
|
||||||
|
assert.Equal(t, "/model/arn%3Aaws%3Abedrock%3Aus-east-1%3A123456789012%3Ainference-profile%2Fglobal.amazon.nova-2-lite-v1%3A0/converse", headers.Get(":path"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateSignatureIgnoresQueryStringInCanonicalURI(t *testing.T) {
|
||||||
|
p := &bedrockProvider{
|
||||||
|
config: ProviderConfig{
|
||||||
|
awsRegion: "ap-northeast-3",
|
||||||
|
awsSecretKey: "test-secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body := []byte(`{"messages":[{"role":"user","content":[{"text":"hello"}]}]}`)
|
||||||
|
pathWithoutQuery := "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream"
|
||||||
|
pathWithQuery := pathWithoutQuery + "?trace=1&foo=bar"
|
||||||
|
|
||||||
|
sigWithoutQuery := p.generateSignature(pathWithoutQuery, "20260312T142942Z", "20260312", body)
|
||||||
|
sigWithQuery := p.generateSignature(pathWithQuery, "20260312T142942Z", "20260312", body)
|
||||||
|
assert.Equal(t, sigWithoutQuery, sigWithQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGenerateSignatureDiffersForRawAndPreEncodedModelPath(t *testing.T) {
|
||||||
|
p := &bedrockProvider{
|
||||||
|
config: ProviderConfig{
|
||||||
|
awsRegion: "ap-northeast-3",
|
||||||
|
awsSecretKey: "test-secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
body := []byte(`{"messages":[{"role":"user","content":[{"text":"hello"}]}]}`)
|
||||||
|
rawPath := "/model/global.amazon.nova-2-lite-v1:0/converse-stream"
|
||||||
|
preEncodedPath := "/model/global.amazon.nova-2-lite-v1%3A0/converse-stream"
|
||||||
|
|
||||||
|
rawSignature := p.generateSignature(rawPath, "20260312T142942Z", "20260312", body)
|
||||||
|
preEncodedSignature := p.generateSignature(preEncodedPath, "20260312T142942Z", "20260312", body)
|
||||||
|
assert.NotEqual(t, rawSignature, preEncodedSignature)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user