fix(storage): 兼容 COS 忽略 Range 的完整响应 (#383)
This commit is contained in:
@@ -26,6 +26,8 @@ type OSSConnectionTestResult struct {
|
||||
TestedDigest string `json:"testedDigest"`
|
||||
}
|
||||
|
||||
var errOSSConnectionReadMismatch = errors.New("对象存储读取内容不一致")
|
||||
|
||||
func (s *Service) TestAdminOSSSetting(actor *model.User, req OSSSettingRequest) (*OSSConnectionTestResult, error) {
|
||||
if err := s.RequireAdmin(actor); err != nil {
|
||||
return nil, err
|
||||
@@ -110,11 +112,7 @@ func verifyOSSConnection(value ossSettingValue, testKey string) error {
|
||||
if err != nil {
|
||||
rangeErr = err
|
||||
} else {
|
||||
data, readErr := io.ReadAll(io.LimitReader(stream.body, 5))
|
||||
closeErr := stream.body.Close()
|
||||
if readErr != nil || closeErr != nil || string(data) != "ying" {
|
||||
rangeErr = errors.New("对象存储 Range 读取测试失败")
|
||||
}
|
||||
rangeErr = verifyOSSConnectionRead(stream, payload)
|
||||
}
|
||||
if err := deleteOSSObject(testValue, testKey); err != nil {
|
||||
return storageConnectionTestError(testValue.Provider, "删除", err)
|
||||
@@ -125,6 +123,34 @@ func verifyOSSConnection(value ossSettingValue, testKey string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyOSSConnectionRead(stream *ossObjectStream, payload []byte) error {
|
||||
if stream == nil || stream.body == nil {
|
||||
return fmt.Errorf("%w:响应为空", errOSSConnectionReadMismatch)
|
||||
}
|
||||
data, readErr := io.ReadAll(io.LimitReader(stream.body, int64(len(payload)+1)))
|
||||
closeErr := stream.body.Close()
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("对象存储响应读取失败:%w", readErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("对象存储响应关闭失败:%w", closeErr)
|
||||
}
|
||||
|
||||
// HTTP 允许服务端忽略 Range 并返回 200 + 完整对象。应用的资源代理也会
|
||||
// 原样透传这种响应,因此连接测试应同时接受完整读取与标准的 206 分段读取。
|
||||
switch stream.statusCode {
|
||||
case http.StatusPartialContent:
|
||||
if len(payload) >= 4 && bytes.Equal(data, payload[:4]) {
|
||||
return nil
|
||||
}
|
||||
case http.StatusOK:
|
||||
if bytes.Equal(data, payload) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w(HTTP %d,读取 %d 字节)", errOSSConnectionReadMismatch, stream.statusCode, len(data))
|
||||
}
|
||||
|
||||
func storageConnectionTestError(provider string, operation string, cause error) error {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
providerName := "对象存储"
|
||||
@@ -146,6 +172,9 @@ func storageConnectionTestError(provider string, operation string, cause error)
|
||||
if errors.As(cause, &networkErr) && networkErr.Timeout() {
|
||||
return WrapAppError(http.StatusGatewayTimeout, fmt.Sprintf("%s %s测试超时,请检查 Endpoint 和服务端网络", providerName, operation), cause)
|
||||
}
|
||||
if operation == "Range 读取" && errors.Is(cause, errOSSConnectionReadMismatch) {
|
||||
return WrapAppError(http.StatusBadGateway, fmt.Sprintf("%s Range 读取返回的数据与刚写入对象不一致,请检查 Endpoint 或出网代理是否忽略、改写了 Range 请求", providerName), cause)
|
||||
}
|
||||
|
||||
if provider == tencentCOSProvider {
|
||||
var responseErr *cos.ErrorResponse
|
||||
|
||||
@@ -77,6 +77,39 @@ func TestTencentCOSConnectionTestUsesStorageEndpointInsteadOfCDN(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSSConnectionReadAcceptsFullObjectWhenRangeIsIgnored(t *testing.T) {
|
||||
payload := []byte("yingce-storage-test")
|
||||
stream := &ossObjectStream{
|
||||
body: io.NopCloser(strings.NewReader(string(payload))),
|
||||
statusCode: http.StatusOK,
|
||||
contentLength: int64(len(payload)),
|
||||
}
|
||||
if err := verifyOSSConnectionRead(stream, payload); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSSConnectionReadRejectsUnexpectedContent(t *testing.T) {
|
||||
payload := []byte("yingce-storage-test")
|
||||
stream := &ossObjectStream{
|
||||
body: io.NopCloser(strings.NewReader("error page")),
|
||||
statusCode: http.StatusOK,
|
||||
contentLength: 10,
|
||||
}
|
||||
err := verifyOSSConnectionRead(stream, payload)
|
||||
if err == nil {
|
||||
t.Fatal("verifyOSSConnectionRead() error = nil")
|
||||
}
|
||||
if !errors.Is(err, errOSSConnectionReadMismatch) {
|
||||
t.Fatalf("verifyOSSConnectionRead() error = %v", err)
|
||||
}
|
||||
publicErr := storageConnectionTestError(tencentCOSProvider, "Range 读取", err)
|
||||
var appErr *AppError
|
||||
if !errors.As(publicErr, &appErr) || !strings.Contains(appErr.Message, "忽略、改写了 Range 请求") {
|
||||
t.Fatalf("storageConnectionTestError() = %#v", publicErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTencentCOSConnectionTestReturnsActionableAuthError(t *testing.T) {
|
||||
t.Setenv("CANVAS_ALLOW_PRIVATE_UPSTREAMS", "true")
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user