This commit is contained in:
MengMengCode
2026-08-19 21:41:54 +08:00
parent ad66456d2f
commit 2c843d82a4
2 changed files with 62 additions and 2 deletions
+22 -2
View File
@@ -295,7 +295,7 @@ func validateNotificationField(
}
}
if name == "proxy" && value != "" {
if _, err := parseOutboundURL(value, false); err != nil {
if _, err := parseProxyURL(value); err != nil {
return fmt.Errorf("%s is not a valid HTTP URL", field)
}
}
@@ -989,7 +989,7 @@ func validateOutboundURL(
}
func validateNotificationProxyURL(ctx context.Context, raw string) (*url.URL, error) {
parsed, err := parseOutboundURL(raw, false)
parsed, err := parseProxyURL(raw)
if err != nil {
return nil, err
}
@@ -999,6 +999,26 @@ func validateNotificationProxyURL(ctx context.Context, raw string) (*url.URL, er
return parsed, nil
}
// parseProxyURL parses an HTTP(S) proxy URL. Unlike parseOutboundURL, it
// permits embedded userinfo (http://user:pass@host:port) because HTTP proxies
// commonly authenticate with Proxy-Authorization derived from the URL.
func parseProxyURL(raw string) (*url.URL, error) {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.Hostname() == "" || parsed.IsAbs() == false {
return nil, errors.New("proxy must be an absolute HTTP URL")
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, errors.New("proxy URL must use HTTP or HTTPS")
}
if parsed.Port() != "" {
port, err := strconv.Atoi(parsed.Port())
if err != nil || port < 1 || port > 65535 {
return nil, errors.New("proxy URL has an invalid port")
}
}
return parsed, nil
}
func parseOutboundURL(raw string, requireHTTPS bool) (*url.URL, error) {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil || parsed.Hostname() == "" || parsed.IsAbs() == false {
+40
View File
@@ -420,6 +420,11 @@ func TestNotificationSettingsRejectsUnknownAndMalformedInput(t *testing.T) {
body: `{"lark":{"enabled":false,"url":"https://example.com/open-apis/bot/v2/hook/token"}}`,
code: "invalid_notification_config",
},
{
name: "webhook URL with embedded credentials",
body: `{"webhook":{"enabled":true,"urls":["http://user:[email protected]"]}}`,
code: "invalid_notification_config",
},
{
name: "null body",
body: `null`,
@@ -994,6 +999,41 @@ func TestRestrictedNotificationClientCapsTimeoutAndRedirects(t *testing.T) {
}
}
func TestNotificationProxyAcceptsAuthenticatedURL(t *testing.T) {
test := newSettingsAPITest(t)
body := `{"telegram":{"enabled":true,"bot_token":"123456:abc","chat_id":"1","proxy":"http://user:[email protected]:8080"}}`
recorder := test.request(t, http.MethodPut, "/api/settings/notifications", body)
if recorder.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body)
}
response := decodeSettingsResponse(t, recorder)
data, ok := response["data"].(map[string]any)
if !ok {
t.Fatalf("data missing: %#v", response)
}
telegram, ok := data["telegram"].(map[string]any)
if !ok {
t.Fatalf("telegram response missing: %#v", data)
}
if telegram["proxy"] != "http://user:[email protected]:8080" {
t.Fatalf("proxy not preserved: %#v", telegram["proxy"])
}
}
func TestNotificationProxyRejectsMalformedURL(t *testing.T) {
test := newSettingsAPITest(t)
body := `{"telegram":{"enabled":true,"bot_token":"123456:abc","chat_id":"1","proxy":"not-a-url"}}`
recorder := test.request(t, http.MethodPut, "/api/settings/notifications", body)
if recorder.Code != http.StatusBadRequest {
t.Fatalf("status = %d, body = %s", recorder.Code, recorder.Body)
}
response := decodeSettingsResponse(t, recorder)
detail, ok := response["error"].(map[string]any)
if !ok || detail["code"] != "invalid_notification_config" {
t.Fatalf("error = %#v", detail)
}
}
func TestRouteSettingsAPIReturnsFalseForUnknownPath(t *testing.T) {
test := newSettingsAPITest(t)
request := httptest.NewRequest(http.MethodGet, "/api/not-settings", nil)