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.

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