xmpp-webhook/main.go

177 wiersze
4.3 KiB
Go

package main
import (
"context"
"crypto/tls"
"encoding/xml"
"io"
"log"
"net/http"
"os"
"strings"
"github.com/tmsmr/xmpp-webhook/parser"
"mellium.im/sasl"
"mellium.im/xmlstream"
"mellium.im/xmpp"
"mellium.im/xmpp/dial"
"mellium.im/xmpp/jid"
"mellium.im/xmpp/stanza"
)
func panicOnErr(err error) {
if err != nil {
panic(err)
}
}
type MessageBody struct {
stanza.Message
Body string `xml:"body"`
}
func initXMPP(address jid.JID, pass string, skipTLSVerify bool, useXMPPS bool) (*xmpp.Session, error) {
tlsConfig := tls.Config{InsecureSkipVerify: skipTLSVerify}
var dialer dial.Dialer
// only use the tls config for the dialer if necessary
if skipTLSVerify {
dialer = dial.Dialer{NoTLS: !useXMPPS, TLSConfig: &tlsConfig}
} else {
dialer = dial.Dialer{NoTLS: !useXMPPS}
}
conn, err := dialer.Dial(context.TODO(), "tcp", address)
if err != nil {
return nil, err
}
// we need the domain in the tls config if we want to verify the cert
if !skipTLSVerify {
tlsConfig.ServerName = address.Domainpart()
}
return xmpp.NewSession(
context.TODO(),
address.Domain(),
address,
conn,
0,
xmpp.NewNegotiator(xmpp.StreamConfig{Features: func(_ *xmpp.Session, f ...xmpp.StreamFeature) []xmpp.StreamFeature {
if f != nil {
return f
}
return []xmpp.StreamFeature{
xmpp.BindResource(),
xmpp.StartTLS(&tlsConfig),
xmpp.SASL("", pass, sasl.ScramSha256Plus, sasl.ScramSha256, sasl.ScramSha1Plus, sasl.ScramSha1, sasl.Plain),
}
}}),
)
}
func closeXMPP(session *xmpp.Session) {
_ = session.Close()
_ = session.Conn().Close()
}
func main() {
// get xmpp credentials, message recipients
xi := os.Getenv("XMPP_ID")
xp := os.Getenv("XMPP_PASS")
xr := os.Getenv("XMPP_RECIPIENTS")
// get tls settings from env
_, skipTLSVerify := os.LookupEnv("XMPP_SKIP_VERIFY")
_, useXMPPS := os.LookupEnv("XMPP_OVER_TLS")
// get listen address
listenAddress := os.Getenv("XMPP_WEBHOOK_LISTEN_ADDRESS")
if len(listenAddress) == 0 {
listenAddress = ":4321"
}
// check if xmpp credentials and recipient list are supplied
if xi == "" || xp == "" || xr == "" {
log.Fatal("XMPP_ID, XMPP_PASS or XMPP_RECIPIENTS not set")
}
myjid, err := jid.Parse(xi)
panicOnErr(err)
// connect to xmpp server
xmppSession, err := initXMPP(myjid, xp, skipTLSVerify, useXMPPS)
panicOnErr(err)
defer closeXMPP(xmppSession)
// send initial presence
panicOnErr(xmppSession.Send(context.TODO(), stanza.Presence{Type: stanza.AvailablePresence}.Wrap(nil)))
// listen for messages and echo them
go func() {
err = xmppSession.Serve(xmpp.HandlerFunc(func(t xmlstream.TokenReadEncoder, start *xml.StartElement) error {
d := xml.NewTokenDecoder(t)
// ignore elements that aren't messages
if start.Name.Local != "message" {
return nil
}
// parse message into struct
msg := MessageBody{}
err = d.DecodeElement(&msg, start)
if err != nil && err != io.EOF {
return nil
}
// ignore empty messages and stanzas that aren't messages
if msg.Body == "" || msg.Type != stanza.ChatMessage {
return nil
}
// create reply with identical contents
reply := MessageBody{
Message: stanza.Message{
To: msg.From.Bare(),
From: myjid,
Type: stanza.ChatMessage,
},
Body: msg.Body,
}
// try to send reply, ignore errors
_ = t.Encode(reply)
return nil
}))
panicOnErr(err)
}()
// create chan for messages (webhooks -> xmpp)
messages := make(chan string)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// wait for messages from the webhooks and send them to all recipients
go func() {
for m := range messages {
for _, r := range strings.Split(xr, ",") {
recipient, err := jid.Parse(r)
panicOnErr(err)
// try to send message, ignore errors
_ = xmppSession.Encode(ctx, MessageBody{
Message: stanza.Message{
To: recipient,
From: myjid,
Type: stanza.ChatMessage,
},
Body: m,
})
}
}
}()
// initialize handlers with associated parser functions
http.Handle("/grafana", newMessageHandler(messages, parser.GrafanaParserFunc))
http.Handle("/slack", newMessageHandler(messages, parser.SlackParserFunc))
http.Handle("/alertmanager", newMessageHandler(messages, parser.AlertmanagerParserFunc))
// listen for requests
_ = http.ListenAndServe(listenAddress, nil)
}