package storage import ( "fmt" "net/url" "strings" "unicode" ) // ObjectKeyFromCDNURL 将属于 cdnBase 的 HTTPS URL 转成原始 S3 对象 key。 // cdnBase 可省略 scheme,也可带 base path。外站或不属于 base path 的 URL // 返回 owned=false 且不报错;格式非法或试图伪装路径的 URL 返回错误。 func ObjectKeyFromCDNURL(rawURL, cdnBase string) (key string, owned bool, err error) { base, err := parseCDNBase(cdnBase) if err != nil { return "", false, err } parsed, err := parseCDNURL(rawURL, false) if err != nil { return "", false, err } if !strings.EqualFold(parsed.Host, base.Host) { return "", false, nil } basePath := base.Path if basePath != "" { if parsed.Path == basePath { return "", false, fmt.Errorf("CDN URL does not contain an object key") } prefix := basePath + "/" if !strings.HasPrefix(parsed.Path, prefix) { return "", false, nil } key = strings.TrimPrefix(parsed.Path, prefix) } else { key = strings.TrimPrefix(parsed.Path, "/") } if key == "" { return "", false, fmt.Errorf("CDN URL does not contain an object key") } // 只去掉 cdnBase 的路径和前导分隔符;不添加或移除 bucket,也不 Clean, // 因而返回的解码路径仍是实际 S3 key。 return key, true, nil } func parseCDNBase(cdnBase string) (*url.URL, error) { if cdnBase == "" { return nil, fmt.Errorf("CDN base URL is empty") } rawBase := cdnBase if strings.HasPrefix(rawBase, "//") { rawBase = "https:" + rawBase } else if !strings.Contains(rawBase, "://") { rawBase = "https://" + rawBase } return parseCDNURL(rawBase, true) } func parseCDNURL(rawURL string, isBase bool) (*url.URL, error) { if rawURL == "" { return nil, fmt.Errorf("URL is empty") } if strings.Contains(rawURL, `\`) || containsControl(rawURL) { return nil, fmt.Errorf("URL contains a backslash or control character") } parsed, err := url.Parse(rawURL) if err != nil { return nil, fmt.Errorf("parse URL: %w", err) } if !parsed.IsAbs() || !strings.EqualFold(parsed.Scheme, "https") || parsed.Host == "" || parsed.Hostname() == "" || parsed.Opaque != "" { return nil, fmt.Errorf("URL must be an absolute HTTPS URL") } if parsed.User != nil { return nil, fmt.Errorf("URL must not contain userinfo") } if parsed.RawQuery != "" || parsed.ForceQuery { return nil, fmt.Errorf("URL must not contain a query") } if parsed.Fragment != "" || strings.Contains(rawURL, "#") { return nil, fmt.Errorf("URL must not contain a fragment") } // 必须在 url.Parse 的 Path 解码之外检查 EscapedPath:编码斜杠在 Path 中 // 与普通分隔符无法区分,若放行会导致 CDN 与 S3 对 key 的解释不一致。 escapedPath := strings.ToLower(parsed.EscapedPath()) if strings.Contains(escapedPath, "%2f") || strings.Contains(escapedPath, "%5c") { return nil, fmt.Errorf("URL path contains an encoded slash or backslash") } if strings.Contains(parsed.Path, `\`) || containsControl(parsed.Path) { return nil, fmt.Errorf("URL path contains a backslash or control character") } if isBase && strings.HasSuffix(parsed.Path, "/") { // 配置中的 CDN base 允许一个末尾斜杠;其余空路径段仍会被拒绝。 parsed.Path = strings.TrimSuffix(parsed.Path, "/") parsed.RawPath = "" } if parsed.Path == "" { if isBase { return parsed, nil } return nil, fmt.Errorf("URL path is empty") } if !strings.HasPrefix(parsed.Path, "/") { return nil, fmt.Errorf("URL path must be absolute") } for _, segment := range strings.Split(strings.TrimPrefix(parsed.Path, "/"), "/") { if segment == "" { return nil, fmt.Errorf("URL path contains an empty segment") } if segment == "." || segment == ".." { return nil, fmt.Errorf("URL path contains a dot segment") } } return parsed, nil } func containsControl(value string) bool { for _, r := range value { if unicode.IsControl(r) { return true } } return false }