Docs and Matrix tests
parent
0ff8e968ca
commit
18bd3c0e55
|
@ -9,7 +9,9 @@ those out, too.
|
||||||
[create a pull request](https://github.com/binwiederhier/ntfy/pulls), and I'll happily include it. Also note, that
|
[create a pull request](https://github.com/binwiederhier/ntfy/pulls), and I'll happily include it. Also note, that
|
||||||
I cannot guarantee that all of these examples are functional. Many of them I have not tried myself.
|
I cannot guarantee that all of these examples are functional. Many of them I have not tried myself.
|
||||||
|
|
||||||
## A long process is done: backups, copying data, pipelines, ...
|
## Cronjobs
|
||||||
|
ntfy is perfect for any kind of cronjobs or just when long processes are done (backups, pipelines, rsync copy commands, ...).
|
||||||
|
|
||||||
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
|
I started adding notifications pretty much all of my scripts. Typically, I just chain the <tt>curl</tt> call
|
||||||
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
|
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
|
||||||
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
|
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
|
||||||
|
@ -21,6 +23,15 @@ rsync -a root@laptop /backups/laptop \
|
||||||
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups
|
|| curl -H tags:warning -H prio:high -d "Laptop backup failed" ntfy.sh/backups
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
|
||||||
|
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
|
||||||
|
|
||||||
|
``` cron
|
||||||
|
# Check github/ntfy user
|
||||||
|
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Low disk space alerts
|
## Low disk space alerts
|
||||||
Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but
|
Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but
|
||||||
effective.
|
effective.
|
||||||
|
@ -42,11 +53,7 @@ if [ -n "$avail" ]; then
|
||||||
fi
|
fi
|
||||||
```
|
```
|
||||||
|
|
||||||
## Server-sent messages in your web app
|
## SSH login alerts
|
||||||
Just as you can [subscribe to topics in the Web UI](subscribe/web.md), you can use ntfy in your own
|
|
||||||
web application. Check out the <a href="/example.html">live example</a>.
|
|
||||||
|
|
||||||
## Notify on SSH login
|
|
||||||
Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I
|
Years ago my home server was broken into. That shook me hard, so every time someone logs into any machine that I
|
||||||
own, I now message myself. Here's an example of how to use <a href="https://en.wikipedia.org/wiki/Linux_PAM">PAM</a>
|
own, I now message myself. Here's an example of how to use <a href="https://en.wikipedia.org/wiki/Linux_PAM">PAM</a>
|
||||||
to notify yourself on SSH login.
|
to notify yourself on SSH login.
|
||||||
|
@ -102,7 +109,7 @@ One of my co-workers uses the following Ansible task to let him know when things
|
||||||
body: "{{ inventory_hostname }} reseeding complete"
|
body: "{{ inventory_hostname }} reseeding complete"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Watchtower notifications (shoutrrr)
|
## Watchtower (shoutrrr)
|
||||||
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
|
||||||
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||||
|
|
||||||
|
@ -121,16 +128,7 @@ Or, if you only want to send notifications using shoutrrr:
|
||||||
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Random cronjobs
|
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
|
||||||
Alright, here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
|
|
||||||
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
|
|
||||||
|
|
||||||
``` cron
|
|
||||||
# Check github/ntfy user
|
|
||||||
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
|
|
||||||
```
|
|
||||||
|
|
||||||
## Download notifications (Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd)
|
|
||||||
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
|
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
|
||||||
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
|
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
|
||||||
|
|
||||||
|
@ -343,7 +341,7 @@ You can use the HTTP request node to send messages with [Node-RED](https://noder
|
||||||
|
|
||||||
![Node red picture flow](static/img/nodered-picture.png)
|
![Node red picture flow](static/img/nodered-picture.png)
|
||||||
|
|
||||||
## Gatus service health check
|
## Gatus
|
||||||
|
|
||||||
An example for a custom alert with [Gatus](https://github.com/TwiN/gatus):
|
An example for a custom alert with [Gatus](https://github.com/TwiN/gatus):
|
||||||
``` yaml
|
``` yaml
|
||||||
|
@ -435,11 +433,38 @@ notify:
|
||||||
```
|
```
|
||||||
|
|
||||||
## Uptime Kuma
|
## Uptime Kuma
|
||||||
- Go to your [Uptime Kuma](https://github.com/louislam/uptime-kuma) Settings > Notifications, click on **Setup Notification**
|
Go to your [Uptime Kuma](https://github.com/louislam/uptime-kuma) Settings > Notifications, click on **Setup Notification**.
|
||||||
- ![Uptime Kuma Settings](static/img/uptimekuma-settings.png)
|
Then set your desired **title** (e.g. "Uptime Kuma"), **ntfy topic**, **Server URL** and **priority (1-5)**:
|
||||||
- Set your desired **title** (e.g. "Uptime Kuma"), **ntfy topic**, **Server URL** and **priority (1-5)**
|
|
||||||
- ![Uptime Kuma Setup](static/img/uptimekuma-setup.png)
|
<div id="uptimekuma-screenshots" class="screenshots">
|
||||||
- You can now test the notifications and apply them to monitors.
|
<a href="../../static/img/uptimekuma-settings.png"><img src="../../static/img/uptimekuma-settings.png"/></a>
|
||||||
- ![Uptime Kuma iOS Test](static/img/uptimekuma-ios-test.jpg)
|
<a href="../../static/img/uptimekuma-setup.png"><img src="../../static/img/uptimekuma-setup.png"/></a>
|
||||||
- ![Uptime Kuma iOS Down](static/img/uptimekuma-ios-down.jpg)
|
</div>
|
||||||
- ![Uptime Kuma iOS Up](static/img/uptimekuma-ios-up.jpg)
|
|
||||||
|
|
||||||
|
You can now test the notifications and apply them to monitors:
|
||||||
|
|
||||||
|
<div id="uptimekuma-monitor-screenshots" class="screenshots">
|
||||||
|
<a href="../../static/img/uptimekuma-ios-test.jpg"><img src="../../static/img/uptimekuma-ios-test.jpg"/></a>
|
||||||
|
<a href="../../static/img/uptimekuma-ios-down.jpg"><img src="../../static/img/uptimekuma-ios-down.jpg"/></a>
|
||||||
|
<a href="../../static/img/uptimekuma-ios-up.jpg"><img src="../../static/img/uptimekuma-ios-up.jpg"/></a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Apprise
|
||||||
|
ntfy is integrated natively into [Apprise](https://github.com/caronc/apprise) (also check out the
|
||||||
|
[Apprise/ntfy wiki page](https://github.com/caronc/apprise/wiki/Notify_ntfy)).
|
||||||
|
|
||||||
|
You can use it like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
apprise -vv -t "Test Message Title" -b "Test Message Body" \
|
||||||
|
ntfy://mytopic
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with your own server like this:
|
||||||
|
|
||||||
|
```
|
||||||
|
apprise -vv -t "Test Message Title" -b "Test Message Body" \
|
||||||
|
ntfy://ntfy.example.com/mytopic
|
||||||
|
```
|
||||||
|
|
||||||
|
|
|
@ -2735,6 +2735,22 @@ parameter (or any of its aliases `unifiedpush` or `up`) to `1` to [disable Fireb
|
||||||
option is mostly equivalent to `Firebase: no`, but was introduced to allow future flexibility. The flag additionally
|
option is mostly equivalent to `Firebase: no`, but was introduced to allow future flexibility. The flag additionally
|
||||||
enables auto-detection of the message encoding. If the message is binary, it'll be encoded as base64.
|
enables auto-detection of the message encoding. If the message is binary, it'll be encoded as base64.
|
||||||
|
|
||||||
|
### Matrix Gateway
|
||||||
|
The ntfy server implements a [Matrix Push Gateway](https://spec.matrix.org/v1.2/push-gateway-api/) (in combination with
|
||||||
|
[UnifiedPush](https://unifiedpush.org) as the [Provider Push Protocol](https://unifiedpush.org/developers/gateway/)). This makes it easier to integrate
|
||||||
|
with self-hosted [Matrix](https://matrix.org/) servers (such as [synapse](https://github.com/matrix-org/synapse)), since
|
||||||
|
you don't have to set up a separate push proxy (such as [common-proxies](https://github.com/UnifiedPush/common-proxies)).
|
||||||
|
|
||||||
|
In short, ntfy accepts Matrix messages on the `/_matrix/push/v1/notify` endpoint (see [Push Gateway API](https://spec.matrix.org/v1.2/push-gateway-api/)),
|
||||||
|
and forwards them to the ntfy topic defined in the `pushkey` of the message. The message will then be forwarded to the
|
||||||
|
ntfy Android app, and passed on to the Matrix client there.
|
||||||
|
|
||||||
|
There is a nice diagram in the [Push Gateway docs](https://spec.matrix.org/v1.2/push-gateway-api/). In this diagram, the
|
||||||
|
ntfy server plays the role of the Push Gateway, as well as the Push Provider. UnifiedPush is the Provider Push Protocol.
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
|
@ -2,6 +2,27 @@
|
||||||
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
|
||||||
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
|
||||||
|
|
||||||
|
## ntfy server v1.26.0 (UNRELEASED)
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* ntfy now is a [Matrix Push Gateway](https://spec.matrix.org/v1.2/push-gateway-api/) (in combination with [UnifiedPush](https://unifiedpush.org) as the [Provider Push Protocol](https://unifiedpush.org/developers/gateway/), [#319](https://github.com/binwiederhier/ntfy/issues/319)/[#326](https://github.com/binwiederhier/ntfy/pull/326), thanks to [@MayeulC](https://github.com/MayeulC) for reporting)
|
||||||
|
* Windows CLI is now available via [Scoop](https://scoop.sh) ([ScoopInstaller#3594](https://github.com/ScoopInstaller/Main/pull/3594), [#311](https://github.com/binwiederhier/ntfy/pull/311), [#269](https://github.com/binwiederhier/ntfy/issues/269), thanks to [@kzshantonu](https://github.com/kzshantonu))
|
||||||
|
* [Uptime Kuma](https://github.com/louislam/uptime-kuma) now allows publishing to ntfy ([uptime-kuma#1674](https://github.com/louislam/uptime-kuma/pull/1674), thanks to [@philippdormann](https://github.com/philippdormann))
|
||||||
|
* Display ntfy version in `ntfy serve` command ([#314](https://github.com/binwiederhier/ntfy/issues/314), thanks to [@poblabs](https://github.com/poblabs))
|
||||||
|
|
||||||
|
**Bugs:**
|
||||||
|
|
||||||
|
* Web app: Show "notifications not supported" alert on HTTP ([#323](https://github.com/binwiederhier/ntfy/issues/323), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
||||||
|
|
||||||
|
**Documentation**
|
||||||
|
|
||||||
|
* Added [example](examples.md) for [Uptime Kuma](https://github.com/louislam/uptime-kuma) integration ([#315](https://github.com/binwiederhier/ntfy/pull/315), thanks to [@philippdormann](https://github.com/philippdormann))
|
||||||
|
* Fix Docker install instructions ([#320](https://github.com/binwiederhier/ntfy/issues/320), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
||||||
|
* Add clarifying comments to base-url ([#322](https://github.com/binwiederhier/ntfy/issues/322), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
||||||
|
* Update FAQ for iOS app ([#321](https://github.com/binwiederhier/ntfy/issues/321), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
||||||
|
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
## ntfy Android app v1.14.0 (UNRELEASED)
|
## ntfy Android app v1.14.0 (UNRELEASED)
|
||||||
|
@ -11,26 +32,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||||
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
|
* Italian (thanks to [@Genio2003](https://hosted.weblate.org/user/Genio2003/))
|
||||||
|
|
||||||
|
|
||||||
## ntfy server v1.26.0 (UNRELEASED)
|
|
||||||
|
|
||||||
**Bugs:**
|
|
||||||
|
|
||||||
* Web app: Show "notifications not supported" alert on HTTP ([#323](https://github.com/binwiederhier/ntfy/issues/323), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Windows CLI is now available via [Scoop](https://scoop.sh) ([ScoopInstaller#3594](https://github.com/ScoopInstaller/Main/pull/3594), [#311](https://github.com/binwiederhier/ntfy/pull/311), [#269](https://github.com/binwiederhier/ntfy/issues/269), thanks to [@kzshantonu](https://github.com/kzshantonu))
|
|
||||||
* [Uptime Kuma](https://github.com/louislam/uptime-kuma) now allows publishing to ntfy ([uptime-kuma#1674](https://github.com/louislam/uptime-kuma/pull/1674), thanks to [@philippdormann](https://github.com/philippdormann))
|
|
||||||
* Display ntfy version in `ntfy serve` command ([#314](https://github.com/binwiederhier/ntfy/issues/314), thanks to [@poblabs](https://github.com/poblabs))
|
|
||||||
|
|
||||||
**Documentation**
|
|
||||||
|
|
||||||
* Added [example](examples.md) for [Uptime Kuma](https://github.com/louislam/uptime-kuma) integration ([#315](https://github.com/binwiederhier/ntfy/pull/315), thanks to [@philippdormann](https://github.com/philippdormann))
|
|
||||||
* Fix Docker install instructions ([#320](https://github.com/binwiederhier/ntfy/issues/320), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
|
||||||
* Add clarifying comments to base-url ([#322](https://github.com/binwiederhier/ntfy/issues/322), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
|
||||||
* Update FAQ for iOS app ([#321](https://github.com/binwiederhier/ntfy/issues/321), thanks to [@milksteakjellybeans](https://github.com/milksteakjellybeans) for reporting)
|
|
||||||
|
|
||||||
|
|
||||||
## ntfy iOS app v1.2 (UNRELEASED)
|
## ntfy iOS app v1.2 (UNRELEASED)
|
||||||
|
|
||||||
This release adds support for authentication/authorization for self-hosted servers. It also allows you to
|
This release adds support for authentication/authorization for self-hosted servers. It also allows you to
|
||||||
|
|
|
@ -60,7 +60,8 @@ figure video {
|
||||||
}
|
}
|
||||||
|
|
||||||
.screenshots img {
|
.screenshots img {
|
||||||
height: 230px;
|
max-height: 230px;
|
||||||
|
max-width: 300px;
|
||||||
margin: 3px;
|
margin: 3px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
filter: drop-shadow(2px 2px 2px #ddd);
|
filter: drop-shadow(2px 2px 2px #ddd);
|
||||||
|
|
|
@ -87,7 +87,7 @@ recommended way to subscribe to a topic**. The notable exception is JavaScript,
|
||||||
### Subscribe as SSE stream
|
### Subscribe as SSE stream
|
||||||
Using [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) in JavaScript, you can consume
|
Using [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) in JavaScript, you can consume
|
||||||
notifications via a [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) stream. It's incredibly
|
notifications via a [Server-Sent Events (SSE)](https://en.wikipedia.org/wiki/Server-sent_events) stream. It's incredibly
|
||||||
easy to use. Here's what it looks like. You may also want to check out the [live example](/example.html).
|
easy to use. Here's what it looks like. You may also want to check out the [full example on GitHub](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-eventsource).
|
||||||
|
|
||||||
=== "Command line (curl)"
|
=== "Command line (curl)"
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,56 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>ntfy.sh: EventSource Example</title>
|
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
|
||||||
<style>
|
|
||||||
body { font-size: 1.2em; line-height: 130%; }
|
|
||||||
#events { font-family: monospace; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>ntfy.sh: EventSource Example</h1>
|
|
||||||
<p>
|
|
||||||
This is an example showing how to use <a href="https://ntfy.sh">ntfy.sh</a> with
|
|
||||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a>.<br/>
|
|
||||||
This example doesn't need a server. You can just save the HTML page and run it from anywhere.
|
|
||||||
</p>
|
|
||||||
<button id="publishButton">Send test notification</button>
|
|
||||||
<p><b>Log:</b></p>
|
|
||||||
<div id="events"></div>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
const publishURL = `https://ntfy.sh/example`;
|
|
||||||
const subscribeURL = `https://ntfy.sh/example/sse`;
|
|
||||||
const events = document.getElementById('events');
|
|
||||||
const eventSource = new EventSource(subscribeURL);
|
|
||||||
|
|
||||||
// Publish button
|
|
||||||
document.getElementById("publishButton").onclick = () => {
|
|
||||||
fetch(publishURL, {
|
|
||||||
method: 'POST', // works with PUT as well, though that sends an OPTIONS request too!
|
|
||||||
body: `It is ${new Date().toString()}. This is a test.`
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
// Incoming events
|
|
||||||
eventSource.onopen = () => {
|
|
||||||
let event = document.createElement('div');
|
|
||||||
event.innerHTML = `EventSource connected to ${subscribeURL}`;
|
|
||||||
events.appendChild(event);
|
|
||||||
};
|
|
||||||
eventSource.onerror = (e) => {
|
|
||||||
let event = document.createElement('div');
|
|
||||||
event.innerHTML = `EventSource error: Failed to connect to ${subscribeURL}`;
|
|
||||||
events.appendChild(event);
|
|
||||||
};
|
|
||||||
eventSource.onmessage = (e) => {
|
|
||||||
let event = document.createElement('div');
|
|
||||||
event.innerHTML = e.data;
|
|
||||||
events.appendChild(event);
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -75,9 +75,6 @@ var (
|
||||||
disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
|
disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
|
||||||
attachURLRegex = regexp.MustCompile(`^https?://`)
|
attachURLRegex = regexp.MustCompile(`^https?://`)
|
||||||
|
|
||||||
//go:embed "example.html"
|
|
||||||
exampleSource string
|
|
||||||
|
|
||||||
//go:embed site
|
//go:embed site
|
||||||
webFs embed.FS
|
webFs embed.FS
|
||||||
webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
|
webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
|
||||||
|
@ -283,8 +280,6 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
||||||
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
return s.ensureWebEnabled(s.handleHome)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
|
|
||||||
return s.ensureWebEnabled(s.handleExample)(w, r, v)
|
|
||||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||||
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
||||||
|
@ -357,11 +352,6 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
|
||||||
_, err := io.WriteString(w, exampleSource)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
appRoot := "/"
|
appRoot := "/"
|
||||||
if !s.config.WebRootIsApp {
|
if !s.config.WebRootIsApp {
|
||||||
|
@ -435,7 +425,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleMatrixDiscovery(w http.ResponseWriter) error {
|
func (s *Server) handleMatrixDiscovery(w http.ResponseWriter) error {
|
||||||
return handleMatrixDiscovery(w)
|
return writeMatrixDiscoveryResponse(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*message, error) {
|
func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*message, error) {
|
||||||
|
|
|
@ -11,9 +11,36 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
// Matrix Push Gateway / UnifiedPush / ntfy integration:
|
||||||
matrixPushKeyHeader = "X-Matrix-Pushkey"
|
//
|
||||||
)
|
// ntfy implements a Matrix Push Gateway (as defined in https://spec.matrix.org/v1.2/push-gateway-api/),
|
||||||
|
// in combination with UnifiedPush as the Provider Push Protocol (as defined in https://unifiedpush.org/developers/gateway/).
|
||||||
|
//
|
||||||
|
// In the picture below, ntfy is the Push Gateway (mostly in this file), as well as the Push Provider (ntfy's
|
||||||
|
// main functionality). UnifiedPush is the Provider Push Protocol, as implemented by the ntfy server and the
|
||||||
|
// ntfy Android app.
|
||||||
|
//
|
||||||
|
// +--------------------+ +-------------------+
|
||||||
|
// Matrix HTTP | | | |
|
||||||
|
// Notification Protocol | App Developer | | Device Vendor |
|
||||||
|
// | | | |
|
||||||
|
// +-------------------+ | +----------------+ | | +---------------+ |
|
||||||
|
// | | | | | | | | | |
|
||||||
|
// | Matrix homeserver +-----> Push Gateway +------> Push Provider | |
|
||||||
|
// | | | | | | | | | |
|
||||||
|
// +-^-----------------+ | +----------------+ | | +----+----------+ |
|
||||||
|
// | | | | | |
|
||||||
|
// Matrix | | | | | |
|
||||||
|
// Client/Server API + | | | | |
|
||||||
|
// | | +--------------------+ +-------------------+
|
||||||
|
// | +--+-+ |
|
||||||
|
// | | <-------------------------------------------+
|
||||||
|
// +---+ |
|
||||||
|
// | | Provider Push Protocol
|
||||||
|
// +----+
|
||||||
|
//
|
||||||
|
// Mobile Device or Client
|
||||||
|
//
|
||||||
|
|
||||||
// matrixRequest represents a Matrix message, as it is sent to a Push Gateway (as per
|
// matrixRequest represents a Matrix message, as it is sent to a Push Gateway (as per
|
||||||
// this spec: https://spec.matrix.org/v1.2/push-gateway-api/).
|
// this spec: https://spec.matrix.org/v1.2/push-gateway-api/).
|
||||||
|
@ -30,6 +57,7 @@ const (
|
||||||
// ]
|
// ]
|
||||||
// }
|
// }
|
||||||
// }
|
// }
|
||||||
|
//
|
||||||
type matrixRequest struct {
|
type matrixRequest struct {
|
||||||
Notification *struct {
|
Notification *struct {
|
||||||
Devices []*struct {
|
Devices []*struct {
|
||||||
|
@ -38,10 +66,13 @@ type matrixRequest struct {
|
||||||
} `json:"notification"`
|
} `json:"notification"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matrixResponse represents the response to a Matrix push gateway message, as defined
|
||||||
|
// in the spec (https://spec.matrix.org/v1.2/push-gateway-api/).
|
||||||
type matrixResponse struct {
|
type matrixResponse struct {
|
||||||
Rejected []string `json:"rejected"`
|
Rejected []string `json:"rejected"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// errMatrix represents an error when handing Matrix gateway messages
|
||||||
type errMatrix struct {
|
type errMatrix struct {
|
||||||
pushKey string
|
pushKey string
|
||||||
err error
|
err error
|
||||||
|
@ -54,6 +85,12 @@ func (e errMatrix) Error() string {
|
||||||
return fmt.Sprintf("message with push key %s rejected", e.pushKey)
|
return fmt.Sprintf("message with push key %s rejected", e.pushKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// matrixPushKeyHeader is a header that's used internally to pass the Matrix push key (from the matrixRequest)
|
||||||
|
// along with the request. The push key is only used if an error occurs down the line.
|
||||||
|
matrixPushKeyHeader = "X-Matrix-Pushkey"
|
||||||
|
)
|
||||||
|
|
||||||
// newRequestFromMatrixJSON reads the request body as a Matrix JSON message, parses the "pushkey", and creates a new
|
// newRequestFromMatrixJSON reads the request body as a Matrix JSON message, parses the "pushkey", and creates a new
|
||||||
// HTTP request that looks like a normal ntfy request from it.
|
// HTTP request that looks like a normal ntfy request from it.
|
||||||
//
|
//
|
||||||
|
@ -82,7 +119,7 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int)
|
||||||
} else if m.Notification == nil || len(m.Notification.Devices) == 0 || m.Notification.Devices[0].PushKey == "" {
|
} else if m.Notification == nil || len(m.Notification.Devices) == 0 || m.Notification.Devices[0].PushKey == "" {
|
||||||
return nil, errHTTPBadRequestMatrixMessageInvalid
|
return nil, errHTTPBadRequestMatrixMessageInvalid
|
||||||
}
|
}
|
||||||
pushKey := m.Notification.Devices[0].PushKey
|
pushKey := m.Notification.Devices[0].PushKey // We ignore other devices for now, see discussion in #316
|
||||||
if !strings.HasPrefix(pushKey, baseURL+"/") {
|
if !strings.HasPrefix(pushKey, baseURL+"/") {
|
||||||
return nil, &errMatrix{pushKey: pushKey, err: errHTTPBadRequestMatrixPushkeyBaseURLMismatch}
|
return nil, &errMatrix{pushKey: pushKey, err: errHTTPBadRequestMatrixPushkeyBaseURLMismatch}
|
||||||
}
|
}
|
||||||
|
@ -94,21 +131,27 @@ func newRequestFromMatrixJSON(r *http.Request, baseURL string, messageLimit int)
|
||||||
return newRequest, nil
|
return newRequest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleMatrixDiscovery(w http.ResponseWriter) error {
|
// writeMatrixDiscoveryResponse writes the UnifiedPush Matrix Gateway Discovery response to the given http.ResponseWriter,
|
||||||
|
// as per the spec (https://unifiedpush.org/developers/gateway/).
|
||||||
|
func writeMatrixDiscoveryResponse(w http.ResponseWriter) error {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
_, err := io.WriteString(w, `{"unifiedpush":{"gateway":"matrix"}}`+"\n")
|
_, err := io.WriteString(w, `{"unifiedpush":{"gateway":"matrix"}}`+"\n")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeMatrixError logs and writes the errMatrix to the given http.ResponseWriter as a matrixResponse
|
||||||
func writeMatrixError(w http.ResponseWriter, r *http.Request, v *visitor, err *errMatrix) error {
|
func writeMatrixError(w http.ResponseWriter, r *http.Request, v *visitor, err *errMatrix) error {
|
||||||
log.Debug("%s Matrix gateway error: %s", logHTTPPrefix(v, r), err.Error())
|
log.Debug("%s Matrix gateway error: %s", logHTTPPrefix(v, r), err.Error())
|
||||||
return writeMatrixResponse(w, err.pushKey)
|
return writeMatrixResponse(w, err.pushKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeMatrixSuccess writes a successful matrixResponse (no rejected push key) to the given http.ResponseWriter
|
||||||
func writeMatrixSuccess(w http.ResponseWriter) error {
|
func writeMatrixSuccess(w http.ResponseWriter) error {
|
||||||
return writeMatrixResponse(w, "")
|
return writeMatrixResponse(w, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// writeMatrixResponse writes a matrixResponse to the given http.ResponseWriter, as defined in
|
||||||
|
// the spec (https://spec.matrix.org/v1.2/push-gateway-api/)
|
||||||
func writeMatrixResponse(w http.ResponseWriter, rejectedPushKey string) error {
|
func writeMatrixResponse(w http.ResponseWriter, rejectedPushKey string) error {
|
||||||
rejected := make([]string, 0)
|
rejected := make([]string, 0)
|
||||||
if rejectedPushKey != "" {
|
if rejectedPushKey != "" {
|
||||||
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMatrix_NewRequestFromMatrixJSON_Success(t *testing.T) {
|
||||||
|
baseURL := "https://ntfy.sh"
|
||||||
|
maxLength := 4096
|
||||||
|
body := `{"notification":{"content":{"body":"I'm floating in a most peculiar way.","msgtype":"m.text"},"counts":{"missed_calls":1,"unread":2},"devices":[{"app_id":"org.matrix.matrixConsole.ios","data":{},"pushkey":"https://ntfy.sh/upABCDEFGHI?up=1","pushkey_ts":12345678,"tweaks":{"sound":"bing"}}],"event_id":"$3957tyerfgewrf384","prio":"high","room_alias":"#exampleroom:matrix.org","room_id":"!slw48wfj34rtnrf:example.com","room_name":"Mission Control","sender":"@exampleuser:matrix.org","sender_display_name":"Major Tom","type":"m.room.message"}}`
|
||||||
|
r, _ := http.NewRequest("POST", "http://ntfy.example.com/_matrix/push/v1/notify", strings.NewReader(body))
|
||||||
|
newRequest, err := newRequestFromMatrixJSON(r, baseURL, maxLength)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.Equal(t, "POST", newRequest.Method)
|
||||||
|
require.Equal(t, "https://ntfy.sh/upABCDEFGHI?up=1", newRequest.URL.String())
|
||||||
|
require.Equal(t, "https://ntfy.sh/upABCDEFGHI?up=1", newRequest.Header.Get("X-Matrix-Pushkey"))
|
||||||
|
require.Equal(t, body, readAll(t, newRequest.Body))
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -171,10 +172,6 @@ func TestServer_StaticSites(t *testing.T) {
|
||||||
require.Equal(t, 301, rr.Code)
|
require.Equal(t, 301, rr.Code)
|
||||||
|
|
||||||
// Docs test removed, it was failing annoyingly.
|
// Docs test removed, it was failing annoyingly.
|
||||||
|
|
||||||
rr = request(t, s, "GET", "/example.html", "", nil)
|
|
||||||
require.Equal(t, 200, rr.Code)
|
|
||||||
require.Contains(t, rr.Body.String(), "</html>")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestServer_WebEnabled(t *testing.T) {
|
func TestServer_WebEnabled(t *testing.T) {
|
||||||
|
@ -185,9 +182,6 @@ func TestServer_WebEnabled(t *testing.T) {
|
||||||
rr := request(t, s, "GET", "/", "", nil)
|
rr := request(t, s, "GET", "/", "", nil)
|
||||||
require.Equal(t, 404, rr.Code)
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
rr = request(t, s, "GET", "/example.html", "", nil)
|
|
||||||
require.Equal(t, 404, rr.Code)
|
|
||||||
|
|
||||||
rr = request(t, s, "GET", "/config.js", "", nil)
|
rr = request(t, s, "GET", "/config.js", "", nil)
|
||||||
require.Equal(t, 404, rr.Code)
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
@ -201,9 +195,6 @@ func TestServer_WebEnabled(t *testing.T) {
|
||||||
rr = request(t, s2, "GET", "/", "", nil)
|
rr = request(t, s2, "GET", "/", "", nil)
|
||||||
require.Equal(t, 200, rr.Code)
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
rr = request(t, s2, "GET", "/example.html", "", nil)
|
|
||||||
require.Equal(t, 200, rr.Code)
|
|
||||||
|
|
||||||
rr = request(t, s2, "GET", "/config.js", "", nil)
|
rr = request(t, s2, "GET", "/config.js", "", nil)
|
||||||
require.Equal(t, 200, rr.Code)
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
@ -1390,3 +1381,11 @@ func toHTTPError(t *testing.T, s string) *errHTTP {
|
||||||
func basicAuth(s string) string {
|
func basicAuth(s string) string {
|
||||||
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(s)))
|
return fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(s)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readAll(t *testing.T, rc io.ReadCloser) string {
|
||||||
|
b, err := io.ReadAll(rc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ package server
|
||||||
import (
|
import (
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -304,14 +303,6 @@ func newTestBackend(t *testing.T, handler func(http.ResponseWriter, *http.Reques
|
||||||
return conf, backend
|
return conf, backend
|
||||||
}
|
}
|
||||||
|
|
||||||
func readAll(t *testing.T, rc io.ReadCloser) string {
|
|
||||||
b, err := io.ReadAll(rc)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return string(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState {
|
func fakeConnState(t *testing.T, remoteAddr string) *smtp.ConnectionState {
|
||||||
ip, err := net.ResolveIPAddr("ip", remoteAddr)
|
ip, err := net.ResolveIPAddr("ip", remoteAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -18,7 +18,8 @@ type PeekedReadCloser struct {
|
||||||
closed bool
|
closed bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Peek reads the underlying ReadCloser into memory up until the limit and returns a PeekedReadCloser
|
// Peek reads the underlying ReadCloser into memory up until the limit and returns a PeekedReadCloser.
|
||||||
|
// It does not return an error if limit is reached. Instead, LimitReached will be set to true.
|
||||||
func Peek(underlying io.ReadCloser, limit int) (*PeekedReadCloser, error) {
|
func Peek(underlying io.ReadCloser, limit int) (*PeekedReadCloser, error) {
|
||||||
if underlying == nil {
|
if underlying == nil {
|
||||||
underlying = io.NopCloser(strings.NewReader(""))
|
underlying = io.NopCloser(strings.NewReader(""))
|
||||||
|
|
Loading…
Reference in New Issue