server.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502
  1. /*
  2. * EliasDB
  3. *
  4. * Copyright 2016 Matthias Ladkau. All rights reserved.
  5. *
  6. * This Source Code Form is subject to the terms of the Mozilla Public
  7. * License, v. 2.0. If a copy of the MPL was not distributed with this
  8. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
  9. */
  10. /*
  11. Package server contains the code for the EliasDB server.
  12. */
  13. package server
  14. import (
  15. "bytes"
  16. "fmt"
  17. "io/ioutil"
  18. "log"
  19. "net/http"
  20. "os"
  21. "path/filepath"
  22. "sync"
  23. "time"
  24. "devt.de/krotik/common/cryptutil"
  25. "devt.de/krotik/common/datautil"
  26. "devt.de/krotik/common/errorutil"
  27. "devt.de/krotik/common/fileutil"
  28. "devt.de/krotik/common/httputil"
  29. "devt.de/krotik/common/httputil/access"
  30. "devt.de/krotik/common/httputil/auth"
  31. "devt.de/krotik/common/httputil/user"
  32. "devt.de/krotik/common/lockutil"
  33. "devt.de/krotik/common/timeutil"
  34. "devt.de/krotik/eliasdb/api"
  35. "devt.de/krotik/eliasdb/api/ac"
  36. "devt.de/krotik/eliasdb/api/v1"
  37. "devt.de/krotik/eliasdb/cluster"
  38. "devt.de/krotik/eliasdb/cluster/manager"
  39. "devt.de/krotik/eliasdb/config"
  40. "devt.de/krotik/eliasdb/graph"
  41. "devt.de/krotik/eliasdb/graph/graphstorage"
  42. )
  43. /*
  44. Using custom consolelogger type so we can test log.Fatal calls with unit tests. Overwrite
  45. these if the server should not call os.Exit on a fatal error.
  46. */
  47. type consolelogger func(v ...interface{})
  48. var fatal = consolelogger(log.Fatal)
  49. var print = consolelogger(log.Print)
  50. /*
  51. Base path for all file (used by unit tests)
  52. */
  53. var basepath = ""
  54. /*
  55. UserDBPassphrase is the passphrase which will be used for the user db (only used if
  56. access control is enabled)
  57. */
  58. var UserDBPassphrase = ""
  59. /*
  60. StartServer runs the EliasDB server. The server uses config.Config for all its configuration
  61. parameters.
  62. */
  63. func StartServer() {
  64. StartServerWithSingleOp(nil)
  65. }
  66. /*
  67. StartServerWithSingleOp runs the EliasDB server. If the singleOperation function is
  68. not nil then the server executes the function and exists if the function returns true.
  69. */
  70. func StartServerWithSingleOp(singleOperation func(*graph.Manager) bool) {
  71. var err error
  72. var gs graphstorage.Storage
  73. print(fmt.Sprintf("EliasDB %v", config.ProductVersion))
  74. // Ensure we have a configuration - use the default configuration if nothing was set
  75. if config.Config == nil {
  76. config.LoadDefaultConfig()
  77. }
  78. // Create graph storage
  79. if config.Bool(config.MemoryOnlyStorage) {
  80. print("Starting memory only datastore")
  81. gs = graphstorage.NewMemoryGraphStorage(config.MemoryOnlyStorage)
  82. if config.Bool(config.EnableReadOnly) {
  83. print("Ignoring EnableReadOnly setting")
  84. }
  85. } else {
  86. loc := filepath.Join(basepath, config.Str(config.LocationDatastore))
  87. readonly := config.Bool(config.EnableReadOnly)
  88. if readonly {
  89. print("Starting datastore (readonly) in ", loc)
  90. } else {
  91. print("Starting datastore in ", loc)
  92. }
  93. // Ensure path for database exists
  94. ensurePath(loc)
  95. gs, err = graphstorage.NewDiskGraphStorage(loc, readonly)
  96. if err != nil {
  97. fatal(err)
  98. return
  99. }
  100. }
  101. // Check if clustering is enabled
  102. if config.Bool(config.EnableCluster) {
  103. print("Reading cluster config")
  104. cconfig, err := fileutil.LoadConfig(filepath.Join(basepath, config.Str(config.ClusterConfigFile)),
  105. manager.DefaultConfig)
  106. if err != nil {
  107. fatal("Failed to load cluster config:", err)
  108. return
  109. }
  110. print("Opening cluster state info")
  111. si, err := manager.NewDefaultStateInfo(filepath.Join(basepath, config.Str(config.ClusterStateInfoFile)))
  112. if err != nil {
  113. fatal("Failed to load cluster state info:", err)
  114. return
  115. }
  116. loghist := int(config.Int(config.ClusterLogHistory))
  117. print(fmt.Sprintf("Starting cluster (log history: %v)", loghist))
  118. ds, err := cluster.NewDistributedStorage(gs, cconfig, si)
  119. if err != nil {
  120. fatal("Failed to create distributed storage:", err)
  121. return
  122. }
  123. gs = ds
  124. // Make the distributed storage and the cluster log available for the REST API
  125. api.DD = ds
  126. api.DDLog = datautil.NewRingBuffer(loghist)
  127. logFunc := func(v ...interface{}) {
  128. api.DDLog.Log(timeutil.MakeTimestamp(), " ", fmt.Sprint(v...))
  129. }
  130. logPrintFunc := func(v ...interface{}) {
  131. print("[Cluster] ", fmt.Sprint(v...))
  132. api.DDLog.Log(timeutil.MakeTimestamp(), " ", fmt.Sprint(v...))
  133. }
  134. manager.LogDebug = logFunc
  135. manager.LogInfo = logPrintFunc
  136. // Kick off the cluster
  137. ds.MemberManager.Start()
  138. }
  139. // Create GraphManager
  140. print("Creating GraphManager instance")
  141. api.GS = gs
  142. api.GM = graph.NewGraphManager(gs)
  143. defer func() {
  144. print("Closing datastore")
  145. if err := gs.Close(); err != nil {
  146. fatal(err)
  147. return
  148. }
  149. os.RemoveAll(filepath.Join(basepath, config.Str(config.LockFile)))
  150. }()
  151. // Handle single operation - these are operations which work on the GraphManager
  152. // and then exit.
  153. if singleOperation != nil && singleOperation(api.GM) {
  154. return
  155. }
  156. // Setting other API parameters
  157. // Setup cookie expiry
  158. cookieMaxAge := int(config.Int(config.CookieMaxAgeSeconds))
  159. auth.CookieMaxLifetime = cookieMaxAge
  160. user.CookieMaxLifetime = cookieMaxAge
  161. user.UserSessionManager.Provider.(*user.MemorySessionProvider).SetExpiry(cookieMaxAge)
  162. api.APIHost = config.Str(config.HTTPSHost) + ":" + config.Str(config.HTTPSPort)
  163. v1.ResultCacheMaxSize = uint64(config.Int(config.ResultCacheMaxSize))
  164. v1.ResultCacheMaxAge = config.Int(config.ResultCacheMaxAgeSeconds)
  165. // Check if HTTPS key and certificate are in place
  166. keyPath := filepath.Join(basepath, config.Str(config.LocationHTTPS), config.Str(config.HTTPSKey))
  167. certPath := filepath.Join(basepath, config.Str(config.LocationHTTPS), config.Str(config.HTTPSCertificate))
  168. keyExists, _ := fileutil.PathExists(keyPath)
  169. certExists, _ := fileutil.PathExists(certPath)
  170. if !keyExists || !certExists {
  171. // Ensure path for ssl files exists
  172. ensurePath(filepath.Join(basepath, config.Str(config.LocationHTTPS)))
  173. print("Creating key (", config.Str(config.HTTPSKey), ") and certificate (",
  174. config.Str(config.HTTPSCertificate), ") in: ", config.Str(config.LocationHTTPS))
  175. // Generate a certificate and private key
  176. err = cryptutil.GenCert(filepath.Join(basepath, config.Str(config.LocationHTTPS)),
  177. config.Str(config.HTTPSCertificate), config.Str(config.HTTPSKey),
  178. "localhost", "", 365*24*time.Hour, false, 4096, "")
  179. if err != nil {
  180. fatal("Failed to generate ssl key and certificate:", err)
  181. return
  182. }
  183. }
  184. // Register public REST endpoints - these will never be checked for authentication
  185. api.RegisterRestEndpoints(api.GeneralEndpointMap)
  186. // Setup access control
  187. if config.Bool(config.EnableAccessControl) {
  188. // Register REST endpoints for access control
  189. api.RegisterRestEndpoints(ac.PublicAccessControlEndpointMap)
  190. // Setup user database
  191. ac.UserDB, err = datautil.NewEnforcedUserDB(filepath.Join(basepath, config.Str(config.LocationUserDB)),
  192. UserDBPassphrase)
  193. if err == nil {
  194. var ok bool
  195. // Setup access control - this will initialise the global ACL (access
  196. // control lists) object
  197. if ok, err = fileutil.PathExists(filepath.Join(basepath, config.Str(config.LocationAccessDB))); !ok && err == nil {
  198. err = ioutil.WriteFile(filepath.Join(basepath, config.Str(config.LocationAccessDB)), ac.DefaultAccessDB, 0600)
  199. }
  200. if err == nil {
  201. tab, err := access.NewPersistedACLTable(filepath.Join(basepath, config.Str(config.LocationAccessDB)), 3*time.Second)
  202. if err == nil {
  203. ac.InitACLs(tab)
  204. }
  205. }
  206. }
  207. if err == nil {
  208. // Make sure there are the initial accounts (circumventing the
  209. // enforced password constrains by using the embedded UserDB directly)
  210. if len(ac.UserDB.AllUsers()) == 0 {
  211. ac.UserDB.UserDB.AddUserEntry("elias", "elias", nil)
  212. ac.UserDB.UserDB.AddUserEntry("johndoe", "doe", nil)
  213. }
  214. // Setup the AuthHandler object which provides cookie based authentication
  215. // for endpoints which are registered with its HandleFunc
  216. ac.AuthHandler = auth.NewCookieAuthHandleFuncWrapper(http.HandleFunc)
  217. // Connect the UserDB object to the AuthHandler - this provides authentication for users
  218. ac.AuthHandler.SetAuthFunc(ac.UserDB.CheckUserPassword)
  219. // Connect the ACL object to the AuthHandler - this provides authorization for users
  220. ac.AuthHandler.SetAccessFunc(ac.ACL.CheckHTTPRequest)
  221. // Make login page a "public" page i.e. a page which can be reached without
  222. // authentication
  223. ac.AuthHandler.AddPublicPage("/login.html",
  224. httputil.SingleFileServer(filepath.Join(
  225. config.Str(config.LocationWebFolder), "login.html"),
  226. nil).ServeHTTP)
  227. // Also make the fingerprint.json a public page
  228. ac.AuthHandler.AddPublicPage("/fingerprint.json",
  229. httputil.SingleFileServer(filepath.Join(
  230. config.Str(config.LocationWebFolder), "fingerprint.json"),
  231. nil).ServeHTTP)
  232. // Adding special handlers which redirect to the login page
  233. ac.AuthHandler.CallbackSessionExpired = ac.CallbackSessionExpired
  234. ac.AuthHandler.CallbackUnauthorized = ac.CallbackUnauthorized
  235. // Finally set the HandleFunc of the AuthHandler as the HandleFunc of the API
  236. api.HandleFunc = ac.AuthHandler.HandleFunc
  237. // After the api.HandleFunc has been set we can now register the management
  238. // endpoints which should be subject to access control
  239. api.RegisterRestEndpoints(ac.AccessManagementEndpointMap)
  240. }
  241. }
  242. // Register EliasDB API endpoints - depending on if access control has been enabled
  243. // these will require authentication and authorization for a given user
  244. api.RegisterRestEndpoints(v1.V1EndpointMap)
  245. // Register normal web server
  246. if config.Bool(config.EnableWebFolder) {
  247. webFolder := filepath.Join(basepath, config.Str(config.LocationWebFolder))
  248. print("Ensuring web folder: ", webFolder)
  249. ensurePath(webFolder)
  250. fs := http.FileServer(http.Dir(webFolder))
  251. api.HandleFunc("/", fs.ServeHTTP)
  252. // Write login
  253. if config.Bool(config.EnableAccessControl) {
  254. loginFile := filepath.Join(webFolder, "login.html")
  255. print("Ensuring login page: ", loginFile)
  256. if res, _ := fileutil.PathExists(loginFile); !res {
  257. errorutil.AssertOk(ioutil.WriteFile(loginFile, []byte(LoginSRC[1:]), 0644))
  258. }
  259. }
  260. // Write terminal(s)
  261. if config.Bool(config.EnableWebTerminal) {
  262. ensurePath(filepath.Join(webFolder, api.APIRoot))
  263. termFile := filepath.Join(webFolder, api.APIRoot, "term.html")
  264. print("Ensuring web terminal: ", termFile)
  265. if res, _ := fileutil.PathExists(termFile); !res {
  266. errorutil.AssertOk(ioutil.WriteFile(termFile, []byte(TermSRC[1:]), 0644))
  267. }
  268. }
  269. if config.Bool(config.EnableClusterTerminal) {
  270. ensurePath(filepath.Join(webFolder, api.APIRoot))
  271. termFile := filepath.Join(webFolder, api.APIRoot, "cluster.html")
  272. if config.Bool(config.EnableCluster) {
  273. // Add the url to the member info of the member manager
  274. api.DD.MemberManager.MemberInfo()[manager.MemberInfoTermURL] =
  275. fmt.Sprintf("https://%v:%v%v/%v", config.Str(config.HTTPSHost),
  276. config.Str(config.HTTPSPort), api.APIRoot, "cluster.html")
  277. }
  278. print("Ensuring cluster terminal: ", termFile)
  279. if res, _ := fileutil.PathExists(termFile); !res {
  280. errorutil.AssertOk(ioutil.WriteFile(termFile, []byte(ClusterTermSRC[1:]), 0644))
  281. }
  282. }
  283. }
  284. // Start HTTPS server and enable REST API
  285. hs := &httputil.HTTPServer{}
  286. var wg sync.WaitGroup
  287. wg.Add(1)
  288. port := config.Str(config.HTTPSPort)
  289. print("Starting server on: ", api.APIHost)
  290. go hs.RunHTTPSServer(basepath+config.Str(config.LocationHTTPS), config.Str(config.HTTPSCertificate),
  291. config.Str(config.HTTPSKey), ":"+port, &wg)
  292. // Wait until the server has started
  293. wg.Wait()
  294. // HTTPS Server has started
  295. if hs.LastError != nil {
  296. fatal(hs.LastError)
  297. return
  298. }
  299. // Read server certificate and write a fingerprint file
  300. fpfile := filepath.Join(basepath, config.Str(config.LocationWebFolder), "fingerprint.json")
  301. print("Writing fingerprint file: ", fpfile)
  302. certs, _ := cryptutil.ReadX509CertsFromFile(certPath)
  303. if len(certs) > 0 {
  304. buf := bytes.Buffer{}
  305. buf.WriteString("{\n")
  306. buf.WriteString(fmt.Sprintf(` "md5" : "%s",`, cryptutil.Md5CertFingerprint(certs[0])))
  307. buf.WriteString("\n")
  308. buf.WriteString(fmt.Sprintf(` "sha1" : "%s",`, cryptutil.Sha1CertFingerprint(certs[0])))
  309. buf.WriteString("\n")
  310. buf.WriteString(fmt.Sprintf(` "sha256" : "%s"`, cryptutil.Sha256CertFingerprint(certs[0])))
  311. buf.WriteString("\n")
  312. buf.WriteString("}\n")
  313. ioutil.WriteFile(fpfile, buf.Bytes(), 0644)
  314. }
  315. // Create a lockfile so the server can be shut down
  316. lf := lockutil.NewLockFile(basepath+config.Str(config.LockFile), time.Duration(2)*time.Second)
  317. lf.Start()
  318. go func() {
  319. // Check if the lockfile watcher is running and
  320. // call shutdown once it has finished
  321. for lf.WatcherRunning() {
  322. time.Sleep(time.Duration(1) * time.Second)
  323. }
  324. print("Lockfile was modified")
  325. hs.Shutdown()
  326. }()
  327. // Add to the wait group so we can wait for the shutdown
  328. wg.Add(1)
  329. print("Waiting for shutdown")
  330. wg.Wait()
  331. print("Shutting down")
  332. if config.Bool(config.EnableCluster) {
  333. // Shutdown cluster
  334. gs.(*cluster.DistributedStorage).MemberManager.Shutdown()
  335. }
  336. }
  337. /*
  338. ensurePath ensures that a given relative path exists.
  339. */
  340. func ensurePath(path string) {
  341. if res, _ := fileutil.PathExists(path); !res {
  342. if err := os.Mkdir(path, 0770); err != nil {
  343. fatal("Could not create directory:", err.Error())
  344. return
  345. }
  346. }
  347. }