A webmail client. Forked from https://git.sr.ht/~migadu/alps
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 

341 líneas
7.7 KiB

  1. package koushin
  2. import (
  3. "fmt"
  4. "net/http"
  5. "net/url"
  6. "strings"
  7. "sync"
  8. "time"
  9. "github.com/labstack/echo/v4"
  10. )
  11. const cookieName = "koushin_session"
  12. // Server holds all the koushin server state.
  13. type Server struct {
  14. e *echo.Echo
  15. Sessions *SessionManager
  16. mutex sync.RWMutex // used for server reload
  17. plugins []Plugin
  18. luaPlugins []Plugin
  19. // maps protocols to URLs (protocol can be empty for auto-discovery)
  20. upstreams map[string]*url.URL
  21. imap struct {
  22. host string
  23. tls bool
  24. insecure bool
  25. }
  26. smtp struct {
  27. host string
  28. tls bool
  29. insecure bool
  30. }
  31. defaultTheme string
  32. }
  33. func newServer(e *echo.Echo, options *Options) (*Server, error) {
  34. s := &Server{e: e, defaultTheme: options.Theme}
  35. s.upstreams = make(map[string]*url.URL, len(options.Upstreams))
  36. for _, upstream := range options.Upstreams {
  37. u, err := parseUpstream(upstream)
  38. if err != nil {
  39. return nil, fmt.Errorf("failed to parse upstream %q: %v", upstream, err)
  40. }
  41. if _, ok := s.upstreams[u.Scheme]; ok {
  42. return nil, fmt.Errorf("found two upstream servers for scheme %q", u.Scheme)
  43. }
  44. s.upstreams[u.Scheme] = u
  45. }
  46. if err := s.parseIMAPUpstream(); err != nil {
  47. return nil, err
  48. }
  49. if err := s.parseSMTPUpstream(); err != nil {
  50. return nil, err
  51. }
  52. s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP)
  53. return s, nil
  54. }
  55. func parseUpstream(s string) (*url.URL, error) {
  56. if !strings.ContainsAny(s, ":/") {
  57. // This is a raw domain name, make it an URL with an empty scheme
  58. s = "//" + s
  59. }
  60. return url.Parse(s)
  61. }
  62. type NoUpstreamError struct {
  63. schemes []string
  64. }
  65. func (err *NoUpstreamError) Error() string {
  66. return fmt.Sprintf("no upstream server configured for schemes %v", err.schemes)
  67. }
  68. // Upstream retrieves the configured upstream server URL for the provided
  69. // schemes. If no configured upstream server matches, a *NoUpstreamError is
  70. // returned. An empty URL.Scheme means that the caller needs to perform
  71. // auto-discovery with URL.Host.
  72. func (s *Server) Upstream(schemes ...string) (*url.URL, error) {
  73. var urls []*url.URL
  74. for _, scheme := range append(schemes, "") {
  75. u, ok := s.upstreams[scheme]
  76. if ok {
  77. urls = append(urls, u)
  78. }
  79. }
  80. if len(urls) == 0 {
  81. return nil, &NoUpstreamError{schemes}
  82. }
  83. if len(urls) > 1 {
  84. return nil, fmt.Errorf("multiple upstream servers are configured for schemes %v", schemes)
  85. }
  86. return urls[0], nil
  87. }
  88. func (s *Server) parseIMAPUpstream() error {
  89. u, err := s.Upstream("imap", "imaps", "imap+insecure")
  90. if err != nil {
  91. return fmt.Errorf("failed to parse upstream IMAP server: %v", err)
  92. }
  93. if u.Scheme == "" {
  94. u, err = discoverIMAP(u.Host)
  95. if err != nil {
  96. return fmt.Errorf("failed to discover IMAP server: %v", err)
  97. }
  98. }
  99. s.imap.host = u.Host
  100. switch u.Scheme {
  101. case "imap":
  102. // This space is intentionally left blank
  103. case "imaps":
  104. s.imap.tls = true
  105. case "imap+insecure":
  106. s.imap.insecure = true
  107. default:
  108. panic("unreachable")
  109. }
  110. s.e.Logger.Printf("Configured upstream IMAP server: %v", u)
  111. return nil
  112. }
  113. func (s *Server) parseSMTPUpstream() error {
  114. u, err := s.Upstream("smtp", "smtps", "smtp+insecure")
  115. if _, ok := err.(*NoUpstreamError); ok {
  116. return nil
  117. } else if err != nil {
  118. return fmt.Errorf("failed to parse upstream SMTP server: %v", err)
  119. }
  120. if u.Scheme == "" {
  121. u, err = discoverSMTP(u.Host)
  122. if err != nil {
  123. s.e.Logger.Printf("Failed to discover SMTP server: %v", err)
  124. return nil
  125. }
  126. }
  127. s.smtp.host = u.Host
  128. switch u.Scheme {
  129. case "smtp":
  130. // This space is intentionally left blank
  131. case "smtps":
  132. s.smtp.tls = true
  133. case "smtp+insecure":
  134. s.smtp.insecure = true
  135. default:
  136. panic("unreachable")
  137. }
  138. s.e.Logger.Printf("Configured upstream SMTP server: %v", u)
  139. return nil
  140. }
  141. func (s *Server) load() error {
  142. plugins := append([]Plugin(nil), plugins...)
  143. for _, p := range plugins {
  144. s.e.Logger.Printf("Registered plugin '%v'", p.Name())
  145. }
  146. luaPlugins, err := loadAllLuaPlugins(s.e.Logger)
  147. if err != nil {
  148. return fmt.Errorf("failed to load plugins: %v", err)
  149. }
  150. plugins = append(plugins, luaPlugins...)
  151. renderer := newRenderer(s.e.Logger, s.defaultTheme)
  152. if err := renderer.Load(plugins); err != nil {
  153. return fmt.Errorf("failed to load templates: %v", err)
  154. }
  155. // Once we've loaded plugins and templates from disk (which can take time),
  156. // swap them in the Server struct
  157. s.mutex.Lock()
  158. defer s.mutex.Unlock()
  159. // Close previous Lua plugins
  160. for _, p := range s.luaPlugins {
  161. if err := p.Close(); err != nil {
  162. s.e.Logger.Printf("Failed to unload plugin '%v': %v", p.Name(), err)
  163. }
  164. }
  165. s.plugins = plugins
  166. s.luaPlugins = luaPlugins
  167. s.e.Renderer = renderer
  168. for _, p := range plugins {
  169. p.SetRoutes(s.e.Group(""))
  170. }
  171. return nil
  172. }
  173. // Reload loads Lua plugins and templates from disk.
  174. func (s *Server) Reload() error {
  175. s.e.Logger.Printf("Reloading server")
  176. return s.load()
  177. }
  178. // Context is the context used by HTTP handlers.
  179. //
  180. // Use a type assertion to get it from a echo.Context:
  181. //
  182. // ctx := ectx.(*koushin.Context)
  183. type Context struct {
  184. echo.Context
  185. Server *Server
  186. Session *Session // nil if user isn't logged in
  187. }
  188. var aLongTimeAgo = time.Unix(233431200, 0)
  189. // SetSession sets a cookie for the provided session. Passing a nil session
  190. // unsets the cookie.
  191. func (ctx *Context) SetSession(s *Session) {
  192. cookie := http.Cookie{
  193. Name: cookieName,
  194. HttpOnly: true,
  195. // TODO: domain, secure
  196. }
  197. if s != nil {
  198. cookie.Value = s.token
  199. } else {
  200. cookie.Expires = aLongTimeAgo // unset the cookie
  201. }
  202. ctx.SetCookie(&cookie)
  203. }
  204. func isPublic(path string) bool {
  205. if strings.HasPrefix(path, "/plugins/") {
  206. parts := strings.Split(path, "/")
  207. return len(parts) >= 4 && parts[3] == "assets"
  208. }
  209. return path == "/login" || strings.HasPrefix(path, "/themes/")
  210. }
  211. func redirectToLogin(ctx *Context) error {
  212. path := ctx.Request().URL.Path
  213. to := "/login"
  214. if path != "/" && path != "/login" {
  215. to += "?next=" + url.QueryEscape(ctx.Request().URL.String())
  216. }
  217. return ctx.Redirect(http.StatusFound, to)
  218. }
  219. func handleUnauthenticated(next echo.HandlerFunc, ctx *Context) error {
  220. // Require auth for all requests except /login and assets
  221. if isPublic(ctx.Request().URL.Path) {
  222. return next(ctx)
  223. } else {
  224. return redirectToLogin(ctx)
  225. }
  226. }
  227. type Options struct {
  228. Upstreams []string
  229. Theme string
  230. }
  231. // New creates a new server.
  232. func New(e *echo.Echo, options *Options) (*Server, error) {
  233. s, err := newServer(e, options)
  234. if err != nil {
  235. return nil, err
  236. }
  237. if err := s.load(); err != nil {
  238. return nil, err
  239. }
  240. e.HTTPErrorHandler = func(err error, c echo.Context) {
  241. code := http.StatusInternalServerError
  242. if he, ok := err.(*echo.HTTPError); ok {
  243. code = he.Code
  244. } else {
  245. c.Logger().Error(err)
  246. }
  247. // TODO: hide internal errors
  248. c.String(code, err.Error())
  249. }
  250. e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
  251. return func(ectx echo.Context) error {
  252. s.mutex.RLock()
  253. err := next(ectx)
  254. s.mutex.RUnlock()
  255. return err
  256. }
  257. })
  258. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  259. return func(ectx echo.Context) error {
  260. // `style-src 'unsafe-inline'` is required for e-mails with
  261. // embedded stylesheets
  262. ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
  263. return next(ectx)
  264. }
  265. })
  266. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  267. return func(ectx echo.Context) error {
  268. ctx := &Context{Context: ectx, Server: s}
  269. ctx.Set("context", ctx)
  270. cookie, err := ctx.Cookie(cookieName)
  271. if err == http.ErrNoCookie {
  272. return handleUnauthenticated(next, ctx)
  273. } else if err != nil {
  274. return err
  275. }
  276. ctx.Session, err = ctx.Server.Sessions.get(cookie.Value)
  277. if err == errSessionExpired {
  278. ctx.SetSession(nil)
  279. return handleUnauthenticated(next, ctx)
  280. } else if err != nil {
  281. return err
  282. }
  283. ctx.Session.ping()
  284. return next(ctx)
  285. }
  286. })
  287. e.Static("/themes", "themes")
  288. return s, nil
  289. }