serverconf.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. package engine
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "net/url"
  7. "os"
  8. "path/filepath"
  9. "strings"
  10. log "github.com/sirupsen/logrus"
  11. "github.com/xyproto/algernon/utils"
  12. lua "github.com/xyproto/gopher-lua"
  13. bolt "github.com/xyproto/permissionbolt/v2"
  14. redis "github.com/xyproto/permissions2/v2"
  15. mariadb "github.com/xyproto/permissionsql/v2"
  16. "github.com/xyproto/pinterface"
  17. postgres "github.com/xyproto/pstore"
  18. "github.com/xyproto/simpleredis/v2"
  19. )
  20. // Info returns a string with various info about the current configuration
  21. func (ac *Config) Info() string {
  22. var sb strings.Builder
  23. if !ac.singleFileMode {
  24. sb.WriteString("Server directory:\t" + ac.serverDirOrFilename + "\n")
  25. } else {
  26. sb.WriteString("Filename:\t\t" + ac.serverDirOrFilename + "\n")
  27. }
  28. if !ac.productionMode {
  29. sb.WriteString("Server address:\t\t" + ac.serverAddr + "\n")
  30. } // else port 80 and 443
  31. if ac.dbName == "" {
  32. sb.WriteString("Database:\t\tDisabled\n")
  33. } else {
  34. sb.WriteString("Database:\t\t" + ac.dbName + "\n")
  35. }
  36. if ac.luaServerFilename != "" {
  37. sb.WriteString("Server filename:\t" + ac.luaServerFilename + "\n")
  38. }
  39. // Write the status of flags that can be toggled
  40. utils.WriteStatus(&sb, "Options", map[string]bool{
  41. "Debug": ac.debugMode,
  42. "Production": ac.productionMode,
  43. "Auto-refresh": ac.autoRefresh,
  44. "Dev": ac.devMode,
  45. "Server": ac.serverMode,
  46. "StatCache": ac.cacheFileStat,
  47. })
  48. sb.WriteString("Cache mode:\t\t" + ac.cacheMode.String() + "\n")
  49. if ac.cacheSize != 0 {
  50. sb.WriteString(fmt.Sprintf("Cache size:\t\t%d bytes\n", ac.cacheSize))
  51. }
  52. if ac.serverLogFile != "" {
  53. sb.WriteString("Log file:\t\t" + ac.serverLogFile + "\n")
  54. }
  55. if !(ac.serveJustHTTP2 || ac.serveJustHTTP) {
  56. sb.WriteString("TLS certificate:\t" + ac.serverCert + "\n")
  57. sb.WriteString("TLS key:\t\t" + ac.serverKey + "\n")
  58. }
  59. if ac.autoRefresh {
  60. sb.WriteString("Event server:\t\t" + ac.eventAddr + "\n")
  61. }
  62. if ac.autoRefreshDir != "" {
  63. sb.WriteString("Only watching:\t\t" + ac.autoRefreshDir + "\n")
  64. }
  65. if ac.redisAddr != ac.defaultRedisColonPort {
  66. sb.WriteString("Redis address:\t\t" + ac.redisAddr + "\n")
  67. }
  68. if ac.disableRateLimiting {
  69. sb.WriteString("Request limit:\t\tOff\n")
  70. } else {
  71. sb.WriteString(fmt.Sprintf("Request limit:\t\t%d/sec per visitor\n", ac.limitRequests))
  72. }
  73. if ac.redisDBindex != 0 {
  74. sb.WriteString(fmt.Sprintf("Redis database index:\t%d\n", ac.redisDBindex))
  75. }
  76. if ac.largeFileSize > 0 {
  77. sb.WriteString(fmt.Sprintf("Large file threshold:\t%v bytes\n", ac.largeFileSize))
  78. }
  79. if ac.writeTimeout > 0 {
  80. sb.WriteString(fmt.Sprintf("Large file timeout:\t%v sec\n", ac.writeTimeout))
  81. }
  82. if len(ac.serverConfigurationFilenames) > 0 {
  83. sb.WriteString(fmt.Sprintf("Server configuration:\t%v\n", ac.serverConfigurationFilenames))
  84. }
  85. if ac.internalLogFilename != os.DevNull {
  86. sb.WriteString("Internal log file:\t" + ac.internalLogFilename + "\n")
  87. }
  88. return strings.TrimSpace(sb.String())
  89. }
  90. // LoadServerConfigFunctions makes functions related to server configuration and
  91. // permissions available to the given Lua struct.
  92. func (ac *Config) LoadServerConfigFunctions(L *lua.LState, filename string) error {
  93. if ac.perm == nil {
  94. return errors.New("perm is nil when loading server config functions")
  95. }
  96. // Set a default host and port. Maybe useful for alg applications.
  97. L.SetGlobal("SetAddr", L.NewFunction(func(L *lua.LState) int {
  98. ac.serverAddrLua = L.ToString(1)
  99. return 0 // number of results
  100. }))
  101. // Set the default cookie secret. This is for the server config, before
  102. // the userstate has been instanciated.
  103. L.SetGlobal("SetCookieSecret", L.NewFunction(func(L *lua.LState) int {
  104. ac.cookieSecret = L.ToString(1)
  105. return 0 // number of results
  106. }))
  107. // Get the default cookie secret. THis is for the server config, before
  108. // the userstate has been instanciated.
  109. L.SetGlobal("CookieSecret", L.NewFunction(func(L *lua.LState) int {
  110. L.Push(lua.LString(ac.cookieSecret))
  111. return 1 // number of results
  112. }))
  113. // Clear the default path prefixes. This makes everything public.
  114. L.SetGlobal("ClearPermissions", L.NewFunction(func(L *lua.LState) int {
  115. ac.perm.Clear()
  116. return 0 // number of results
  117. }))
  118. // Registers a path prefix, for instance "/secret",
  119. // as having *user* rights.
  120. L.SetGlobal("AddUserPrefix", L.NewFunction(func(L *lua.LState) int {
  121. path := L.ToString(1)
  122. ac.perm.AddUserPath(path)
  123. return 0 // number of results
  124. }))
  125. // Registers a path prefix, for instance "/secret",
  126. // as having *admin* rights.
  127. L.SetGlobal("AddAdminPrefix", L.NewFunction(func(L *lua.LState) int {
  128. path := L.ToString(1)
  129. ac.perm.AddAdminPath(path)
  130. return 0 // number of results
  131. }))
  132. // Add a new reverse proxy given a: path prefix, endpoint and endpoint URL
  133. L.SetGlobal("AddReverseProxy", L.NewFunction(func(L *lua.LState) int {
  134. var rp ReverseProxy
  135. rp.PathPrefix = L.ToString(1)
  136. endpointURLString := L.ToString(2)
  137. parsedURL, err := url.Parse(endpointURLString)
  138. if err != nil {
  139. log.Errorf("could not parse endpoint URL: %s: %v", endpointURLString, err)
  140. }
  141. rp.Endpoint = *parsedURL
  142. if ac.reverseProxyConfig == nil {
  143. ac.reverseProxyConfig = NewReverseProxyConfig()
  144. }
  145. ac.reverseProxyConfig.Add(&rp)
  146. return 0 // number of results
  147. }))
  148. // Sets a Lua function as a custom "permissions denied" page handler.
  149. L.SetGlobal("DenyHandler", L.NewFunction(func(L *lua.LState) int {
  150. luaDenyFunc := L.ToFunction(1)
  151. // Custom handler for when permissions are denied
  152. ac.perm.SetDenyFunction(func(w http.ResponseWriter, req *http.Request) {
  153. // Set up a new Lua state with the current http.ResponseWriter and *http.Request, without caching
  154. ac.LoadCommonFunctions(w, req, filename, L, nil, nil)
  155. // Then run the given Lua function
  156. L.Push(luaDenyFunc)
  157. if err := L.PCall(0, lua.MultRet, nil); err != nil {
  158. // Non-fatal error
  159. log.Error("Permission denied handler failed:", err)
  160. // Use the default permission handler from now on if the lua function fails
  161. ac.perm.SetDenyFunction(redis.PermissionDenied)
  162. ac.perm.DenyFunction()(w, req)
  163. }
  164. })
  165. return 0 // number of results
  166. }))
  167. // Sets a Lua function to be run once the server is done parsing configuration and arguments.
  168. L.SetGlobal("OnReady", L.NewFunction(func(L *lua.LState) int {
  169. luaReadyFunc := L.ToFunction(1)
  170. // Custom handler for when permissions are denied.
  171. // Put the *lua.LState in a closure.
  172. ac.serverReadyFunctionLua = func() {
  173. // Run the given Lua function
  174. L.Push(luaReadyFunc)
  175. if err := L.PCall(0, lua.MultRet, nil); err != nil {
  176. // Non-fatal error
  177. log.Error("The OnReady function failed:", err)
  178. }
  179. }
  180. return 0 // number of results
  181. }))
  182. // Set a access log filename. If blank, the log will go to the console (or browser, if debug mode is set).
  183. L.SetGlobal("LogTo", L.NewFunction(func(L *lua.LState) int {
  184. filename := L.ToString(1)
  185. ac.serverLogFile = filename
  186. // Log as JSON by default
  187. log.SetFormatter(&log.JSONFormatter{})
  188. // Log to stderr if an empty filename is given
  189. if filename == "" {
  190. log.SetOutput(os.Stderr)
  191. L.Push(lua.LBool(true))
  192. return 1 // number of results
  193. }
  194. // Try opening/creating the given filename, for appending
  195. f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_APPEND, ac.defaultPermissions)
  196. if err != nil {
  197. log.Error(err)
  198. L.Push(lua.LBool(false))
  199. return 1 // number of results
  200. }
  201. // Set the file to log to and return
  202. log.SetOutput(f)
  203. L.Push(lua.LBool(true))
  204. return 1 // number of results
  205. }))
  206. // Use a single Lua file as the server, instead of directory structure
  207. L.SetGlobal("ServerFile", L.NewFunction(func(L *lua.LState) int {
  208. givenFilename := L.ToString(1)
  209. serverFilename := filepath.Join(filepath.Dir(filename), givenFilename)
  210. if !ac.fs.Exists(serverFilename) {
  211. log.Error("Could not find", serverFilename)
  212. L.Push(lua.LBool(false))
  213. return 1 // number of results
  214. }
  215. ac.luaServerFilename = serverFilename
  216. L.Push(lua.LBool(true))
  217. return 1 // number of results
  218. }))
  219. // Set the server directory
  220. L.SetGlobal("ServerDir", L.NewFunction(func(L *lua.LState) int {
  221. givenDirectory := L.ToString(1)
  222. if !ac.fs.Exists(givenDirectory) {
  223. log.Error("Could not find", givenDirectory)
  224. L.Push(lua.LBool(false))
  225. return 1 // number of results
  226. }
  227. ac.serverDirOrFilename = filepath.Clean(givenDirectory)
  228. L.Push(lua.LBool(true))
  229. return 1 // number of results
  230. }))
  231. L.SetGlobal("ServerInfo", L.NewFunction(func(L *lua.LState) int {
  232. // Return the string, but drop the final newline
  233. L.Push(lua.LString(ac.Info()))
  234. return 1 // number of results
  235. }))
  236. return nil
  237. }
  238. // DatabaseBackend tries to retrieve a database backend, using one of the
  239. // available permission middleware packages. It assign a name to dbName
  240. // (used for the status output) and returns a IPermissions struct.
  241. func (ac *Config) DatabaseBackend() (pinterface.IPermissions, error) {
  242. var (
  243. err error
  244. perm pinterface.IPermissions
  245. )
  246. // If Bolt is to be used and no filename is given
  247. if ac.useBolt && (ac.boltFilename == "") {
  248. ac.boltFilename = ac.defaultBoltFilename
  249. }
  250. if ac.boltFilename != "" {
  251. // New permissions middleware, using a Bolt database
  252. perm, err = bolt.NewWithConf(ac.boltFilename)
  253. if err != nil {
  254. if err.Error() == "timeout" {
  255. tempFile, errTemp := os.CreateTemp("", "algernon")
  256. if errTemp != nil {
  257. log.Fatal("Unable to find a temporary file to use:", errTemp)
  258. }
  259. ac.boltFilename = tempFile.Name() + ".db"
  260. } else {
  261. log.Errorf("Could not use Bolt as database backend: %s", err)
  262. }
  263. } else {
  264. ac.dbName = "Bolt (" + ac.boltFilename + ")"
  265. }
  266. // Try the new database filename if there was a timeout
  267. if ac.dbName == "" && ac.boltFilename != ac.defaultBoltFilename {
  268. perm, err = bolt.NewWithConf(ac.boltFilename)
  269. if err != nil {
  270. if err.Error() == "timeout" {
  271. log.Error("The Bolt database timed out!")
  272. } else {
  273. log.Errorf("Could not use Bolt as database backend: %s", err)
  274. }
  275. } else {
  276. ac.dbName = "Bolt, temporary"
  277. }
  278. }
  279. }
  280. if ac.dbName == "" && ac.mariadbDSN != "" {
  281. // New permissions middleware, using a MariaDB/MySQL database
  282. perm, err = mariadb.NewWithDSN(ac.mariadbDSN, ac.mariaDatabase)
  283. if err != nil {
  284. log.Errorf("Could not use MariaDB/MySQL as database backend: %s", err)
  285. } else {
  286. // The connection string may contain a password, so don't include it in the dbName
  287. ac.dbName = "MariaDB/MySQL"
  288. }
  289. }
  290. if ac.dbName == "" && ac.mariaDatabase != "" {
  291. // Given a database, but not a host, connect to localhost
  292. // New permissions middleware, using a MariaDB/MySQL database
  293. perm, err = mariadb.NewWithConf("test:@127.0.0.1/" + ac.mariaDatabase)
  294. if err != nil {
  295. if ac.mariaDatabase != "" {
  296. log.Errorf("Could not use MariaDB/MySQL as database backend: %s", err)
  297. } else {
  298. log.Warnf("Could not use MariaDB/MySQL as database backend: %s", err)
  299. }
  300. } else {
  301. // The connection string may contain a password, so don't include it in the dbName
  302. ac.dbName = "MariaDB/MySQL"
  303. }
  304. }
  305. if ac.dbName == "" && ac.postgresDSN != "" {
  306. // New permissions middleware, using a PostgreSQL database
  307. perm, err = postgres.NewWithDSN(ac.postgresDSN, ac.postgresDatabase)
  308. if err != nil {
  309. log.Errorf("Could not use PostgreSQL as database backend: %s", err)
  310. } else {
  311. // The connection string may contain a password, so don't include it in the dbName
  312. ac.dbName = "PostgreSQL"
  313. }
  314. }
  315. if ac.dbName == "" && ac.postgresDatabase != "" {
  316. // Given a database, but not a host, connect to localhost
  317. // New permissions middleware, using a PostgreSQL database
  318. perm, err = postgres.NewWithConf("postgres:@127.0.0.1/" + ac.postgresDatabase)
  319. if err != nil {
  320. if ac.postgresDatabase != "" {
  321. log.Errorf("Could not use PostgreSQL as database backend: %s", err)
  322. } else {
  323. log.Warnf("Could not use PostgreSQL as database backend: %s", err)
  324. }
  325. } else {
  326. // The connection string may contain a password, so don't include it in the dbName
  327. ac.dbName = "PostgreSQL"
  328. }
  329. }
  330. if ac.dbName == "" && ac.redisAddrSpecified {
  331. // New permissions middleware, using a Redis database
  332. log.Info("Testing redis connection")
  333. if err := simpleredis.TestConnectionHost(ac.redisAddr); err != nil {
  334. log.Info("Redis connection failed")
  335. // Only output an error when a Redis host other than the default host+port was specified
  336. if ac.singleFileMode {
  337. log.Warnf("Could not use Redis as database backend: %s", err)
  338. } else {
  339. log.Errorf("Could not use Redis as database backend: %s", err)
  340. }
  341. } else {
  342. log.Info("Redis connection worked out")
  343. var err error
  344. log.Info("Connecting to Redis...")
  345. perm, err = redis.NewWithRedisConf2(ac.redisDBindex, ac.redisAddr)
  346. if err != nil {
  347. log.Warnf("Could not use Redis as database backend: %s", err)
  348. } else {
  349. ac.dbName = "Redis"
  350. }
  351. }
  352. }
  353. if ac.dbName == "" && ac.boltFilename == "" {
  354. ac.boltFilename = ac.defaultBoltFilename
  355. perm, err = bolt.NewWithConf(ac.boltFilename)
  356. if err != nil {
  357. if err.Error() == "timeout" {
  358. tempFile, errTemp := os.CreateTemp("", "algernon")
  359. if errTemp != nil {
  360. log.Fatal("Unable to find a temporary file to use:", errTemp)
  361. }
  362. ac.boltFilename = tempFile.Name() + ".db"
  363. } else {
  364. log.Errorf("Could not use Bolt as database backend: %s", err)
  365. }
  366. } else {
  367. ac.dbName = "Bolt (" + ac.boltFilename + ")"
  368. }
  369. // Try the new database filename if there was a timeout
  370. if ac.boltFilename != ac.defaultBoltFilename {
  371. perm, err = bolt.NewWithConf(ac.boltFilename)
  372. if err != nil {
  373. if err.Error() == "timeout" {
  374. log.Error("The Bolt database timed out!")
  375. } else {
  376. log.Errorf("Could not use Bolt as database backend: %s", err)
  377. }
  378. } else {
  379. ac.dbName = "Bolt, temporary"
  380. }
  381. }
  382. }
  383. if ac.dbName == "" {
  384. // This may typically happen if Algernon is already running
  385. return nil, errors.New("could not find a usable database backend")
  386. }
  387. if ac.verboseMode {
  388. log.Info("Database backend success: " + ac.dbName)
  389. }
  390. if perm != nil && ac.clearDefaultPathPrefixes {
  391. perm.Clear()
  392. }
  393. return perm, nil
  394. }