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.
 
 
 
 

461 lines
10 KiB

  1. package alps
  2. import (
  3. "encoding/json"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "net/url"
  8. "strings"
  9. "sync"
  10. "time"
  11. "github.com/fernet/fernet-go"
  12. "github.com/labstack/echo/v4"
  13. )
  14. const (
  15. cookieName = "alps_session"
  16. loginTokenCookieName = "alps_login_token"
  17. )
  18. // Server holds all the alps server state.
  19. type Server struct {
  20. e *echo.Echo
  21. Sessions *SessionManager
  22. Options *Options
  23. mutex sync.RWMutex // used for server reload
  24. plugins []Plugin
  25. // maps protocols to URLs (protocol can be empty for auto-discovery)
  26. upstreams map[string]*url.URL
  27. imap struct {
  28. host string
  29. tls bool
  30. insecure bool
  31. }
  32. smtp struct {
  33. host string
  34. tls bool
  35. insecure bool
  36. }
  37. }
  38. func newServer(e *echo.Echo, options *Options) (*Server, error) {
  39. s := &Server{e: e, Options: options}
  40. s.upstreams = make(map[string]*url.URL, len(options.Upstreams))
  41. for _, upstream := range options.Upstreams {
  42. u, err := parseUpstream(upstream)
  43. if err != nil {
  44. return nil, fmt.Errorf("failed to parse upstream %q: %v", upstream, err)
  45. }
  46. if _, ok := s.upstreams[u.Scheme]; ok {
  47. return nil, fmt.Errorf("found two upstream servers for scheme %q", u.Scheme)
  48. }
  49. s.upstreams[u.Scheme] = u
  50. }
  51. if err := s.parseIMAPUpstream(); err != nil {
  52. return nil, err
  53. }
  54. if err := s.parseSMTPUpstream(); err != nil {
  55. return nil, err
  56. }
  57. s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP, e.Logger, options.Debug)
  58. return s, nil
  59. }
  60. func (s *Server) Close() {
  61. s.Sessions.Close()
  62. }
  63. func parseUpstream(s string) (*url.URL, error) {
  64. if !strings.ContainsAny(s, ":/") {
  65. // This is a raw domain name, make it an URL with an empty scheme
  66. s = "//" + s
  67. }
  68. return url.Parse(s)
  69. }
  70. type NoUpstreamError struct {
  71. schemes []string
  72. }
  73. func (err *NoUpstreamError) Error() string {
  74. return fmt.Sprintf("no upstream server configured for schemes %v", err.schemes)
  75. }
  76. // Upstream retrieves the configured upstream server URL for the provided
  77. // schemes. If no configured upstream server matches, a *NoUpstreamError is
  78. // returned. An empty URL.Scheme means that the caller needs to perform
  79. // auto-discovery with URL.Host.
  80. func (s *Server) Upstream(schemes ...string) (*url.URL, error) {
  81. var urls []*url.URL
  82. for _, scheme := range append(schemes, "") {
  83. u, ok := s.upstreams[scheme]
  84. if ok {
  85. urls = append(urls, u)
  86. }
  87. }
  88. if len(urls) == 0 {
  89. return nil, &NoUpstreamError{schemes}
  90. }
  91. if len(urls) > 1 {
  92. return nil, fmt.Errorf("multiple upstream servers are configured for schemes %v", schemes)
  93. }
  94. return urls[0], nil
  95. }
  96. func (s *Server) parseIMAPUpstream() error {
  97. u, err := s.Upstream("imap", "imaps", "imap+insecure")
  98. if err != nil {
  99. return fmt.Errorf("failed to parse upstream IMAP server: %v", err)
  100. }
  101. if u.Scheme == "" {
  102. u, err = discoverIMAP(u.Host)
  103. if err != nil {
  104. return fmt.Errorf("failed to discover IMAP server: %v", err)
  105. }
  106. }
  107. switch u.Scheme {
  108. case "imaps":
  109. s.imap.tls = true
  110. case "imap+insecure":
  111. s.imap.insecure = true
  112. }
  113. s.imap.host = u.Host
  114. if !strings.ContainsRune(s.imap.host, ':') {
  115. if u.Scheme == "imaps" {
  116. s.imap.host += ":993"
  117. } else {
  118. s.imap.host += ":143"
  119. }
  120. }
  121. c, err := s.dialIMAP()
  122. if err != nil {
  123. return fmt.Errorf("failed to connect to IMAP server: %v", err)
  124. }
  125. c.Close()
  126. s.e.Logger.Printf("Configured upstream IMAP server: %v", u)
  127. return nil
  128. }
  129. func (s *Server) parseSMTPUpstream() error {
  130. u, err := s.Upstream("smtp", "smtps", "smtp+insecure")
  131. if _, ok := err.(*NoUpstreamError); ok {
  132. return nil
  133. } else if err != nil {
  134. return fmt.Errorf("failed to parse upstream SMTP server: %v", err)
  135. }
  136. if u.Scheme == "" {
  137. u, err = discoverSMTP(u.Host)
  138. if err != nil {
  139. s.e.Logger.Printf("Failed to discover SMTP server: %v", err)
  140. return nil
  141. }
  142. }
  143. switch u.Scheme {
  144. case "smtps":
  145. s.smtp.tls = true
  146. case "smtp+insecure":
  147. s.smtp.insecure = true
  148. }
  149. s.smtp.host = u.Host
  150. if !strings.ContainsRune(s.smtp.host, ':') {
  151. if u.Scheme == "smtps" {
  152. s.smtp.host += ":465"
  153. } else {
  154. s.smtp.host += ":587"
  155. }
  156. }
  157. c, err := s.dialSMTP()
  158. if err != nil {
  159. return fmt.Errorf("failed to connect to SMTP server: %v", err)
  160. }
  161. c.Close()
  162. s.e.Logger.Printf("Configured upstream SMTP server: %v", u)
  163. return nil
  164. }
  165. func (s *Server) load() error {
  166. var plugins []Plugin
  167. for _, load := range pluginLoaders {
  168. l, err := load(s)
  169. if err != nil {
  170. return fmt.Errorf("failed to load plugins: %v", err)
  171. }
  172. for _, p := range l {
  173. s.e.Logger.Printf("Loaded plugin %q", p.Name())
  174. }
  175. plugins = append(plugins, l...)
  176. }
  177. renderer := newRenderer(s.e.Logger, s.Options.Theme)
  178. if err := renderer.Load(plugins); err != nil {
  179. return fmt.Errorf("failed to load templates: %v", err)
  180. }
  181. // Once we've loaded plugins and templates from disk (which can take time),
  182. // swap them in the Server struct
  183. s.mutex.Lock()
  184. defer s.mutex.Unlock()
  185. // Close previous plugins
  186. for _, p := range s.plugins {
  187. if err := p.Close(); err != nil {
  188. s.e.Logger.Printf("Failed to unload plugin %q: %v", p.Name(), err)
  189. }
  190. }
  191. s.plugins = plugins
  192. s.e.Renderer = renderer
  193. for _, p := range plugins {
  194. p.SetRoutes(s.e.Group(""))
  195. }
  196. return nil
  197. }
  198. // Reload loads Lua plugins and templates from disk.
  199. func (s *Server) Reload() error {
  200. s.e.Logger.Printf("Reloading server")
  201. return s.load()
  202. }
  203. // Logger returns this server's logger.
  204. func (s *Server) Logger() echo.Logger {
  205. return s.e.Logger
  206. }
  207. // Context is the context used by HTTP handlers.
  208. //
  209. // Use a type assertion to get it from a echo.Context:
  210. //
  211. // ctx := ectx.(*alps.Context)
  212. type Context struct {
  213. echo.Context
  214. Server *Server
  215. Session *Session // nil if user isn't logged in
  216. }
  217. var aLongTimeAgo = time.Unix(233431200, 0)
  218. // SetSession sets a cookie for the provided session. Passing a nil session
  219. // unsets the cookie.
  220. func (ctx *Context) SetSession(s *Session) {
  221. cookie := http.Cookie{
  222. Name: cookieName,
  223. HttpOnly: true,
  224. SameSite: http.SameSiteStrictMode,
  225. Secure: ctx.IsTLS(),
  226. }
  227. if s != nil {
  228. cookie.Value = s.token
  229. } else {
  230. cookie.Expires = aLongTimeAgo // unset the cookie
  231. }
  232. ctx.SetCookie(&cookie)
  233. }
  234. type loginToken struct {
  235. Username string
  236. Password string
  237. }
  238. func (ctx *Context) SetLoginToken(username, password string) {
  239. cookie := http.Cookie{
  240. Expires: time.Now().Add(30 * 24 * time.Hour),
  241. Name: loginTokenCookieName,
  242. HttpOnly: true,
  243. SameSite: http.SameSiteStrictMode,
  244. Secure: ctx.IsTLS(),
  245. Path: "/login",
  246. }
  247. if username == "" {
  248. cookie.Expires = aLongTimeAgo // unset the cookie
  249. ctx.SetCookie(&cookie)
  250. return
  251. }
  252. loginToken := loginToken{username, password}
  253. payload, err := json.Marshal(loginToken)
  254. if err != nil {
  255. panic(err) // Should never happen
  256. }
  257. fkey := ctx.Server.Options.LoginKey
  258. if fkey == nil {
  259. return
  260. }
  261. bytes, err := fernet.EncryptAndSign(payload, fkey)
  262. if err != nil {
  263. log.Printf("Warning: login token encryption failed: %v", err)
  264. return
  265. }
  266. cookie.Value = string(bytes)
  267. ctx.SetCookie(&cookie)
  268. }
  269. func (ctx *Context) GetLoginToken() (string, string) {
  270. cookie, err := ctx.Cookie(loginTokenCookieName)
  271. if err != nil || cookie == nil {
  272. return "", ""
  273. }
  274. fkey := ctx.Server.Options.LoginKey
  275. if fkey == nil {
  276. return "", ""
  277. }
  278. bytes := fernet.VerifyAndDecrypt([]byte(cookie.Value),
  279. 24*time.Hour*30, []*fernet.Key{fkey})
  280. if bytes == nil {
  281. return "", ""
  282. }
  283. var token loginToken
  284. err = json.Unmarshal(bytes, &token)
  285. if err != nil {
  286. panic(err) // Should never happen
  287. }
  288. return token.Username, token.Password
  289. }
  290. func isPublic(path string) bool {
  291. if strings.HasPrefix(path, "/plugins/") {
  292. parts := strings.Split(path, "/")
  293. return len(parts) >= 4 && parts[3] == "assets"
  294. }
  295. return path == "/login" || strings.HasPrefix(path, "/themes/")
  296. }
  297. func redirectToLogin(ctx *Context) error {
  298. path := ctx.Request().URL.Path
  299. to := "/login"
  300. if path != "/" && path != "/login" {
  301. to += "?next=" + url.QueryEscape(ctx.Request().URL.String())
  302. }
  303. return ctx.Redirect(http.StatusFound, to)
  304. }
  305. func handleUnauthenticated(next echo.HandlerFunc, ctx *Context) error {
  306. // Require auth for all requests except /login and assets
  307. if isPublic(ctx.Request().URL.Path) {
  308. return next(ctx)
  309. } else {
  310. return redirectToLogin(ctx)
  311. }
  312. }
  313. type Options struct {
  314. Upstreams []string
  315. Theme string
  316. Debug bool
  317. LoginKey *fernet.Key
  318. }
  319. // New creates a new server.
  320. func New(e *echo.Echo, options *Options) (*Server, error) {
  321. s, err := newServer(e, options)
  322. if err != nil {
  323. return nil, err
  324. }
  325. if err := s.load(); err != nil {
  326. return nil, err
  327. }
  328. e.HTTPErrorHandler = func(err error, ctx echo.Context) {
  329. code := http.StatusInternalServerError
  330. if he, ok := err.(*echo.HTTPError); ok {
  331. code = he.Code
  332. }
  333. type ErrorRenderData struct {
  334. BaseRenderData
  335. Code int
  336. Err error
  337. Status string
  338. }
  339. rdata := ErrorRenderData{
  340. BaseRenderData: *NewBaseRenderData(ctx),
  341. Err: err,
  342. Code: code,
  343. Status: http.StatusText(code),
  344. }
  345. if err := ctx.Render(code, "error.html", &rdata); err != nil {
  346. ctx.Logger().Error(fmt.Errorf(
  347. "Error occured rendering error page: %w. How meta.", err))
  348. }
  349. ctx.Logger().Error(err)
  350. }
  351. e.Pre(func(next echo.HandlerFunc) echo.HandlerFunc {
  352. return func(ectx echo.Context) error {
  353. s.mutex.RLock()
  354. err := next(ectx)
  355. s.mutex.RUnlock()
  356. return err
  357. }
  358. })
  359. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  360. return func(ectx echo.Context) error {
  361. // `style-src 'unsafe-inline'` is required for e-mails with
  362. // embedded stylesheets
  363. ectx.Response().Header().Set("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'")
  364. // DNS prefetching has privacy implications
  365. ectx.Response().Header().Set("X-DNS-Prefetch-Control", "off")
  366. return next(ectx)
  367. }
  368. })
  369. e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
  370. return func(ectx echo.Context) error {
  371. ctx := &Context{Context: ectx, Server: s}
  372. ctx.Set("context", ctx)
  373. cookie, err := ctx.Cookie(cookieName)
  374. if err == http.ErrNoCookie {
  375. return handleUnauthenticated(next, ctx)
  376. } else if err != nil {
  377. return err
  378. }
  379. ctx.Session, err = ctx.Server.Sessions.get(cookie.Value)
  380. if err == ErrSessionExpired {
  381. ctx.SetSession(nil)
  382. return handleUnauthenticated(next, ctx)
  383. } else if err != nil {
  384. return err
  385. }
  386. ctx.Session.ping()
  387. return next(ctx)
  388. }
  389. })
  390. e.Static("/themes", "themes")
  391. return s, nil
  392. }