A webmail client. Forked from https://git.sr.ht/~migadu/alps
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.
 
 
 
 

362 lines
8.2 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. c, err := s.dialIMAP()
  111. if err != nil {
  112. return fmt.Errorf("failed to connect to IMAP server: %v", err)
  113. }
  114. c.Close()
  115. s.e.Logger.Printf("Configured upstream IMAP server: %v", u)
  116. return nil
  117. }
  118. func (s *Server) parseSMTPUpstream() error {
  119. u, err := s.Upstream("smtp", "smtps", "smtp+insecure")
  120. if _, ok := err.(*NoUpstreamError); ok {
  121. return nil
  122. } else if err != nil {
  123. return fmt.Errorf("failed to parse upstream SMTP server: %v", err)
  124. }
  125. if u.Scheme == "" {
  126. u, err = discoverSMTP(u.Host)
  127. if err != nil {
  128. s.e.Logger.Printf("Failed to discover SMTP server: %v", err)
  129. return nil
  130. }
  131. }
  132. s.smtp.host = u.Host
  133. switch u.Scheme {
  134. case "smtp":
  135. // This space is intentionally left blank
  136. case "smtps":
  137. s.smtp.tls = true
  138. case "smtp+insecure":
  139. s.smtp.insecure = true
  140. default:
  141. panic("unreachable")
  142. }
  143. c, err := s.dialSMTP()
  144. if err != nil {
  145. return fmt.Errorf("failed to connect to SMTP server: %v", err)
  146. }
  147. c.Close()
  148. s.e.Logger.Printf("Configured upstream SMTP server: %v", u)
  149. return nil
  150. }
  151. func (s *Server) load() error {
  152. var plugins []Plugin
  153. for _, load := range pluginLoaders {
  154. l, err := load(s)
  155. if err != nil {
  156. return fmt.Errorf("failed to load plugins: %v", err)
  157. }
  158. for _, p := range l {
  159. s.e.Logger.Printf("Loaded plugin %q", p.Name())
  160. }
  161. plugins = append(plugins, l...)
  162. }
  163. luaPlugins, err := loadAllLuaPlugins(s.e.Logger)
  164. if err != nil {
  165. return fmt.Errorf("failed to load plugins: %v", err)
  166. }
  167. plugins = append(plugins, luaPlugins...)
  168. renderer := newRenderer(s.e.Logger, s.defaultTheme)
  169. if err := renderer.Load(plugins); err != nil {
  170. return fmt.Errorf("failed to load templates: %v", err)
  171. }
  172. // Once we've loaded plugins and templates from disk (which can take time),
  173. // swap them in the Server struct
  174. s.mutex.Lock()
  175. defer s.mutex.Unlock()
  176. // Close previous Lua plugins
  177. for _, p := range s.luaPlugins {
  178. if err := p.Close(); err != nil {
  179. s.e.Logger.Printf("Failed to unload plugin '%v': %v", p.Name(), err)
  180. }
  181. }
  182. s.plugins = plugins
  183. s.luaPlugins = luaPlugins
  184. s.e.Renderer = renderer
  185. for _, p := range plugins {
  186. p.SetRoutes(s.e.Group(""))
  187. }
  188. return nil
  189. }
  190. // Reload loads Lua plugins and templates from disk.
  191. func (s *Server) Reload() error {
  192. s.e.Logger.Printf("Reloading server")
  193. return s.load()
  194. }
  195. // Context is the context used by HTTP handlers.
  196. //
  197. // Use a type assertion to get it from a echo.Context:
  198. //
  199. // ctx := ectx.(*koushin.Context)
  200. type Context struct {
  201. echo.Context
  202. Server *Server
  203. Session *Session // nil if user isn't logged in
  204. }
  205. var aLongTimeAgo = time.Unix(233431200, 0)
  206. // SetSession sets a cookie for the provided session. Passing a nil session
  207. // unsets the cookie.
  208. func (ctx *Context) SetSession(s *Session) {
  209. cookie := http.Cookie{
  210. Name: cookieName,
  211. HttpOnly: true,
  212. // TODO: domain, secure
  213. }
  214. if s != nil {
  215. cookie.Value = s.token
  216. } else {
  217. cookie.Expires = aLongTimeAgo // unset the cookie
  218. }
  219. ctx.SetCookie(&cookie)
  220. }
  221. func isPublic(path string) bool {
  222. if strings.HasPrefix(path, "/plugins/") {
  223. parts := strings.Split(path, "/")
  224. return len(parts) >= 4 && parts[3] == "assets"
  225. }
  226. return path == "/login" || strings.HasPrefix(path, "/themes/")
  227. }
  228. func redirectToLogin(ctx *Context) error {
  229. path := ctx.Request().URL.Path
  230. to := "/login"
  231. if path != "/" && path != "/login" {
  232. to += "?next=" + url.QueryEscape(ctx.Request().URL.String())
  233. }
  234. return ctx.Redirect(http.StatusFound, to)
  235. }
  236. func handleUnauthenticated(next echo.HandlerFunc, ctx *Context) error {
  237. // Require auth for all requests except /login and assets
  238. if isPublic(ctx.Request().URL.Path) {
  239. return next(ctx)
  240. } else {
  241. return redirectToLogin(ctx)
  242. }
  243. }
  244. type Options struct {
  245. Upstreams []string
  246. Theme string
  247. }
  248. // New creates a new server.
  249. func New(e *echo.Echo, options *Options) (*Server, error) {
  250. s, err := newServer(e, options)
  251. if err != nil {
  252. return nil, err
  253. }
  254. if err := s.load(); err != nil {
  255. return nil, err
  256. }
  257. e.HTTPErrorHandler = func(err error, c echo.Context) {
  258. code := http.StatusInternalServerError
  259. if he, ok := err.(*echo.HTTPError); ok {
  260. code = he.Code
  261. } else {
  262. c.Logger().Error(err)
  263. }
  264. // TODO: hide internal errors
  265. c.String(code, err.Error())
  266. }
  267. e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
  268. return func(ectx echo.Context) error {
  269. s.mutex.RLock()
  270. err := next(ectx)
  271. s.mutex.RUnlock()
  272. return err
  273. }
  274. })
  275. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  276. return func(ectx echo.Context) error {
  277. // `style-src 'unsafe-inline'` is required for e-mails with
  278. // embedded stylesheets
  279. ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
  280. // DNS prefetching has privacy implications
  281. ectx.Response().Header().Set("X-DNS-Prefetch-Control", "off")
  282. return next(ectx)
  283. }
  284. })
  285. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  286. return func(ectx echo.Context) error {
  287. ctx := &Context{Context: ectx, Server: s}
  288. ctx.Set("context", ctx)
  289. cookie, err := ctx.Cookie(cookieName)
  290. if err == http.ErrNoCookie {
  291. return handleUnauthenticated(next, ctx)
  292. } else if err != nil {
  293. return err
  294. }
  295. ctx.Session, err = ctx.Server.Sessions.get(cookie.Value)
  296. if err == errSessionExpired {
  297. ctx.SetSession(nil)
  298. return handleUnauthenticated(next, ctx)
  299. } else if err != nil {
  300. return err
  301. }
  302. ctx.Session.ping()
  303. return next(ctx)
  304. }
  305. })
  306. e.Static("/themes", "themes")
  307. return s, nil
  308. }