Initial Commit of Matrix Handler
This commit is contained in:
BIN
pkg/.DS_Store
vendored
Normal file
BIN
pkg/.DS_Store
vendored
Normal file
Binary file not shown.
45
pkg/config/config.go
Normal file
45
pkg/config/config.go
Normal 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
32
pkg/generic/generic.go
Normal 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
96
pkg/matrix/matrix.go
Normal 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
BIN
pkg/router/.DS_Store
vendored
Normal file
Binary file not shown.
253
pkg/router/router.go
Normal file
253
pkg/router/router.go
Normal 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
43
pkg/server/server.go
Normal 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()
|
||||
}
|
Reference in New Issue
Block a user