Files
nezha_domains/model/notification_test.go
T
2026-05-17 10:24:19 +08:00

365 lines
11 KiB
Go

package model
import (
"io"
"net/http"
"strings"
"testing"
"time"
)
var (
msg = "msg"
reqTypeForm = "application/x-www-form-urlencoded"
reqTypeJSON = "application/json"
)
type testSt struct {
url string
body string
header string
reqType uint8
reqMethod uint8
expectURL string
expectBody string
expectMethod string
expectContentType string
expectHeader map[string]string
}
func execCase(t *testing.T, item testSt) {
trueBool := true
n := Notification{
URL: item.url,
RequestMethod: item.reqMethod,
RequestType: item.reqType,
RequestBody: item.body,
RequestHeader: item.header,
FormatMetricUnits: &trueBool,
}
server := Server{
Common: Common{},
Name: "ServerName",
Note: "",
DisplayIndex: 0,
Host: &Host{
Platform: "",
PlatformVersion: "",
CPU: nil,
MemTotal: 0,
DiskTotal: 0,
SwapTotal: 8888,
Arch: "",
Virtualization: "",
BootTime: 0,
Version: "",
},
State: &HostState{
CPU: 0,
MemUsed: 0,
SwapUsed: 8888,
DiskUsed: 0,
NetInTransfer: 0,
NetOutTransfer: 0,
NetInSpeed: 0,
NetOutSpeed: 0,
Uptime: 0,
Load1: 0,
Load5: 0,
Load15: 0,
TcpConnCount: 0,
UdpConnCount: 0,
ProcessCount: 0,
},
GeoIP: &GeoIP{
IP: IP{
IPv4Addr: "1.1.1.1",
},
CountryCode: "",
},
LastActive: time.Time{},
TaskStream: nil,
PrevTransferInSnapshot: 0,
PrevTransferOutSnapshot: 0,
}
ns := NotificationServerBundle{
Notification: &n,
Server: &server,
Loc: time.Local,
}
if item.expectURL != ns.reqURL(msg) {
t.Fatalf("Expected %s, but got %s", item.expectURL, ns.reqURL(msg))
}
reqBody, err := ns.reqBody(msg)
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectBody != reqBody {
t.Fatalf("Expected %s, but got %s", item.expectBody, reqBody)
}
reqMethod, err := n.reqMethod()
if err != nil {
t.Fatalf("Error: %s", err)
}
if item.expectMethod != reqMethod {
t.Fatalf("Expected %s, but got %s", item.expectMethod, reqMethod)
}
req, err := http.NewRequest("", "", strings.NewReader(""))
if err != nil {
t.Fatalf("Error: %s", err)
}
n.setContentType(req)
if item.expectContentType != req.Header.Get("Content-Type") {
t.Fatalf("Expected %s, but got %s", item.expectContentType, req.Header.Get("Content-Type"))
}
n.setRequestHeader(req)
for k, v := range item.expectHeader {
if v != req.Header.Get(k) {
t.Fatalf("Expected %s, but got %s", v, req.Header.Get(k))
}
}
}
func TestNotification(t *testing.T) {
cases := []testSt{
{
url: "https://example.com",
body: `{"asd":"dsa"}`,
header: `{"asd":"dsa"}`,
reqMethod: NotificationRequestMethodGET,
expectURL: "https://example.com",
expectMethod: http.MethodGet,
expectContentType: "",
expectHeader: map[string]string{"asd": "dsa"},
expectBody: "",
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"asd":"dsa"}`,
reqMethod: NotificationRequestMethodGET,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodGet,
expectContentType: "",
expectBody: "",
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"asd":"#NEZHA#"}`,
reqMethod: NotificationRequestMethodPOST,
reqType: NotificationRequestTypeForm,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeForm,
expectBody: "asd=" + msg,
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"#NEZHA#":"#NEZHA#"}`,
reqMethod: NotificationRequestMethodPOST,
reqType: NotificationRequestTypeForm,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeForm,
expectBody: "%23NEZHA%23=" + msg,
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"asd":"#NEZHA#"}`,
reqMethod: NotificationRequestMethodPOST,
reqType: NotificationRequestTypeJSON,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeJSON,
expectBody: `{"asd":"msg"}`,
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"#NEZHA#":"#NEZHA#"}`,
reqMethod: NotificationRequestMethodPOST,
header: `{"asd":"dsa11"}`,
reqType: NotificationRequestTypeJSON,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeJSON,
expectBody: `{"msg":"msg"}`,
expectHeader: map[string]string{"asd": "dsa11"},
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAP#"}`,
reqMethod: NotificationRequestMethodPOST,
header: `{"asd":"dsa11"}`,
reqType: NotificationRequestTypeJSON,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeJSON,
expectBody: `{"Server":"ServerName","ServerIP":"1.1.1.1","ServerSWAP":"100.00 %"}`,
expectHeader: map[string]string{"asd": "dsa11"},
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"#NEZHA#":"#NEZHA#","Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAP#"}`,
reqMethod: NotificationRequestMethodPOST,
reqType: NotificationRequestTypeForm,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeForm,
expectBody: "%23NEZHA%23=" + msg + "&Server=ServerName&ServerIP=1.1.1.1&ServerSWAP=100.00+%25",
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":#SERVER.SWAPUSED#}`,
reqMethod: NotificationRequestMethodPOST,
header: `{"asd":"dsa11"}`,
reqType: NotificationRequestTypeJSON,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeJSON,
expectBody: `{"Server":"ServerName","ServerIP":"1.1.1.1","ServerSWAP":8888}`,
expectHeader: map[string]string{"asd": "dsa11"},
},
{
url: "https://example.com/?m=#NEZHA#",
body: `{"#NEZHA#":"#NEZHA#","Server":"#SERVER.NAME#","ServerIP":"#SERVER.IP#","ServerSWAP":"#SERVER.SWAPUSED#"}`,
reqMethod: NotificationRequestMethodPOST,
reqType: NotificationRequestTypeForm,
expectURL: "https://example.com/?m=" + msg,
expectMethod: http.MethodPost,
expectContentType: reqTypeForm,
expectBody: "%23NEZHA%23=" + msg + "&Server=ServerName&ServerIP=1.1.1.1&ServerSWAP=8888",
},
}
for _, c := range cases {
execCase(t, c)
}
}
func TestNotificationResponseErrorDoesNotReflectNonSuccessResponseBody(t *testing.T) {
const internalResponseBody = "internal service says token=secret"
resp := &http.Response{
StatusCode: http.StatusTeapot,
Status: "418 I'm a teapot",
Body: io.NopCloser(strings.NewReader(internalResponseBody)),
}
err := notificationResponseError(resp)
if strings.Contains(err.Error(), internalResponseBody) {
t.Fatalf("expected upstream response body to be hidden from error, got %q", err.Error())
}
}
func TestNotificationSendRejectsLoopbackTarget(t *testing.T) {
verifyTLS := true
notification := &Notification{
URL: "http://127.0.0.1/internal",
RequestMethod: NotificationRequestMethodGET,
VerifyTLS: &verifyTLS,
}
bundle := NotificationServerBundle{
Notification: notification,
Loc: time.Local,
}
err := bundle.Send("probe")
if err == nil {
t.Fatal("expected loopback notification URL to be rejected")
}
if !strings.Contains(err.Error(), "not allowed") {
t.Fatalf("expected not allowed error, got %q", err.Error())
}
}
func TestNotificationTargetRejectsBlockedRanges(t *testing.T) {
cases := []string{
"http://0.0.0.0/",
"http://10.1.2.3/",
"http://100.64.0.1/",
"http://127.0.0.1/",
"http://127.255.255.254/",
"http://169.254.169.254/",
"http://172.16.0.1/",
"http://192.0.0.1/",
"http://192.0.2.1/",
"http://192.168.1.1/",
"http://198.18.0.1/",
"http://198.51.100.1/",
"http://203.0.113.1/",
"http://224.0.0.1/",
"http://240.0.0.1/",
"http://[::]/",
"http://[::1]/",
"http://[::ffff:127.0.0.1]/",
"http://[64:ff9b::1]/",
"http://[100::1]/",
"http://[2001:0:0:0:0:0:0:1]/",
"http://[2001:db8::1]/",
"http://[fc00::1]/",
"http://[fe80::1]/",
"http://[ff00::1]/",
"ftp://example.com/",
"file:///etc/passwd",
"http:///path",
}
for _, rawURL := range cases {
t.Run(rawURL, func(t *testing.T) {
if _, _, err := resolveNotificationTarget(rawURL); err == nil {
t.Fatalf("expected %s to be rejected", rawURL)
}
})
}
}
func TestNotificationTargetAllowsPublicAddresses(t *testing.T) {
cases := []string{
"http://1.1.1.1/path",
"https://8.8.8.8/",
"https://[2606:4700:4700::1111]/",
}
for _, rawURL := range cases {
t.Run(rawURL, func(t *testing.T) {
parsedURL, _, err := resolveNotificationTarget(rawURL)
if err != nil {
t.Fatalf("expected %s to be allowed, got %v", rawURL, err)
}
if parsedURL == nil {
t.Fatalf("expected parsed url for %s", rawURL)
}
})
}
}
func TestNotificationHTTPClientPreservesTLSServerName(t *testing.T) {
client, err := newNotificationHTTPClient("https://1.1.1.1/webhook", true)
if err != nil {
t.Fatalf("expected public HTTPS URL to create client: %v", err)
}
transport, ok := client.Transport.(*http.Transport)
if !ok {
t.Fatalf("expected http.Transport, got %T", client.Transport)
}
if transport.TLSClientConfig == nil || transport.TLSClientConfig.ServerName != "1.1.1.1" {
t.Fatalf("expected TLS ServerName 1.1.1.1, got %#v", transport.TLSClientConfig)
}
}
func TestNotificationHTTPClientRejectsRedirects(t *testing.T) {
client, err := newNotificationHTTPClient("https://1.1.1.1/webhook", true)
if err != nil {
t.Fatalf("expected client construction: %v", err)
}
req, err := http.NewRequest(http.MethodGet, "https://1.1.1.1/start", nil)
if err != nil {
t.Fatalf("new request: %v", err)
}
via := []*http.Request{req}
if err := client.CheckRedirect(req, via); err != http.ErrUseLastResponse {
t.Fatalf("expected ErrUseLastResponse, got %v", err)
}
}