You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

251 lines
8.6 KiB

package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
_ "net/http/pprof"
"os"
"path/filepath"
_ "github.com/lib/pq"
"github.com/matrix-org/dugong"
"github.com/matrix-org/go-neb/api"
"github.com/matrix-org/go-neb/api/handlers"
"github.com/matrix-org/go-neb/clients"
"github.com/matrix-org/go-neb/database"
_ "github.com/matrix-org/go-neb/metrics"
"github.com/matrix-org/go-neb/polling"
_ "github.com/matrix-org/go-neb/realms/github"
_ "github.com/matrix-org/go-neb/realms/jira"
_ "github.com/matrix-org/go-neb/services/alertmanager"
_ "github.com/matrix-org/go-neb/services/echo"
_ "github.com/matrix-org/go-neb/services/giphy"
_ "github.com/matrix-org/go-neb/services/github"
_ "github.com/matrix-org/go-neb/services/google"
_ "github.com/matrix-org/go-neb/services/guggy"
_ "github.com/matrix-org/go-neb/services/imgur"
_ "github.com/matrix-org/go-neb/services/jira"
_ "github.com/matrix-org/go-neb/services/rssbot"
_ "github.com/matrix-org/go-neb/services/slackapi"
_ "github.com/matrix-org/go-neb/services/travisci"
_ "github.com/matrix-org/go-neb/services/wikipedia"
"github.com/matrix-org/go-neb/types"
"github.com/matrix-org/util"
_ "github.com/mattn/go-sqlite3"
"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
// loadFromConfig loads a config file and returns a ConfigFile
func loadFromConfig(db *database.ServiceDB, configFilePath string) (*api.ConfigFile, error) {
// ::Horrible hacks ahead::
// The config is represented as YAML, and we want to convert that into NEB types.
// However, NEB types make liberal use of json.RawMessage which the YAML parser
// doesn't like. We can't implement MarshalYAML/UnmarshalYAML as a custom type easily
// because YAML is insane and supports numbers as keys. The YAML parser therefore has the
// generic form of map[interface{}]interface{} - but the JSON parser doesn't know
// how to parse that.
//
// The hack that follows gets around this by type asserting all parsed YAML keys as
// strings then re-encoding/decoding as JSON. That is:
// YAML bytes -> map[interface]interface -> map[string]interface -> JSON bytes -> NEB types
// Convert to YAML bytes
contents, err := ioutil.ReadFile(configFilePath)
if err != nil {
return nil, err
}
// Convert to map[interface]interface
var cfg map[interface{}]interface{}
if err = yaml.Unmarshal(contents, &cfg); err != nil {
return nil, fmt.Errorf("Failed to unmarshal YAML: %s", err)
}
// Convert to map[string]interface
dict := convertKeysToStrings(cfg)
// Convert to JSON bytes
b, err := json.Marshal(dict)
if err != nil {
return nil, fmt.Errorf("Failed to marshal config as JSON: %s", err)
}
// Finally, Convert to NEB types
var c api.ConfigFile
if err := json.Unmarshal(b, &c); err != nil {
return nil, fmt.Errorf("Failed to convert to config file: %s", err)
}
// sanity check (at least 1 client and 1 service)
if len(c.Clients) == 0 || len(c.Services) == 0 {
return nil, fmt.Errorf("At least 1 client and 1 service must be specified")
}
return &c, nil
}
func convertKeysToStrings(iface interface{}) interface{} {
obj, isObj := iface.(map[interface{}]interface{})
if isObj {
strObj := make(map[string]interface{})
for k, v := range obj {
strObj[k.(string)] = convertKeysToStrings(v) // handle nested objects
}
return strObj
}
arr, isArr := iface.([]interface{})
if isArr {
for i := range arr {
arr[i] = convertKeysToStrings(arr[i]) // handle nested objects
}
return arr
}
return iface // base type like string or number
}
func insertServicesFromConfig(clis *clients.Clients, serviceReqs []api.ConfigureServiceRequest) error {
for i, s := range serviceReqs {
if err := s.Check(); err != nil {
return fmt.Errorf("config: Service[%d] : %s", i, err)
}
service, err := types.CreateService(s.ID, s.Type, s.UserID, s.Config)
if err != nil {
return fmt.Errorf("config: Service[%d] : %s", i, err)
}
// Fetch the client for this service and register/poll
c, err := clis.Client(s.UserID)
if err != nil {
return fmt.Errorf("config: Service[%d] : %s", i, err)
}
if err = service.Register(nil, c); err != nil {
return fmt.Errorf("config: Service[%d] : %s", i, err)
}
if _, err := database.GetServiceDB().StoreService(service); err != nil {
return fmt.Errorf("config: Service[%d] : %s", i, err)
}
service.PostRegister(nil)
}
return nil
}
func loadDatabase(databaseType, databaseURL, configYAML string) (*database.ServiceDB, error) {
if databaseType == "" && databaseURL == "" {
databaseType = "sqlite3"
databaseURL = ":memory:?_busy_timeout=5000"
}
db, err := database.Open(databaseType, databaseURL)
if err == nil {
database.SetServiceDB(db) // set singleton
}
return db, err
}
func setup(e envVars, mux *http.ServeMux, matrixClient *http.Client) {
err := types.BaseURL(e.BaseURL)
if err != nil {
log.WithError(err).Panic("Failed to get base url")
}
db, err := loadDatabase(e.DatabaseType, e.DatabaseURL, e.ConfigFile)
if err != nil {
log.WithError(err).Panic("Failed to open database")
}
// Populate the database from the config file if one was supplied.
var cfg *api.ConfigFile
if e.ConfigFile != "" {
if cfg, err = loadFromConfig(db, e.ConfigFile); err != nil {
log.WithError(err).WithField("config_file", e.ConfigFile).Panic("Failed to load config file")
}
if err := db.InsertFromConfig(cfg); err != nil {
log.WithError(err).Panic("Failed to persist config data into in-memory DB")
}
log.Info("Inserted ", len(cfg.Clients), " clients")
log.Info("Inserted ", len(cfg.Realms), " realms")
log.Info("Inserted ", len(cfg.Sessions), " sessions")
}
matrixClients := clients.New(db, matrixClient)
if err := matrixClients.Start(); err != nil {
log.WithError(err).Panic("Failed to start up clients")
}
// Handle non-admin paths for normal NEB functioning
mux.Handle("/metrics", prometheus.Handler())
mux.Handle("/test", prometheus.InstrumentHandler("test", util.MakeJSONAPI(&handlers.Heartbeat{})))
wh := handlers.NewWebhook(db, matrixClients)
mux.HandleFunc("/services/hooks/", prometheus.InstrumentHandlerFunc("webhookHandler", util.Protect(wh.Handle)))
rh := &handlers.RealmRedirect{db}
mux.HandleFunc("/realms/redirects/", prometheus.InstrumentHandlerFunc("realmRedirectHandler", util.Protect(rh.Handle)))
// Read exclusively from the config file if one was supplied.
// Otherwise, add HTTP listeners for new Services/Sessions/Clients/etc.
if e.ConfigFile != "" {
if err := insertServicesFromConfig(matrixClients, cfg.Services); err != nil {
log.WithError(err).Panic("Failed to insert services")
}
log.Info("Inserted ", len(cfg.Services), " services")
} else {
mux.Handle("/admin/getService", prometheus.InstrumentHandler("getService", util.MakeJSONAPI(&handlers.GetService{db})))
mux.Handle("/admin/getSession", prometheus.InstrumentHandler("getSession", util.MakeJSONAPI(&handlers.GetSession{db})))
mux.Handle("/admin/configureClient", prometheus.InstrumentHandler("configureClient", util.MakeJSONAPI(&handlers.ConfigureClient{matrixClients})))
mux.Handle("/admin/configureService", prometheus.InstrumentHandler("configureService", util.MakeJSONAPI(handlers.NewConfigureService(db, matrixClients))))
mux.Handle("/admin/configureAuthRealm", prometheus.InstrumentHandler("configureAuthRealm", util.MakeJSONAPI(&handlers.ConfigureAuthRealm{db})))
mux.Handle("/admin/requestAuthSession", prometheus.InstrumentHandler("requestAuthSession", util.MakeJSONAPI(&handlers.RequestAuthSession{db})))
mux.Handle("/admin/removeAuthSession", prometheus.InstrumentHandler("removeAuthSession", util.MakeJSONAPI(&handlers.RemoveAuthSession{db})))
}
polling.SetClients(matrixClients)
if err := polling.Start(); err != nil {
log.WithError(err).Panic("Failed to start polling")
}
}
type envVars struct {
BindAddress string
DatabaseType string
DatabaseURL string
BaseURL string
LogDir string
ConfigFile string
}
func main() {
e := envVars{
BindAddress: os.Getenv("BIND_ADDRESS"),
DatabaseType: os.Getenv("DATABASE_TYPE"),
DatabaseURL: os.Getenv("DATABASE_URL"),
BaseURL: os.Getenv("BASE_URL"),
LogDir: os.Getenv("LOG_DIR"),
ConfigFile: os.Getenv("CONFIG_FILE"),
}
if e.LogDir != "" {
log.AddHook(dugong.NewFSHook(
filepath.Join(e.LogDir, "go-neb.log"),
&log.TextFormatter{
TimestampFormat: "2006-01-02 15:04:05.000000",
DisableColors: true,
DisableTimestamp: false,
DisableSorting: false,
}, &dugong.DailyRotationSchedule{GZip: false},
))
log.SetOutput(ioutil.Discard)
}
log.Infof("Go-NEB (%+v)", e)
setup(e, http.DefaultServeMux, http.DefaultClient)
log.Fatal(http.ListenAndServe(e.BindAddress, nil))
}