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

  1. package main
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "io/ioutil"
  6. "net/http"
  7. _ "net/http/pprof"
  8. "os"
  9. "path/filepath"
  10. _ "github.com/lib/pq"
  11. "github.com/matrix-org/dugong"
  12. "github.com/matrix-org/go-neb/api"
  13. "github.com/matrix-org/go-neb/api/handlers"
  14. "github.com/matrix-org/go-neb/clients"
  15. "github.com/matrix-org/go-neb/database"
  16. _ "github.com/matrix-org/go-neb/metrics"
  17. "github.com/matrix-org/go-neb/polling"
  18. _ "github.com/matrix-org/go-neb/realms/github"
  19. _ "github.com/matrix-org/go-neb/realms/jira"
  20. _ "github.com/matrix-org/go-neb/services/alertmanager"
  21. _ "github.com/matrix-org/go-neb/services/echo"
  22. _ "github.com/matrix-org/go-neb/services/giphy"
  23. _ "github.com/matrix-org/go-neb/services/github"
  24. _ "github.com/matrix-org/go-neb/services/google"
  25. _ "github.com/matrix-org/go-neb/services/guggy"
  26. _ "github.com/matrix-org/go-neb/services/imgur"
  27. _ "github.com/matrix-org/go-neb/services/jira"
  28. _ "github.com/matrix-org/go-neb/services/rssbot"
  29. _ "github.com/matrix-org/go-neb/services/slackapi"
  30. _ "github.com/matrix-org/go-neb/services/travisci"
  31. _ "github.com/matrix-org/go-neb/services/wikipedia"
  32. "github.com/matrix-org/go-neb/types"
  33. "github.com/matrix-org/util"
  34. _ "github.com/mattn/go-sqlite3"
  35. "github.com/prometheus/client_golang/prometheus"
  36. log "github.com/sirupsen/logrus"
  37. yaml "gopkg.in/yaml.v2"
  38. )
  39. // loadFromConfig loads a config file and returns a ConfigFile
  40. func loadFromConfig(db *database.ServiceDB, configFilePath string) (*api.ConfigFile, error) {
  41. // ::Horrible hacks ahead::
  42. // The config is represented as YAML, and we want to convert that into NEB types.
  43. // However, NEB types make liberal use of json.RawMessage which the YAML parser
  44. // doesn't like. We can't implement MarshalYAML/UnmarshalYAML as a custom type easily
  45. // because YAML is insane and supports numbers as keys. The YAML parser therefore has the
  46. // generic form of map[interface{}]interface{} - but the JSON parser doesn't know
  47. // how to parse that.
  48. //
  49. // The hack that follows gets around this by type asserting all parsed YAML keys as
  50. // strings then re-encoding/decoding as JSON. That is:
  51. // YAML bytes -> map[interface]interface -> map[string]interface -> JSON bytes -> NEB types
  52. // Convert to YAML bytes
  53. contents, err := ioutil.ReadFile(configFilePath)
  54. if err != nil {
  55. return nil, err
  56. }
  57. // Convert to map[interface]interface
  58. var cfg map[interface{}]interface{}
  59. if err = yaml.Unmarshal(contents, &cfg); err != nil {
  60. return nil, fmt.Errorf("Failed to unmarshal YAML: %s", err)
  61. }
  62. // Convert to map[string]interface
  63. dict := convertKeysToStrings(cfg)
  64. // Convert to JSON bytes
  65. b, err := json.Marshal(dict)
  66. if err != nil {
  67. return nil, fmt.Errorf("Failed to marshal config as JSON: %s", err)
  68. }
  69. // Finally, Convert to NEB types
  70. var c api.ConfigFile
  71. if err := json.Unmarshal(b, &c); err != nil {
  72. return nil, fmt.Errorf("Failed to convert to config file: %s", err)
  73. }
  74. // sanity check (at least 1 client and 1 service)
  75. if len(c.Clients) == 0 || len(c.Services) == 0 {
  76. return nil, fmt.Errorf("At least 1 client and 1 service must be specified")
  77. }
  78. return &c, nil
  79. }
  80. func convertKeysToStrings(iface interface{}) interface{} {
  81. obj, isObj := iface.(map[interface{}]interface{})
  82. if isObj {
  83. strObj := make(map[string]interface{})
  84. for k, v := range obj {
  85. strObj[k.(string)] = convertKeysToStrings(v) // handle nested objects
  86. }
  87. return strObj
  88. }
  89. arr, isArr := iface.([]interface{})
  90. if isArr {
  91. for i := range arr {
  92. arr[i] = convertKeysToStrings(arr[i]) // handle nested objects
  93. }
  94. return arr
  95. }
  96. return iface // base type like string or number
  97. }
  98. func insertServicesFromConfig(clis *clients.Clients, serviceReqs []api.ConfigureServiceRequest) error {
  99. for i, s := range serviceReqs {
  100. if err := s.Check(); err != nil {
  101. return fmt.Errorf("config: Service[%d] : %s", i, err)
  102. }
  103. service, err := types.CreateService(s.ID, s.Type, s.UserID, s.Config)
  104. if err != nil {
  105. return fmt.Errorf("config: Service[%d] : %s", i, err)
  106. }
  107. // Fetch the client for this service and register/poll
  108. c, err := clis.Client(s.UserID)
  109. if err != nil {
  110. return fmt.Errorf("config: Service[%d] : %s", i, err)
  111. }
  112. if err = service.Register(nil, c); err != nil {
  113. return fmt.Errorf("config: Service[%d] : %s", i, err)
  114. }
  115. if _, err := database.GetServiceDB().StoreService(service); err != nil {
  116. return fmt.Errorf("config: Service[%d] : %s", i, err)
  117. }
  118. service.PostRegister(nil)
  119. }
  120. return nil
  121. }
  122. func loadDatabase(databaseType, databaseURL, configYAML string) (*database.ServiceDB, error) {
  123. if configYAML != "" {
  124. databaseType = "sqlite3"
  125. databaseURL = ":memory:?_busy_timeout=5000"
  126. }
  127. db, err := database.Open(databaseType, databaseURL)
  128. if err == nil {
  129. database.SetServiceDB(db) // set singleton
  130. }
  131. return db, err
  132. }
  133. func setup(e envVars, mux *http.ServeMux, matrixClient *http.Client) {
  134. err := types.BaseURL(e.BaseURL)
  135. if err != nil {
  136. log.WithError(err).Panic("Failed to get base url")
  137. }
  138. db, err := loadDatabase(e.DatabaseType, e.DatabaseURL, e.ConfigFile)
  139. if err != nil {
  140. log.WithError(err).Panic("Failed to open database")
  141. }
  142. // Populate the database from the config file if one was supplied.
  143. var cfg *api.ConfigFile
  144. if e.ConfigFile != "" {
  145. if cfg, err = loadFromConfig(db, e.ConfigFile); err != nil {
  146. log.WithError(err).WithField("config_file", e.ConfigFile).Panic("Failed to load config file")
  147. }
  148. if err := db.InsertFromConfig(cfg); err != nil {
  149. log.WithError(err).Panic("Failed to persist config data into in-memory DB")
  150. }
  151. log.Info("Inserted ", len(cfg.Clients), " clients")
  152. log.Info("Inserted ", len(cfg.Realms), " realms")
  153. log.Info("Inserted ", len(cfg.Sessions), " sessions")
  154. }
  155. matrixClients := clients.New(db, matrixClient)
  156. if err := matrixClients.Start(); err != nil {
  157. log.WithError(err).Panic("Failed to start up clients")
  158. }
  159. // Handle non-admin paths for normal NEB functioning
  160. mux.Handle("/metrics", prometheus.Handler())
  161. mux.Handle("/test", prometheus.InstrumentHandler("test", util.MakeJSONAPI(&handlers.Heartbeat{})))
  162. wh := handlers.NewWebhook(db, matrixClients)
  163. mux.HandleFunc("/services/hooks/", prometheus.InstrumentHandlerFunc("webhookHandler", util.Protect(wh.Handle)))
  164. rh := &handlers.RealmRedirect{db}
  165. mux.HandleFunc("/realms/redirects/", prometheus.InstrumentHandlerFunc("realmRedirectHandler", util.Protect(rh.Handle)))
  166. // Read exclusively from the config file if one was supplied.
  167. // Otherwise, add HTTP listeners for new Services/Sessions/Clients/etc.
  168. if e.ConfigFile != "" {
  169. if err := insertServicesFromConfig(matrixClients, cfg.Services); err != nil {
  170. log.WithError(err).Panic("Failed to insert services")
  171. }
  172. log.Info("Inserted ", len(cfg.Services), " services")
  173. } else {
  174. mux.Handle("/admin/getService", prometheus.InstrumentHandler("getService", util.MakeJSONAPI(&handlers.GetService{db})))
  175. mux.Handle("/admin/getSession", prometheus.InstrumentHandler("getSession", util.MakeJSONAPI(&handlers.GetSession{db})))
  176. mux.Handle("/admin/configureClient", prometheus.InstrumentHandler("configureClient", util.MakeJSONAPI(&handlers.ConfigureClient{matrixClients})))
  177. mux.Handle("/admin/configureService", prometheus.InstrumentHandler("configureService", util.MakeJSONAPI(handlers.NewConfigureService(db, matrixClients))))
  178. mux.Handle("/admin/configureAuthRealm", prometheus.InstrumentHandler("configureAuthRealm", util.MakeJSONAPI(&handlers.ConfigureAuthRealm{db})))
  179. mux.Handle("/admin/requestAuthSession", prometheus.InstrumentHandler("requestAuthSession", util.MakeJSONAPI(&handlers.RequestAuthSession{db})))
  180. mux.Handle("/admin/removeAuthSession", prometheus.InstrumentHandler("removeAuthSession", util.MakeJSONAPI(&handlers.RemoveAuthSession{db})))
  181. }
  182. polling.SetClients(matrixClients)
  183. if err := polling.Start(); err != nil {
  184. log.WithError(err).Panic("Failed to start polling")
  185. }
  186. }
  187. type envVars struct {
  188. BindAddress string
  189. DatabaseType string
  190. DatabaseURL string
  191. BaseURL string
  192. LogDir string
  193. ConfigFile string
  194. }
  195. func main() {
  196. e := envVars{
  197. BindAddress: os.Getenv("BIND_ADDRESS"),
  198. DatabaseType: os.Getenv("DATABASE_TYPE"),
  199. DatabaseURL: os.Getenv("DATABASE_URL"),
  200. BaseURL: os.Getenv("BASE_URL"),
  201. LogDir: os.Getenv("LOG_DIR"),
  202. ConfigFile: os.Getenv("CONFIG_FILE"),
  203. }
  204. if e.LogDir != "" {
  205. log.AddHook(dugong.NewFSHook(
  206. filepath.Join(e.LogDir, "go-neb.log"),
  207. &log.TextFormatter{
  208. TimestampFormat: "2006-01-02 15:04:05.000000",
  209. DisableColors: true,
  210. DisableTimestamp: false,
  211. DisableSorting: false,
  212. }, &dugong.DailyRotationSchedule{GZip: false},
  213. ))
  214. log.SetOutput(ioutil.Discard)
  215. }
  216. log.Infof("Go-NEB (%+v)", e)
  217. setup(e, http.DefaultServeMux, http.DefaultClient)
  218. log.Fatal(http.ListenAndServe(e.BindAddress, nil))
  219. }