Capstan is a Golang web framework that shares some similarities with others in its segment.
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.

302 lines
7.5KB

  1. package capstan
  2. import (
  3. "net/http"
  4. "os"
  5. "os/signal"
  6. "strings"
  7. "sync"
  8. "git.destrealm.org/go/capstan/config"
  9. "git.destrealm.org/go/capstan/mappers"
  10. "git.destrealm.org/go/capstan/render"
  11. "git.destrealm.org/go/capstan/session"
  12. "git.destrealm.org/go/logging"
  13. "github.com/go-chi/chi/middleware"
  14. )
  15. type application struct {
  16. router *Router
  17. logger *logging.Log
  18. config *config.Config
  19. dependencies *mappers.DependencyMapper
  20. extensions *ExtensionManager
  21. subapps map[string]Application
  22. master Application
  23. shutdownHooks []ShutdownHook
  24. proxy ProxyHandler
  25. sync.RWMutex
  26. }
  27. // New returns a new Capstan application with the optional configuration `conf`.
  28. // If no configuration is specified one will be created with its values
  29. // initialized to sensible defaults.
  30. func New(conf ...*config.Config) *application {
  31. dependencies := mappers.NewDependencyMapper()
  32. app := &application{
  33. logger: logging.MustGetLogger("main"),
  34. dependencies: dependencies,
  35. subapps: make(map[string]Application),
  36. shutdownHooks: make([]ShutdownHook, 0),
  37. }
  38. if len(conf) == 1 {
  39. app.config = conf[0]
  40. } else {
  41. app.config = &config.Config{}
  42. }
  43. // Set application defaults.
  44. // TODO: This should probably honor the host application differently.
  45. if app.config.Application == nil {
  46. app.config.Application = &ApplicationConfig{}
  47. }
  48. if !app.config.Application.DisableModeEnvVar {
  49. if app.config.Application.ModeEnvVar == "" {
  50. app.config.Application.ModeEnvVar = "CAPSTAN_ENVIRONMENT"
  51. }
  52. }
  53. // ListenAddress wasn't set, so we'll pick something reasonable.
  54. if app.config.Server.ListenAddress == "" {
  55. app.config.Server.ListenAddress = "localhost:8080"
  56. }
  57. // Init session cookies.
  58. if app.config.Server.Session == nil {
  59. app.config.Server.Session = &config.SessionConfig{
  60. CookieOptions: &http.Cookie{},
  61. }
  62. }
  63. if app.config.Server.Session.CookieOptions.Domain == "" {
  64. app.config.Server.Session.CookieOptions.Domain = app.config.Server.Hostname
  65. }
  66. // Set behavior based on production or development mode.
  67. mode := app.config.Application.EnvironmentMode
  68. if mode == "" {
  69. mode = "production"
  70. }
  71. env, exists := os.LookupEnv(app.config.Application.ModeEnvVar)
  72. if exists {
  73. mode = env
  74. }
  75. switch mode {
  76. case "production":
  77. case "development":
  78. app.logger.SetLevel(logging.Debug)
  79. }
  80. app.router = NewRouter(app)
  81. app.DefaultMiddleware()
  82. app.LoadSession()
  83. dependencies.Register("app", app)
  84. app.extensions = NewExtensionManager(app)
  85. return app
  86. }
  87. // AttachApplication to the current application instance. This converts the
  88. // current application to use multiapp proxying.
  89. func (a *application) AttachApplication(app Application) {
  90. if a.master == nil {
  91. app.SetMaster(a)
  92. } else {
  93. app.SetMaster(a.master)
  94. }
  95. // If the current proxy isn't a multiapp proxy, we'll fix that and set the
  96. // current proxy as the default handler.
  97. p, ok := a.proxy.(MultiAppProxyHandler)
  98. if !ok {
  99. p = NewMultiAppProxy(NewRadixLoader(a.Router().proxy))
  100. a.Router().SetupMuxer()
  101. a.proxy = p
  102. a.Router().SetProxy(p)
  103. }
  104. // Sub-sub-applications must be attached to the master instance.
  105. if a.master != nil {
  106. if pm, ok := a.master.Router().Proxy().(MultiAppProxyHandler); ok {
  107. pm.Loader().AddApplicationHandler(app.Config().Server.CalculatedHostPath(), app.Router().Proxy())
  108. a.master.Application().subapps[app.Config().Server.CalculatedHostPath()] = app
  109. }
  110. }
  111. // TODO: Add options for attaching to the FQDN, hostname only, hostname plus
  112. // port, and/or basepath. This would allow the multiapp application handler
  113. // to be slightly more forgiving when deciding what to load, particularly on
  114. // developer installs.
  115. p.Loader().AddApplicationHandler(app.Config().Server.CalculatedHostPath(), app.Router().proxy)
  116. //p.Loader().AddApplicationHandler(app.Config().CalculatedPath(), app.Router().proxy)
  117. app.Router().SetupMuxer()
  118. a.subapps[app.Config().Server.CalculatedHostPath()] = app
  119. }
  120. // DestroyApplication disables the specified application by replacing its
  121. // handler with a simple 404 handler. This will persist until it the application
  122. // is restarted.
  123. //
  124. // To full implement this correctly, the underlying radix trie implementation
  125. // must support deleting individual entries.
  126. func (a *application) DestroyApplication(app Application) {
  127. if p, ok := a.proxy.(MultiAppProxyHandler); ok {
  128. p.Loader().AddApplicationHandler(app.Config().Server.CalculatedHostPath(), http.HandlerFunc(http.NotFound))
  129. }
  130. if a.master != nil {
  131. if p, ok := a.master.Router().Proxy().(MultiAppProxyHandler); ok {
  132. p.Loader().AddApplicationHandler(app.Config().Server.CalculatedHostPath(), http.HandlerFunc(http.NotFound))
  133. }
  134. }
  135. }
  136. func (a *application) Application() *application {
  137. return a
  138. }
  139. func (a *application) LoadSession() {
  140. if a.config.Server.Session != nil {
  141. session.New(a.config.Server.Session)
  142. }
  143. }
  144. func (a *application) BindGroup(path string) *RouterGroup {
  145. return a.router.Group(path)
  146. }
  147. func (a *application) Bind(controller Controller) error {
  148. return a.router.Bind(controller)
  149. }
  150. func (a *application) ReplaceController(from, to Controller) error {
  151. return nil
  152. }
  153. func (a *application) ReplacePath(from, to string) {
  154. a.router.ReplacePath(from, to)
  155. }
  156. func (a *application) Unmount(path string) {
  157. a.router.Unmount(path)
  158. }
  159. func (a *application) UnmountController(controller Controller) error {
  160. return nil
  161. }
  162. func (a *application) UnmountGroup(path string) error {
  163. return nil
  164. }
  165. func (a *application) DefaultMiddleware() {
  166. noColor := NoColor != ""
  167. if v, ok := os.LookupEnv("NO_COLOR"); ok {
  168. switch strings.ToLower(v) {
  169. case "1":
  170. fallthrough
  171. case "yes":
  172. fallthrough
  173. case "true":
  174. noColor = true
  175. }
  176. }
  177. if a.config.Server.RealIP {
  178. a.router.Middleware(middleware.RealIP)
  179. }
  180. logger := a.logger
  181. if a.config.Server.RequestLog != "" {
  182. logger = logging.MustGetLogger(a.config.Server.RequestLog)
  183. //logger.SetLevel(a.config.LogLevel)
  184. }
  185. middleware.DefaultLogger = middleware.RequestLogger(&middleware.DefaultLogFormatter{
  186. Logger: logger,
  187. NoColor: noColor,
  188. })
  189. a.router.Middleware(middleware.Logger)
  190. }
  191. func (a *application) SetMiddleware(middleware ...func(http.Handler) http.Handler) Application {
  192. a.router.Middleware(middleware...)
  193. return a
  194. }
  195. func (a *application) SetDefaultRenderer(renderer render.Renderer) Application {
  196. a.router.SetRenderer(renderer)
  197. return a
  198. }
  199. func (a *application) SetMaster(app Application) {
  200. a.master = app
  201. }
  202. func (a *application) RegisterShutdownHook(hook ShutdownHook) {
  203. a.shutdownHooks = append(a.shutdownHooks, hook)
  204. }
  205. // TODO: Replace with a run method or something suitable for CLI (ab)use. In
  206. // particular, make sure we can rename the application as appropriate.
  207. func (a *application) Listen() error {
  208. wg := sync.WaitGroup{}
  209. if len(a.shutdownHooks) > 0 {
  210. interrupt := make(chan os.Signal, 1)
  211. signal.Notify(interrupt, os.Interrupt)
  212. wg.Add(1)
  213. go func() {
  214. <-interrupt
  215. for _, hook := range a.shutdownHooks {
  216. hook()
  217. }
  218. wg.Done()
  219. }()
  220. }
  221. err := a.router.Listen()
  222. if err != nil {
  223. a.logger.Error(err)
  224. }
  225. wg.Wait()
  226. return err
  227. }
  228. func (a *application) Stop() {
  229. a.router.Close()
  230. }
  231. func (a *application) Config() *config.Config {
  232. return a.config
  233. }
  234. func (a *application) Dependencies() *mappers.DependencyMapper {
  235. return a.dependencies
  236. }
  237. func (a *application) Logger() *logging.Log {
  238. return a.logger
  239. }
  240. func (a *application) Router() *Router {
  241. return a.router
  242. }
  243. func (a *application) Extensions() *ExtensionManager {
  244. return a.extensions
  245. }
  246. // Reload the Capstan server. This reconfigures all dependent services and
  247. // rebinds all endpoints.
  248. func (a *application) Reload(config *config.Config) {
  249. a.router.Rebind()
  250. }