Initial Commit of Matrix Handler

This commit is contained in:
2021-02-04 01:25:16 -06:00
parent d416d0d6f5
commit 1242858d84
263 changed files with 69605 additions and 0 deletions

BIN
pkg/.DS_Store vendored Normal file

Binary file not shown.

45
pkg/config/config.go Normal file
View File

@ -0,0 +1,45 @@
package config
import (
"encoding/json"
"fmt"
"log"
"os"
)
// Config contains the basic configuration information.
type Config struct {
Port int `json:"port"`
LogLevel string `json:"logLevel"`
Name string `json:"name"`
Matrix struct {
Homeserver string `json:"homeserver"`
Port int `json:"port"`
} `json:"matrix"`
}
//GetConfig gets the configuration values for the api using the file in the supplied configPath.
func GetConfig(configPath string) (Config, error) {
if _, err := os.Stat(configPath); os.IsNotExist(err) {
return Config{}, fmt.Errorf("could not find the config file at path %s", configPath)
}
log.Println("Loading Configuration File: " + configPath)
return loadConfigFromFile(configPath)
}
//if the config loaded from the file errors, no defaults will be loaded and the app will exit.
func loadConfigFromFile(configPath string) (conf Config, err error) {
file, err := os.Open(configPath)
if err != nil {
log.Printf("Error opening config file: %v", err)
} else {
defer file.Close()
err = json.NewDecoder(file).Decode(&conf)
if err != nil {
log.Printf("Error decoding config file: %v", err)
}
}
return conf, err
}

32
pkg/generic/generic.go Normal file
View File

@ -0,0 +1,32 @@
package generic
import (
"io/ioutil"
"net/http"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/config"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/matrix"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/router"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
// Handle is the incoming handler for Generic-type requests.
func Handle(cfg config.Config) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
// Get Matrix Token for User/Pass in path
token := matrix.GetToken(cfg, vars)
reqBody, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("An error has occurred")
}
matrix.PublishText(cfg, vars, reqBody, token)
router.Respond(w, 200, nil)
}
}

96
pkg/matrix/matrix.go Normal file
View File

@ -0,0 +1,96 @@
package matrix
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/config"
"github.com/rs/zerolog/log"
)
// GetToken will get the access token from Matrix to perform communications.
func GetToken(cfg config.Config, vars map[string]string) string {
matrixConfig := struct {
Type string `json:"type"`
Username string `json:"user"`
Password string `json:"password"`
}{
Type: "m.login.password",
Username: vars["matrixUser"],
Password: vars["matrixPassword"],
}
reqBody, err := json.Marshal(matrixConfig)
if err != nil {
log.Fatal().Err(err).Msg(err.Error())
}
s := fmt.Sprintf("%v:%v/_matrix/client/r0/login", cfg.Matrix.Homeserver, cfg.Matrix.Port)
req, err := http.NewRequest(http.MethodPost, s, bytes.NewBuffer(reqBody))
if err != nil {
log.Error().Err(err).Msg("matrix.GetToken.req" + err.Error())
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("matrix.GetToken.resp" + err.Error())
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("matrix.GetToken.body" + err.Error())
}
respBody := struct {
AccessToken string `json:"access_token"`
}{}
err = json.Unmarshal(body, &respBody)
if err != nil {
log.Error().Err(err).Msg("matrix.GetToken.respBody" + err.Error())
}
return respBody.AccessToken
}
// PublishText will publish the data to Matrix using the specified vars.
func PublishText(cfg config.Config, vars map[string]string, data []byte, token string) {
matrixPublish := struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
}{
MsgType: "m.text",
Body: string(data),
}
reqBody, err := json.Marshal(matrixPublish)
if err != nil {
log.Fatal().Err(err).Msg(err.Error())
}
s := fmt.Sprintf("%v:%v/_matrix/client/r0/rooms/%v/send/m.room.message?access_token=%v", cfg.Matrix.Homeserver, cfg.Matrix.Port, vars["matrixRoom"], token)
req, err := http.NewRequest(http.MethodPost, s, bytes.NewBuffer(reqBody))
if err != nil {
log.Error().Err(err).Msg("matrix.PublishText.req" + err.Error())
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Error().Err(err).Msg("matrix.PublishText.resp" + err.Error())
}
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
defer resp.Body.Close()
}

BIN
pkg/router/.DS_Store vendored Normal file

Binary file not shown.

253
pkg/router/router.go Normal file
View File

@ -0,0 +1,253 @@
package router
import (
"encoding/json"
"net/http"
"sync"
"time"
"github.com/rs/zerolog/log"
_ "net/http/pprof" // debug
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
// swagger:model ErrorResponßse
type ErrorResponse struct {
Err string `json:"error"`
}
type BuildInfo struct {
Start time.Time `json:"-"`
Uptime string `json:"uptime,omitempty"`
Version string `json:"version,omitempty"`
BuildDate string `json:"build_date,omitempty"`
BuildHost string `json:"build_host,omitempty"`
GitURL string `json:"git_url,omitempty"`
Branch string `json:"branch,omitempty"`
Debug bool `json:"debug"`
}
type metrics struct {
InFlight prometheus.Gauge
Counter *prometheus.CounterVec
Duration *prometheus.HistogramVec
}
var m *metrics
func init() {
m = &metrics{
InFlight: prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "http_requests_in_flight",
Help: "In Flight HTTP requests.",
},
),
Counter: prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Counter of HTTP requests.",
},
[]string{"handler", "code", "method"},
),
Duration: prometheus.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Histogram of latencies for HTTP requests.",
Buckets: []float64{.01, .05, .1, .2, .4, 1, 3, 8, 20, 60, 120},
},
[]string{"handler", "code", "method"},
),
}
m.register()
}
func (m *metrics) handler(path string, handler http.Handler) (string, http.Handler) {
return path,
promhttp.InstrumentHandlerCounter(m.Counter.MustCurryWith(prometheus.Labels{"handler": path}),
promhttp.InstrumentHandlerInFlight(m.InFlight,
promhttp.InstrumentHandlerDuration(m.Duration.MustCurryWith(prometheus.Labels{"handler": path}),
handler,
),
),
)
}
func (m *metrics) handlerFunc(path string, f http.HandlerFunc) (string, http.HandlerFunc) {
return path,
promhttp.InstrumentHandlerCounter(m.Counter.MustCurryWith(prometheus.Labels{"handler": path}),
promhttp.InstrumentHandlerInFlight(m.InFlight,
promhttp.InstrumentHandlerDuration(m.Duration.MustCurryWith(prometheus.Labels{"handler": path}),
f,
),
),
)
}
func (m *metrics) register() {
prometheus.MustRegister(m.InFlight, m.Counter, m.Duration)
}
func recovery(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
defer r.Body.Close()
if r := recover(); r != nil {
var err error
switch t := r.(type) {
case string:
err = errors.New(t)
case error:
err = errors.WithStack(t)
default:
err = errors.New("unknown error")
}
log.Error().Stack().Caller().Err(err).Msg("an unexpected error occurred")
notify(err)
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}()
h.ServeHTTP(w, r)
})
}
func notify(err error) {
// stack := pkgerrors.MarshalStack(err)
// TODO: send error notification with stacktrace
}
type Router struct {
Info *BuildInfo
mux *mux.Router
metrics *metrics
}
// GetRoute ...
func (r *Router) GetRoute(name string) *mux.Route {
return r.mux.GetRoute(name)
}
// Handle implements http.Handler.
func (r *Router) Handle(path string, handler http.Handler) *mux.Route {
handler = recovery(handler)
return r.mux.Handle(path, handler)
}
// HandleFunc implements http.Handler.
func (r *Router) HandleFunc(path string, f http.HandlerFunc) *mux.Route {
return r.Handle(path, f)
}
// Handle implements http.Handler wrapping handler with m.
func (r *Router) HandleWithMetrics(path string, handler http.Handler) *mux.Route {
handler = recovery(handler)
return r.mux.Handle(r.metrics.handler(path, handler))
}
// HandleFunc implements http.Handler wrapping handler func with m.
func (r *Router) HandleFuncWithMetrics(path string, f http.HandlerFunc) *mux.Route {
return r.HandleWithMetrics(path, f)
}
// ServeHTTP implements http.Handler.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
r.mux.ServeHTTP(w, req)
}
func (r *Router) PathPrefix(prefix string) *mux.Route {
return r.mux.PathPrefix(prefix)
}
// New returns a new Router.
func NewRouter(buildinfo *BuildInfo) *Router {
router := &Router{
Info: buildinfo,
mux: mux.NewRouter(),
metrics: m,
}
router.Handle("/info", info(router.Info)).Methods(http.MethodGet, http.MethodHead).Name("INFO")
router.Handle("/health", health()).Methods(http.MethodGet, http.MethodHead).Name("HEALTH")
router.Handle("/metrics", promhttp.Handler()).Name("METRICS")
if buildinfo.Debug {
log.Warn().Msg("pprof enabled")
router.mux.PathPrefix("/debug/pprof").Handler(http.DefaultServeMux).Name("PPROF")
go func() {
log.Error().Err(http.ListenAndServe("localhost:6060", nil)).Send()
}()
}
return router
}
func health() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
Respond(w, http.StatusOK, []byte(http.StatusText(http.StatusOK)))
}
}
func info(buildinfo *BuildInfo) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
buildinfo.Uptime = time.Now().Sub(buildinfo.Start).String()
RespondWithJSON(w, http.StatusOK, buildinfo)
}
}
func RespondWithError(w http.ResponseWriter, code int, err error) {
RespondWithJSON(w, code, ErrorResponse{Err: err.Error()})
}
func RespondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
body, _ := json.Marshal(payload)
w.Header().Set("Content-Type", "application/json")
Respond(w, code, body)
}
func Respond(w http.ResponseWriter, code int, body []byte) {
w.WriteHeader(code)
w.Write(body)
}
type ReadyChecker interface {
Ready() bool
}
func Ready(dependencies ...ReadyChecker) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ready := []byte("OK")
numDeps := len(dependencies)
if numDeps == 0 {
Respond(w, http.StatusOK, ready)
return
}
wg := sync.WaitGroup{}
checks := make(chan bool, numDeps)
for _, dep := range dependencies {
wg.Add(1)
if dep != nil {
go func(d ReadyChecker) {
checks <- d.Ready()
wg.Done()
}(dep)
}
}
wg.Wait()
close(checks)
for ok := range checks {
if !ok {
Respond(w, http.StatusServiceUnavailable, nil)
return
}
}
Respond(w, http.StatusOK, ready)
return
}
}

43
pkg/server/server.go Normal file
View File

@ -0,0 +1,43 @@
package server
import (
"fmt"
"net/http"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/config"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/generic"
"git.linuxrocker.com/mattburchett/matrix-handler/pkg/router"
"github.com/rs/cors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
// Run ...
func Run(info *router.BuildInfo) error {
conf, err := config.GetConfig("config.json")
if err != nil {
log.Fatal().Err(err)
}
level, err := zerolog.ParseLevel(conf.LogLevel)
if err != nil {
level = zerolog.ErrorLevel
log.Warn().Err(err).Msgf("unable to parse log level, logging level is set to %s", level.String())
}
zerolog.SetGlobalLevel(level)
log.Logger = log.With().Str("app", conf.Name).Logger()
router := router.NewRouter(info)
router.HandleWithMetrics("/generic/{matrixRoom}/{matrixUser}/{matrixPassword}", generic.Handle(conf)).Methods(http.MethodPost)
srv := http.Server{
Addr: fmt.Sprintf(":%d", conf.Port),
Handler: cors.Default().Handler(router),
}
log.Info().Msgf("Server running on %v", srv.Addr)
return srv.ListenAndServe()
}