WIP: attachments

pull/82/head
Philipp Heckel 2022-01-04 00:55:08 +01:00
parent eb5b86ffe2
commit 38788bb2e9
9 changed files with 290 additions and 129 deletions

View File

@ -30,7 +30,7 @@ var flagsServe = []cli.Flag{
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultGlobalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),

View File

@ -592,6 +592,26 @@ Here's an example with a custom message, tags and a priority:
file_get_contents('https://ntfy.sh/mywebhook/publish?message=Webhook+triggered&priority=high&tags=warning,skull');
```
## Send files + URLs
```
curl -T image.jpg ntfy.sh/howdy
curl \
-T flower.jpg \
-H "Message: Here's a flower for you" \
-H "Filename: flower.jpg" \
ntfy.sh/howdy
curl \
-T files.zip \
"ntfy.sh/howdy?m=Important+documents+attached"
curl \
-d "A link for you" \
-H "Link: https://unifiedpush.org" \
"ntfy.sh/howdy"
```
## E-mail notifications
You can forward messages to e-mail by specifying an address in the header. This can be useful for messages that
you'd like to persist longer, or to blast-notify yourself on all possible channels.
@ -883,6 +903,7 @@ and can be passed as **HTTP headers** or **query parameters in the URL**. They a
| `X-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
| `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) |
| `X-Delay` | `Delay`, `X-At`, `At`, `X-In`, `In` | Timestamp or duration for [delayed delivery](#scheduled-delivery) |
| `X-Filename` | `Filename`, `file`, `f` | XXXXXXXXXXXXXXXX |
| `X-Email` | `X-E-Mail`, `Email`, `E-Mail`, `mail`, `e` | E-mail address for [e-mail notifications](#e-mail-notifications) |
| `X-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |

2
go.mod
View File

@ -27,6 +27,7 @@ require (
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/disintegration/imaging v1.6.2 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
@ -38,6 +39,7 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
golang.org/x/text v0.3.7 // indirect

5
go.sum
View File

@ -89,6 +89,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
@ -264,6 +266,9 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ=
golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=

View File

@ -22,27 +22,35 @@ const (
title TEXT NOT NULL,
priority INT NOT NULL,
tags TEXT NOT NULL,
attachment_name TEXT NOT NULL,
attachment_type TEXT NOT NULL,
attachment_size INT NOT NULL,
attachment_expires INT NOT NULL,
attachment_url TEXT NOT NULL,
published INT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
COMMIT;
`
insertMessageQuery = `INSERT INTO messages (id, time, topic, message, title, priority, tags, published) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
insertMessageQuery = `
INSERT INTO messages (id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectMessagesSinceTimeQuery = `
SELECT id, time, topic, message, title, priority, tags
SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time ASC
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT id, time, topic, message, title, priority, tags
SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY time ASC
`
selectMessagesDueQuery = `
SELECT id, time, topic, message, title, priority, tags
SELECT id, time, topic, message, title, priority, tags, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url
FROM messages
WHERE time <= ? AND published = 0
`
@ -54,7 +62,7 @@ const (
// Schema management queries
const (
currentSchemaVersion = 2
currentSchemaVersion = 3
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@ -78,6 +86,17 @@ const (
migrate1To2AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN published INT NOT NULL DEFAULT(1);
`
// 2 -> 3
migrate2To3AlterMessagesTableQuery = `
BEGIN;
ALTER TABLE messages ADD COLUMN attachment_name TEXT NOT NULL;
ALTER TABLE messages ADD COLUMN attachment_type TEXT NOT NULL;
ALTER TABLE messages ADD COLUMN attachment_size INT NOT NULL;
ALTER TABLE messages ADD COLUMN attachment_expires INT NOT NULL;
ALTER TABLE messages ADD COLUMN attachment_url TEXT NOT NULL;
COMMIT;
`
)
type sqliteCache struct {
@ -104,7 +123,32 @@ func (c *sqliteCache) AddMessage(m *message) error {
return errUnexpectedMessageType
}
published := m.Time <= time.Now().Unix()
_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","), published)
tags := strings.Join(m.Tags, ",")
var attachmentName, attachmentType, attachmentURL string
var attachmentSize, attachmentExpires int64
if m.Attachment != nil {
attachmentName = m.Attachment.Name
attachmentType = m.Attachment.Type
attachmentSize = m.Attachment.Size
attachmentExpires = m.Attachment.Expires
attachmentURL = m.Attachment.URL
}
_, err := c.db.Exec(
insertMessageQuery,
m.ID,
m.Time,
m.Topic,
m.Message,
m.Title,
m.Priority,
tags,
attachmentName,
attachmentType,
attachmentSize,
attachmentExpires,
attachmentURL,
published,
)
return err
}
@ -185,16 +229,26 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
defer rows.Close()
messages := make([]*message, 0)
for rows.Next() {
var timestamp int64
var timestamp, attachmentSize, attachmentExpires int64
var priority int
var id, topic, msg, title, tagsStr string
if err := rows.Scan(&id, &timestamp, &topic, &msg, &title, &priority, &tagsStr); err != nil {
var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentURL string
if err := rows.Scan(&id, &timestamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentURL); err != nil {
return nil, err
}
var tags []string
if tagsStr != "" {
tags = strings.Split(tagsStr, ",")
}
var att *attachment
if attachmentName != "" && attachmentURL != "" {
att = &attachment{
Name: attachmentName,
Type: attachmentType,
Size: attachmentSize,
Expires: attachmentExpires,
URL: attachmentURL,
}
}
messages = append(messages, &message{
ID: id,
Time: timestamp,
@ -204,6 +258,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
Title: title,
Priority: priority,
Tags: tags,
Attachment: att,
})
}
if err := rows.Err(); err != nil {
@ -241,6 +296,8 @@ func setupDB(db *sql.DB) error {
return migrateFrom0(db)
} else if schemaVersion == 1 {
return migrateFrom1(db)
} else if schemaVersion == 2 {
return migrateFrom2(db)
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
@ -280,5 +337,16 @@ func migrateFrom1(db *sql.DB) error {
if _, err := db.Exec(updateSchemaVersion, 2); err != nil {
return err
}
return migrateFrom2(db)
}
func migrateFrom2(db *sql.DB) error {
log.Print("Migrating cache database schema: from 2 to 3")
if _, err := db.Exec(migrate2To3AlterMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(updateSchemaVersion, 3); err != nil {
return err
}
return nil // Update this when a new version is added
}

View File

@ -15,21 +15,25 @@ const (
DefaultMaxDelay = 3 * 24 * time.Hour
DefaultMessageLimit = 4096
DefaultAttachmentSizeLimit = 5 * 1024 * 1024
DefaultAttachmentExpiryDuration = 3 * time.Hour
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
)
// Defines all the limits
// - global topic limit: max number of topics overall
// - total topic limit: max number of topics overall
// - per visitor subscription limit: max number of subscriptions (active HTTP connections) per per-visitor/IP
// - per visitor request limit: max number of PUT/GET/.. requests (here: 60 requests bucket, replenished at a rate of one per 10 seconds)
// - per visitor email limit: max number of emails (here: 16 email bucket, replenished at a rate of one per hour)
// - per visitor subscription limit: max number of subscriptions (active HTTP connections) per per-visitor/IP
// - per visitor attachment size limit:
const (
DefaultGlobalTopicLimit = 5000
DefaultTotalTopicLimit = 5000
DefaultVisitorSubscriptionLimit = 30
DefaultVisitorRequestLimitBurst = 60
DefaultVisitorRequestLimitReplenish = 10 * time.Second
DefaultVisitorEmailLimitBurst = 16
DefaultVisitorEmailLimitReplenish = time.Hour
DefaultVisitorSubscriptionLimit = 30
DefaultVisitorAttachmentBytesLimitBurst = 50 * 1024 * 1024
DefaultVisitorAttachmentBytesLimitReplenish = time.Hour
)
// Config is the main config struct for the application. Use New to instantiate a default config struct.
@ -44,6 +48,7 @@ type Config struct {
CacheDuration time.Duration
AttachmentCacheDir string
AttachmentSizeLimit int64
AttachmentExpiryDuration time.Duration
KeepaliveInterval time.Duration
ManagerInterval time.Duration
AtSenderInterval time.Duration
@ -60,11 +65,13 @@ type Config struct {
MaxDelay time.Duration
TotalTopicLimit int
TotalAttachmentSizeLimit int64
VisitorSubscriptionLimit int
VisitorRequestLimitBurst int
VisitorRequestLimitReplenish time.Duration
VisitorEmailLimitBurst int
VisitorEmailLimitReplenish time.Duration
VisitorSubscriptionLimit int
VisitorAttachmentBytesLimitBurst int64
VisitorAttachmentBytesLimitReplenish time.Duration
BehindProxy bool
}
@ -81,6 +88,7 @@ func NewConfig() *Config {
CacheDuration: DefaultCacheDuration,
AttachmentCacheDir: "",
AttachmentSizeLimit: DefaultAttachmentSizeLimit,
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
KeepaliveInterval: DefaultKeepaliveInterval,
ManagerInterval: DefaultManagerInterval,
MessageLimit: DefaultMessageLimit,
@ -88,12 +96,14 @@ func NewConfig() *Config {
MaxDelay: DefaultMaxDelay,
AtSenderInterval: DefaultAtSenderInterval,
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
TotalTopicLimit: DefaultGlobalTopicLimit,
TotalTopicLimit: DefaultTotalTopicLimit,
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
VisitorAttachmentBytesLimitBurst: DefaultVisitorAttachmentBytesLimitBurst,
VisitorAttachmentBytesLimitReplenish: DefaultVisitorAttachmentBytesLimitReplenish,
BehindProxy: false,
}
}

View File

@ -32,6 +32,8 @@ type message struct {
type attachment struct {
Name string `json:"name"`
Type string `json:"type"`
Size int64 `json:"size"`
Expires int64 `json:"expires"`
URL string `json:"url"`
}

View File

@ -9,6 +9,7 @@ import (
firebase "firebase.google.com/go"
"firebase.google.com/go/messaging"
"fmt"
"github.com/disintegration/imaging"
"github.com/emersion/go-smtp"
"google.golang.org/api/option"
"heckel.io/ntfy/util"
@ -101,7 +102,8 @@ var (
staticRegex = regexp.MustCompile(`^/static/.+`)
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
disallowedTopics = []string{"docs", "static", "file"}
previewRegex = regexp.MustCompile(`^/preview/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
disallowedTopics = []string{"docs", "static", "file", "preview"}
templateFnMap = template.FuncMap{
"durationToHuman": util.DurationToHuman,
@ -123,6 +125,7 @@ var (
docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPNotFoundTooLarge = &errHTTP{40402, http.StatusNotFound, "page not found: preview not available, file too large", ""}
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
errHTTPTooManyRequestsLimitSubscriptions = &errHTTP{42903, http.StatusTooManyRequests, "limit reached: too many active subscriptions, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
@ -137,9 +140,8 @@ var (
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"}
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40011, http.StatusBadRequest, "attachments disallowed", ""}
errHTTPBadRequestAttachmentsPublishDisallowed = &errHTTP{40011, http.StatusBadRequest, "invalid message: invalid encoding or too large, and attachments are not allowed", ""}
errHTTPBadRequestMessageTooLarge = &errHTTP{40013, http.StatusBadRequest, "invalid message: too large", ""}
errHTTPBadRequestInvalidMessage = &errHTTP{40011, http.StatusBadRequest, "invalid message: invalid encoding or too large, and attachments are not allowed", ""}
errHTTPBadRequestMessageTooLarge = &errHTTP{40012, http.StatusBadRequest, "invalid message: too large", ""}
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""}
)
@ -226,6 +228,13 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) {
"title": m.Title,
"message": m.Message,
}
if m.Attachment != nil {
data["attachment_name"] = m.Attachment.Name
data["attachment_type"] = m.Attachment.Type
data["attachment_size"] = fmt.Sprintf("%d", m.Attachment.Size)
data["attachment_expires"] = fmt.Sprintf("%d", m.Attachment.Expires)
data["attachment_url"] = m.Attachment.URL
}
}
_, err := msg.Send(context.Background(), &messaging.Message{
Topic: m.Topic,
@ -316,8 +325,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
return s.handleStatic(w, r)
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
return s.handleDocs(w, r)
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) {
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
return s.handleFile(w, r)
} else if r.Method == http.MethodGet && previewRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
return s.handlePreview(w, r)
} else if r.Method == http.MethodOptions {
return s.handleOptions(w, r)
} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) {
@ -375,7 +386,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error {
if s.config.AttachmentCacheDir == "" {
return errHTTPBadRequestAttachmentsDisallowed
return errHTTPInternalError
}
matches := fileRegex.FindStringSubmatch(r.URL.Path)
if len(matches) != 2 {
@ -397,6 +408,39 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error {
return err
}
func (s *Server) handlePreview(w http.ResponseWriter, r *http.Request) error {
if s.config.AttachmentCacheDir == "" {
return errHTTPInternalError
}
matches := previewRegex.FindStringSubmatch(r.URL.Path)
if len(matches) != 2 {
return errHTTPInternalErrorInvalidFilePath
}
messageID := matches[1]
file := filepath.Join(s.config.AttachmentCacheDir, messageID)
stat, err := os.Stat(file)
if err != nil {
return errHTTPNotFound
}
if stat.Size() > 20*1024*1024 {
return errHTTPInternalError
}
img, err := imaging.Open(file)
if err != nil {
return errHTTPNotFoundTooLarge
}
var width, height int
if width >= height {
width = 200
height = int(float32(img.Bounds().Dy()) / float32(img.Bounds().Dx()) * float32(width))
} else {
height = 200
width = int(float32(img.Bounds().Dx()) / float32(img.Bounds().Dy()) * float32(height))
}
preview := imaging.Resize(img, width, height, imaging.Lanczos)
return imaging.Encode(w, preview, imaging.PNG)
}
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
t, err := s.topicFromPath(r.URL.Path)
if err != nil {
@ -409,9 +453,13 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
m := newDefaultMessage(t.ID, "")
if !body.LimitReached && utf8.Valid(body.PeakedBytes) {
m.Message = strings.TrimSpace(string(body.PeakedBytes))
} else if err := s.writeAttachment(v, m, body); err != nil {
} else if s.config.AttachmentCacheDir != "" {
if err := s.writeAttachment(r, v, m, body); err != nil {
return err
}
} else {
return errHTTPBadRequestInvalidMessage
}
cache, firebase, email, err := s.parsePublishParams(r, m)
if err != nil {
return err
@ -522,29 +570,30 @@ func readParam(r *http.Request, names ...string) string {
return ""
}
func (s *Server) writeAttachment(v *visitor, m *message, body *util.PeakedReadCloser) error {
if s.config.AttachmentCacheDir == "" || !util.FileExists(s.config.AttachmentCacheDir) {
return errHTTPBadRequestAttachmentsPublishDisallowed
func (s *Server) writeAttachment(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser) error {
if s.config.AttachmentCacheDir == "" {
return errHTTPBadRequestInvalidMessage
}
contentType := http.DetectContentType(body.PeakedBytes)
exts, err := mime.ExtensionsByType(contentType)
if err != nil {
return err
}
ext := ".bin"
if len(exts) > 0 {
exts, err := mime.ExtensionsByType(contentType)
if err == nil && len(exts) > 0 {
ext = exts[0]
}
filename := fmt.Sprintf("attachment%s", ext)
filename := readParam(r, "x-filename", "filename", "file", "f")
if filename == "" {
filename = fmt.Sprintf("attachment%s", ext)
}
file := filepath.Join(s.config.AttachmentCacheDir, m.ID)
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
if err != nil {
return err
}
defer f.Close()
fileSizeLimiter := util.NewLimiter(s.config.AttachmentSizeLimit)
limitWriter := util.NewLimitWriter(f, fileSizeLimiter)
if _, err := io.Copy(limitWriter, body); err != nil {
maxSizeLimiter := util.NewLimiter(s.config.AttachmentSizeLimit) //FIXME visitor limit
limitWriter := util.NewLimitWriter(f, maxSizeLimiter)
size, err := io.Copy(limitWriter, body)
if err != nil {
os.Remove(file)
if err == util.ErrLimitReached {
return errHTTPBadRequestMessageTooLarge
@ -555,10 +604,12 @@ func (s *Server) writeAttachment(v *visitor, m *message, body *util.PeakedReadCl
os.Remove(file)
return err
}
m.Message = fmt.Sprintf("You received a file: %s", filename)
m.Message = fmt.Sprintf("You received a file: %s", filename) // May be overwritten later
m.Attachment = &attachment{
Name: filename,
Type: contentType,
Size: size,
Expires: time.Now().Add(s.config.AttachmentExpiryDuration).Unix(),
URL: fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext),
}
return nil

View File

@ -24,8 +24,9 @@ type visitor struct {
config *Config
ip string
requests *rate.Limiter
emails *rate.Limiter
subscriptions *util.Limiter
emails *rate.Limiter
attachments *rate.Limiter
seen time.Time
mu sync.Mutex
}
@ -35,8 +36,9 @@ func newVisitor(conf *Config, ip string) *visitor {
config: conf,
ip: ip,
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
//attachments: rate.NewLimiter(rate.Every(conf.VisitorAttachmentBytesLimitReplenish * 1024), conf.VisitorAttachmentBytesLimitBurst),
seen: time.Now(),
}
}