From 96a12d98c9abd69f9f88dd89baafa69293567296 Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Tue, 15 Mar 2022 11:39:51 -0400 Subject: [PATCH] WIP: Templating --- cmd/serve.go | 9 +++++++ go.mod | 3 +++ go.sum | 6 +++++ server/config.go | 3 ++- server/server.go | 64 ++++++++++++++++++++++++++++++++++-------------- 5 files changed, 66 insertions(+), 19 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 2fd878d5..d56d9e51 100644 --- a/cmd/serve.go +++ b/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 diff --git a/go.mod b/go.mod index 6fda5029..9c6689ce 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index 03148b23..c3df8b58 100644 --- a/go.sum +++ b/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= diff --git a/server/config.go b/server/config.go index e866e17a..268bc437 100644 --- a/server/config.go +++ b/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 diff --git a/server/server.go b/server/server.go index e1ed94b8..2ba53681 100644 --- a/server/server.go +++ b/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)