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.

256 lines
8.8 KiB

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