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.
 
 
 
 

368 lines
8.2 KiB

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