From dadb6419ee3351b1a5f9aa6f87dde67b934e7b42 Mon Sep 17 00:00:00 2001
From: "Philipp C. Heckel"
Date: Sun, 5 Dec 2021 12:51:08 -0500
Subject: [PATCH 001/335] Update LICENSE
---
LICENSE | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/LICENSE b/LICENSE
index 261eeb9e..80877693 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright [yyyy] [name of copyright owner]
+ Copyright 2021 Philipp C. Heckel
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
From 3daa590732c565a5292d840de513b216d9ad1c9e Mon Sep 17 00:00:00 2001
From: "Philipp C. Heckel"
Date: Sun, 5 Dec 2021 12:52:20 -0500
Subject: [PATCH 002/335] Update LICENSE.GPLv2
---
LICENSE.GPLv2 | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/LICENSE.GPLv2 b/LICENSE.GPLv2
index 1f963da0..4bf894c5 100644
--- a/LICENSE.GPLv2
+++ b/LICENSE.GPLv2
@@ -290,8 +290,8 @@ to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
-
- Copyright (C)
+ ntfy
+ Copyright (C) 2021 Philipp C. Heckel
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
From 4a3a0de198573fdbe7337c7bf55c1ac635b00ec2 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Sun, 5 Dec 2021 16:28:12 -0500
Subject: [PATCH 003/335] Bump version
---
Makefile | 9 ++-
README.md | 182 ++----------------------------------------------
docs/install.md | 18 ++---
3 files changed, 24 insertions(+), 185 deletions(-)
diff --git a/Makefile b/Makefile
index 988dc779..4b7cd777 100644
--- a/Makefile
+++ b/Makefile
@@ -116,7 +116,14 @@ clean: .PHONY
# Releasing targets
-release: build-deps
+release-check-tags:
+ $(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
+ if grep -q $(LATEST_TAG) docs/install.md; then\
+ echo "ERROR: Must update docs/install.md with latest tag first.";\
+ exit 1;\
+ fi
+
+release: build-deps release-check-tags
goreleaser release --rm-dist --debug
release-snapshot: build-deps
diff --git a/README.md b/README.md
index bc2129dc..93eed30f 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@

-# ntfy.sh | simple HTTP-based pub-sub
+# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[](https://github.com/binwiederhier/ntfy/releases/latest)
[](https://gophers.slack.com/archives/C01JMTPGF2Q)
@@ -19,181 +19,13 @@ too.
-## Usage
+## **[Documentation](https://ntfy.sh/docs/)**
-### Publishing messages
-
-Publishing messages can be done via PUT or POST using. Topics are created on the fly by subscribing or publishing to them.
-Because there is no sign-up, **the topic is essentially a password**, so pick something that's not easily guessable.
-
-Here's an example showing how to publish a message using `curl`:
-
-```
-curl -d "long process is done" ntfy.sh/mytopic
-```
-
-Here's an example in JS with `fetch()` (see [full example](examples)):
-
-```
-fetch('https://ntfy.sh/mytopic', {
- method: 'POST', // PUT works too
- body: 'Hello from the other side.'
-})
-```
-
-### Subscribe to a topic
-You can create and subscribe to a topic either in this web UI, or in your own app by subscribing to an
-[EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), a JSON feed, or raw feed.
-
-#### Subscribe via web
-If you subscribe to a topic via this web UI in the field below, messages published to any subscribed topic
-will show up as **desktop notification**.
-
-You can try this easily on **[ntfy.sh](https://ntfy.sh)**.
-
-#### Subscribe via phone
-You can use the [Ntfy Android App](https://play.google.com/store/apps/details?id=io.heckel.ntfy) to receive
-notifications directly on your phone. Just like the server, this app is also [open source](https://github.com/binwiederhier/ntfy-android).
-
-#### Subscribe via your app, or via the CLI
-Using [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource) in JS, you can consume
-notifications like this (see [full example](examples)):
-
-```javascript
-const eventSource = new EventSource('https://ntfy.sh/mytopic/sse');
-eventSource.onmessage = (e) => {
- // Do something with e.data
-};
-```
-
-You can also use the same `/sse` endpoint via `curl` or any other HTTP library:
-
-```
-$ curl -s ntfy.sh/mytopic/sse
-event: open
-data: {"id":"weSj9RtNkj","time":1635528898,"event":"open","topic":"mytopic"}
-
-data: {"id":"p0M5y6gcCY","time":1635528909,"event":"message","topic":"mytopic","message":"Hi!"}
-
-event: keepalive
-data: {"id":"VNxNIg5fpt","time":1635528928,"event":"keepalive","topic":"test"}
-```
-
-To consume JSON instead, use the `/json` endpoint, which prints one message per line:
-
-```
-$ curl -s ntfy.sh/mytopic/json
-{"id":"SLiKI64DOt","time":1635528757,"event":"open","topic":"mytopic"}
-{"id":"hwQ2YpKdmg","time":1635528741,"event":"message","topic":"mytopic","message":"Hi!"}
-{"id":"DGUDShMCsc","time":1635528787,"event":"keepalive","topic":"mytopic"}
-```
-
-Or use the `/raw` endpoint if you need something super simple (empty lines are keepalive messages):
-
-```
-$ curl -s ntfy.sh/mytopic/raw
-
-This is a notification
-```
-
-#### Message buffering and polling
-Messages are buffered in memory for a few hours to account for network interruptions of subscribers.
-You can read back what you missed by using the `since=...` query parameter. It takes either a
-duration (e.g. `10m` or `30s`) or a Unix timestamp (e.g. `1635528757`):
-
-```
-$ curl -s "ntfy.sh/mytopic/json?since=10m"
-# Same output as above, but includes messages from up to 10 minutes ago
-```
-
-You can also just poll for messages if you don't like the long-standing connection using the `poll=1`
-query parameter. The connection will end after all available messages have been read. This parameter has to be
-combined with `since=`.
-
-```
-$ curl -s "ntfy.sh/mytopic/json?poll=1&since=10m"
-# Returns messages from up to 10 minutes ago and ends the connection
-```
-
-## Examples
-There are a few usage examples in the [examples](examples) directory. I'm sure there are tons of other ways to use it.
-
-## Installation
-Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
-deb/rpm packages.
-
-1. Install ntfy using one of the methods described below
-2. Then (optionally) edit `/etc/ntfy/config.yml`
-3. Then just run it with `ntfy` (or `systemctl start ntfy` when using the deb/rpm).
-
-### Binaries and packages
-**Debian/Ubuntu** (*from a repository*)**:**
-```bash
-curl -sSL https://archive.heckel.io/apt/pubkey.txt | sudo apt-key add -
-sudo apt install apt-transport-https
-sudo sh -c "echo 'deb [arch=amd64] https://archive.heckel.io/apt debian main' > /etc/apt/sources.list.d/archive.heckel.io.list"
-sudo apt update
-sudo apt install ntfy
-```
-
-**Debian/Ubuntu** (*manual install*)**:**
-```bash
-wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_amd64.deb
-dpkg -i ntfy_1.5.0_amd64.deb
-```
-
-**Fedora/RHEL/CentOS:**
-```bash
-rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_amd64.rpm
-```
-
-**Docker:**
-Without cache:
-```
-docker run -p 80:80 -it binwiederhier/ntfy
-```
-
-With cache:
-```bash
-docker run \
- -v /var/cache/ntfy:/var/cache/ntfy \
- -p 80:80 \
- -it \
- binwiederhier/ntfy \
- --cache-file /var/cache/ntfy/cache.db
-```
-
-**Go:**
-```bash
-go get -u heckel.io/ntfy
-```
-
-**Manual install:**
-```bash
-# x86_64/amd64
-wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_x86_64.tar.gz
-
-# armv7
-wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.tar.gz
-
-# arm64/v8
-wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.tar.gz
-
-# Extract and run
-sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
-./ntfy
-```
-
-## Building
-Building `ntfy` is simple. Here's how you do it:
-
-```
-make build-simple
-# Builds to dist/ntfy_linux_amd64/ntfy
-```
-
-To build releases, I use [GoReleaser](https://goreleaser.com/). If you have that installed, you can run `make build` or
-`make build-snapshot`.
+[Getting started](https://ntfy.sh/docs/) |
+[Android/iOS](https://ntfy.sh/docs/subscribe/phone/) |
+[API](https://ntfy.sh/docs/publish/) |
+[Install / Self-hosting](https://ntfy.sh/docs/install/) |
+[Building](https://ntfy.sh/docs/develop/)
## Contributing
I welcome any and all contributions. Just create a PR or an issue.
diff --git a/docs/install.md b/docs/install.md
index 66c6e1c8..d929ed10 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -20,21 +20,21 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_x86_64.tar.gz
+ wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_x86_64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
sudo ./ntfy
```
=== "armv7/armhf"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.tar.gz
+ wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_armv7.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
sudo ./ntfy
```
=== "arm64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.tar.gz
+ wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_arm64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_*.tar.gz ntfy
sudo ./ntfy
```
@@ -82,7 +82,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_amd64.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -90,7 +90,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -98,7 +98,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
- wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.deb
+ wget https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -108,21 +108,21 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_amd64.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_armv7.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
- sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.0/ntfy_1.5.0_linux_arm64.rpm
+ sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.5.1/ntfy_1.5.1_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
From 4cc53633d8f21f459ad8db461e4ad4fc4ed0e875 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Sun, 5 Dec 2021 16:31:06 -0500
Subject: [PATCH 004/335] Remove check tags thing
---
Makefile | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Makefile b/Makefile
index 4b7cd777..9c0c26f4 100644
--- a/Makefile
+++ b/Makefile
@@ -123,7 +123,7 @@ release-check-tags:
exit 1;\
fi
-release: build-deps release-check-tags
+release: build-deps
goreleaser release --rm-dist --debug
release-snapshot: build-deps
From d0d1f9e5c7288674033ad7047d451f5915386fea Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Mon, 6 Dec 2021 16:43:06 -0500
Subject: [PATCH 005/335] Docs for "tuning for scale"
---
Makefile | 4 +-
config/ntfy.service | 1 +
docs/config.md | 173 ++++++++++++++++++++++++++++++++++++++++++--
3 files changed, 171 insertions(+), 7 deletions(-)
diff --git a/Makefile b/Makefile
index 9c0c26f4..46c69c76 100644
--- a/Makefile
+++ b/Makefile
@@ -118,12 +118,12 @@ clean: .PHONY
release-check-tags:
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
- if grep -q $(LATEST_TAG) docs/install.md; then\
+ if ! grep -q $(LATEST_TAG) docs/install.md; then\
echo "ERROR: Must update docs/install.md with latest tag first.";\
exit 1;\
fi
-release: build-deps
+release: build-deps release-check-tags
goreleaser release --rm-dist --debug
release-snapshot: build-deps
diff --git a/config/ntfy.service b/config/ntfy.service
index 4a70cd02..21acea50 100644
--- a/config/ntfy.service
+++ b/config/ntfy.service
@@ -5,6 +5,7 @@ After=network.target
[Service]
ExecStart=/usr/bin/ntfy
Restart=on-failure
+LimitNOFILE=10000
[Install]
WantedBy=multi-user.target
diff --git a/docs/config.md b/docs/config.md
index 4f9afc09..d256ebf5 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -34,16 +34,124 @@ Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscri
## Behind a proxy (TLS, etc.)
!!! warning
- If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise all visitors are rate limited
- as if they are one.
+ If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
+ [rate limited](#rate-limiting) as if they are one.
-**Rate limiting:** If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
+### Rate limiting
+If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary
identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
-**TLS/SSL:** ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you
-are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself.
+### TLS/SSL
+ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you
+are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).
+
+### nginx/Apache2 configs
+For your convenience, here's a working config that'll help configure things behind a proxy. In this
+example, ntfy runs on `:13222` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
+or the root domain:
+
+=== "nginx (/etc/nginx/sites-*/ntfy)"
+ ```
+ server {
+ listen 80;
+ server_name ntfy.sh;
+
+ location / {
+ proxy_pass http://127.0.0.1:13222;
+ proxy_http_version 1.1;
+
+ proxy_buffering off;
+ proxy_redirect off;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 1m;
+ proxy_send_timeout 1m;
+ proxy_read_timeout 1m;
+ }
+ }
+
+ server {
+ listen 443 ssl;
+ server_name ntfy.sh;
+
+ ssl_session_cache builtin:1000 shared:SSL:10m;
+ ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+ ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
+ ssl_prefer_server_ciphers on;
+
+ ssl_certificate /etc/letsencrypt/live/nopaste.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/nopaste.net/privkey.pem;
+
+ location / {
+ proxy_pass http://127.0.0.1:13222;
+ proxy_http_version 1.1;
+
+ proxy_buffering off;
+ proxy_redirect off;
+
+ proxy_set_header Host $http_host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ proxy_connect_timeout 1m;
+ proxy_send_timeout 1m;
+ proxy_read_timeout 1m;
+ }
+ }
+ ```
+
+=== "Apache2 (/etc/apache2/sites-*/ntfy.conf"
+ ```
+
+ ServerName ntfy.sh
+
+ SetEnv proxy-nokeepalive 1
+ SetEnv proxy-sendchunked 1
+
+ ProxyPass / http://127.0.0.1:13222/
+ ProxyPassReverse / http://127.0.0.1:13222/
+
+ # Higher than the max message size of 512k
+ LimitRequestBody 102400
+
+ # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
+ # it to work with curl without the annoying https:// prefix
+ RewriteEngine on
+ RewriteCond %{REQUEST_METHOD} GET
+ RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
+
+
+
+ ServerName ntfy.sh
+
+ SSLEngine on
+ SSLCertificateFile /etc/letsencrypt/live/ntfy.sh/fullchain.pem
+ SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
+ Include /etc/letsencrypt/options-ssl-apache.conf
+
+ SetEnv proxy-nokeepalive 1
+ SetEnv proxy-sendchunked 1
+
+ ProxyPass / http://127.0.0.1:13222/
+ ProxyPassReverse / http://127.0.0.1:13222/
+
+ # Higher than the max message size of 512k
+ LimitRequestBody 102400
+
+ # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
+ # it to work with curl without the annoying https:// prefix
+ RewriteEngine on
+ RewriteCond %{REQUEST_METHOD} GET
+ RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
+
+ ```
## Firebase (FCM)
!!! info
@@ -99,6 +207,61 @@ request every 10s (defined by `visitor-request-limit-replenish`)
During normal usage, you shouldn't encounter this limit at all, and even if you burst a few requests shortly (e.g. when you
reconnect after a connection drop), it shouldn't have any effect.
+
+## Tuning for scale
+If you're running ntfy for your home server, you probably don't need to worry about scale at all. In its default config,
+if it's not behind a proxy, the ntfy server can keep about **as many connections as the open file limit allows**.
+This limit is typically called `nofile`. Other than that, RAM and CPU are obviously relevant. You may also want to check
+out [this discussion on Reddit](https://www.reddit.com/r/golang/comments/r9u4ee/how_many_actively_connected_http_clients_can_a_go/).
+
+Depending on *how you run it*, here are a few limits that are relevant:
+
+### For systemd services
+If you're running ntfy in a systemd service (e.g. for .deb/.rpm packages), the main limiting factor is the
+`LimitNOFILE` setting in the systemd unit. The default open files limit for `ntfy.service` is 10000. You can override it
+by creating a `/etc/systemd/system/ntfy.service.d/override.conf` file. As far as I can tell, `/etc/security/limits.conf`
+is not relevant.
+
+=== "/etc/systemd/system/ntfy.service.d/override.conf"
+ ```
+ # Allow 20,000 ntfy connections (and give room for other file handles)
+ [Service]
+ LimitNOFILE=20500
+ ```
+
+### Outside of systemd
+If you're running outside systemd, you may want to adjust your `/etc/security/limits.conf` file to
+increase the `nofile` setting. Here's an example that increases the limit to 5000. You can find out the current setting
+by running `ulimit -n`, or manually override it temporarily by running `ulimit -n 50000`.
+
+=== "/etc/security/limits.conf"
+ ```
+ # Increase open files limit globally
+ * hard nofile 20500
+ ```
+
+### Proxy limits (nginx, Apache2)
+If you are running [behind a proxy](#behind-a-proxy-tls-etc) (e.g. nginx, Apache), the open files limit of the proxy is also
+relevant. So if your proxy runs inside of systemd, increase the limits in systemd for the proxy. Typically, the proxy
+open files limit has to be **double the number of how many connections you'd like to support**, because the proxy has
+to maintain the client connection and the connection to ntfy.
+
+=== "/etc/nginx/nginx.conf"
+ ```
+ events {
+ # Allow 20,000 proxy connections (2x of the desired ntfy connection count;
+ # and give room for other file handles)
+ worker_connections 40500;
+ }
+ ```
+=== "/etc/systemd/system/nginx.service.d/override.conf"
+ ```
+ # Allow 40,000 proxy connections (2x of the desired ntfy connection count;
+ # and give room for other file handles)
+ [Service]
+ LimitNOFILE=40500
+ ```
+
## Config options
Each config option can be set in the config file `/etc/ntfy/config.yml` (e.g. `listen-http: :80`) or as a
CLI option (e.g. `--listen-http :80`. Here's a list of all available options. Alternatively, you can set an environment
From faa7de9f37f7ac5d746ebd954235d35907e7e799 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Mon, 6 Dec 2021 19:04:45 -0500
Subject: [PATCH 006/335] Drop shadow color
---
docs/static/css/extra.css | 17 +++++++++++++++--
mkdocs.yml | 2 --
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/docs/static/css/extra.css b/docs/static/css/extra.css
index 797c1aa7..a42c63d4 100644
--- a/docs/static/css/extra.css
+++ b/docs/static/css/extra.css
@@ -1,3 +1,9 @@
+:root {
+ --md-primary-fg-color: #3a9784;
+ --md-primary-fg-color--light: #3a9784;
+ --md-primary-fg-color--dark: #3a9784;
+}
+
.md-header__button.md-logo :is(img, svg) {
width: unset !important;
}
@@ -6,11 +12,18 @@ article {
padding-bottom: 50px;
}
-figure iframe, figure img, figure video {
- filter: drop-shadow(3px 3px 3px #ccc);
+figure img, figure video {
border-radius: 7px;
}
+body[data-md-color-scheme="default"] figure img, body[data-md-color-scheme="default"] figure video {
+ filter: drop-shadow(3px 3px 3px #ccc);
+}
+
+body[data-md-color-scheme="slate"] figure img, body[data-md-color-scheme="slate"] figure video {
+ filter: drop-shadow(3px 3px 3px #1a1313);
+}
+
figure video {
width: 100%;
max-height: 450px;
diff --git a/mkdocs.yml b/mkdocs.yml
index d775238b..6f6492fc 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -17,13 +17,11 @@ theme:
palette:
- media: "(prefers-color-scheme: light)" # Light mode
scheme: default
- primary: teal
toggle:
icon: material/lightbulb-outline
name: Switch to light mode
- media: "(prefers-color-scheme: dark)" # Dark mode
scheme: slate
- primary: teal
accent: indigo
toggle:
icon: material/lightbulb
From f1fac8da75bbbfee1bce5859aa3e259546b06486 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Mon, 6 Dec 2021 20:05:06 -0500
Subject: [PATCH 007/335] Proxy docs
---
docs/config.md | 15 ++++++++++++---
mkdocs.yml | 4 ++--
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/docs/config.md b/docs/config.md
index d256ebf5..547ce84f 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -32,17 +32,26 @@ Subscribers can retrieve cached messaging using the [`poll=1` parameter](subscri
[`since=` parameter](subscribe/api.md#fetching-cached-messages).
## Behind a proxy (TLS, etc.)
-
!!! warning
- If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
+ If you are running ntfy behind a proxy, you must set the `behind-proxy` flag. Otherwise, all visitors are
[rate limited](#rate-limiting) as if they are one.
+It may be desirable to run ntfy behind a proxy, e.g. so you can provide TLS certificates using Let's Encrypt using certbot,
+or simply because you'd like to share the ports (80/443) with other services. Whatever your reasons may be, there are a
+few things to consider.
+
### Rate limiting
If you are running ntfy behind a proxy (e.g. nginx, HAproxy or Apache), you should set the `behind-proxy`
flag. This will instruct the [rate limiting](#rate-limiting) logic to use the `X-Forwarded-For` header as the primary
identifier for a visitor, as opposed to the remote IP address. If the `behind-proxy` flag is not set, all visitors will
be counted as one, because from the perspective of the ntfy server, they all share the proxy's IP address.
+=== "/etc/ntfy/config.yml"
+ ```
+ # Tell ntfy to use "X-Forwarded-For" to identify visitors
+ behind-proxy: true
+ ```
+
### TLS/SSL
ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you
are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).
@@ -107,7 +116,7 @@ or the root domain:
}
```
-=== "Apache2 (/etc/apache2/sites-*/ntfy.conf"
+=== "Apache2 (/etc/apache2/sites-*/ntfy.conf)"
```
ServerName ntfy.sh
diff --git a/mkdocs.yml b/mkdocs.yml
index 6f6492fc..6758aea7 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -19,13 +19,13 @@ theme:
scheme: default
toggle:
icon: material/lightbulb-outline
- name: Switch to light mode
+ name: Switch to dark mode
- media: "(prefers-color-scheme: dark)" # Dark mode
scheme: slate
accent: indigo
toggle:
icon: material/lightbulb
- name: Switch to dark mode
+ name: Switch to light mode
features:
- search.suggest
- search.highlight
From da8f90d3885e1aec99f338fe5a0c4d5614441645 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 10:38:58 -0500
Subject: [PATCH 008/335] gofmt
---
Makefile | 2 +-
config/config.go | 12 ++++++------
server/server.go | 4 ++--
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/Makefile b/Makefile
index 46c69c76..b4166d28 100644
--- a/Makefile
+++ b/Makefile
@@ -123,7 +123,7 @@ release-check-tags:
exit 1;\
fi
-release: build-deps release-check-tags
+release: build-deps release-check-tags check
goreleaser release --rm-dist --debug
release-snapshot: build-deps
diff --git a/config/config.go b/config/config.go
index bffa9782..2dbed003 100644
--- a/config/config.go
+++ b/config/config.go
@@ -27,9 +27,9 @@ const (
// Config is the main config struct for the application. Use New to instantiate a default config struct.
type Config struct {
ListenHTTP string
- ListenHTTPS string
- KeyFile string
- CertFile string
+ ListenHTTPS string
+ KeyFile string
+ CertFile string
FirebaseKeyFile string
CacheFile string
CacheDuration time.Duration
@@ -46,9 +46,9 @@ type Config struct {
func New(listenHTTP string) *Config {
return &Config{
ListenHTTP: listenHTTP,
- ListenHTTPS: "",
- KeyFile: "",
- CertFile: "",
+ ListenHTTPS: "",
+ KeyFile: "",
+ CertFile: "",
FirebaseKeyFile: "",
CacheFile: "",
CacheDuration: DefaultCacheDuration,
diff --git a/server/server.go b/server/server.go
index 1780ad25..0704a8b4 100644
--- a/server/server.go
+++ b/server/server.go
@@ -83,7 +83,7 @@ var (
rawRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
staticRegex = regexp.MustCompile(`^/static/.+`)
- docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
+ docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
//go:embed "index.gohtml"
indexSource string
@@ -97,7 +97,7 @@ var (
webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
//go:embed docs
- docsStaticFs embed.FS
+ docsStaticFs embed.FS
docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
errHTTPBadRequest = &errHTTP{http.StatusBadRequest, http.StatusText(http.StatusBadRequest)}
From be50af0a7a2ab86f78c8d21ad0e2a8c42fd64c31 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 11:45:15 -0500
Subject: [PATCH 009/335] Begin unit tests, relates to #35
---
go.mod | 23 ++++++-----
go.sum | 56 +++++++++++---------------
server/cache.go | 7 ++++
server/cache_mem.go | 8 ++--
server/cache_mem_test.go | 12 ++++++
server/cache_sqlite.go | 19 +++++----
server/cache_sqlite_test.go | 23 +++++++++++
server/cache_test.go | 79 +++++++++++++++++++++++++++++++++++++
server/server.go | 19 +++++----
util/embedfs.go | 12 ++++++
util/util.go | 1 +
11 files changed, 198 insertions(+), 61 deletions(-)
create mode 100644 server/cache_mem_test.go
create mode 100644 server/cache_sqlite_test.go
create mode 100644 server/cache_test.go
diff --git a/go.mod b/go.mod
index dc198b2b..9987eebc 100644
--- a/go.mod
+++ b/go.mod
@@ -9,34 +9,37 @@ require (
github.com/BurntSushi/toml v0.4.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
github.com/mattn/go-sqlite3 v1.14.9
+ github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.3.0
- golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 // indirect
- golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac
- google.golang.org/api v0.60.0
+ golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 // indirect
+ golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
+ google.golang.org/api v0.61.0
gopkg.in/yaml.v2 v2.4.0 // indirect
)
require (
- cloud.google.com/go v0.97.0 // indirect
+ cloud.google.com/go v0.99.0 // indirect
github.com/census-instrumentation/opencensus-proto v0.3.0 // indirect
- github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 // indirect
- github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 // indirect
- github.com/envoyproxy/go-control-plane v0.10.0 // indirect
+ github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 // indirect
+ github.com/davecgh/go-spew v1.1.0 // indirect
+ github.com/envoyproxy/go-control-plane v0.10.1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/googleapis/gax-go/v2 v2.1.1 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d // indirect
- golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
+ golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/appengine v1.6.7 // indirect
- google.golang.org/genproto v0.0.0-20211101144312-62acf1d99145 // indirect
- google.golang.org/grpc v1.41.0 // indirect
+ google.golang.org/genproto v0.0.0-20211206220100-3cb06788ce7f // indirect
+ google.golang.org/grpc v1.42.0 // indirect
google.golang.org/protobuf v1.27.1 // indirect
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
diff --git a/go.sum b/go.sum
index ba19948f..48252afe 100644
--- a/go.sum
+++ b/go.sum
@@ -24,8 +24,9 @@ cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWc
cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=
cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
-cloud.google.com/go v0.97.0 h1:3DXvAyifywvq64LfkKaMOmkWPS1CikIQdMe2lY9vxU8=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
+cloud.google.com/go v0.99.0 h1:y/cM2iqGgGi5D5DQZl6D9STN/3dR/Vx5Mp8s752oJTY=
+cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -34,8 +35,6 @@ cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4g
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
-cloud.google.com/go/firestore v1.6.0 h1:dMIWvm+3O0E3DM7kcZPH0FBQ94Xg/OMkdTNDaY9itbI=
-cloud.google.com/go/firestore v1.6.0/go.mod h1:afJwI0vaXwAG54kI7A//lP/lSPDkQORQuMkv56TxEPU=
cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -56,10 +55,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw=
github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
-github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
-github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/census-instrumentation/opencensus-proto v0.3.0 h1:t/LhUZLVitR1Ow2YOnduCsavhwFUklBMoGVYUCqmCqk=
github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -74,20 +71,20 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
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-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
-github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403 h1:cqQfy1jclcSy/FwLjemeg3SR1yaINm74aQyupQ0Bl8M=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4 h1:hzAQntlaYRkVSFEfj9OTWlVV1H155FMD8BTKktLv0QI=
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
-github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed h1:OZmjad4L3H8ncOIR8rnb5MREYqG8ixi5+WbeUsquF0c=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1 h1:zH8ljVhhq7yC0MIeUL/IviMtY8hx2mK8cN9wEYb8ggw=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
+github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490 h1:KwaoQzs/WeUxxJqiJsZ4euOly1Az/IgZXXSxlD/UBNk=
+github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -95,12 +92,10 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
-github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0 h1:dulLQAYQFYtG5MTplgNGHWuV2D+OBD+Z8lmDBmbLg+s=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
-github.com/envoyproxy/go-control-plane v0.10.0 h1:WVt4HEPbdRbRD/PKKPbPnIVavO6gk/h673jWyIJ016k=
-github.com/envoyproxy/go-control-plane v0.10.0/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
-github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
+github.com/envoyproxy/go-control-plane v0.10.1 h1:cgDRLG7bs59Zd+apAWuzLQL95obVYAymNJek76W3mgw=
+github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v0.6.2 h1:JiO+kJTpmYGjEodY7O1Zk8oZcNz1+f30UtwtXoFUPzE=
github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=
@@ -111,7 +106,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
-github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -202,11 +196,11 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=
-github.com/mattn/go-sqlite3 v1.14.8/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA=
github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
@@ -215,7 +209,6 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
@@ -223,6 +216,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
@@ -316,7 +310,6 @@ golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420 h1:a8jGStKg0XqKDlKqjLrXn0ioF5MH36pT7Z0BRTqLhbk=
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@@ -336,8 +329,8 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
-golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5 h1:v79phzBz03tsVCUTbvTBmmC3CUXF5mKYt7DA4ZVldpM=
-golang.org/x/oauth2 v0.0.0-20211028175245-ba495a64dcb5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8 h1:RerP+noqYHUQ8CMRcPlC2nvTa4dcBIjegkuWdcUDuqg=
+golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -397,8 +390,8 @@ golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
-golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881 h1:TyHqChC80pFkXWraUUf6RuB5IqFdQieMLwwCJokV2pc=
+golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -407,15 +400,14 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs=
-golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
+golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -503,8 +495,8 @@ google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqiv
google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=
google.golang.org/api v0.58.0/go.mod h1:cAbP2FsxoGVNwtgNAmmn3y5G1TWAiVYRmg4yku3lv+E=
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
-google.golang.org/api v0.60.0 h1:eq/zs5WPH4J9undYM9IP1O7dSr7Yh8Y0GtSCpzGzIUk=
-google.golang.org/api v0.60.0/go.mod h1:d7rl65NZAkEQ90JFzqBjcRq1TVeG5ZoGV3sSpEnnVb4=
+google.golang.org/api v0.61.0 h1:TXXKS1slM3b2bZNJwD5DV/Tp6/M2cLzLOLh9PjDhrw8=
+google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -572,11 +564,11 @@ google.golang.org/genproto v0.0.0-20210917145530-b395a37504d4/go.mod h1:eFjDcFEc
google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211016002631-37fc39342514/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211021150943-2b146023228c h1:FqrtZMB5Wr+/RecOM3uPJNPfWR8Upb5hAPnt7PU6i4k=
-google.golang.org/genproto v0.0.0-20211021150943-2b146023228c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
-google.golang.org/genproto v0.0.0-20211101144312-62acf1d99145 h1:vum3nDKdleYb+aePXKFEDT2+ghuH00EgYp9B7Q7EZZE=
-google.golang.org/genproto v0.0.0-20211101144312-62acf1d99145/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
+google.golang.org/genproto v0.0.0-20211206220100-3cb06788ce7f h1:QH7+Ym+7e2XV1dZIHapkXoeqHyNaCzn6MNp3JBaYYUc=
+google.golang.org/genproto v0.0.0-20211206220100-3cb06788ce7f/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -601,10 +593,9 @@ google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQ
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=
-google.golang.org/grpc v1.40.0 h1:AGJ0Ih4mHjSeibYkFGh1dD9KJ/eOtZ93I6hoHhukQ5Q=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.41.0 h1:f+PlOh7QV4iIJkPrx5NQ7qaNGFQ3OTse67yaDHfju4E=
-google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k=
+google.golang.org/grpc v1.42.0 h1:XT2/MFpuPFsEX2fWh3YQtHkZ+WYZFQRfaUgLZYj/p6A=
+google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -628,6 +619,7 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/server/cache.go b/server/cache.go
index fb13095c..5e76f5d0 100644
--- a/server/cache.go
+++ b/server/cache.go
@@ -1,10 +1,17 @@
package server
import (
+ "errors"
_ "github.com/mattn/go-sqlite3" // SQLite driver
"time"
)
+var (
+ errUnexpectedMessageType = errors.New("unexpected message type")
+)
+
+// cache implements a cache for messages of type "message" events,
+// i.e. message structs with the Event messageEvent.
type cache interface {
AddMessage(m *message) error
Messages(topic string, since sinceTime) ([]*message, error)
diff --git a/server/cache_mem.go b/server/cache_mem.go
index 83b0f36d..d524ecc2 100644
--- a/server/cache_mem.go
+++ b/server/cache_mem.go
@@ -1,7 +1,6 @@
package server
import (
- _ "github.com/mattn/go-sqlite3" // SQLite driver
"sync"
"time"
)
@@ -22,6 +21,9 @@ func newMemCache() *memCache {
func (s *memCache) AddMessage(m *message) error {
s.mu.Lock()
defer s.mu.Unlock()
+ if m.Event != messageEvent {
+ return errUnexpectedMessageType
+ }
if _, ok := s.messages[m.Topic]; !ok {
s.messages[m.Topic] = make([]*message, 0)
}
@@ -32,7 +34,7 @@ func (s *memCache) AddMessage(m *message) error {
func (s *memCache) Messages(topic string, since sinceTime) ([]*message, error) {
s.mu.Lock()
defer s.mu.Unlock()
- if _, ok := s.messages[topic]; !ok {
+ if _, ok := s.messages[topic]; !ok || since.IsNone() {
return make([]*message, 0), nil
}
messages := make([]*message, 0) // copy!
@@ -62,7 +64,7 @@ func (s *memCache) Topics() (map[string]*topic, error) {
func (s *memCache) Prune(keep time.Duration) error {
s.mu.Lock()
defer s.mu.Unlock()
- for topic, _ := range s.messages {
+ for topic := range s.messages {
s.pruneTopic(topic, keep)
}
return nil
diff --git a/server/cache_mem_test.go b/server/cache_mem_test.go
new file mode 100644
index 00000000..8c591b40
--- /dev/null
+++ b/server/cache_mem_test.go
@@ -0,0 +1,12 @@
+package server
+
+import (
+ "testing"
+)
+
+func TestMemCache_Messages(t *testing.T) {
+ testCacheMessages(t, newMemCache())
+}
+func TestMemCache_MessagesTagsPrioAndTitle(t *testing.T) {
+ testCacheMessagesTagsPrioAndTitle(t, newMemCache())
+}
diff --git a/server/cache_sqlite.go b/server/cache_sqlite.go
index a6211d3e..6c53d6f2 100644
--- a/server/cache_sqlite.go
+++ b/server/cache_sqlite.go
@@ -81,11 +81,17 @@ func newSqliteCache(filename string) (*sqliteCache, error) {
}
func (c *sqliteCache) AddMessage(m *message) error {
+ if m.Event != messageEvent {
+ return errUnexpectedMessageType
+ }
_, err := c.db.Exec(insertMessageQuery, m.ID, m.Time, m.Topic, m.Message, m.Title, m.Priority, strings.Join(m.Tags, ","))
return err
}
func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error) {
+ if since.IsNone() {
+ return make([]*message, 0), nil
+ }
rows, err := c.db.Query(selectMessagesSinceTimeQuery, topic, since.Time().Unix())
if err != nil {
return nil, err
@@ -99,9 +105,6 @@ func (c *sqliteCache) Messages(topic string, since sinceTime) ([]*message, error
if err := rows.Scan(&id, ×tamp, &msg, &title, &priority, &tagsStr); err != nil {
return nil, err
}
- if msg == "" {
- msg = " " // Hack: never return empty messages; this should not happen
- }
var tags []string
if tagsStr != "" {
tags = strings.Split(tagsStr, ",")
@@ -141,13 +144,13 @@ func (c *sqliteCache) MessageCount(topic string) (int, error) {
return count, nil
}
-func (s *sqliteCache) Topics() (map[string]*topic, error) {
- rows, err := s.db.Query(selectTopicsQuery)
+func (c *sqliteCache) Topics() (map[string]*topic, error) {
+ rows, err := c.db.Query(selectTopicsQuery)
if err != nil {
return nil, err
}
defer rows.Close()
- topics := make(map[string]*topic, 0)
+ topics := make(map[string]*topic)
for rows.Next() {
var id string
var last int64
@@ -162,8 +165,8 @@ func (s *sqliteCache) Topics() (map[string]*topic, error) {
return topics, nil
}
-func (s *sqliteCache) Prune(keep time.Duration) error {
- _, err := s.db.Exec(pruneMessagesQuery, time.Now().Add(-1*keep).Unix())
+func (c *sqliteCache) Prune(keep time.Duration) error {
+ _, err := c.db.Exec(pruneMessagesQuery, time.Now().Add(-1*keep).Unix())
return err
}
diff --git a/server/cache_sqlite_test.go b/server/cache_sqlite_test.go
new file mode 100644
index 00000000..214f7219
--- /dev/null
+++ b/server/cache_sqlite_test.go
@@ -0,0 +1,23 @@
+package server
+
+import (
+ "path/filepath"
+ "testing"
+)
+
+func TestSqliteCache_AddMessage(t *testing.T) {
+ testCacheMessages(t, newSqliteTestCache(t))
+}
+
+func TestSqliteCache_MessagesTagsPrioAndTitle(t *testing.T) {
+ testCacheMessagesTagsPrioAndTitle(t, newSqliteTestCache(t))
+}
+
+func newSqliteTestCache(t *testing.T) cache {
+ filename := filepath.Join(t.TempDir(), "cache.db")
+ c, err := newSqliteCache(filename)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return c
+}
diff --git a/server/cache_test.go b/server/cache_test.go
new file mode 100644
index 00000000..fdf87d53
--- /dev/null
+++ b/server/cache_test.go
@@ -0,0 +1,79 @@
+package server
+
+import (
+ "github.com/stretchr/testify/assert"
+ "testing"
+ "time"
+)
+
+func testCacheMessages(t *testing.T, c cache) {
+ m1 := newDefaultMessage("mytopic", "my message")
+ m1.Time = 1
+
+ m2 := newDefaultMessage("mytopic", "my other message")
+ m2.Time = 2
+
+ assert.Nil(t, c.AddMessage(m1))
+ assert.Nil(t, c.AddMessage(newDefaultMessage("example", "my example message")))
+ assert.Nil(t, c.AddMessage(m2))
+
+ // Adding invalid
+ assert.Equal(t, errUnexpectedMessageType, c.AddMessage(newKeepaliveMessage("mytopic"))) // These should not be added!
+ assert.Equal(t, errUnexpectedMessageType, c.AddMessage(newOpenMessage("example"))) // These should not be added!
+
+ // mytopic: count
+ count, err := c.MessageCount("mytopic")
+ assert.Nil(t, err)
+ assert.Equal(t, 2, count)
+
+ // mytopic: since all
+ messages, _ := c.Messages("mytopic", sinceAllMessages)
+ assert.Equal(t, 2, len(messages))
+ assert.Equal(t, "my message", messages[0].Message)
+ assert.Equal(t, "mytopic", messages[0].Topic)
+ assert.Equal(t, messageEvent, messages[0].Event)
+ assert.Equal(t, "", messages[0].Title)
+ assert.Equal(t, 0, messages[0].Priority)
+ assert.Nil(t, messages[0].Tags)
+ assert.Equal(t, "my other message", messages[1].Message)
+
+ // mytopic: since none
+ messages, _ = c.Messages("mytopic", sinceNoMessages)
+ assert.Empty(t, messages)
+
+ // mytopic: since 2
+ messages, _ = c.Messages("mytopic", sinceTime(time.Unix(2, 0)))
+ assert.Equal(t, 1, len(messages))
+ assert.Equal(t, "my other message", messages[0].Message)
+
+ // example: count
+ count, err = c.MessageCount("example")
+ assert.Nil(t, err)
+ assert.Equal(t, 1, count)
+
+ // example: since all
+ messages, _ = c.Messages("example", sinceAllMessages)
+ assert.Equal(t, "my example message", messages[0].Message)
+
+ // non-existing: count
+ count, err = c.MessageCount("doesnotexist")
+ assert.Nil(t, err)
+ assert.Equal(t, 0, count)
+
+ // non-existing: since all
+ messages, _ = c.Messages("doesnotexist", sinceAllMessages)
+ assert.Empty(t, messages)
+}
+
+func testCacheMessagesTagsPrioAndTitle(t *testing.T, c cache) {
+ m := newDefaultMessage("mytopic", "some message")
+ m.Tags = []string{"tag1", "tag2"}
+ m.Priority = 5
+ m.Title = "some title"
+ assert.Nil(t, c.AddMessage(m))
+
+ messages, _ := c.Messages("mytopic", sinceAllMessages)
+ assert.Equal(t, []string{"tag1", "tag2"}, messages[0].Tags)
+ assert.Equal(t, 5, messages[0].Priority)
+ assert.Equal(t, "some title", messages[0].Title)
+}
diff --git a/server/server.go b/server/server.go
index 0704a8b4..f16b866a 100644
--- a/server/server.go
+++ b/server/server.go
@@ -3,8 +3,7 @@ package server
import (
"bytes"
"context"
- "embed"
- _ "embed" // required for go:embed
+ "embed" // required for go:embed
"encoding/json"
firebase "firebase.google.com/go"
"firebase.google.com/go/messaging"
@@ -27,7 +26,7 @@ import (
// TODO add "max messages in a topic" limit
// TODO implement "since="
-// Server is the main server
+// Server is the main server, providing the UI and API for ntfy
type Server struct {
config *config.Config
topics map[string]*topic
@@ -105,6 +104,8 @@ var (
errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)}
)
+// New instantiates a new Server. It creates the cache and adds a Firebase
+// subscriber (if configured).
func New(conf *config.Config) (*Server, error) {
var firebaseSubscriber subscriber
if conf.FirebaseKeyFile != "" {
@@ -170,6 +171,8 @@ func createFirebaseSubscriber(conf *config.Config) (subscriber, error) {
}, nil
}
+// Run executes the main server. It listens on HTTP (+ HTTPS, if configured), and starts
+// a manager go routine to print stats and prune messages.
func (s *Server) Run() error {
go func() {
ticker := time.NewTicker(s.config.ManagerInterval)
@@ -241,11 +244,11 @@ func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
})
}
-func (s *Server) handleEmpty(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request) error {
return nil
}
-func (s *Server) handleExample(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
_, err := io.WriteString(w, exampleSource)
return err
}
@@ -260,7 +263,7 @@ func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
return nil
}
-func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
+func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, _ *visitor) error {
t, err := s.topicFromID(r.URL.Path[1:])
if err != nil {
return err
@@ -466,7 +469,7 @@ func parseSince(r *http.Request) (sinceTime, error) {
return sinceNoMessages, errHTTPBadRequest
}
-func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
w.Header().Set("Access-Control-Allow-Origin", "*") // CORS, allow cross-origin requests
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
return nil
@@ -570,5 +573,5 @@ func (s *Server) visitor(r *http.Request) *visitor {
func (s *Server) fail(w http.ResponseWriter, r *http.Request, code int, err error) {
log.Printf("[%s] %s - %d - %s", r.RemoteAddr, r.Method, code, err.Error())
w.WriteHeader(code)
- io.WriteString(w, fmt.Sprintf("%s\n", http.StatusText(code)))
+ _, _ = io.WriteString(w, fmt.Sprintf("%s\n", http.StatusText(code)))
}
diff --git a/util/embedfs.go b/util/embedfs.go
index 07cec1b9..58c4529d 100644
--- a/util/embedfs.go
+++ b/util/embedfs.go
@@ -8,11 +8,23 @@ import (
"time"
)
+// CachingEmbedFS is a wrapper around embed.FS that allows setting a ModTime, so that the
+// default static file server can send 304s back. It can be used like this:
+//
+// var (
+// //go:embed docs
+// docsStaticFs embed.FS
+// docsStaticCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: docsStaticFs}
+// )
+//
+// http.FileServer(http.FS(docsStaticCached)).ServeHTTP(w, r)
+//
type CachingEmbedFS struct {
ModTime time.Time
FS embed.FS
}
+// Open opens a file in the embedded filesystem and returns a fs.File with the static ModTime
func (f CachingEmbedFS) Open(name string) (fs.File, error) {
file, err := f.FS.Open(name)
if err != nil {
diff --git a/util/util.go b/util/util.go
index eda167f9..fcb9c298 100644
--- a/util/util.go
+++ b/util/util.go
@@ -15,6 +15,7 @@ var (
random = rand.New(rand.NewSource(time.Now().UnixNano()))
)
+// FileExists checks if a file exists, and returns true if it does
func FileExists(filename string) bool {
stat, _ := os.Stat(filename)
return stat != nil
From 37fafd09e7eb7057be43ec460578e10bc570a8f3 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 11:50:48 -0500
Subject: [PATCH 010/335] GitHub workflow
---
.github/workflows/test.yaml | 18 ++++++++++++++++++
.gitignore | 2 +-
2 files changed, 19 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/test.yaml
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
new file mode 100644
index 00000000..f9e90f3d
--- /dev/null
+++ b/.github/workflows/test.yaml
@@ -0,0 +1,18 @@
+name: test
+on: [push, pull_request]
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Go
+ uses: actions/setup-go@v2
+ with:
+ go-version: '1.16.x'
+ - name: Checkout code
+ uses: actions/checkout@v2
+ - name: Install test dependencies
+ run: sudo apt-get install netcat-openbsd
+ - name: Run tests, formatting, vetting and linting
+ run: make check
+ - name: Run and upload coverage to codecov.io
+ run: make coverage coverage-upload
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 616f246b..93a1dee2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
dist/
+build/
.idea/
-site/
server/docs/
*.iml
From fd71589f60d582fc485979b7554faf57b9414bd9 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 12:23:42 -0500
Subject: [PATCH 011/335] More test; begin test infra stuff
---
.github/workflows/test.yaml | 16 +++++++++++-----
Makefile | 7 ++++---
config/config_test.go | 12 ++++++++++++
server/server_test.go | 36 ++++++++++++++++++++++++++++++++++++
4 files changed, 63 insertions(+), 8 deletions(-)
create mode 100644 config/config_test.go
create mode 100644 server/server_test.go
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index f9e90f3d..7c224f32 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -7,12 +7,18 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
- go-version: '1.16.x'
+ go-version: '1.17.x'
- name: Checkout code
uses: actions/checkout@v2
- - name: Install test dependencies
- run: sudo apt-get install netcat-openbsd
+ - name: Install dependencies
+ run: sudo apt update && sudo apt install -y python3-pip
+ - name: Install mkdocs
+ run: sudo pip3 install mkdocs mkdocs-material mkdocs-minify-plugin
+ - name: Build docs
+ run: make docs
- name: Run tests, formatting, vetting and linting
run: make check
- - name: Run and upload coverage to codecov.io
- run: make coverage coverage-upload
\ No newline at end of file
+ - name: Run coverage
+ run: make coverage
+ # - name: Upload coverage to codecov.io
+ # run: make coverage-upload
diff --git a/Makefile b/Makefile
index b4166d28..142db10f 100644
--- a/Makefile
+++ b/Makefile
@@ -38,6 +38,10 @@ help:
@echo " make install-lint - Install golint"
+# Documentation
+docs: .PHONY
+ mkdocs build
+
# Test/check targets
check: test fmt-check vet lint staticcheck
@@ -88,9 +92,6 @@ staticcheck: .PHONY
# Building targets
-docs: .PHONY
- mkdocs build
-
build-deps: docs
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
diff --git a/config/config_test.go b/config/config_test.go
new file mode 100644
index 00000000..d7282511
--- /dev/null
+++ b/config/config_test.go
@@ -0,0 +1,12 @@
+package config_test
+
+import (
+ "github.com/stretchr/testify/assert"
+ "heckel.io/ntfy/config"
+ "testing"
+)
+
+func TestConfig_New(t *testing.T) {
+ c := config.New(":1234")
+ assert.Equal(t, ":1234", c.ListenHTTP)
+}
diff --git a/server/server_test.go b/server/server_test.go
new file mode 100644
index 00000000..eac14374
--- /dev/null
+++ b/server/server_test.go
@@ -0,0 +1,36 @@
+package server
+
+import (
+ "encoding/json"
+ "github.com/stretchr/testify/assert"
+ "heckel.io/ntfy/config"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestServer_Publish(t *testing.T) {
+ s := newTestServer(t, newTestConfig())
+
+ rr := httptest.NewRecorder()
+ req, _ := http.NewRequest("PUT", "/mytopic", strings.NewReader("my message"))
+ s.handle(rr, req)
+
+ var m message
+ assert.Nil(t, json.NewDecoder(rr.Body).Decode(&m))
+ assert.NotEmpty(t, m.ID)
+ assert.Equal(t, "my message", m.Message)
+}
+
+func newTestConfig() *config.Config {
+ return config.New(":80")
+}
+
+func newTestServer(t *testing.T, config *config.Config) *Server {
+ server, err := New(config)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return server
+}
From 829e8f6ea64547658fa0210ae9ffddaab7e8d58f Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 13:24:50 -0500
Subject: [PATCH 012/335] pip list
---
.github/workflows/test.yaml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 7c224f32..4f0bc17e 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -12,8 +12,10 @@ jobs:
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y python3-pip
+ - name: x
+ run: pip3 list
- name: Install mkdocs
- run: sudo pip3 install mkdocs mkdocs-material mkdocs-minify-plugin
+ run: pip3 install mkdocs mkdocs-material mkdocs-minify-plugin
- name: Build docs
run: make docs
- name: Run tests, formatting, vetting and linting
From a3ce12585b1321caee9e893717982d334900f1c5 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 13:27:15 -0500
Subject: [PATCH 013/335] Upgrade jinja
---
.github/workflows/test.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 4f0bc17e..3a452a23 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -12,8 +12,8 @@ jobs:
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y python3-pip
- - name: x
- run: pip3 list
+ - name: Install latest jinja2 version
+ run: pip3 install jinja2
- name: Install mkdocs
run: pip3 install mkdocs mkdocs-material mkdocs-minify-plugin
- name: Build docs
From ab3cc47e272aa0d21f8b3fd134265611916b6416 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 13:37:01 -0500
Subject: [PATCH 014/335] Move python deps to requirements.txt
---
.github/workflows/test.yaml | 6 +-----
Makefile | 5 ++++-
requirements.txt | 9 +++++++++
3 files changed, 14 insertions(+), 6 deletions(-)
create mode 100644 requirements.txt
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 3a452a23..1a3495bb 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -12,11 +12,7 @@ jobs:
uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt update && sudo apt install -y python3-pip
- - name: Install latest jinja2 version
- run: pip3 install jinja2
- - name: Install mkdocs
- run: pip3 install mkdocs mkdocs-material mkdocs-minify-plugin
- - name: Build docs
+ - name: Build docs (required for tests)
run: make docs
- name: Run tests, formatting, vetting and linting
run: make check
diff --git a/Makefile b/Makefile
index 142db10f..4b9a3447 100644
--- a/Makefile
+++ b/Makefile
@@ -39,7 +39,10 @@ help:
# Documentation
-docs: .PHONY
+docs-deps: .PHONY
+ pip3 install -r requirements.txt
+
+docs: docs-deps
mkdocs build
# Test/check targets
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 00000000..246f8923
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,9 @@
+# The documentation uses 'mkdocs', which is written in Python
+
+# See https://github.com/squidfunk/mkdocs-material/issues/2030
+jinja2>=2.11.1
+
+# mkdocs
+mkdocs
+mkdocs-material
+mkdocs-minify-plugin
From 94b70fbcb9591ccaf86789a49cc2fb4a569897f1 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 14:06:33 -0500
Subject: [PATCH 015/335] First server tests
---
Makefile | 2 +-
server/server_test.go | 76 ++++++++++++++++++++++++++++++++++++-------
2 files changed, 66 insertions(+), 12 deletions(-)
diff --git a/Makefile b/Makefile
index 4b9a3447..1bba6562 100644
--- a/Makefile
+++ b/Makefile
@@ -86,7 +86,7 @@ lint:
staticcheck: .PHONY
rm -rf build/staticcheck
- which staticcheck || go get honnef.co/go/tools/cmd/staticcheck
+ which staticcheck || go install honnef.co/go/tools/cmd/staticcheck@latest
mkdir -p build/staticcheck
ln -s "$(GO)" build/staticcheck/go
PATH="$(PWD)/build/staticcheck:$(PATH)" staticcheck ./...
diff --git a/server/server_test.go b/server/server_test.go
index eac14374..9d9e7a6a 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -1,30 +1,59 @@
package server
import (
+ "bufio"
"encoding/json"
"github.com/stretchr/testify/assert"
"heckel.io/ntfy/config"
"net/http"
"net/http/httptest"
+ "path/filepath"
"strings"
"testing"
)
-func TestServer_Publish(t *testing.T) {
- s := newTestServer(t, newTestConfig())
+func TestServer_PublishAndPoll(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
- rr := httptest.NewRecorder()
- req, _ := http.NewRequest("PUT", "/mytopic", strings.NewReader("my message"))
- s.handle(rr, req)
+ response1 := request(t, s, "PUT", "/mytopic", "my first message")
+ msg1 := toMessage(t, response1.Body.String())
+ assert.NotEmpty(t, msg1.ID)
+ assert.Equal(t, "my first message", msg1.Message)
- var m message
- assert.Nil(t, json.NewDecoder(rr.Body).Decode(&m))
- assert.NotEmpty(t, m.ID)
- assert.Equal(t, "my message", m.Message)
+ response2 := request(t, s, "PUT", "/mytopic", "my second message")
+ msg2 := toMessage(t, response2.Body.String())
+ assert.NotEqual(t, msg1.ID, msg2.ID)
+ assert.NotEmpty(t, msg2.ID)
+ assert.Equal(t, "my second message", msg2.Message)
+
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "")
+ messages := toMessages(t, response.Body.String())
+ assert.Equal(t, 2, len(messages))
}
-func newTestConfig() *config.Config {
- return config.New(":80")
+func TestServer_PublishAndSubscribe(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
+
+ response1 := request(t, s, "PUT", "/mytopic", "my first message")
+ msg1 := toMessage(t, response1.Body.String())
+ assert.NotEmpty(t, msg1.ID)
+ assert.Equal(t, "my first message", msg1.Message)
+
+ response2 := request(t, s, "PUT", "/mytopic", "my second message")
+ msg2 := toMessage(t, response2.Body.String())
+ assert.NotEqual(t, msg1.ID, msg2.ID)
+ assert.NotEmpty(t, msg2.ID)
+ assert.Equal(t, "my second message", msg2.Message)
+
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "")
+ messages := toMessages(t, response.Body.String())
+ assert.Equal(t, 2, len(messages))
+}
+
+func newTestConfig(t *testing.T) *config.Config {
+ conf := config.New(":80")
+ conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
+ return conf
}
func newTestServer(t *testing.T, config *config.Config) *Server {
@@ -34,3 +63,28 @@ func newTestServer(t *testing.T, config *config.Config) *Server {
}
return server
}
+
+func request(t *testing.T, s *Server, method, url, body string) *httptest.ResponseRecorder {
+ rr := httptest.NewRecorder()
+ req, err := http.NewRequest(method, url, strings.NewReader(body))
+ if err != nil {
+ t.Fatal(err)
+ }
+ s.handle(rr, req)
+ return rr
+}
+
+func toMessages(t *testing.T, s string) []*message {
+ messages := make([]*message, 0)
+ scanner := bufio.NewScanner(strings.NewReader(s))
+ for scanner.Scan() {
+ messages = append(messages, toMessage(t, scanner.Text()))
+ }
+ return messages
+}
+
+func toMessage(t *testing.T, s string) *message {
+ var m message
+ assert.Nil(t, json.NewDecoder(strings.NewReader(s)).Decode(&m))
+ return &m
+}
From fc16b0531a1128beed8d9a41814d5d678c498ac0 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 14:46:10 -0500
Subject: [PATCH 016/335] Open and keepalive tests
---
server/server_test.go | 51 +++++++++++++++++++++++++++++++------------
1 file changed, 37 insertions(+), 14 deletions(-)
diff --git a/server/server_test.go b/server/server_test.go
index 9d9e7a6a..c7b29011 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -2,6 +2,7 @@ package server
import (
"bufio"
+ "context"
"encoding/json"
"github.com/stretchr/testify/assert"
"heckel.io/ntfy/config"
@@ -10,6 +11,7 @@ import (
"path/filepath"
"strings"
"testing"
+ "time"
)
func TestServer_PublishAndPoll(t *testing.T) {
@@ -29,25 +31,46 @@ func TestServer_PublishAndPoll(t *testing.T) {
response := request(t, s, "GET", "/mytopic/json?poll=1", "")
messages := toMessages(t, response.Body.String())
assert.Equal(t, 2, len(messages))
+ assert.Equal(t, "my first message", messages[0].Message)
+ assert.Equal(t, "my second message", messages[1].Message)
}
-func TestServer_PublishAndSubscribe(t *testing.T) {
- s := newTestServer(t, newTestConfig(t))
+func TestServer_SubscribeOpenAndKeepalive(t *testing.T) {
+ c := newTestConfig(t)
+ c.KeepaliveInterval = time.Second
+ s := newTestServer(t, c)
- response1 := request(t, s, "PUT", "/mytopic", "my first message")
- msg1 := toMessage(t, response1.Body.String())
- assert.NotEmpty(t, msg1.ID)
- assert.Equal(t, "my first message", msg1.Message)
+ rr := httptest.NewRecorder()
+ ctx, cancel := context.WithCancel(context.Background())
+ req, err := http.NewRequestWithContext(ctx, "GET", "/mytopic/json", nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ doneChan := make(chan bool)
+ go func() {
+ s.handle(rr, req)
+ doneChan <- true
+ }()
+ time.Sleep(1300 * time.Millisecond)
+ cancel()
+ <-doneChan
- response2 := request(t, s, "PUT", "/mytopic", "my second message")
- msg2 := toMessage(t, response2.Body.String())
- assert.NotEqual(t, msg1.ID, msg2.ID)
- assert.NotEmpty(t, msg2.ID)
- assert.Equal(t, "my second message", msg2.Message)
-
- response := request(t, s, "GET", "/mytopic/json?poll=1", "")
- messages := toMessages(t, response.Body.String())
+ messages := toMessages(t, rr.Body.String())
assert.Equal(t, 2, len(messages))
+
+ assert.Equal(t, openEvent, messages[0].Event)
+ assert.Equal(t, "mytopic", messages[0].Topic)
+ assert.Equal(t, "", messages[0].Message)
+ assert.Equal(t, "", messages[0].Title)
+ assert.Equal(t, 0, messages[0].Priority)
+ assert.Nil(t, messages[0].Tags)
+
+ assert.Equal(t, keepaliveEvent, messages[1].Event)
+ assert.Equal(t, "mytopic", messages[1].Topic)
+ assert.Equal(t, "", messages[1].Message)
+ assert.Equal(t, "", messages[1].Title)
+ assert.Equal(t, 0, messages[1].Priority)
+ assert.Nil(t, messages[1].Tags)
}
func newTestConfig(t *testing.T) *config.Config {
From aa9764848a1a792c1e9858776bcd55c4ec250e76 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 14:50:46 -0500
Subject: [PATCH 017/335] Enable codecov.io
---
.github/workflows/test.yaml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 1a3495bb..451107f4 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -18,5 +18,5 @@ jobs:
run: make check
- name: Run coverage
run: make coverage
- # - name: Upload coverage to codecov.io
- # run: make coverage-upload
+ - name: Upload coverage to codecov.io
+ run: make coverage-upload
From 40fbce07dbb0bfae4e44a601bb8448a1aeef202a Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 15:39:42 -0500
Subject: [PATCH 018/335] Test for simple pub sub
---
Makefile | 2 +-
README.md | 4 +++
docs/publish.md | 2 +-
server/server.go | 13 +++++---
server/server_test.go | 69 ++++++++++++++++++++++++++++++++++++++++---
5 files changed, 80 insertions(+), 10 deletions(-)
diff --git a/Makefile b/Makefile
index 1bba6562..5a88647e 100644
--- a/Makefile
+++ b/Makefile
@@ -72,7 +72,7 @@ coverage-upload:
# Lint/formatting targets
fmt:
- $(GO) fmt ./...
+ gofmt -s -w .
fmt-check:
test -z $(shell gofmt -l .)
diff --git a/README.md b/README.md
index 93eed30f..2357185f 100644
--- a/README.md
+++ b/README.md
@@ -2,6 +2,10 @@
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[](https://github.com/binwiederhier/ntfy/releases/latest)
+[](https://pkg.go.dev/heckel.io/ntfy)
+[](https://github.com/binwiederhier/ntfy/actions)
+[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
+[](https://codecov.io/gh/binwiederhier/ntfy)
[](https://gophers.slack.com/archives/C01JMTPGF2Q)
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
diff --git a/docs/publish.md b/docs/publish.md
index 22329cdf..375840ec 100644
--- a/docs/publish.md
+++ b/docs/publish.md
@@ -279,7 +279,7 @@ Here's an **excerpt of emojis** I've found very useful in alert messages:
-You can set tags with the `X-Tags` header (or any of its aliases: `Tags`, or `ta`). Specify multiple tags by separating
+You can set tags with the `X-Tags` header (or any of its aliases: `Tags`, `tag`, or `ta`). Specify multiple tags by separating
them with a comma, e.g. `tag1,tag2,tag3`.
=== "Command line (curl)"
diff --git a/server/server.go b/server/server.go
index f16b866a..bfc767c8 100644
--- a/server/server.go
+++ b/server/server.go
@@ -306,17 +306,22 @@ func parseHeaders(header http.Header) (title string, priority int, tags []string
priority = 1
case "2", "low":
priority = 2
+ case "3", "default":
+ priority = 3
case "4", "high":
priority = 4
case "5", "max", "urgent":
priority = 5
default:
- priority = 3
+ priority = 0
}
}
- tagsStr := readHeader(header, "x-tags", "tags", "ta")
+ tagsStr := readHeader(header, "x-tags", "tag", "tags", "ta")
if tagsStr != "" {
- tags = strings.Split(tagsStr, ",")
+ tags = make([]string, 0)
+ for _, s := range strings.Split(tagsStr, ",") {
+ tags = append(tags, strings.TrimSpace(s))
+ }
}
return title, priority, tags
}
@@ -325,7 +330,7 @@ func readHeader(header http.Header, names ...string) string {
for _, name := range names {
value := header.Get(name)
if value != "" {
- return value
+ return strings.TrimSpace(value)
}
}
return ""
diff --git a/server/server_test.go b/server/server_test.go
index c7b29011..904e4b4a 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -17,18 +17,18 @@ import (
func TestServer_PublishAndPoll(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
- response1 := request(t, s, "PUT", "/mytopic", "my first message")
+ response1 := request(t, s, "PUT", "/mytopic", "my first message", nil)
msg1 := toMessage(t, response1.Body.String())
assert.NotEmpty(t, msg1.ID)
assert.Equal(t, "my first message", msg1.Message)
- response2 := request(t, s, "PUT", "/mytopic", "my second message")
+ response2 := request(t, s, "PUT", "/mytopic", "my second message", nil)
msg2 := toMessage(t, response2.Body.String())
assert.NotEqual(t, msg1.ID, msg2.ID)
assert.NotEmpty(t, msg2.ID)
assert.Equal(t, "my second message", msg2.Message)
- response := request(t, s, "GET", "/mytopic/json?poll=1", "")
+ response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
messages := toMessages(t, response.Body.String())
assert.Equal(t, 2, len(messages))
assert.Equal(t, "my first message", messages[0].Message)
@@ -73,6 +73,42 @@ func TestServer_SubscribeOpenAndKeepalive(t *testing.T) {
assert.Nil(t, messages[1].Tags)
}
+func TestServer_PublishAndSubscribe(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
+
+ subscribeRR := httptest.NewRecorder()
+ subscribeCancel := subscribe(t, s, "/mytopic/json", subscribeRR)
+
+ publishFirstRR := request(t, s, "PUT", "/mytopic", "my first message", nil)
+ assert.Equal(t, 200, publishFirstRR.Code)
+
+ publishSecondRR := request(t, s, "PUT", "/mytopic", "my other message", map[string]string{
+ "Title": " This is a title ",
+ "X-Tags": "tag1,tag 2, tag3",
+ "p": "1",
+ })
+ assert.Equal(t, 200, publishSecondRR.Code)
+
+ subscribeCancel()
+ messages := toMessages(t, subscribeRR.Body.String())
+ assert.Equal(t, 3, len(messages))
+ assert.Equal(t, openEvent, messages[0].Event)
+
+ assert.Equal(t, messageEvent, messages[1].Event)
+ assert.Equal(t, "mytopic", messages[1].Topic)
+ assert.Equal(t, "my first message", messages[1].Message)
+ assert.Equal(t, "", messages[1].Title)
+ assert.Equal(t, 0, messages[1].Priority)
+ assert.Nil(t, messages[1].Tags)
+
+ assert.Equal(t, messageEvent, messages[2].Event)
+ assert.Equal(t, "mytopic", messages[2].Topic)
+ assert.Equal(t, "my other message", messages[2].Message)
+ assert.Equal(t, "This is a title", messages[2].Title)
+ assert.Equal(t, 1, messages[2].Priority)
+ assert.Equal(t, []string{"tag1", "tag 2", "tag3"}, messages[2].Tags)
+}
+
func newTestConfig(t *testing.T) *config.Config {
conf := config.New(":80")
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
@@ -87,16 +123,41 @@ func newTestServer(t *testing.T, config *config.Config) *Server {
return server
}
-func request(t *testing.T, s *Server, method, url, body string) *httptest.ResponseRecorder {
+func request(t *testing.T, s *Server, method, url, body string, headers map[string]string) *httptest.ResponseRecorder {
rr := httptest.NewRecorder()
req, err := http.NewRequest(method, url, strings.NewReader(body))
if err != nil {
t.Fatal(err)
}
+ if headers != nil {
+ for k, v := range headers {
+ req.Header.Set(k, v)
+ }
+ }
s.handle(rr, req)
return rr
}
+func subscribe(t *testing.T, s *Server, url string, rr *httptest.ResponseRecorder) context.CancelFunc {
+ ctx, cancel := context.WithCancel(context.Background())
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ t.Fatal(err)
+ }
+ done := make(chan bool)
+ go func() {
+ s.handle(rr, req)
+ done <- true
+ }()
+ cancelAndWaitForDone := func() {
+ time.Sleep(100 * time.Millisecond)
+ cancel()
+ <-done
+ }
+ time.Sleep(100 * time.Millisecond)
+ return cancelAndWaitForDone
+}
+
func toMessages(t *testing.T, s string) []*message {
messages := make([]*message, 0)
scanner := bufio.NewScanner(strings.NewReader(s))
From c9f1b0225133d470a929404e84bd49f0debb84c4 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 15:43:51 -0500
Subject: [PATCH 019/335] Unnecessary check around range
---
server/server_test.go | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/server/server_test.go b/server/server_test.go
index 904e4b4a..ca3f3ce9 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -129,10 +129,8 @@ func request(t *testing.T, s *Server, method, url, body string, headers map[stri
if err != nil {
t.Fatal(err)
}
- if headers != nil {
- for k, v := range headers {
- req.Header.Set(k, v)
- }
+ for k, v := range headers {
+ req.Header.Set(k, v)
}
s.handle(rr, req)
return rr
From 802ef17cb43f162fae4c5dc9d4901c3ed47033a0 Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Tue, 7 Dec 2021 16:03:01 -0500
Subject: [PATCH 020/335] Fix data races
---
server/server.go | 3 +++
util/util.go | 6 +++++-
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/server/server.go b/server/server.go
index bfc767c8..654725fe 100644
--- a/server/server.go
+++ b/server/server.go
@@ -386,8 +386,11 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visi
if err != nil {
return err
}
+ var wlock sync.Mutex
poll := r.URL.Query().Has("poll")
sub := func(msg *message) error {
+ wlock.Lock()
+ defer wlock.Unlock()
m, err := encoder(msg)
if err != nil {
return err
diff --git a/util/util.go b/util/util.go
index fcb9c298..742ca31e 100644
--- a/util/util.go
+++ b/util/util.go
@@ -4,6 +4,7 @@ import (
"fmt"
"math/rand"
"os"
+ "sync"
"time"
)
@@ -12,7 +13,8 @@ const (
)
var (
- random = rand.New(rand.NewSource(time.Now().UnixNano()))
+ random = rand.New(rand.NewSource(time.Now().UnixNano()))
+ randomMutex = sync.Mutex{}
)
// FileExists checks if a file exists, and returns true if it does
@@ -23,6 +25,8 @@ func FileExists(filename string) bool {
// RandomString returns a random string with a given length
func RandomString(length int) string {
+ randomMutex.Lock() // Who would have thought that random.Intn() is not thread-safe?!
+ defer randomMutex.Unlock()
b := make([]byte, length)
for i := range b {
b[i] = randomStringCharset[random.Intn(len(randomStringCharset))]
From 6fb4bdc001251baf138bae1251af826c5225a8cc Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Wed, 8 Dec 2021 11:53:59 -0500
Subject: [PATCH 021/335] Docs on "scaling"
---
docs/config.md | 42 ++++++++++++++++++++++++++++++------------
1 file changed, 30 insertions(+), 12 deletions(-)
diff --git a/docs/config.md b/docs/config.md
index 547ce84f..57a12a96 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -56,9 +56,14 @@ be counted as one, because from the perspective of the ntfy server, they all sha
ntfy supports HTTPS/TLS by setting the `listen-https` [config option](#config-options). However, if you
are behind a proxy, it is recommended that TLS/SSL termination is done by the proxy itself (see below).
-### nginx/Apache2 configs
+I highly recommend using [certbot](https://certbot.eff.org/). I use it with the [dns-route53 plugin](https://certbot-dns-route53.readthedocs.io/en/stable/),
+which lets you use [AWS Route 53](https://aws.amazon.com/route53/) as the challenge. That's much easier than using the
+HTTP challenge. I've found [this guide](https://nandovieira.com/using-lets-encrypt-in-development-with-nginx-and-aws-route53) to
+be incredibly helpful.
+
+### nginx/Apache2
For your convenience, here's a working config that'll help configure things behind a proxy. In this
-example, ntfy runs on `:13222` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
+example, ntfy runs on `:2586` and we proxy traffic to it. We also redirect HTTP to HTTPS for GET requests against a topic
or the root domain:
=== "nginx (/etc/nginx/sites-*/ntfy)"
@@ -66,9 +71,22 @@ or the root domain:
server {
listen 80;
server_name ntfy.sh;
-
+
location / {
- proxy_pass http://127.0.0.1:13222;
+ # Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
+ # it to work with curl without the annoying https:// prefix
+ set $redirect_https "";
+ if ($request_method = GET) {
+ set $redirect_https "yes";
+ }
+ if ($request_uri ~* "^/[-_a-z0-9]{0,64}$") {
+ set $redirect_https "${redirect_https}yes";
+ }
+ if ($redirect_https = "yesyes") {
+ return 302 https://$http_host$request_uri$is_args$query_string;
+ }
+
+ proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_buffering off;
@@ -94,11 +112,11 @@ or the root domain:
ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
ssl_prefer_server_ciphers on;
- ssl_certificate /etc/letsencrypt/live/nopaste.net/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/nopaste.net/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/ntfy.sh/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/ntfy.sh/privkey.pem;
location / {
- proxy_pass http://127.0.0.1:13222;
+ proxy_pass http://127.0.0.1:2586;
proxy_http_version 1.1;
proxy_buffering off;
@@ -124,8 +142,8 @@ or the root domain:
SetEnv proxy-nokeepalive 1
SetEnv proxy-sendchunked 1
- ProxyPass / http://127.0.0.1:13222/
- ProxyPassReverse / http://127.0.0.1:13222/
+ ProxyPass / http://127.0.0.1:2586/
+ ProxyPassReverse / http://127.0.0.1:2586/
# Higher than the max message size of 512k
LimitRequestBody 102400
@@ -148,8 +166,8 @@ or the root domain:
SetEnv proxy-nokeepalive 1
SetEnv proxy-sendchunked 1
- ProxyPass / http://127.0.0.1:13222/
- ProxyPassReverse / http://127.0.0.1:13222/
+ ProxyPass / http://127.0.0.1:2586/
+ ProxyPassReverse / http://127.0.0.1:2586/
# Higher than the max message size of 512k
LimitRequestBody 102400
@@ -258,7 +276,7 @@ to maintain the client connection and the connection to ntfy.
=== "/etc/nginx/nginx.conf"
```
events {
- # Allow 20,000 proxy connections (2x of the desired ntfy connection count;
+ # Allow 40,000 proxy connections (2x of the desired ntfy connection count;
# and give room for other file handles)
worker_connections 40500;
}
From e2c419c021e92428fe4bbcd074732332517e3b7b Mon Sep 17 00:00:00 2001
From: "Philipp C. Heckel"
Date: Wed, 8 Dec 2021 15:11:35 -0500
Subject: [PATCH 022/335] Update README.md
---
README.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/README.md b/README.md
index 2357185f..c212781b 100644
--- a/README.md
+++ b/README.md
@@ -7,6 +7,7 @@
[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[](https://codecov.io/gh/binwiederhier/ntfy)
[](https://gophers.slack.com/archives/C01JMTPGF2Q)
+[](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)
**ntfy** (pronounce: *notify*) is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern) notification service.
It allows you to **send notifications to your phone or desktop via scripts** from any computer, entirely **without signup or cost**.
From 3a009eac9d106de9a191bc3e673d03ac04a1518c Mon Sep 17 00:00:00 2001
From: Philipp Heckel
Date: Wed, 8 Dec 2021 16:08:50 -0500
Subject: [PATCH 023/335] More unit tests
---
server/server_test.go | 50 ++++++++++++++++++++++++++++++++++++++++---
1 file changed, 47 insertions(+), 3 deletions(-)
diff --git a/server/server_test.go b/server/server_test.go
index ca3f3ce9..e4e9448f 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -22,17 +22,30 @@ func TestServer_PublishAndPoll(t *testing.T) {
assert.NotEmpty(t, msg1.ID)
assert.Equal(t, "my first message", msg1.Message)
- response2 := request(t, s, "PUT", "/mytopic", "my second message", nil)
+ response2 := request(t, s, "PUT", "/mytopic", "my second\n\nmessage", nil)
msg2 := toMessage(t, response2.Body.String())
assert.NotEqual(t, msg1.ID, msg2.ID)
assert.NotEmpty(t, msg2.ID)
- assert.Equal(t, "my second message", msg2.Message)
+ assert.Equal(t, "my second\n\nmessage", msg2.Message)
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
messages := toMessages(t, response.Body.String())
assert.Equal(t, 2, len(messages))
assert.Equal(t, "my first message", messages[0].Message)
- assert.Equal(t, "my second message", messages[1].Message)
+ assert.Equal(t, "my second\n\nmessage", messages[1].Message)
+
+ response = request(t, s, "GET", "/mytopic/sse?poll=1", "", nil)
+ lines := strings.Split(strings.TrimSpace(response.Body.String()), "\n")
+ assert.Equal(t, 3, len(lines))
+ assert.Equal(t, "my first message", toMessage(t, strings.TrimPrefix(lines[0], "data: ")).Message)
+ assert.Equal(t, "", lines[1])
+ assert.Equal(t, "my second\n\nmessage", toMessage(t, strings.TrimPrefix(lines[2], "data: ")).Message)
+
+ response = request(t, s, "GET", "/mytopic/raw?poll=1", "", nil)
+ lines = strings.Split(strings.TrimSpace(response.Body.String()), "\n")
+ assert.Equal(t, 2, len(lines))
+ assert.Equal(t, "my first message", lines[0])
+ assert.Equal(t, "my second message", lines[1]) // \n -> " "
}
func TestServer_SubscribeOpenAndKeepalive(t *testing.T) {
@@ -109,6 +122,37 @@ func TestServer_PublishAndSubscribe(t *testing.T) {
assert.Equal(t, []string{"tag1", "tag 2", "tag3"}, messages[2].Tags)
}
+func TestServer_StaticSites(t *testing.T) {
+ s := newTestServer(t, newTestConfig(t))
+
+ rr := request(t, s, "GET", "/", "", nil)
+ assert.Equal(t, 200, rr.Code)
+ assert.Contains(t, rr.Body.String(), "