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.

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