Skip to content
Snippets Groups Projects
Commit bf1fb628 authored by Lars Seipel's avatar Lars Seipel
Browse files

initial import

parents
No related branches found
No related tags found
No related merge requests found
FROM golang:1.15 as builder
WORKDIR /build
COPY go.mod go.mod
COPY go.sum go.sum
RUN go mod download
COPY main.go main.go
COPY webhooks/ webhooks/
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on go build -a -o bbbatscale-support-notify main.go
FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /build/bbbatscale-support-notify .
USER nonroot:nonroot
ENTRYPOINT ["/bbbatscale-support-notify"]
DOCKER ?= docker
IMG ?= registry.code.fbi.h-da.de/its/bbbatscale-support-notify:latest
.PHONY: docker-build
docker-build:
$(DOCKER) build -t $(IMG) .
go.mod 0 → 100644
module code.fbi.h-da.de/its/bbbatscale-support-notify
go 1.15
require (
go.uber.org/multierr v1.6.0 // indirect
go.uber.org/zap v1.16.0
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 // indirect
maunium.net/go/mautrix v0.7.13
)
go.sum 0 → 100644
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/gorilla/mux v1.7.4/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lib/pq v1.7.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.16.0 h1:uFRZXykJGK9lLY4HtgSw44DnIcAM+kRBP7x5m+NpAOM=
go.uber.org/zap v1.16.0/go.mod h1:MA8QOfq0BHJwdXa996Y4dYkAqRKB8/1K1QMMZVaNZjQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.7.13 h1:qfnvLxvQafvLgHbdZF/+9qs9gyArYf8fUnzfQbjgQaU=
maunium.net/go/mautrix v0.7.13/go.mod h1:Jn0ijwXwMFvJFIN9IljirIVKpZQbZP/Dk7pdX2qDmXk=
main.go 0 → 100644
package main
import (
"encoding/hex"
"flag"
"fmt"
"net/http"
"os"
"time"
"code.fbi.h-da.de/its/bbbatscale-support-notify/webhooks"
"go.uber.org/zap"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/id"
)
var (
matrixHomeserver = flag.String("homeserver", os.Getenv("MATRIX_HOMESERVER"),
"Matrix homeserver")
matrixUserID = flag.String("userid", os.Getenv("MATRIX_USERID"),
"Matrix user ID")
matrixToken = flag.String("token", os.Getenv("MATRIX_TOKEN"),
"Matrix access token")
matrixRoomID = flag.String("roomid", os.Getenv("MATRIX_ROOMID"),
"Matrix room ID")
laddr = flag.String("listen",
getEnvOrDefault("NOTIFY_LISTEN_ADDR", ":8443"), "listen for requests on this `addr`")
noTLS = flag.Bool("notls", os.Getenv("NOTIFY_NOTLS") != "",
"serve plain-text HTTP instead of HTTPS")
certFile = flag.String("cert",
os.Getenv("NOTIFY_CERT"), "X.509 certificate to present to clients (PEM-encoded)")
keyFile = flag.String("key",
os.Getenv("NOTIFY_KEY"), "private key matching configured cert")
hookSecret = flag.String("secret",
os.Getenv("NOTIFY_HOOKSECRET"), "hex-encoded secret for authenticating webhooks")
)
func main() {
logger, err := zap.NewProduction()
if err != nil {
// Just tear it down already.
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
zap.RedirectStdLog(logger)
log := logger.Sugar()
flag.Parse()
m, err := mautrix.NewClient(*matrixHomeserver, id.UserID(*matrixUserID),
*matrixToken)
if err != nil {
log.Fatalw("mautrix.NewClient returned with error",
"err", err,
"homeserver", *matrixHomeserver,
"userid", *matrixUserID,
)
}
n := &notifier{
matrix: m,
roomID: id.RoomID(*matrixRoomID),
}
secretKey, err := hex.DecodeString(*hookSecret)
if err != nil {
log.Fatal("decode hook secret: ", err)
}
ch, handler, err := webhooks.Receive(
webhooks.Authenticate(secretKey),
webhooks.WithLogger(log.Desugar()),
)
if err != nil {
log.Fatal("webhooks.Receive: ", err)
}
go func() {
for message := range ch {
switch message.Event {
case "SUPPORT_CHAT_INCOMING_MESSAGE":
chatMsg := webhookPayloadToChatMessage(message.Payload)
if err := n.notify(chatMsg); err != nil {
log.Error("notify: ", err)
}
default:
log.Info("received message for event: ", message.Event)
}
}
}()
http.Handle("/notify", handler)
if *noTLS {
err = http.ListenAndServe(*laddr, nil)
} else {
err = http.ListenAndServeTLS(*laddr, *certFile, *keyFile, nil)
}
if err != nil {
log.Fatalw("ListenAndServe failed",
"err", err,
"addr", *laddr,
"tls", !*noTLS,
"cert", *certFile,
"key", *keyFile,
)
}
}
type notifier struct {
matrix *mautrix.Client
roomID id.RoomID
}
func (n *notifier) notify(m chatMessage) error {
const notificationFmt = "Incoming support message from %s (%s):\n> %s"
msg := fmt.Sprintf(notificationFmt, m.user, m.displayName, m.message)
_, err := n.matrix.SendNotice(n.roomID, msg)
return err
}
type chatMessage struct {
user string
displayName string
message string
timestamp time.Time
}
func webhookPayloadToChatMessage(payload map[string]interface{}) chatMessage {
var m chatMessage
m.user, _ = payload["username"].(string)
m.displayName, _ = payload["userDisplayName"].(string)
m.message, _ = payload["message"].(string)
if timestamp, ok := payload["timestamp"].(string); ok {
if t, err := time.Parse(time.RFC3339, timestamp); err == nil {
m.timestamp = t
}
}
return m
}
func getEnvOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
package webhooks
import (
"bytes"
"crypto/hmac"
"crypto/sha512"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"strconv"
"strings"
"go.uber.org/zap"
)
// 2Mi should be enough for everybody
const maxBodySize = 2 << 20
// Message describes a received web hook. The dynamic type of Payload depends
type Message struct {
Event string `json:"event"`
Timestamp int64 `json:"ts"`
Payload map[string]interface{} `json:"payload"`
}
// Receive returns an http.Handler that receives webhooks from BBBAtScale and
// sends their contents to the returned channel.
func Receive(opts ...ReceiverOption) (<-chan Message, http.Handler, error) {
r := new(receiver)
for _, opt := range opts {
opt(r)
}
if r.ch == nil {
r.ch = make(chan Message)
}
if r.log == nil {
r.log = zap.NewNop()
}
return r.ch, r, nil
}
// A ReceiverOption can be passed to Receive to customize behaviour of a
// webhook Receiver.
type ReceiverOption func(*receiver)
// Authenticate enables authentication of incoming webhooks using HMAC-SHA512
// and the provided key.
func Authenticate(key []byte) ReceiverOption {
return func(r *receiver) {
r.macKey = key
}
}
// WithLogger instructs the receiver to log messages to l.
func WithLogger(l *zap.Logger) ReceiverOption {
return func(r *receiver) {
r.log = l
}
}
// WithChanBufferSize configures the channel returned from Receive with a
// buffer size equal to n.
func WithChanBufferSize(n int) ReceiverOption {
return func(r *receiver) {
r.ch = make(chan Message, n)
}
}
// The receiver is an http.Handler receiving web hooks from BBBatScale.
type receiver struct {
ch chan Message
log *zap.Logger
// Used for authenticating incoming requests. A SHA512-HMAC computed
// over the request body must match the tag sent in X-Hook-Signature
// header.
macKey []byte
}
func (wr *receiver) ServeHTTP(w http.ResponseWriter, r *http.Request) {
log := wr.log.Sugar().With(
"method", r.Method,
"url", r.URL.String(),
"remote", r.RemoteAddr,
"ua", r.UserAgent(),
"xff", r.Header.Get("X-Forwarded-For"),
)
switch r.Method {
case "POST":
default:
w.Header().Set("Allow", "POST")
if r.Method == "OPTIONS" {
return
}
code := http.StatusMethodNotAllowed
http.Error(w, http.StatusText(code), code)
log.Info("method not allowed")
return
}
p, err := ioutil.ReadAll(http.MaxBytesReader(w, r.Body, maxBodySize))
if err != nil {
code := http.StatusBadRequest
http.Error(w, http.StatusText(code), code)
log.Warn("read", err)
return
}
sig := r.Header.Get("X-Hook-Signature")
ok := wr.verifyTag(sig, p)
if !ok {
code := http.StatusForbidden
http.Error(w, http.StatusText(code), code)
log.Warnw("invalid mac tag", "tag", sig)
return
}
var v Message
if err := json.Unmarshal(p, &v); err != nil {
code := http.StatusBadRequest
http.Error(w, http.StatusText(code), code)
log.Warn("unmarshal: ", err)
return
}
// Basic sanity check to avoid passing empty messages up the channel.
if v.Event == "" {
code := http.StatusBadRequest
http.Error(w, http.StatusText(code), code)
log.Warn("event missing from message")
return
}
log.Infow("incoming hook", "event", v.Event)
wr.ch <- v
}
func (wr *receiver) verifyTag(header string, body []byte) bool {
if len(wr.macKey) == 0 {
// request authentication disabled
return true
}
v1, t, err := disassembleHookSignature(header)
if err != nil {
return false
}
tag, err := hex.DecodeString(v1)
if err != nil {
return false
}
var b bytes.Buffer
fmt.Fprintf(&b, "%d.%s", t, body)
mac := hmac.New(sha512.New, wr.macKey)
mac.Write(b.Bytes())
expect := mac.Sum(nil)
return hmac.Equal(tag, expect)
}
func disassembleHookSignature(s string) (v1 string, t int64, err error) {
elems := strings.Split(s, ",")
if len(elems) < 2 {
return "", 0, fmt.Errorf(
"invalid argument: need at least v1 and t parts: %q", s)
}
for _, e := range elems {
kv := strings.SplitN(e, "=", 2)
if len(kv) != 2 {
return "", 0, fmt.Errorf(
"invalid argument: expect k=v, got %q", e)
}
switch kv[0] {
case "v1":
if kv[1] == "" {
return "", 0, errors.New(
"invalid argument: missing value for v1")
}
v1 = kv[1]
case "t":
d, err := strconv.ParseInt(kv[1], 10, 64)
if err != nil {
return "", 0, fmt.Errorf(
"invalid argument: t: %v", err)
}
if d <= 0 {
return "", 0, fmt.Errorf(
"invalid argument: t: %d", d)
}
t = d
default:
// skip keys we don't know how to handle
}
}
if v1 == "" {
return "", 0, errors.New("invalid argument: missing v1")
}
if t == 0 {
return "", 0, errors.New("invalid argument: missing t")
}
return v1, t, nil
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment