129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
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
|
||
}
|