Compare commits
33 Commits
v2.9.0
...
templating
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9247dac50d | ||
|
|
de65d07518 | ||
|
|
1966f80855 | ||
|
|
4b2e38320d | ||
|
|
83356f565e | ||
|
|
7fd5f0b29d | ||
|
|
03737dbf5c | ||
|
|
867cf28080 | ||
|
|
b2eb5b94bd | ||
|
|
df7d6baec5 | ||
|
|
4c0ec3f75b | ||
|
|
12bbe9a1ae | ||
|
|
7936c38feb | ||
|
|
d0beaa900f | ||
|
|
0ab6171962 | ||
|
|
b7c2898b9c | ||
|
|
d155ebb3a4 | ||
|
|
3d5bb92774 | ||
|
|
a2f1a45097 | ||
|
|
b43fff7c7e | ||
|
|
b186c7a324 | ||
|
|
e50b6f4075 | ||
|
|
e5342d5eca | ||
|
|
a25fc1daaa | ||
|
|
6e75108aa9 | ||
|
|
0735a5cb48 | ||
|
|
088aede833 | ||
|
|
2119954f67 | ||
|
|
220b012ae3 | ||
|
|
e1278a5e92 | ||
|
|
0ae62d36d2 | ||
|
|
70729edb2b | ||
|
|
de2f7d3e9b |
24
README.md
24
README.md
@@ -9,7 +9,6 @@
|
|||||||
[](https://discord.gg/cT7ECsZj9w)
|
[](https://discord.gg/cT7ECsZj9w)
|
||||||
[](https://matrix.to/#/#ntfy:matrix.org)
|
[](https://matrix.to/#/#ntfy:matrix.org)
|
||||||
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
||||||
[](https://discuss.ntfy.sh/c/ntfy)
|
|
||||||
[](https://ntfy.statuspage.io/)
|
[](https://ntfy.statuspage.io/)
|
||||||
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||||
|
|
||||||
@@ -50,7 +49,6 @@ works best for you:
|
|||||||
|
|
||||||
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
|
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
|
||||||
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
|
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
|
||||||
* [Lemmy discussion board](https://discuss.ntfy.sh/c/ntfy) - asynchronous forum (_new as of June 2023_)
|
|
||||||
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
||||||
|
|
||||||
## Announcements/beta testers
|
## Announcements/beta testers
|
||||||
@@ -71,7 +69,7 @@ for the server and the Android app. Or, if you'd like to help translate 🇩🇪
|
|||||||
## Sponsors
|
## Sponsors
|
||||||
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
|
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
|
||||||
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
|
||||||
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
|
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks who have sponsored ntfy in the past, or are still sponsoring ntfy:
|
||||||
|
|
||||||
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
||||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||||
@@ -168,6 +166,26 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
|
|||||||
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.png" width="40px" /></a>
|
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.png" width="40px" /></a>
|
||||||
<a href="https://github.com/tka85"><img src="https://github.com/tka85.png" width="40px" /></a>
|
<a href="https://github.com/tka85"><img src="https://github.com/tka85.png" width="40px" /></a>
|
||||||
<a href="https://github.com/beekeeb"><img src="https://github.com/beekeeb.png" width="40px" /></a>
|
<a href="https://github.com/beekeeb"><img src="https://github.com/beekeeb.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Emiliaaah"><img src="https://github.com/Emiliaaah.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/zark0s"><img src="https://github.com/zark0s.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/tomershvueli"><img src="https://github.com/tomershvueli.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/CataIana"><img src="https://github.com/CataIana.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/ajay-actuary"><img src="https://github.com/ajay-actuary.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/mursec"><img src="https://github.com/mursec.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/FrameXX"><img src="https://github.com/FrameXX.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/vovayartsev"><img src="https://github.com/vovayartsev.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/dwain-lab"><img src="https://github.com/dwain-lab.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/brookmg"><img src="https://github.com/brookmg.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/siebej"><img src="https://github.com/siebej.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/rxsantos"><img src="https://github.com/rxsantos.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/hermannx5"><img src="https://github.com/hermannx5.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/rwxd"><img src="https://github.com/rwxd.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/Integral-Tech"><img src="https://github.com/Integral-Tech.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/TheTomik1"><img src="https://github.com/TheTomik1.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/dav23r"><img src="https://github.com/dav23r.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/stannynuytkens"><img src="https://github.com/stannynuytkens.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/danbartram"><img src="https://github.com/danbartram.png" width="40px" /></a>
|
||||||
|
<a href="https://github.com/arthurgleckler"><img src="https://github.com/arthurgleckler.png" width="40px" /></a>
|
||||||
|
|
||||||
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
|
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
|
||||||
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCLI_Access_Show(t *testing.T) {
|
func TestCLI_Access_Show(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
s, conf, port := newTestServerWithAuth(t)
|
s, conf, port := newTestServerWithAuth(t)
|
||||||
defer test.StopServer(t, s, port)
|
defer test.StopServer(t, s, port)
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ func TestCLI_Access_Show(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
|
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
s, conf, port := newTestServerWithAuth(t)
|
s, conf, port := newTestServerWithAuth(t)
|
||||||
defer test.StopServer(t, s, port)
|
defer test.StopServer(t, s, port)
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestNewYamlSourceFromFile(t *testing.T) {
|
func TestNewYamlSourceFromFile(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
filename := filepath.Join(t.TempDir(), "server.yml")
|
filename := filepath.Join(t.TempDir(), "server.yml")
|
||||||
contents := `
|
contents := `
|
||||||
# Normal options
|
# Normal options
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
testMessage := util.RandomString(10)
|
testMessage := util.RandomString(10)
|
||||||
app, _, _, _ := newTestApp()
|
app, _, _, _ := newTestApp()
|
||||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
||||||
@@ -35,6 +36,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
s, port := test.StartServer(t)
|
s, port := test.StartServer(t)
|
||||||
defer test.StopServer(t, s, port)
|
defer test.StopServer(t, s, port)
|
||||||
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||||
@@ -51,6 +53,7 @@ func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestCLI_Publish_All_The_Things(t *testing.T) {
|
func TestCLI_Publish_All_The_Things(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
s, port := test.StartServer(t)
|
s, port := test.StartServer(t)
|
||||||
defer test.StopServer(t, s, port)
|
defer test.StopServer(t, s, port)
|
||||||
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||||
|
|||||||
@@ -144,9 +144,18 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
|||||||
|
|
||||||
## Blog + forum posts
|
## Blog + forum posts
|
||||||
|
|
||||||
|
- [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024
|
||||||
- [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024
|
- [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024
|
||||||
|
- [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024
|
||||||
|
- [ZFS and SMART Warnings via Ntfy](https://rair.dev/zfs-smart-ntfy/) - rair.dev - 2/2024
|
||||||
|
- [Automating Security Camera Notifications With Home Assistant and Ntfy](https://runtimeterror.dev/automating-camera-notifications-home-assistant-ntfy/) ⭐ - runtimeterror.dev - 2/2024
|
||||||
|
- [Ntfy: self-hosted notification service](https://medium.com/@williamdonze/ntfy-self-hosted-notification-service-0f3eada6e657) ⭐ - williamdonze.medium.com - 1/2024
|
||||||
|
- [Let’s Supercharge Snowflake Alerts with Cool ntfy Open-source Notifications!](https://sarathi-data-ml-cloud.medium.com/lets-supercharge-snowflake-alerts-with-cool-ntfy-open-source-notifications-296da442c331) - sarathi-data-ml-cloud.medium.com - 1/2024
|
||||||
|
- [Setting up NTFY with Ngnix-Proxy-Manager, authentication and Ansible notifications](https://random-it-blog.de/rocky-linux/setting-up-ntfy-with-ngnix-proxy-manager-authentication-and-ansible-notifications/) - random-it-blog.de - 12/2023
|
||||||
- [Introducing the Monitoring Ntfy.sh Integration Module: Real-time Notifications for Drupal Monitoring](https://cyberschorsch.dev/drupal/introducing-monitoring-ntfysh-integration-module-real-time-notifications-drupal-monitoring) - cyberschorsch.dev - 11/2023
|
- [Introducing the Monitoring Ntfy.sh Integration Module: Real-time Notifications for Drupal Monitoring](https://cyberschorsch.dev/drupal/introducing-monitoring-ntfysh-integration-module-real-time-notifications-drupal-monitoring) - cyberschorsch.dev - 11/2023
|
||||||
- [How to install Ntfy.sh on CasaOS using BigBearCasaOS](https://www.youtube.com/watch?v=wSWhtSNwTd8) - youtube.com - 10/2023
|
- [How to install Ntfy.sh on CasaOS using BigBearCasaOS](https://www.youtube.com/watch?v=wSWhtSNwTd8) - youtube.com - 10/2023
|
||||||
|
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-update-notifications-ntfy/) - rair.dev - 9/2023
|
||||||
|
- [Easy Push Notifications With ntfy.sh](https://runtimeterror.dev/easy-push-notifications-with-ntfy/) ⭐ - runtimeterror.dev - 9/2023
|
||||||
- [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023
|
- [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023
|
||||||
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
|
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
|
||||||
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
|
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
|
||||||
|
|||||||
@@ -3557,6 +3557,34 @@ ntfy server plays the role of the Push Gateway, as well as the Push Provider. Un
|
|||||||
!!! info
|
!!! info
|
||||||
This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy.
|
This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy.
|
||||||
|
|
||||||
|
### Message and Title Templates
|
||||||
|
Some services let you specify a webhook URL but do not let you modify the webhook body (e.g., Grafana). Instead of using a separate
|
||||||
|
bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title
|
||||||
|
which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON).
|
||||||
|
|
||||||
|
Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to "yes". Then, include templates
|
||||||
|
in your message and/or title (no other fields can be filled with a template at this time) by including paths to the
|
||||||
|
appropriate JSON fields surrounded by `${` and `}`. See an example below.
|
||||||
|
See [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax.
|
||||||
|
[https://gjson.dev/](https://gjson.dev/) is a great resource for testing your templates.
|
||||||
|
|
||||||
|
=== "HTTP"
|
||||||
|
``` http
|
||||||
|
POST /mytopic HTTP/1.1
|
||||||
|
Host: ntfy.sh
|
||||||
|
X-Message: Error message: ${error.desc}
|
||||||
|
X-Title: ${hostname}: A ${error.level} error has occurred
|
||||||
|
X-Template: yes
|
||||||
|
|
||||||
|
{"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
|
||||||
|
```
|
||||||
|
|
||||||
|
The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space".
|
||||||
|
|
||||||
|
For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`.
|
||||||
|
Alternatively, you can include the params in the webhook URL. For example, by
|
||||||
|
appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL.
|
||||||
|
|
||||||
## Public topics
|
## Public topics
|
||||||
Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics
|
Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics
|
||||||
that you can use to try out what [authentication and access control](#authentication) looks like.
|
that you can use to try out what [authentication and access control](#authentication) looks like.
|
||||||
|
|||||||
@@ -1338,6 +1338,12 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
## Not released yet
|
## Not released yet
|
||||||
|
|
||||||
|
### ntfy server v2.9.1 (UNRELEASED)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* You can now include a message and/or title template that will be filled with values from a JSON body, great for services that let you specify a webhook URL but do not let you change the webhook body (such as Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
|
||||||
|
|
||||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|||||||
25
go.mod
25
go.mod
@@ -6,9 +6,9 @@ toolchain go1.21.3
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go/firestore v1.15.0 // indirect
|
cloud.google.com/go/firestore v1.15.0 // indirect
|
||||||
cloud.google.com/go/storage v1.39.0 // indirect
|
cloud.google.com/go/storage v1.39.1 // indirect
|
||||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||||
github.com/emersion/go-smtp v0.18.0
|
github.com/emersion/go-smtp v0.18.0
|
||||||
github.com/gabriel-vasile/mimetype v1.4.3
|
github.com/gabriel-vasile/mimetype v1.4.3
|
||||||
github.com/gorilla/websocket v1.5.1
|
github.com/gorilla/websocket v1.5.1
|
||||||
@@ -21,7 +21,7 @@ require (
|
|||||||
golang.org/x/sync v0.6.0
|
golang.org/x/sync v0.6.0
|
||||||
golang.org/x/term v0.18.0
|
golang.org/x/term v0.18.0
|
||||||
golang.org/x/time v0.5.0
|
golang.org/x/time v0.5.0
|
||||||
google.golang.org/api v0.168.0
|
google.golang.org/api v0.170.0
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,14 +35,15 @@ require (
|
|||||||
github.com/microcosm-cc/bluemonday v1.0.26
|
github.com/microcosm-cc/bluemonday v1.0.26
|
||||||
github.com/prometheus/client_golang v1.19.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0
|
github.com/stripe/stripe-go/v74 v74.30.0
|
||||||
|
github.com/tidwall/gjson v1.17.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.112.1 // indirect
|
cloud.google.com/go v0.112.1 // indirect
|
||||||
cloud.google.com/go/compute v1.25.0 // indirect
|
cloud.google.com/go/compute v1.25.1 // indirect
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||||
cloud.google.com/go/iam v1.1.6 // indirect
|
cloud.google.com/go/iam v1.1.7 // indirect
|
||||||
cloud.google.com/go/longrunning v0.5.5 // indirect
|
cloud.google.com/go/longrunning v0.5.6 // indirect
|
||||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
@@ -60,7 +61,7 @@ require (
|
|||||||
github.com/google/s2a-go v0.1.7 // indirect
|
github.com/google/s2a-go v0.1.7 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||||
github.com/googleapis/gax-go/v2 v2.12.2 // indirect
|
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
@@ -69,7 +70,9 @@ require (
|
|||||||
github.com/prometheus/procfs v0.13.0 // indirect
|
github.com/prometheus/procfs v0.13.0 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/stretchr/objx v0.5.0 // indirect
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||||
go.opencensus.io v0.24.0 // indirect
|
go.opencensus.io v0.24.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||||
@@ -81,9 +84,9 @@ require (
|
|||||||
golang.org/x/text v0.14.0 // indirect
|
golang.org/x/text v0.14.0 // indirect
|
||||||
google.golang.org/appengine v1.6.8 // indirect
|
google.golang.org/appengine v1.6.8 // indirect
|
||||||
google.golang.org/appengine/v2 v2.0.5 // indirect
|
google.golang.org/appengine/v2 v2.0.5 // indirect
|
||||||
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 // indirect
|
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 // indirect
|
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||||
google.golang.org/grpc v1.62.1 // indirect
|
google.golang.org/grpc v1.62.1 // indirect
|
||||||
google.golang.org/protobuf v1.33.0 // indirect
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
51
go.sum
51
go.sum
@@ -1,18 +1,18 @@
|
|||||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||||
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
||||||
cloud.google.com/go/compute v1.25.0 h1:H1/4SqSUhjPFE7L5ddzHOfY2bCAvjwNRZPNl6Ni5oYU=
|
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||||
cloud.google.com/go/compute v1.25.0/go.mod h1:GR7F0ZPZH8EhChlMo9FkLd7eUTwEymjqQagxzilIxIE=
|
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||||
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
|
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
|
||||||
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
|
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
|
||||||
cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc=
|
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
|
||||||
cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI=
|
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
|
||||||
cloud.google.com/go/longrunning v0.5.5 h1:GOE6pZFdSrTb4KAiKnXsJBtlE6mEyaW44oKyMILWnOg=
|
cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE=
|
||||||
cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s=
|
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
|
||||||
cloud.google.com/go/storage v1.39.0 h1:brbjUa4hbDHhpQf48tjqMaXEV+f1OGoaTmQau9tmCsA=
|
cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
|
||||||
cloud.google.com/go/storage v1.39.0/go.mod h1:OAEj/WZwUYjA3YHQ10/YcN9ttGuEpLwvaoyBXIPikEk=
|
cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
|
||||||
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
|
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
|
||||||
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
|
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
|
||||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||||
@@ -33,8 +33,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj
|
|||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
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=
|
||||||
@@ -98,8 +98,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||||
github.com/googleapis/gax-go/v2 v2.12.2 h1:mhN09QQW1jEWeMF74zGR81R30z4VJzjZsfkUhuHF+DA=
|
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
|
||||||
github.com/googleapis/gax-go/v2 v2.12.2/go.mod h1:61M8vcyyXR2kqKFxKrfA22jaA8JGF7Dc8App1U3H6jc=
|
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
@@ -143,10 +143,17 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
|
|||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
|
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
|
||||||
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||||
|
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||||
|
github.com/tidwall/gjson v1.17.1/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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e h1:+SOyEddqYF09QP7vr7CgJ1eti3pY9Fn3LHO1M1r/0sI=
|
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||||
github.com/xrash/smetrics v0.0.0-20231213231151-1d8dd44e695e/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||||
@@ -240,8 +247,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||||
google.golang.org/api v0.168.0 h1:MBRe+Ki4mMN93jhDDbpuRLjRddooArz4FeSObvUMmjY=
|
google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
|
||||||
google.golang.org/api v0.168.0/go.mod h1:gpNOiMA2tZ4mf5R9Iwf4rK/Dcz0fbdIgWYWVoxmsyLg=
|
google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
|
||||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||||
@@ -251,12 +258,12 @@ google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7
|
|||||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||||
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8 h1:Fe8QycXyEd9mJgnwB9kmw00WgB43eQ/xYO5C6gceybQ=
|
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 h1:PgNlNSx2Nq2/j4juYzQBG0/Zdr+WP4z5N01Vk4VYBCY=
|
||||||
google.golang.org/genproto v0.0.0-20240304212257-790db918fca8/go.mod h1:yA7a1bW1kwl459Ol0m0lV4hLTfrL/7Bkk4Mj2Ir1mWI=
|
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237/go.mod h1:9sVD8c25Af3p0rGs7S7LLsxWKFiJt/65LdSyqXBkX/Y=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8 h1:8eadJkXbwDEMNwcB5O0s5Y5eCfyuCLdvaiOIaGTrWmQ=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20240304212257-790db918fca8/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y=
|
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8 h1:IR+hp6ypxjH24bkMfEJ0yHR21+gwPWdV+/IBrPQyn3k=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240304212257-790db918fca8/go.mod h1:UCOku4NytXMJuLQE5VuqA5lX3PcHCBo8pxNyvkf4xBs=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ var (
|
|||||||
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
|
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
|
||||||
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
|
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
|
||||||
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
|
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
|
||||||
|
errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil}
|
||||||
|
errHTTPBadRequestTemplatedMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", nil}
|
||||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import (
|
|||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"heckel.io/ntfy/v2/log"
|
"heckel.io/ntfy/v2/log"
|
||||||
"heckel.io/ntfy/v2/user"
|
"heckel.io/ntfy/v2/user"
|
||||||
@@ -109,6 +110,8 @@ var (
|
|||||||
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})?$`)
|
||||||
urlRegex = regexp.MustCompile(`^https?://`)
|
urlRegex = regexp.MustCompile(`^https?://`)
|
||||||
phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`)
|
phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`)
|
||||||
|
templateVarRegex = regexp.MustCompile(`\${([^}]+)}`)
|
||||||
|
templateVarFormat = "${%s}"
|
||||||
|
|
||||||
//go:embed site
|
//go:embed site
|
||||||
webFs embed.FS
|
webFs embed.FS
|
||||||
@@ -123,12 +126,12 @@ var (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
firebaseControlTopic = "~control" // See Android if changed
|
firebaseControlTopic = "~control" // See Android if changed
|
||||||
firebasePollTopic = "~poll" // See iOS if changed
|
firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now)
|
||||||
emptyMessageBody = "triggered" // Used if message body is empty
|
emptyMessageBody = "triggered" // Used if message body is empty
|
||||||
newMessageBody = "New message" // Used in poll requests as generic message
|
newMessageBody = "New message" // Used in poll requests as generic message
|
||||||
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
||||||
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
|
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
|
||||||
jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body
|
jsonBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher)
|
||||||
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
|
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
|
||||||
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
|
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
|
||||||
messagesHistoryMax = 10 // Number of message count values to keep in memory
|
messagesHistoryMax = 10 // Number of message count values to keep in memory
|
||||||
@@ -673,7 +676,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
|
|||||||
// - avoid abuse (e.g. 1 uploader, 1k downloaders)
|
// - avoid abuse (e.g. 1 uploader, 1k downloaders)
|
||||||
// - and also uses the higher bandwidth limits of a paying user
|
// - and also uses the higher bandwidth limits of a paying user
|
||||||
m, err := s.messageCache.Message(messageID)
|
m, err := s.messageCache.Message(messageID)
|
||||||
if err == errMessageNotFound {
|
if errors.Is(err, errMessageNotFound) {
|
||||||
if s.config.CacheBatchTimeout > 0 {
|
if s.config.CacheBatchTimeout > 0 {
|
||||||
// Strange edge case: If we immediately after upload request the file (the web app does this for images),
|
// Strange edge case: If we immediately after upload request the file (the web app does this for images),
|
||||||
// and messages are persisted asynchronously, retry fetching from the database
|
// and messages are persisted asynchronously, retry fetching from the database
|
||||||
@@ -738,7 +741,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
m := newDefaultMessage(t.ID, "")
|
m := newDefaultMessage(t.ID, "")
|
||||||
cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m)
|
cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return nil, e.With(t)
|
return nil, e.With(t)
|
||||||
}
|
}
|
||||||
@@ -769,7 +772,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
|||||||
if cache {
|
if cache {
|
||||||
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
|
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
|
||||||
}
|
}
|
||||||
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
|
if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if m.Message == "" {
|
if m.Message == "" {
|
||||||
@@ -872,7 +875,7 @@ func (s *Server) sendToFirebase(v *visitor, m *message) {
|
|||||||
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
|
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
|
||||||
if err := s.firebaseClient.Send(v, m); err != nil {
|
if err := s.firebaseClient.Send(v, m); err != nil {
|
||||||
minc(metricFirebasePublishedFailure)
|
minc(metricFirebasePublishedFailure)
|
||||||
if err == errFirebaseTemporarilyBanned {
|
if errors.Is(err, errFirebaseTemporarilyBanned) {
|
||||||
logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error())
|
logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error())
|
||||||
} else {
|
} else {
|
||||||
logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error())
|
logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error())
|
||||||
@@ -924,7 +927,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) {
|
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) {
|
||||||
cache = readBoolParam(r, true, "x-cache", "cache")
|
cache = readBoolParam(r, true, "x-cache", "cache")
|
||||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||||
m.Title = readParam(r, "x-title", "title", "t")
|
m.Title = readParam(r, "x-title", "title", "t")
|
||||||
@@ -940,7 +943,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
}
|
}
|
||||||
if attach != "" {
|
if attach != "" {
|
||||||
if !urlRegex.MatchString(attach) {
|
if !urlRegex.MatchString(attach) {
|
||||||
return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid
|
return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid
|
||||||
}
|
}
|
||||||
m.Attachment.URL = attach
|
m.Attachment.URL = attach
|
||||||
if m.Attachment.Name == "" {
|
if m.Attachment.Name == "" {
|
||||||
@@ -958,19 +961,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
}
|
}
|
||||||
if icon != "" {
|
if icon != "" {
|
||||||
if !urlRegex.MatchString(icon) {
|
if !urlRegex.MatchString(icon) {
|
||||||
return false, false, "", "", false, errHTTPBadRequestIconURLInvalid
|
return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid
|
||||||
}
|
}
|
||||||
m.Icon = icon
|
m.Icon = icon
|
||||||
}
|
}
|
||||||
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
||||||
if s.smtpSender == nil && email != "" {
|
if s.smtpSender == nil && email != "" {
|
||||||
return false, false, "", "", false, errHTTPBadRequestEmailDisabled
|
return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled
|
||||||
}
|
}
|
||||||
call = readParam(r, "x-call", "call")
|
call = readParam(r, "x-call", "call")
|
||||||
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
|
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
|
||||||
return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled
|
return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled
|
||||||
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
||||||
return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid
|
return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid
|
||||||
}
|
}
|
||||||
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
|
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
|
||||||
if messageStr != "" {
|
if messageStr != "" {
|
||||||
@@ -979,27 +982,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
var e error
|
var e error
|
||||||
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
|
return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid
|
||||||
}
|
}
|
||||||
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
|
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
|
||||||
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
||||||
if delayStr != "" {
|
if delayStr != "" {
|
||||||
if !cache {
|
if !cache {
|
||||||
return false, false, "", "", false, errHTTPBadRequestDelayNoCache
|
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache
|
||||||
}
|
}
|
||||||
if email != "" {
|
if email != "" {
|
||||||
return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
||||||
}
|
}
|
||||||
if call != "" {
|
if call != "" {
|
||||||
return false, false, "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
||||||
}
|
}
|
||||||
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
|
return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse
|
||||||
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
||||||
return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
|
return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall
|
||||||
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
||||||
return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
|
return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge
|
||||||
}
|
}
|
||||||
m.Time = delay.Unix()
|
m.Time = delay.Unix()
|
||||||
}
|
}
|
||||||
@@ -1007,13 +1010,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
if actionsStr != "" {
|
if actionsStr != "" {
|
||||||
m.Actions, e = parseActions(actionsStr)
|
m.Actions, e = parseActions(actionsStr)
|
||||||
if e != nil {
|
if e != nil {
|
||||||
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
|
return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
||||||
if markdown || strings.ToLower(contentType) == "text/markdown" {
|
if markdown || strings.ToLower(contentType) == "text/markdown" {
|
||||||
m.ContentType = "text/markdown"
|
m.ContentType = "text/markdown"
|
||||||
}
|
}
|
||||||
|
template = readBoolParam(r, false, "x-template", "template", "tpl")
|
||||||
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
|
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
|
||||||
if unifiedpush {
|
if unifiedpush {
|
||||||
firebase = false
|
firebase = false
|
||||||
@@ -1025,7 +1029,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
cache = false
|
cache = false
|
||||||
email = ""
|
email = ""
|
||||||
}
|
}
|
||||||
return cache, firebase, email, call, unifiedpush, nil
|
return cache, firebase, email, call, template, unifiedpush, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
||||||
@@ -1033,16 +1037,18 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
|||||||
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
|
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
|
||||||
// If a message is flagged as poll request, the body does not matter and is discarded
|
// If a message is flagged as poll request, the body does not matter and is discarded
|
||||||
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
||||||
// If body is binary, encode as base64, if not do not encode
|
// If UnifiedPush is enabled, encode as base64 if body is binary, and do not trim
|
||||||
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
||||||
// Body must be a message, because we attached an external URL
|
// Body must be a message, because we attached an external URL
|
||||||
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
||||||
// Body must be attachment, because we passed a filename
|
// Body must be attachment, because we passed a filename
|
||||||
// 5. curl -T file.txt ntfy.sh/mytopic
|
// 5. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic
|
||||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
// If templating is enabled, read up to 32k and treat message body as JSON
|
||||||
// 6. curl -T file.txt ntfy.sh/mytopic
|
// 6. curl -T file.txt ntfy.sh/mytopic
|
||||||
// If file.txt is > message limit, treat it as an attachment
|
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
|
// 7. curl -T file.txt ntfy.sh/mytopic
|
||||||
|
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
|
||||||
|
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error {
|
||||||
if m.Event == pollRequestEvent { // Case 1
|
if m.Event == pollRequestEvent { // Case 1
|
||||||
return s.handleBodyDiscard(body)
|
return s.handleBodyDiscard(body)
|
||||||
} else if unifiedpush {
|
} else if unifiedpush {
|
||||||
@@ -1051,10 +1057,12 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
|
|||||||
return s.handleBodyAsTextMessage(m, body) // Case 3
|
return s.handleBodyAsTextMessage(m, body) // Case 3
|
||||||
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
||||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
||||||
|
} else if template {
|
||||||
|
return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5
|
||||||
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
||||||
return s.handleBodyAsTextMessage(m, body) // Case 5
|
return s.handleBodyAsTextMessage(m, body) // Case 6
|
||||||
}
|
}
|
||||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 6
|
return s.handleBodyAsAttachment(r, v, m, body) // Case 7
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error {
|
func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error {
|
||||||
@@ -1086,6 +1094,35 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error {
|
||||||
|
body, err := util.Peek(body, jsonBodyBytesLimit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
} else if body.LimitReached {
|
||||||
|
return errHTTPEntityTooLargeJSONBody
|
||||||
|
}
|
||||||
|
peekedBody := strings.TrimSpace(string(body.PeekedBytes))
|
||||||
|
if !gjson.Valid(peekedBody) {
|
||||||
|
return errHTTPBadRequestTemplatedMessageNotJSON
|
||||||
|
}
|
||||||
|
m.Message = replaceGJSONTemplate(m.Message, peekedBody)
|
||||||
|
m.Title = replaceGJSONTemplate(m.Title, peekedBody)
|
||||||
|
if len(m.Message) > s.config.MessageSizeLimit {
|
||||||
|
return errHTTPBadRequestTemplatedMessageTooLarge
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceGJSONTemplate(template string, source string) string {
|
||||||
|
matches := templateVarRegex.FindAllStringSubmatch(template, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
if result := gjson.Get(source, m[1]); result.Exists() {
|
||||||
|
template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return template
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
|
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
|
||||||
if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" {
|
if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" {
|
||||||
return errHTTPBadRequestAttachmentsDisallowed.With(m)
|
return errHTTPBadRequestAttachmentsDisallowed.With(m)
|
||||||
@@ -1128,7 +1165,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
|||||||
util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining),
|
util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining),
|
||||||
}
|
}
|
||||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
|
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
|
||||||
if err == util.ErrLimitReached {
|
if errors.Is(err, util.ErrLimitReached) {
|
||||||
return errHTTPEntityTooLargeAttachment.With(m)
|
return errHTTPEntityTooLargeAttachment.With(m)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"heckel.io/ntfy/v2/user"
|
"heckel.io/ntfy/v2/user"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
@@ -45,7 +46,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
|||||||
return errHTTPBadRequest.Wrap("username invalid, or password missing")
|
return errHTTPBadRequest.Wrap("username invalid, or password missing")
|
||||||
}
|
}
|
||||||
u, err := s.userManager.User(req.Username)
|
u, err := s.userManager.User(req.Username)
|
||||||
if err != nil && err != user.ErrUserNotFound {
|
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||||
return err
|
return err
|
||||||
} else if u != nil {
|
} else if u != nil {
|
||||||
return errHTTPConflictUserExists
|
return errHTTPConflictUserExists
|
||||||
@@ -53,7 +54,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
|||||||
var tier *user.Tier
|
var tier *user.Tier
|
||||||
if req.Tier != "" {
|
if req.Tier != "" {
|
||||||
tier, err = s.userManager.Tier(req.Tier)
|
tier, err = s.userManager.Tier(req.Tier)
|
||||||
if err == user.ErrTierNotFound {
|
if errors.Is(err, user.ErrTierNotFound) {
|
||||||
return errHTTPBadRequestTierInvalid
|
return errHTTPBadRequestTierInvalid
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -76,7 +77,7 @@ func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *vi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u, err := s.userManager.User(req.Username)
|
u, err := s.userManager.User(req.Username)
|
||||||
if err == user.ErrUserNotFound {
|
if errors.Is(err, user.ErrUserNotFound) {
|
||||||
return errHTTPBadRequestUserNotFound
|
return errHTTPBadRequestUserNotFound
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -98,7 +99,7 @@ func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *vi
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_, err = s.userManager.User(req.Username)
|
_, err = s.userManager.User(req.Username)
|
||||||
if err == user.ErrUserNotFound {
|
if errors.Is(err, user.ErrUserNotFound) {
|
||||||
return errHTTPBadRequestUserNotFound
|
return errHTTPBadRequestUserNotFound
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -2625,6 +2625,188 @@ func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) {
|
|||||||
time.Sleep(500 * time.Millisecond)
|
time.Sleep(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||||
|
"X-Message": "${foo}",
|
||||||
|
"X-Title": "${nested.title}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "bar", m.Message)
|
||||||
|
require.Equal(t, "here", m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||||
|
"Message": "${foo} is ${foo}",
|
||||||
|
"Title": "${nested.title} is ${nested.title}",
|
||||||
|
"Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "bar is bar", m.Message)
|
||||||
|
require.Equal(t, "here is here", m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_JSONBody(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}`
|
||||||
|
response := request(t, s, "PUT", "/", body, map[string]string{
|
||||||
|
"m": "${foo}",
|
||||||
|
"t": "${nested.title}",
|
||||||
|
"tpl": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "bar", m.Message)
|
||||||
|
require.Equal(t, "here", m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}`
|
||||||
|
response := request(t, s, "PUT", "/", body, map[string]string{
|
||||||
|
"X-Message": "${foo}",
|
||||||
|
"X-Title": "${nested.title}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, 40042, toHTTPError(t, response.Body.String()).Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||||
|
"X-Message": "${food}",
|
||||||
|
"X-Title": "${nested.titl}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "${food}", m.Message)
|
||||||
|
require.Equal(t, "${nested.titl}", m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||||
|
"X-Message": "${foo} is ${nested.title}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "bar is here", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) {
|
||||||
|
// not intended to work recursively for now
|
||||||
|
// i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar"
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}}`, map[string]string{
|
||||||
|
"X-Message": "${${nested.bar}}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "${${nested.bar}}", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) {
|
||||||
|
// The above example can technically work
|
||||||
|
// ${${nested.bar}} would be interpreted as a nested GJSON path with key "${nested" then key "bar"
|
||||||
|
// so you would probably expect the output to be "works!", BUT the second } in the placeholder is not
|
||||||
|
// included by the regex, so it is still there after replacing the placeholder, thus giving you "works!}"
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}, "${nested":{"bar":"works!"}}`, map[string]string{
|
||||||
|
"X-Message": "${${nested.bar}}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "works!}", m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_FancyGJSON(t *testing.T) {
|
||||||
|
s := newTestServer(t, newTestConfig(t))
|
||||||
|
jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}`
|
||||||
|
response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{
|
||||||
|
"X-Message": `${errors.#(level=="severe")#.url}`,
|
||||||
|
"X-Title": `${errors.#(level=="severe")#|#} Severe Errors`,
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, `["https://severe1.com","https://severe2.com"]`, m.Message)
|
||||||
|
require.Equal(t, `2 Severe Errors`, m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) {
|
||||||
|
c := newTestConfig(t)
|
||||||
|
c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||||
|
"X-Message": "${foo}",
|
||||||
|
"X-Title": "${nested.title}",
|
||||||
|
"X-Template": "yes",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "bar", m.Message)
|
||||||
|
require.Equal(t, "here", m.Title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) {
|
||||||
|
c := newTestConfig(t)
|
||||||
|
c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{
|
||||||
|
"X-Message": "${foo}",
|
||||||
|
"X-Template": "1",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, 400, response.Code)
|
||||||
|
require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestServer_MessageTemplate_Grafana(t *testing.T) {
|
||||||
|
c := newTestConfig(t)
|
||||||
|
s := newTestServer(t, c)
|
||||||
|
body := `{"receiver":"ntfy\\.example\\.com/alerts","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"annotations":{"summary":"15m load average too high"},"startsAt":"2024-03-15T02:28:00Z","endsAt":"2024-03-15T02:42:00Z","generatorURL":"localhost:3000/alerting/grafana/NW9oDw-4z/view","fingerprint":"becbfb94bd81ef48","silenceURL":"localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter","dashboardURL":"","panelURL":"","values":{"B":18.98211314475876,"C":0},"valueString":"[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"}],"groupLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts"},"commonLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"commonAnnotations":{"summary":"15m load average too high"},"externalURL":"localhost:3000/","version":"1","groupKey":"{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}","truncatedAlerts":0,"orgId":1,"title":"[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)","state":"ok","message":"**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter\n"}`
|
||||||
|
response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+${title}&message=${message}", body, nil)
|
||||||
|
|
||||||
|
require.Equal(t, 200, response.Code)
|
||||||
|
m := toMessage(t, response.Body.String())
|
||||||
|
require.Equal(t, "Grafana alert: [RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", m.Title)
|
||||||
|
require.Equal(t, `**Resolved**
|
||||||
|
|
||||||
|
Value: B=18.98211314475876, C=0
|
||||||
|
Labels:
|
||||||
|
- alertname = Load avg 15m too high
|
||||||
|
- grafana_folder = Node alerts
|
||||||
|
- instance = 10.108.0.2:9100
|
||||||
|
- job = node-exporter
|
||||||
|
Annotations:
|
||||||
|
- summary = 15m load average too high
|
||||||
|
Source: localhost:3000/alerting/grafana/NW9oDw-4z/view
|
||||||
|
Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter
|
||||||
|
`, m.Message)
|
||||||
|
}
|
||||||
|
|
||||||
func newTestConfig(t *testing.T) *Config {
|
func newTestConfig(t *testing.T) *Config {
|
||||||
conf := NewConfig()
|
conf := NewConfig()
|
||||||
conf.BaseURL = "http://127.0.0.1:12345"
|
conf.BaseURL = "http://127.0.0.1:12345"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"heckel.io/ntfy/v2/util"
|
"heckel.io/ntfy/v2/util"
|
||||||
"io"
|
"io"
|
||||||
@@ -104,9 +105,9 @@ func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr {
|
|||||||
|
|
||||||
func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
|
func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
|
||||||
obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty)
|
obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty)
|
||||||
if err == util.ErrUnmarshalJSON {
|
if errors.Is(err, util.ErrUnmarshalJSON) {
|
||||||
return nil, errHTTPBadRequestJSONInvalid
|
return nil, errHTTPBadRequestJSONInvalid
|
||||||
} else if err == util.ErrTooLargeJSON {
|
} else if errors.Is(err, util.ErrTooLargeJSON) {
|
||||||
return nil, errHTTPEntityTooLargeJSONBody
|
return nil, errHTTPEntityTooLargeJSONBody
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ func StartServer(t *testing.T) (*server.Server, int) {
|
|||||||
|
|
||||||
// StartServerWithConfig starts a server.Server with a random port and waits for the server to be up
|
// StartServerWithConfig starts a server.Server with a random port and waits for the server to be up
|
||||||
func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) {
|
func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) {
|
||||||
port := 10000 + rand.Intn(20000)
|
port := 10000 + rand.Intn(30000)
|
||||||
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
||||||
conf.AttachmentCacheDir = t.TempDir()
|
conf.AttachmentCacheDir = t.TempDir()
|
||||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package util
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -26,7 +27,7 @@ func Peek(underlying io.ReadCloser, limit int) (*PeekedReadCloser, error) {
|
|||||||
}
|
}
|
||||||
peeked := make([]byte, limit)
|
peeked := make([]byte, limit)
|
||||||
read, err := io.ReadFull(underlying, peeked)
|
read, err := io.ReadFull(underlying, peeked)
|
||||||
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) && err != io.EOF {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return &PeekedReadCloser{
|
return &PeekedReadCloser{
|
||||||
@@ -44,7 +45,7 @@ func (r *PeekedReadCloser) Read(p []byte) (n int, err error) {
|
|||||||
return 0, io.EOF
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
n, err = r.peeked.Read(p)
|
n, err = r.peeked.Read(p)
|
||||||
if err == io.EOF {
|
if errors.Is(err, io.EOF) {
|
||||||
return r.underlying.Read(p)
|
return r.underlying.Read(p)
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|||||||
2473
web/package-lock.json
generated
2473
web/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user