2016-06-15 20:45:05 +08:00
package notifications
import (
2016-06-16 14:15:48 +08:00
"bytes"
2016-10-03 15:38:03 +08:00
"context"
2019-01-11 15:56:50 +08:00
"crypto/tls"
2024-06-19 05:02:33 +08:00
"errors"
2016-07-27 18:09:55 +08:00
"fmt"
2022-08-10 21:37:51 +08:00
"io"
2025-05-10 00:26:20 +08:00
"net"
2016-06-15 20:45:05 +08:00
"net/http"
2024-06-19 05:02:33 +08:00
"net/url"
2025-05-10 00:26:20 +08:00
"time"
2016-06-15 20:45:05 +08:00
2016-06-16 14:15:48 +08:00
"github.com/grafana/grafana/pkg/util"
2016-06-15 20:45:05 +08:00
)
type Webhook struct {
2017-03-24 04:53:54 +08:00
Url string
User string
Password string
Body string
HttpMethod string
HttpHeader map [ string ] string
ContentType string
2024-10-22 18:44:32 +08:00
TLSConfig * tls . Config
2022-07-15 01:15:18 +08:00
// Validation is a function that will validate the response body and statusCode of the webhook. Any returned error will cause the webhook request to be considered failed.
// This can be useful when a webhook service communicates failures in creative ways, such as using the response body instead of the status code.
Validation func ( body [ ] byte , statusCode int ) error
}
// WebhookClient exists to mock the client in tests.
type WebhookClient interface {
Do ( req * http . Request ) ( * http . Response , error )
2016-06-15 20:45:05 +08:00
}
2018-04-27 19:01:32 +08:00
func ( ns * NotificationService ) sendWebRequestSync ( ctx context . Context , webhook * Webhook ) error {
2016-10-18 22:18:16 +08:00
if webhook . HttpMethod == "" {
webhook . HttpMethod = http . MethodPost
}
2022-04-01 02:57:48 +08:00
ns . log . Debug ( "Sending webhook" , "url" , webhook . Url , "http method" , webhook . HttpMethod )
2020-11-24 17:42:54 +08:00
if webhook . HttpMethod != http . MethodPost && webhook . HttpMethod != http . MethodPut {
return fmt . Errorf ( "webhook only supports HTTP methods PUT or POST" )
}
2022-04-11 20:46:21 +08:00
request , err := http . NewRequestWithContext ( ctx , webhook . HttpMethod , webhook . Url , bytes . NewReader ( [ ] byte ( webhook . Body ) ) )
2016-06-15 20:45:05 +08:00
if err != nil {
return err
}
2024-06-19 05:02:33 +08:00
url , err := url . Parse ( webhook . Url )
if err != nil {
// Should not be possible - NewRequestWithContext should also err if the URL is bad.
return err
}
2016-06-15 20:45:05 +08:00
2017-03-24 04:53:54 +08:00
if webhook . ContentType == "" {
webhook . ContentType = "application/json"
}
2020-12-14 22:13:01 +08:00
request . Header . Set ( "Content-Type" , webhook . ContentType )
request . Header . Set ( "User-Agent" , "Grafana" )
2018-05-07 21:40:43 +08:00
2016-12-05 17:44:31 +08:00
if webhook . User != "" && webhook . Password != "" {
2020-12-14 22:13:01 +08:00
request . Header . Set ( "Authorization" , util . GetBasicAuthHeader ( webhook . User , webhook . Password ) )
2016-12-05 17:44:31 +08:00
}
2017-01-19 16:29:40 +08:00
for k , v := range webhook . HttpHeader {
request . Header . Set ( k , v )
2017-01-19 15:25:21 +08:00
}
2025-05-10 00:26:20 +08:00
resp , err := NewTLSClient ( webhook . TLSConfig ) . Do ( request )
2016-06-15 20:45:05 +08:00
if err != nil {
2024-06-19 05:02:33 +08:00
return redactURL ( err )
2016-06-15 20:45:05 +08:00
}
2020-12-15 16:32:06 +08:00
defer func ( ) {
if err := resp . Body . Close ( ) ; err != nil {
ns . log . Warn ( "Failed to close response body" , "err" , err )
}
} ( )
2018-12-27 20:16:58 +08:00
2022-08-10 21:37:51 +08:00
body , err := io . ReadAll ( resp . Body )
2016-10-03 15:38:03 +08:00
if err != nil {
return err
2016-07-27 18:09:55 +08:00
}
2016-10-03 15:38:03 +08:00
2022-07-15 01:15:18 +08:00
if webhook . Validation != nil {
err := webhook . Validation ( body , resp . StatusCode )
if err != nil {
2024-06-19 05:02:33 +08:00
ns . log . Debug ( "Webhook failed validation" , "url" , url . Redacted ( ) , "statuscode" , resp . Status , "body" , string ( body ) , "error" , err )
2022-07-15 01:15:18 +08:00
return fmt . Errorf ( "webhook failed validation: %w" , err )
}
}
if resp . StatusCode / 100 == 2 {
2024-06-19 05:02:33 +08:00
ns . log . Debug ( "Webhook succeeded" , "url" , url . Redacted ( ) , "statuscode" , resp . Status )
2022-07-15 01:15:18 +08:00
return nil
}
2024-06-19 05:02:33 +08:00
ns . log . Debug ( "Webhook failed" , "url" , url . Redacted ( ) , "statuscode" , resp . Status , "body" , string ( body ) )
2022-07-15 01:15:18 +08:00
return fmt . Errorf ( "webhook response status %v" , resp . Status )
2016-06-15 20:45:05 +08:00
}
2024-06-19 05:02:33 +08:00
func redactURL ( err error ) error {
var e * url . Error
if ! errors . As ( err , & e ) {
return err
}
e . URL = "<redacted>"
return e
}
2025-05-10 00:26:20 +08:00
// NewTLSClient creates a new HTTP client with the provided TLS configuration or with default settings.
func NewTLSClient ( tlsConfig * tls . Config ) * http . Client {
nc := func ( tlsConfig * tls . Config ) * http . Client {
return & http . Client {
Timeout : time . Second * 30 ,
Transport : & http . Transport {
TLSClientConfig : tlsConfig ,
Proxy : http . ProxyFromEnvironment ,
Dial : ( & net . Dialer {
Timeout : 30 * time . Second ,
} ) . Dial ,
TLSHandshakeTimeout : 5 * time . Second ,
} ,
}
}
if tlsConfig == nil {
return nc ( & tls . Config { Renegotiation : tls . RenegotiateFreelyAsClient } )
}
return nc ( tlsConfig )
}