Browse Source

WIP: Templating

Philipp Heckel 3 years ago
parent
commit
96a12d98c9
5 changed files with 66 additions and 19 deletions
  1. 9 0
      cmd/serve.go
  2. 3 0
      go.mod
  3. 6 0
      go.sum
  4. 2 1
      server/config.go
  5. 46 18
      server/server.go

+ 9 - 0
cmd/serve.go

@@ -34,6 +34,7 @@ var flagsServe = []cli.Flag{
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
+	altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"M"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, DefaultText: "4K", Usage: "size limit of messages before they are treated as attachments (e.g. 4K, 64K)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
@@ -95,6 +96,7 @@ func execServe(c *cli.Context) error {
 	keepaliveInterval := c.Duration("keepalive-interval")
 	managerInterval := c.Duration("manager-interval")
 	webRoot := c.String("web-root")
+	messageSizeLimitStr := c.String("message-size-limit")
 	smtpSenderAddr := c.String("smtp-sender-addr")
 	smtpSenderUser := c.String("smtp-sender-user")
 	smtpSenderPass := c.String("smtp-sender-pass")
@@ -171,6 +173,12 @@ func execServe(c *cli.Context) error {
 	} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
 		return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
 	}
+	messageSizeLimit, err := parseSize(messageSizeLimitStr, server.DefaultMessageLengthLimit)
+	if err != nil {
+		return err
+	} else if messageSizeLimit > server.MaxMessageLengthLimit {
+		return fmt.Errorf("config option message-size-limit must be lower than %d", server.MaxMessageLengthLimit)
+	}
 
 	// Resolve hosts
 	visitorRequestLimitExemptIPs := make([]string, 0)
@@ -206,6 +214,7 @@ func execServe(c *cli.Context) error {
 	conf.KeepaliveInterval = keepaliveInterval
 	conf.ManagerInterval = managerInterval
 	conf.WebRootIsApp = webRootIsApp
+	conf.MessageLimit = int(messageSizeLimit)
 	conf.SMTPSenderAddr = smtpSenderAddr
 	conf.SMTPSenderUser = smtpSenderUser
 	conf.SMTPSenderPass = smtpSenderPass

+ 3 - 0
go.mod

@@ -14,6 +14,7 @@ require (
 	github.com/mattn/go-sqlite3 v1.14.11
 	github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
 	github.com/stretchr/testify v1.7.0
+	github.com/tidwall/gjson v1.14.0
 	github.com/urfave/cli/v2 v2.3.0
 	golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
 	golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
@@ -38,6 +39,8 @@ require (
 	github.com/pkg/errors v0.9.1 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
 	go.opencensus.io v0.23.0 // indirect
 	golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
 	golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27 // indirect

+ 6 - 0
go.sum

@@ -223,6 +223,12 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
+github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
 github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=

+ 2 - 1
server/config.go

@@ -21,7 +21,8 @@ const (
 // - total topic limit: max number of topics overall
 // - various attachment limits
 const (
-	DefaultMessageLengthLimit       = 4096 // Bytes
+	DefaultMessageLengthLimit       = 4096             // Bytes
+	MaxMessageLengthLimit           = 16 * 1024 * 1024 // 16 MB, sanity size
 	DefaultTotalTopicLimit          = 15000
 	DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB
 	DefaultAttachmentFileSizeLimit  = int64(15 * 1024 * 1024)       // 15 MB

+ 46 - 18
server/server.go

@@ -10,6 +10,7 @@ import (
 	"fmt"
 	"github.com/emersion/go-smtp"
 	"github.com/gorilla/websocket"
+	"github.com/tidwall/gjson"
 	"golang.org/x/sync/errgroup"
 	"heckel.io/ntfy/auth"
 	"heckel.io/ntfy/util"
@@ -397,11 +398,11 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
 		return err
 	}
 	m := newDefaultMessage(t.ID, "")
-	cache, firebase, email, unifiedpush, err := s.parsePublishParams(r, v, m)
+	cache, firebase, email, template, unifiedpush, err := s.parsePublishParams(r, v, m)
 	if err != nil {
 		return err
 	}
-	if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
+	if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
 		return err
 	}
 	if m.Message == "" {
@@ -443,7 +444,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
 	return nil
 }
 
-func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, unifiedpush bool, err error) {
+func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (cache bool, firebase bool, email string, template string, unifiedpush bool, err error) {
 	cache = readBoolParam(r, true, "x-cache", "cache")
 	firebase = readBoolParam(r, true, "x-firebase", "firebase")
 	m.Title = readParam(r, "x-title", "title", "t")
@@ -458,7 +459,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	}
 	if attach != "" {
 		if !attachURLRegex.MatchString(attach) {
-			return false, false, "", false, errHTTPBadRequestAttachmentURLInvalid
+			return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid
 		}
 		m.Attachment.URL = attach
 		if m.Attachment.Name == "" {
@@ -477,11 +478,11 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
 	if email != "" {
 		if err := v.EmailAllowed(); err != nil {
-			return false, false, "", false, errHTTPTooManyRequestsLimitEmails
+			return false, false, "", "", false, errHTTPTooManyRequestsLimitEmails
 		}
 	}
 	if s.mailer == nil && email != "" {
-		return false, false, "", false, errHTTPBadRequestEmailDisabled
+		return false, false, "", "", false, errHTTPBadRequestEmailDisabled
 	}
 	messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
 	if messageStr != "" {
@@ -489,7 +490,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	}
 	m.Priority, err = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
 	if err != nil {
-		return false, false, "", false, errHTTPBadRequestPriorityInvalid
+		return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
 	}
 	tagsStr := readParam(r, "x-tags", "tags", "tag", "ta")
 	if tagsStr != "" {
@@ -501,27 +502,33 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 	delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
 	if delayStr != "" {
 		if !cache {
-			return false, false, "", false, errHTTPBadRequestDelayNoCache
+			return false, false, "", "", false, errHTTPBadRequestDelayNoCache
 		}
 		if email != "" {
-			return false, false, "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
+			return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
 		}
 		delay, err := util.ParseFutureTime(delayStr, time.Now())
 		if err != nil {
-			return false, false, "", false, errHTTPBadRequestDelayCannotParse
+			return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
 		} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
-			return false, false, "", false, errHTTPBadRequestDelayTooSmall
+			return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
 		} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
-			return false, false, "", false, errHTTPBadRequestDelayTooLarge
+			return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
 		}
 		m.Time = delay.Unix()
 	}
+	template = readParam(r, "x-template", "template", "tpl")
+	if template != "" {
+		if template != "json" {
+			return false, false, "", "", false, errors.New("invalid template")
+		}
+	}
 	unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
 	if unifiedpush {
 		firebase = false
 		unifiedpush = true
 	}
-	return cache, firebase, email, unifiedpush, nil
+	return cache, firebase, email, template, unifiedpush, nil
 }
 
 // handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
@@ -536,15 +543,15 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
 //    If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
 // 5. curl -T file.txt ntfy.sh/mytopic
 //    If file.txt is > message limit, treat it as an attachment
-func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser, unifiedpush bool) error {
+func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser, template string, unifiedpush bool) error {
 	if unifiedpush {
 		return s.handleBodyAsMessageAutoDetect(m, body) // Case 1
 	} else if m.Attachment != nil && m.Attachment.URL != "" {
-		return s.handleBodyAsTextMessage(m, body) // Case 2
+		return s.handleBodyAsTextMessage(m, body, template) // Case 2
 	} else if m.Attachment != nil && m.Attachment.Name != "" {
 		return s.handleBodyAsAttachment(r, v, m, body) // Case 3
 	} else if !body.LimitReached && utf8.Valid(body.PeakedBytes) {
-		return s.handleBodyAsTextMessage(m, body) // Case 4
+		return s.handleBodyAsTextMessage(m, body, template) // Case 4
 	}
 	return s.handleBodyAsAttachment(r, v, m, body) // Case 5
 }
@@ -559,12 +566,33 @@ func (s *Server) handleBodyAsMessageAutoDetect(m *message, body *util.PeakedRead
 	return nil
 }
 
-func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeakedReadCloser) error {
+func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeakedReadCloser, template string) error {
 	if !utf8.Valid(body.PeakedBytes) {
 		return errHTTPBadRequestMessageNotUTF8
 	}
 	if len(body.PeakedBytes) > 0 { // Empty body should not override message (publish via GET!)
-		m.Message = strings.TrimSpace(string(body.PeakedBytes)) // Truncates the message to the peak limit if required
+		peakedBody := strings.TrimSpace(string(body.PeakedBytes)) // Truncates the message to the peak limit if required
+		if template == "json" && gjson.Valid(peakedBody) {
+			r := regexp.MustCompile(`\${([^}]+)}`)
+			matches := r.FindAllStringSubmatch(m.Message, -1)
+			for _, v := range matches {
+				query := v[1]
+				result := gjson.Get(peakedBody, query)
+				if result.Exists() {
+					m.Message = strings.ReplaceAll(m.Message, fmt.Sprintf("${%s}", query), result.String())
+				}
+			}
+			matches = r.FindAllStringSubmatch(m.Title, -1)
+			for _, v := range matches {
+				query := v[1]
+				result := gjson.Get(peakedBody, query)
+				if result.Exists() {
+					m.Title = strings.ReplaceAll(m.Title, fmt.Sprintf("${%s}", query), result.String())
+				}
+			}
+		} else {
+			m.Message = peakedBody
+		}
 	}
 	if m.Attachment != nil && m.Attachment.Name != "" && m.Message == "" {
 		m.Message = fmt.Sprintf(defaultAttachmentMessage, m.Attachment.Name)