A webmail client. Forked from https://git.sr.ht/~migadu/alps
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.
 
 
 
 

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