mirror of
https://github.com/truewhile/MeBox.git
synced 2026-10-11 15:56:37 +08:00
fix: validate downloader RPC endpoints
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var downloadClientEndpointPattern = regexp.MustCompile(`^https?://(?:[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\])(?::[0-9]{1,5})?(?:/[A-Za-z0-9._~%!$&'()*+,;=:@/-]*)?$`)
|
||||
|
||||
func NormalizeDownloadClientHost(clientType, raw string) (string, error) {
|
||||
return normalizeDownloadClientEndpoint(clientType, raw)
|
||||
}
|
||||
|
||||
func normalizeDownloadClientEndpoint(clientType, raw string) (string, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "", errors.New("host required")
|
||||
}
|
||||
if strings.ContainsAny(raw, "\r\n\t") {
|
||||
return "", errors.New("host contains invalid control characters")
|
||||
}
|
||||
if !strings.Contains(raw, "://") {
|
||||
raw = "http://" + raw
|
||||
}
|
||||
if !downloadClientEndpointPattern.MatchString(raw) {
|
||||
return "", errors.New("host must be a valid http(s) URL without username, query, or fragment")
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||
return "", errors.New("host must be a valid http(s) URL")
|
||||
}
|
||||
scheme := strings.ToLower(parsed.Scheme)
|
||||
if scheme != "http" && scheme != "https" {
|
||||
return "", errors.New("host only supports http or https")
|
||||
}
|
||||
if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {
|
||||
return "", errors.New("host must not include username, query, or fragment")
|
||||
}
|
||||
if strings.TrimSpace(parsed.Hostname()) == "" {
|
||||
return "", errors.New("host must include a hostname")
|
||||
}
|
||||
if port := parsed.Port(); port != "" {
|
||||
n, err := strconv.Atoi(port)
|
||||
if err != nil || n < 1 || n > 65535 {
|
||||
return "", errors.New("host port must be between 1 and 65535")
|
||||
}
|
||||
}
|
||||
if err := validateDownloadClientPath(clientType, parsed.Path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
parsed.Scheme = scheme
|
||||
parsed.Path = strings.TrimRight(parsed.Path, "/")
|
||||
parsed.RawPath = ""
|
||||
parsed.RawQuery = ""
|
||||
parsed.Fragment = ""
|
||||
return strings.TrimRight(parsed.String(), "/"), nil
|
||||
}
|
||||
|
||||
func validateDownloadClientPath(clientType, rawPath string) error {
|
||||
rawPath = strings.TrimSpace(rawPath)
|
||||
if rawPath == "" || rawPath == "/" {
|
||||
return nil
|
||||
}
|
||||
for _, segment := range strings.Split(rawPath, "/") {
|
||||
if segment == "." || segment == ".." {
|
||||
return errors.New("host path must not contain traversal segments")
|
||||
}
|
||||
}
|
||||
switch clientType {
|
||||
case "qbittorrent", "aria2", "transmission":
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported client type %q", clientType)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadClientRPCURL(clientType, host string) (string, error) {
|
||||
base, err := normalizeDownloadClientEndpoint(clientType, host)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
u, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch clientType {
|
||||
case "aria2":
|
||||
if !strings.HasSuffix(strings.ToLower(u.Path), "/jsonrpc") {
|
||||
u.Path = strings.TrimRight(u.Path, "/") + "/jsonrpc"
|
||||
}
|
||||
case "transmission":
|
||||
if !strings.Contains(strings.ToLower(u.Path), "/rpc") {
|
||||
u.Path = strings.TrimRight(u.Path, "/") + "/transmission/rpc"
|
||||
}
|
||||
case "qbittorrent":
|
||||
default:
|
||||
return "", fmt.Errorf("unsupported client type %q", clientType)
|
||||
}
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func newDownloadClientHTTPRequest(ctx context.Context, method, endpoint string, body io.Reader) (*http.Request, error) {
|
||||
endpoint = strings.TrimSpace(endpoint)
|
||||
if !downloadClientEndpointPattern.MatchString(endpoint) {
|
||||
return nil, errors.New("download client endpoint failed safety validation")
|
||||
}
|
||||
return http.NewRequestWithContext(ctx, method, endpoint, body)
|
||||
}
|
||||
Reference in New Issue
Block a user