WIP: attachments
This commit is contained in:
parent
eb5b86ffe2
commit
38788bb2e9
9 changed files with 290 additions and 129 deletions
|
@ -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-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-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.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-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.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)"}),
|
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)"}),
|
||||||
|
|
|
@ -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');
|
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
|
## 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 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.
|
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-Priority` | `Priority`, `prio`, `p` | [Message priority](#message-priority) |
|
||||||
| `X-Tags` | `Tags`, `Tag`, `ta` | [Tags and emojis](#tags-emojis) |
|
| `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-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-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-Cache` | `Cache` | Allows disabling [message caching](#message-caching) |
|
||||||
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
| `X-Firebase` | `Firebase` | Allows disabling [sending to Firebase](#disable-firebase) |
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -27,6 +27,7 @@ require (
|
||||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
|
||||||
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // 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/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
|
||||||
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
|
github.com/envoyproxy/go-control-plane v0.10.1 // indirect
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.6.2 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
go.opencensus.io v0.23.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/net v0.0.0-20210813160813-60bc85c4be6d // indirect
|
||||||
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
|
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
|
||||||
golang.org/x/text v0.3.7 // indirect
|
golang.org/x/text v0.3.7 // indirect
|
||||||
|
|
5
go.sum
5
go.sum
|
@ -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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
|
||||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
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=
|
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/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-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-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-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-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
|
|
|
@ -22,27 +22,35 @@ const (
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
priority INT NOT NULL,
|
priority INT NOT NULL,
|
||||||
tags TEXT 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
|
published INT NOT NULL
|
||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
CREATE INDEX IF NOT EXISTS idx_topic ON messages (topic);
|
||||||
COMMIT;
|
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`
|
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
|
||||||
selectMessagesSinceTimeQuery = `
|
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
|
FROM messages
|
||||||
WHERE topic = ? AND time >= ? AND published = 1
|
WHERE topic = ? AND time >= ? AND published = 1
|
||||||
ORDER BY time ASC
|
ORDER BY time ASC
|
||||||
`
|
`
|
||||||
selectMessagesSinceTimeIncludeScheduledQuery = `
|
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
|
FROM messages
|
||||||
WHERE topic = ? AND time >= ?
|
WHERE topic = ? AND time >= ?
|
||||||
ORDER BY time ASC
|
ORDER BY time ASC
|
||||||
`
|
`
|
||||||
selectMessagesDueQuery = `
|
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
|
FROM messages
|
||||||
WHERE time <= ? AND published = 0
|
WHERE time <= ? AND published = 0
|
||||||
`
|
`
|
||||||
|
@ -54,7 +62,7 @@ const (
|
||||||
|
|
||||||
// Schema management queries
|
// Schema management queries
|
||||||
const (
|
const (
|
||||||
currentSchemaVersion = 2
|
currentSchemaVersion = 3
|
||||||
createSchemaVersionTableQuery = `
|
createSchemaVersionTableQuery = `
|
||||||
CREATE TABLE IF NOT EXISTS schemaVersion (
|
CREATE TABLE IF NOT EXISTS schemaVersion (
|
||||||
id INT PRIMARY KEY,
|
id INT PRIMARY KEY,
|
||||||
|
@ -78,6 +86,17 @@ const (
|
||||||
migrate1To2AlterMessagesTableQuery = `
|
migrate1To2AlterMessagesTableQuery = `
|
||||||
ALTER TABLE messages ADD COLUMN published INT NOT NULL DEFAULT(1);
|
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 {
|
type sqliteCache struct {
|
||||||
|
@ -104,7 +123,32 @@ func (c *sqliteCache) AddMessage(m *message) error {
|
||||||
return errUnexpectedMessageType
|
return errUnexpectedMessageType
|
||||||
}
|
}
|
||||||
published := m.Time <= time.Now().Unix()
|
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
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,25 +229,36 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
messages := make([]*message, 0)
|
messages := make([]*message, 0)
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var timestamp int64
|
var timestamp, attachmentSize, attachmentExpires int64
|
||||||
var priority int
|
var priority int
|
||||||
var id, topic, msg, title, tagsStr string
|
var id, topic, msg, title, tagsStr, attachmentName, attachmentType, attachmentURL string
|
||||||
if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr); err != nil {
|
if err := rows.Scan(&id, ×tamp, &topic, &msg, &title, &priority, &tagsStr, &attachmentName, &attachmentType, &attachmentSize, &attachmentExpires, &attachmentURL); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
var tags []string
|
var tags []string
|
||||||
if tagsStr != "" {
|
if tagsStr != "" {
|
||||||
tags = strings.Split(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{
|
messages = append(messages, &message{
|
||||||
ID: id,
|
ID: id,
|
||||||
Time: timestamp,
|
Time: timestamp,
|
||||||
Event: messageEvent,
|
Event: messageEvent,
|
||||||
Topic: topic,
|
Topic: topic,
|
||||||
Message: msg,
|
Message: msg,
|
||||||
Title: title,
|
Title: title,
|
||||||
Priority: priority,
|
Priority: priority,
|
||||||
Tags: tags,
|
Tags: tags,
|
||||||
|
Attachment: att,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err := rows.Err(); err != nil {
|
if err := rows.Err(); err != nil {
|
||||||
|
@ -241,6 +296,8 @@ func setupDB(db *sql.DB) error {
|
||||||
return migrateFrom0(db)
|
return migrateFrom0(db)
|
||||||
} else if schemaVersion == 1 {
|
} else if schemaVersion == 1 {
|
||||||
return migrateFrom1(db)
|
return migrateFrom1(db)
|
||||||
|
} else if schemaVersion == 2 {
|
||||||
|
return migrateFrom2(db)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
|
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 {
|
if _, err := db.Exec(updateSchemaVersion, 2); err != nil {
|
||||||
return err
|
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
|
return nil // Update this when a new version is added
|
||||||
}
|
}
|
||||||
|
|
138
server/config.go
138
server/config.go
|
@ -15,85 +15,95 @@ const (
|
||||||
DefaultMaxDelay = 3 * 24 * time.Hour
|
DefaultMaxDelay = 3 * 24 * time.Hour
|
||||||
DefaultMessageLimit = 4096
|
DefaultMessageLimit = 4096
|
||||||
DefaultAttachmentSizeLimit = 5 * 1024 * 1024
|
DefaultAttachmentSizeLimit = 5 * 1024 * 1024
|
||||||
|
DefaultAttachmentExpiryDuration = 3 * time.Hour
|
||||||
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
|
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // Not too frequently to save battery
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines all the limits
|
// 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 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 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 (
|
const (
|
||||||
DefaultGlobalTopicLimit = 5000
|
DefaultTotalTopicLimit = 5000
|
||||||
DefaultVisitorRequestLimitBurst = 60
|
DefaultVisitorSubscriptionLimit = 30
|
||||||
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
DefaultVisitorRequestLimitBurst = 60
|
||||||
DefaultVisitorEmailLimitBurst = 16
|
DefaultVisitorRequestLimitReplenish = 10 * time.Second
|
||||||
DefaultVisitorEmailLimitReplenish = time.Hour
|
DefaultVisitorEmailLimitBurst = 16
|
||||||
DefaultVisitorSubscriptionLimit = 30
|
DefaultVisitorEmailLimitReplenish = time.Hour
|
||||||
|
DefaultVisitorAttachmentBytesLimitBurst = 50 * 1024 * 1024
|
||||||
|
DefaultVisitorAttachmentBytesLimitReplenish = time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config is the main config struct for the application. Use New to instantiate a default config struct.
|
// Config is the main config struct for the application. Use New to instantiate a default config struct.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
BaseURL string
|
BaseURL string
|
||||||
ListenHTTP string
|
ListenHTTP string
|
||||||
ListenHTTPS string
|
ListenHTTPS string
|
||||||
KeyFile string
|
KeyFile string
|
||||||
CertFile string
|
CertFile string
|
||||||
FirebaseKeyFile string
|
FirebaseKeyFile string
|
||||||
CacheFile string
|
CacheFile string
|
||||||
CacheDuration time.Duration
|
CacheDuration time.Duration
|
||||||
AttachmentCacheDir string
|
AttachmentCacheDir string
|
||||||
AttachmentSizeLimit int64
|
AttachmentSizeLimit int64
|
||||||
KeepaliveInterval time.Duration
|
AttachmentExpiryDuration time.Duration
|
||||||
ManagerInterval time.Duration
|
KeepaliveInterval time.Duration
|
||||||
AtSenderInterval time.Duration
|
ManagerInterval time.Duration
|
||||||
FirebaseKeepaliveInterval time.Duration
|
AtSenderInterval time.Duration
|
||||||
SMTPSenderAddr string
|
FirebaseKeepaliveInterval time.Duration
|
||||||
SMTPSenderUser string
|
SMTPSenderAddr string
|
||||||
SMTPSenderPass string
|
SMTPSenderUser string
|
||||||
SMTPSenderFrom string
|
SMTPSenderPass string
|
||||||
SMTPServerListen string
|
SMTPSenderFrom string
|
||||||
SMTPServerDomain string
|
SMTPServerListen string
|
||||||
SMTPServerAddrPrefix string
|
SMTPServerDomain string
|
||||||
MessageLimit int
|
SMTPServerAddrPrefix string
|
||||||
MinDelay time.Duration
|
MessageLimit int
|
||||||
MaxDelay time.Duration
|
MinDelay time.Duration
|
||||||
TotalTopicLimit int
|
MaxDelay time.Duration
|
||||||
TotalAttachmentSizeLimit int64
|
TotalTopicLimit int
|
||||||
VisitorRequestLimitBurst int
|
TotalAttachmentSizeLimit int64
|
||||||
VisitorRequestLimitReplenish time.Duration
|
VisitorSubscriptionLimit int
|
||||||
VisitorEmailLimitBurst int
|
VisitorRequestLimitBurst int
|
||||||
VisitorEmailLimitReplenish time.Duration
|
VisitorRequestLimitReplenish time.Duration
|
||||||
VisitorSubscriptionLimit int
|
VisitorEmailLimitBurst int
|
||||||
BehindProxy bool
|
VisitorEmailLimitReplenish time.Duration
|
||||||
|
VisitorAttachmentBytesLimitBurst int64
|
||||||
|
VisitorAttachmentBytesLimitReplenish time.Duration
|
||||||
|
BehindProxy bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig instantiates a default new server config
|
// NewConfig instantiates a default new server config
|
||||||
func NewConfig() *Config {
|
func NewConfig() *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
BaseURL: "",
|
BaseURL: "",
|
||||||
ListenHTTP: DefaultListenHTTP,
|
ListenHTTP: DefaultListenHTTP,
|
||||||
ListenHTTPS: "",
|
ListenHTTPS: "",
|
||||||
KeyFile: "",
|
KeyFile: "",
|
||||||
CertFile: "",
|
CertFile: "",
|
||||||
FirebaseKeyFile: "",
|
FirebaseKeyFile: "",
|
||||||
CacheFile: "",
|
CacheFile: "",
|
||||||
CacheDuration: DefaultCacheDuration,
|
CacheDuration: DefaultCacheDuration,
|
||||||
AttachmentCacheDir: "",
|
AttachmentCacheDir: "",
|
||||||
AttachmentSizeLimit: DefaultAttachmentSizeLimit,
|
AttachmentSizeLimit: DefaultAttachmentSizeLimit,
|
||||||
KeepaliveInterval: DefaultKeepaliveInterval,
|
AttachmentExpiryDuration: DefaultAttachmentExpiryDuration,
|
||||||
ManagerInterval: DefaultManagerInterval,
|
KeepaliveInterval: DefaultKeepaliveInterval,
|
||||||
MessageLimit: DefaultMessageLimit,
|
ManagerInterval: DefaultManagerInterval,
|
||||||
MinDelay: DefaultMinDelay,
|
MessageLimit: DefaultMessageLimit,
|
||||||
MaxDelay: DefaultMaxDelay,
|
MinDelay: DefaultMinDelay,
|
||||||
AtSenderInterval: DefaultAtSenderInterval,
|
MaxDelay: DefaultMaxDelay,
|
||||||
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
AtSenderInterval: DefaultAtSenderInterval,
|
||||||
TotalTopicLimit: DefaultGlobalTopicLimit,
|
FirebaseKeepaliveInterval: DefaultFirebaseKeepaliveInterval,
|
||||||
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||||
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
VisitorRequestLimitBurst: DefaultVisitorRequestLimitBurst,
|
||||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
VisitorRequestLimitReplenish: DefaultVisitorRequestLimitReplenish,
|
||||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||||
BehindProxy: false,
|
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||||
|
VisitorAttachmentBytesLimitBurst: DefaultVisitorAttachmentBytesLimitBurst,
|
||||||
|
VisitorAttachmentBytesLimitReplenish: DefaultVisitorAttachmentBytesLimitReplenish,
|
||||||
|
BehindProxy: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,11 @@ type message struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type attachment struct {
|
type attachment struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
URL string `json:"url"`
|
Size int64 `json:"size"`
|
||||||
|
Expires int64 `json:"expires"`
|
||||||
|
URL string `json:"url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// messageEncoder is a function that knows how to encode a message
|
// messageEncoder is a function that knows how to encode a message
|
||||||
|
|
133
server/server.go
133
server/server.go
|
@ -9,6 +9,7 @@ import (
|
||||||
firebase "firebase.google.com/go"
|
firebase "firebase.google.com/go"
|
||||||
"firebase.google.com/go/messaging"
|
"firebase.google.com/go/messaging"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/disintegration/imaging"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"google.golang.org/api/option"
|
"google.golang.org/api/option"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
|
@ -101,7 +102,8 @@ var (
|
||||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
staticRegex = regexp.MustCompile(`^/static/.+`)
|
||||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||||
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
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{
|
templateFnMap = template.FuncMap{
|
||||||
"durationToHuman": util.DurationToHuman,
|
"durationToHuman": util.DurationToHuman,
|
||||||
|
@ -122,26 +124,26 @@ var (
|
||||||
docsStaticFs embed.FS
|
docsStaticFs embed.FS
|
||||||
docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
|
docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
|
||||||
|
|
||||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
|
||||||
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
errHTTPNotFoundTooLarge = &errHTTP{40402, http.StatusNotFound, "page not found: preview not available, file too large", ""}
|
||||||
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
errHTTPTooManyRequestsLimitRequests = &errHTTP{42901, http.StatusTooManyRequests, "limit reached: too many requests, 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"}
|
errHTTPTooManyRequestsLimitEmails = &errHTTP{42902, http.StatusTooManyRequests, "limit reached: too many emails, please be nice", "https://ntfy.sh/docs/publish/#limitations"}
|
||||||
errHTTPTooManyRequestsLimitGlobalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "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"}
|
||||||
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications"}
|
errHTTPTooManyRequestsLimitGlobalTopics = &errHTTP{42904, http.StatusTooManyRequests, "limit reached: the total number of topics on the server has been reached, please contact the admin", "https://ntfy.sh/docs/publish/#limitations"}
|
||||||
errHTTPBadRequestDelayNoCache = &errHTTP{40002, http.StatusBadRequest, "cannot disable cache for delayed message", ""}
|
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications"}
|
||||||
errHTTPBadRequestDelayNoEmail = &errHTTP{40003, http.StatusBadRequest, "delayed e-mail notifications are not supported", ""}
|
errHTTPBadRequestDelayNoCache = &errHTTP{40002, http.StatusBadRequest, "cannot disable cache for delayed message", ""}
|
||||||
errHTTPBadRequestDelayCannotParse = &errHTTP{40004, http.StatusBadRequest, "invalid delay parameter: unable to parse delay", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
errHTTPBadRequestDelayNoEmail = &errHTTP{40003, http.StatusBadRequest, "delayed e-mail notifications are not supported", ""}
|
||||||
errHTTPBadRequestDelayTooSmall = &errHTTP{40005, http.StatusBadRequest, "invalid delay parameter: too small, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
errHTTPBadRequestDelayCannotParse = &errHTTP{40004, http.StatusBadRequest, "invalid delay parameter: unable to parse delay", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
||||||
errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
errHTTPBadRequestDelayTooSmall = &errHTTP{40005, http.StatusBadRequest, "invalid delay parameter: too small, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
||||||
errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority"}
|
errHTTPBadRequestDelayTooLarge = &errHTTP{40006, http.StatusBadRequest, "invalid delay parameter: too large, please refer to the docs", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
|
||||||
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"}
|
errHTTPBadRequestPriorityInvalid = &errHTTP{40007, http.StatusBadRequest, "invalid priority parameter", "https://ntfy.sh/docs/publish/#message-priority"}
|
||||||
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
errHTTPBadRequestSinceInvalid = &errHTTP{40008, http.StatusBadRequest, "invalid since parameter", "https://ntfy.sh/docs/subscribe/api/#fetch-cached-messages"}
|
||||||
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
errHTTPBadRequestTopicInvalid = &errHTTP{40009, http.StatusBadRequest, "invalid topic: path invalid", ""}
|
||||||
errHTTPBadRequestAttachmentsDisallowed = &errHTTP{40011, http.StatusBadRequest, "attachments disallowed", ""}
|
errHTTPBadRequestTopicDisallowed = &errHTTP{40010, http.StatusBadRequest, "invalid topic: topic name is disallowed", ""}
|
||||||
errHTTPBadRequestAttachmentsPublishDisallowed = &errHTTP{40011, http.StatusBadRequest, "invalid message: invalid encoding or too large, and attachments are not allowed", ""}
|
errHTTPBadRequestInvalidMessage = &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", ""}
|
errHTTPBadRequestMessageTooLarge = &errHTTP{40012, http.StatusBadRequest, "invalid message: too large", ""}
|
||||||
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
|
errHTTPInternalError = &errHTTP{50001, http.StatusInternalServerError, "internal server error", ""}
|
||||||
errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""}
|
errHTTPInternalErrorInvalidFilePath = &errHTTP{50002, http.StatusInternalServerError, "internal server error: invalid file path", ""}
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -226,6 +228,13 @@ func createFirebaseSubscriber(conf *Config) (subscriber, error) {
|
||||||
"title": m.Title,
|
"title": m.Title,
|
||||||
"message": m.Message,
|
"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{
|
_, err := msg.Send(context.Background(), &messaging.Message{
|
||||||
Topic: m.Topic,
|
Topic: m.Topic,
|
||||||
|
@ -316,8 +325,10 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
|
||||||
return s.handleStatic(w, r)
|
return s.handleStatic(w, r)
|
||||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
||||||
return s.handleDocs(w, r)
|
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)
|
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 {
|
} else if r.Method == http.MethodOptions {
|
||||||
return s.handleOptions(w, r)
|
return s.handleOptions(w, r)
|
||||||
} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) {
|
} 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 {
|
func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error {
|
||||||
if s.config.AttachmentCacheDir == "" {
|
if s.config.AttachmentCacheDir == "" {
|
||||||
return errHTTPBadRequestAttachmentsDisallowed
|
return errHTTPInternalError
|
||||||
}
|
}
|
||||||
matches := fileRegex.FindStringSubmatch(r.URL.Path)
|
matches := fileRegex.FindStringSubmatch(r.URL.Path)
|
||||||
if len(matches) != 2 {
|
if len(matches) != 2 {
|
||||||
|
@ -397,6 +408,39 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request) error {
|
||||||
return err
|
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 {
|
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
t, err := s.topicFromPath(r.URL.Path)
|
t, err := s.topicFromPath(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -409,8 +453,12 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visito
|
||||||
m := newDefaultMessage(t.ID, "")
|
m := newDefaultMessage(t.ID, "")
|
||||||
if !body.LimitReached && utf8.Valid(body.PeakedBytes) {
|
if !body.LimitReached && utf8.Valid(body.PeakedBytes) {
|
||||||
m.Message = strings.TrimSpace(string(body.PeakedBytes))
|
m.Message = strings.TrimSpace(string(body.PeakedBytes))
|
||||||
} else if err := s.writeAttachment(v, m, body); err != nil {
|
} else if s.config.AttachmentCacheDir != "" {
|
||||||
return err
|
if err := s.writeAttachment(r, v, m, body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errHTTPBadRequestInvalidMessage
|
||||||
}
|
}
|
||||||
cache, firebase, email, err := s.parsePublishParams(r, m)
|
cache, firebase, email, err := s.parsePublishParams(r, m)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -522,29 +570,30 @@ func readParam(r *http.Request, names ...string) string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) writeAttachment(v *visitor, m *message, body *util.PeakedReadCloser) error {
|
func (s *Server) writeAttachment(r *http.Request, v *visitor, m *message, body *util.PeakedReadCloser) error {
|
||||||
if s.config.AttachmentCacheDir == "" || !util.FileExists(s.config.AttachmentCacheDir) {
|
if s.config.AttachmentCacheDir == "" {
|
||||||
return errHTTPBadRequestAttachmentsPublishDisallowed
|
return errHTTPBadRequestInvalidMessage
|
||||||
}
|
}
|
||||||
contentType := http.DetectContentType(body.PeakedBytes)
|
contentType := http.DetectContentType(body.PeakedBytes)
|
||||||
exts, err := mime.ExtensionsByType(contentType)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ext := ".bin"
|
ext := ".bin"
|
||||||
if len(exts) > 0 {
|
exts, err := mime.ExtensionsByType(contentType)
|
||||||
|
if err == nil && len(exts) > 0 {
|
||||||
ext = 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)
|
file := filepath.Join(s.config.AttachmentCacheDir, m.ID)
|
||||||
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
f, err := os.OpenFile(file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
fileSizeLimiter := util.NewLimiter(s.config.AttachmentSizeLimit)
|
maxSizeLimiter := util.NewLimiter(s.config.AttachmentSizeLimit) //FIXME visitor limit
|
||||||
limitWriter := util.NewLimitWriter(f, fileSizeLimiter)
|
limitWriter := util.NewLimitWriter(f, maxSizeLimiter)
|
||||||
if _, err := io.Copy(limitWriter, body); err != nil {
|
size, err := io.Copy(limitWriter, body)
|
||||||
|
if err != nil {
|
||||||
os.Remove(file)
|
os.Remove(file)
|
||||||
if err == util.ErrLimitReached {
|
if err == util.ErrLimitReached {
|
||||||
return errHTTPBadRequestMessageTooLarge
|
return errHTTPBadRequestMessageTooLarge
|
||||||
|
@ -555,11 +604,13 @@ func (s *Server) writeAttachment(v *visitor, m *message, body *util.PeakedReadCl
|
||||||
os.Remove(file)
|
os.Remove(file)
|
||||||
return err
|
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{
|
m.Attachment = &attachment{
|
||||||
Name: filename,
|
Name: filename,
|
||||||
Type: contentType,
|
Type: contentType,
|
||||||
URL: fmt.Sprintf("%s/file/%s%s", s.config.BaseURL, m.ID, ext),
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,8 +24,9 @@ type visitor struct {
|
||||||
config *Config
|
config *Config
|
||||||
ip string
|
ip string
|
||||||
requests *rate.Limiter
|
requests *rate.Limiter
|
||||||
emails *rate.Limiter
|
|
||||||
subscriptions *util.Limiter
|
subscriptions *util.Limiter
|
||||||
|
emails *rate.Limiter
|
||||||
|
attachments *rate.Limiter
|
||||||
seen time.Time
|
seen time.Time
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
@ -35,9 +36,10 @@ func newVisitor(conf *Config, ip string) *visitor {
|
||||||
config: conf,
|
config: conf,
|
||||||
ip: ip,
|
ip: ip,
|
||||||
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
requests: rate.NewLimiter(rate.Every(conf.VisitorRequestLimitReplenish), conf.VisitorRequestLimitBurst),
|
||||||
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
|
||||||
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
|
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
|
||||||
seen: time.Now(),
|
emails: rate.NewLimiter(rate.Every(conf.VisitorEmailLimitReplenish), conf.VisitorEmailLimitBurst),
|
||||||
|
//attachments: rate.NewLimiter(rate.Every(conf.VisitorAttachmentBytesLimitReplenish * 1024), conf.VisitorAttachmentBytesLimitBurst),
|
||||||
|
seen: time.Now(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue