handlers.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. package main
  2. import (
  3. "crypto/subtle"
  4. "net/http"
  5. "net/url"
  6. "path"
  7. "regexp"
  8. "strconv"
  9. "github.com/labstack/echo/v4"
  10. "github.com/labstack/echo/v4/middleware"
  11. )
  12. const (
  13. // stdInputMaxLen is the maximum allowed length for a standard input field.
  14. stdInputMaxLen = 200
  15. sortAsc = "asc"
  16. sortDesc = "desc"
  17. )
  18. type okResp struct {
  19. Data interface{} `json:"data"`
  20. }
  21. // pagination represents a query's pagination (limit, offset) related values.
  22. type pagination struct {
  23. PerPage int `json:"per_page"`
  24. Page int `json:"page"`
  25. Offset int `json:"offset"`
  26. Limit int `json:"limit"`
  27. }
  28. var (
  29. reUUID = regexp.MustCompile("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
  30. reLangCode = regexp.MustCompile("[^a-zA-Z_0-9\\-]")
  31. )
  32. // registerHandlers registers HTTP handlers.
  33. func initHTTPHandlers(e *echo.Echo, app *App) {
  34. // Group of private handlers with BasicAuth.
  35. var g *echo.Group
  36. if len(app.constants.AdminUsername) == 0 ||
  37. len(app.constants.AdminPassword) == 0 {
  38. g = e.Group("")
  39. } else {
  40. g = e.Group("", middleware.BasicAuth(basicAuth))
  41. }
  42. // Admin JS app views.
  43. // /admin/static/* file server is registered in initHTTPServer().
  44. e.GET("/", func(c echo.Context) error {
  45. return c.Render(http.StatusOK, "home", publicTpl{Title: "listmonk"})
  46. })
  47. g.GET(path.Join(adminRoot, ""), handleAdminPage)
  48. g.GET(path.Join(adminRoot, "/custom.css"), serveCustomApperance("admin.custom_css"))
  49. g.GET(path.Join(adminRoot, "/custom.js"), serveCustomApperance("admin.custom_js"))
  50. g.GET(path.Join(adminRoot, "/*"), handleAdminPage)
  51. // API endpoints.
  52. g.GET("/api/health", handleHealthCheck)
  53. g.GET("/api/config", handleGetServerConfig)
  54. g.GET("/api/lang/:lang", handleGetI18nLang)
  55. g.GET("/api/dashboard/charts", handleGetDashboardCharts)
  56. g.GET("/api/dashboard/counts", handleGetDashboardCounts)
  57. g.GET("/api/settings", handleGetSettings)
  58. g.PUT("/api/settings", handleUpdateSettings)
  59. g.POST("/api/admin/reload", handleReloadApp)
  60. g.GET("/api/logs", handleGetLogs)
  61. g.GET("/api/subscribers/:id", handleGetSubscriber)
  62. g.GET("/api/subscribers/:id/export", handleExportSubscriberData)
  63. g.GET("/api/subscribers/:id/bounces", handleGetSubscriberBounces)
  64. g.DELETE("/api/subscribers/:id/bounces", handleDeleteSubscriberBounces)
  65. g.POST("/api/subscribers", handleCreateSubscriber)
  66. g.PUT("/api/subscribers/:id", handleUpdateSubscriber)
  67. g.POST("/api/subscribers/:id/optin", handleSubscriberSendOptin)
  68. g.PUT("/api/subscribers/blocklist", handleBlocklistSubscribers)
  69. g.PUT("/api/subscribers/:id/blocklist", handleBlocklistSubscribers)
  70. g.PUT("/api/subscribers/lists/:id", handleManageSubscriberLists)
  71. g.PUT("/api/subscribers/lists", handleManageSubscriberLists)
  72. g.DELETE("/api/subscribers/:id", handleDeleteSubscribers)
  73. g.DELETE("/api/subscribers", handleDeleteSubscribers)
  74. g.GET("/api/bounces", handleGetBounces)
  75. g.GET("/api/bounces/:id", handleGetBounces)
  76. g.DELETE("/api/bounces", handleDeleteBounces)
  77. g.DELETE("/api/bounces/:id", handleDeleteBounces)
  78. // Subscriber operations based on arbitrary SQL queries.
  79. // These aren't very REST-like.
  80. g.POST("/api/subscribers/query/delete", handleDeleteSubscribersByQuery)
  81. g.PUT("/api/subscribers/query/blocklist", handleBlocklistSubscribersByQuery)
  82. g.PUT("/api/subscribers/query/lists", handleManageSubscriberListsByQuery)
  83. g.GET("/api/subscribers", handleQuerySubscribers)
  84. g.GET("/api/subscribers/export",
  85. middleware.GzipWithConfig(middleware.GzipConfig{Level: 9})(handleExportSubscribers))
  86. g.GET("/api/import/subscribers", handleGetImportSubscribers)
  87. g.GET("/api/import/subscribers/logs", handleGetImportSubscriberStats)
  88. g.POST("/api/import/subscribers", handleImportSubscribers)
  89. g.DELETE("/api/import/subscribers", handleStopImportSubscribers)
  90. g.GET("/api/lists", handleGetLists)
  91. g.GET("/api/lists/:id", handleGetLists)
  92. g.POST("/api/lists", handleCreateList)
  93. g.PUT("/api/lists/:id", handleUpdateList)
  94. g.DELETE("/api/lists/:id", handleDeleteLists)
  95. g.GET("/api/campaigns", handleGetCampaigns)
  96. g.GET("/api/campaigns/running/stats", handleGetRunningCampaignStats)
  97. g.GET("/api/campaigns/:id", handleGetCampaign)
  98. g.GET("/api/campaigns/analytics/:type", handleGetCampaignViewAnalytics)
  99. g.GET("/api/campaigns/:id/preview", handlePreviewCampaign)
  100. g.POST("/api/campaigns/:id/preview", handlePreviewCampaign)
  101. g.POST("/api/campaigns/:id/content", handleCampaignContent)
  102. g.POST("/api/campaigns/:id/text", handlePreviewCampaign)
  103. g.POST("/api/campaigns/:id/test", handleTestCampaign)
  104. g.POST("/api/campaigns", handleCreateCampaign)
  105. g.PUT("/api/campaigns/:id", handleUpdateCampaign)
  106. g.PUT("/api/campaigns/:id/status", handleUpdateCampaignStatus)
  107. g.DELETE("/api/campaigns/:id", handleDeleteCampaign)
  108. g.GET("/api/media", handleGetMedia)
  109. g.GET("/api/media/:id", handleGetMedia)
  110. g.POST("/api/media", handleUploadMedia)
  111. g.DELETE("/api/media/:id", handleDeleteMedia)
  112. g.GET("/api/templates", handleGetTemplates)
  113. g.GET("/api/templates/:id", handleGetTemplates)
  114. g.GET("/api/templates/:id/preview", handlePreviewTemplate)
  115. g.POST("/api/templates/preview", handlePreviewTemplate)
  116. g.POST("/api/templates", handleCreateTemplate)
  117. g.PUT("/api/templates/:id", handleUpdateTemplate)
  118. g.PUT("/api/templates/:id/default", handleTemplateSetDefault)
  119. g.DELETE("/api/templates/:id", handleDeleteTemplate)
  120. if app.constants.BounceWebhooksEnabled {
  121. // Private authenticated bounce endpoint.
  122. g.POST("/webhooks/bounce", handleBounceWebhook)
  123. // Public bounce endpoints for webservices like SES.
  124. e.POST("/webhooks/service/:service", handleBounceWebhook)
  125. }
  126. // /public/static/* file server is registered in initHTTPServer().
  127. // Public subscriber facing views.
  128. e.GET("/subscription/form", handleSubscriptionFormPage)
  129. e.POST("/subscription/form", handleSubscriptionForm)
  130. e.GET("/subscription/:campUUID/:subUUID", noIndex(validateUUID(subscriberExists(handleSubscriptionPage),
  131. "campUUID", "subUUID")))
  132. e.POST("/subscription/:campUUID/:subUUID", validateUUID(subscriberExists(handleSubscriptionPage),
  133. "campUUID", "subUUID"))
  134. e.GET("/subscription/optin/:subUUID", noIndex(validateUUID(subscriberExists(handleOptinPage), "subUUID")))
  135. e.POST("/subscription/optin/:subUUID", validateUUID(subscriberExists(handleOptinPage), "subUUID"))
  136. e.POST("/subscription/export/:subUUID", validateUUID(subscriberExists(handleSelfExportSubscriberData),
  137. "subUUID"))
  138. e.POST("/subscription/wipe/:subUUID", validateUUID(subscriberExists(handleWipeSubscriberData),
  139. "subUUID"))
  140. e.GET("/link/:linkUUID/:campUUID/:subUUID", noIndex(validateUUID(handleLinkRedirect,
  141. "linkUUID", "campUUID", "subUUID")))
  142. e.GET("/campaign/:campUUID/:subUUID", noIndex(validateUUID(handleViewCampaignMessage,
  143. "campUUID", "subUUID")))
  144. e.GET("/campaign/:campUUID/:subUUID/px.png", noIndex(validateUUID(handleRegisterCampaignView,
  145. "campUUID", "subUUID")))
  146. e.GET("/public/custom.css", serveCustomApperance("public.custom_css"))
  147. e.GET("/public/custom.js", serveCustomApperance("public.custom_js"))
  148. // Public health API endpoint.
  149. e.GET("/health", handleHealthCheck)
  150. }
  151. // handleAdminPage is the root handler that renders the Javascript admin frontend.
  152. func handleAdminPage(c echo.Context) error {
  153. app := c.Get("app").(*App)
  154. b, err := app.fs.Read(path.Join(adminRoot, "/index.html"))
  155. if err != nil {
  156. return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
  157. }
  158. return c.HTMLBlob(http.StatusOK, b)
  159. }
  160. // handleHealthCheck is a healthcheck endpoint that returns a 200 response.
  161. func handleHealthCheck(c echo.Context) error {
  162. return c.JSON(http.StatusOK, okResp{true})
  163. }
  164. // serveCustomApperance serves the given custom CSS/JS appearance blob
  165. // meant for customizing public and admin pages from the admin settings UI.
  166. func serveCustomApperance(name string) echo.HandlerFunc {
  167. return func(c echo.Context) error {
  168. var (
  169. app = c.Get("app").(*App)
  170. out []byte
  171. hdr string
  172. )
  173. switch name {
  174. case "admin.custom_css":
  175. out = app.constants.Appearance.AdminCSS
  176. hdr = "text/css; charset=utf-8"
  177. case "admin.custom_js":
  178. out = app.constants.Appearance.AdminJS
  179. hdr = "application/javascript; charset=utf-8"
  180. case "public.custom_css":
  181. out = app.constants.Appearance.PublicCSS
  182. hdr = "text/css; charset=utf-8"
  183. case "public.custom_js":
  184. out = app.constants.Appearance.PublicJS
  185. hdr = "application/javascript; charset=utf-8"
  186. }
  187. return c.Blob(http.StatusOK, hdr, out)
  188. }
  189. }
  190. // basicAuth middleware does an HTTP BasicAuth authentication for admin handlers.
  191. func basicAuth(username, password string, c echo.Context) (bool, error) {
  192. app := c.Get("app").(*App)
  193. // Auth is disabled.
  194. if len(app.constants.AdminUsername) == 0 &&
  195. len(app.constants.AdminPassword) == 0 {
  196. return true, nil
  197. }
  198. if subtle.ConstantTimeCompare([]byte(username), app.constants.AdminUsername) == 1 &&
  199. subtle.ConstantTimeCompare([]byte(password), app.constants.AdminPassword) == 1 {
  200. return true, nil
  201. }
  202. return false, nil
  203. }
  204. // validateUUID middleware validates the UUID string format for a given set of params.
  205. func validateUUID(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
  206. return func(c echo.Context) error {
  207. app := c.Get("app").(*App)
  208. for _, p := range params {
  209. if !reUUID.MatchString(c.Param(p)) {
  210. return c.Render(http.StatusBadRequest, tplMessage,
  211. makeMsgTpl(app.i18n.T("public.errorTitle"), "",
  212. app.i18n.T("globals.messages.invalidUUID")))
  213. }
  214. }
  215. return next(c)
  216. }
  217. }
  218. // subscriberExists middleware checks if a subscriber exists given the UUID
  219. // param in a request.
  220. func subscriberExists(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
  221. return func(c echo.Context) error {
  222. var (
  223. app = c.Get("app").(*App)
  224. subUUID = c.Param("subUUID")
  225. )
  226. if _, err := app.core.GetSubscriber(0, subUUID, ""); err != nil {
  227. if er, ok := err.(*echo.HTTPError); ok && er.Code == http.StatusBadRequest {
  228. return c.Render(http.StatusNotFound, tplMessage,
  229. makeMsgTpl(app.i18n.T("public.notFoundTitle"), "", er.Message.(string)))
  230. }
  231. app.log.Printf("error checking subscriber existence: %v", err)
  232. return c.Render(http.StatusInternalServerError, tplMessage,
  233. makeMsgTpl(app.i18n.T("public.errorTitle"), "", app.i18n.T("public.errorProcessingRequest")))
  234. }
  235. return next(c)
  236. }
  237. }
  238. // noIndex adds the HTTP header requesting robots to not crawl the page.
  239. func noIndex(next echo.HandlerFunc, params ...string) echo.HandlerFunc {
  240. return func(c echo.Context) error {
  241. c.Response().Header().Set("X-Robots-Tag", "noindex")
  242. return next(c)
  243. }
  244. }
  245. // getPagination takes form values and extracts pagination values from it.
  246. func getPagination(q url.Values, perPage int) pagination {
  247. var (
  248. page, _ = strconv.Atoi(q.Get("page"))
  249. pp = q.Get("per_page")
  250. )
  251. if pp == "all" {
  252. // No limit.
  253. perPage = 0
  254. } else {
  255. ppi, _ := strconv.Atoi(pp)
  256. if ppi > 0 {
  257. perPage = ppi
  258. }
  259. }
  260. if page < 1 {
  261. page = 0
  262. } else {
  263. page--
  264. }
  265. return pagination{
  266. Page: page + 1,
  267. PerPage: perPage,
  268. Offset: page * perPage,
  269. Limit: perPage,
  270. }
  271. }