Compare commits

..

1 Commits

Author SHA1 Message Date
binwiederhier
db1a1fec0c Custom HTTP response writer 2023-02-17 09:07:57 -05:00
377 changed files with 26438 additions and 67919 deletions

View File

@@ -1,3 +0,0 @@
dist
*/node_modules
Dockerfile*

View File

@@ -1,11 +0,0 @@
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
# Run prettier (https://github.com/binwiederhier/ntfy/pull/746)
6f6a2d1f693070bf72e89d86748080e4825c9164
c87549e71a10bc789eac8036078228f06e515a8e
ca5d736a7169eb6b4b0d849e061d5bf9565dcc53
2e27f58963feb9e4d1c573d4745d07770777fa7d
# Run eslint (https://github.com/binwiederhier/ntfy/pull/748)
f558b4dbe9bb5b9e0e87fada1215de2558353173
8319f1cf26113167fb29fe12edaff5db74caf35f

View File

@@ -1,26 +0,0 @@
---
name: 🐛 Bug Report
about: Report any errors and problems
title: ''
labels: '🪲 bug'
assignees: ''
---
:lady_beetle: **Describe the bug**
<!-- A clear and concise description of the problem. -->
:computer: **Components impacted**
<!-- ntfy server, Android app, iOS app, web app -->
:bulb: **Screenshots and/or logs**
<!--
If applicable, add screenshots or share logs help explain your problem.
To get logs from the ...
- ntfy server: Enable "log-level: trace" in your server.yml file
- Android app: Go to "Settings" -> "Record logs", then eventually "Copy/upload logs"
- web app: Press "F12" and find the "Console" window
-->
:crystal_ball: **Additional context**
<!-- Add any other context about the problem here. -->

View File

@@ -1,26 +0,0 @@
---
name: 💡 Feature/Enhancement Request
about: Got a great idea? Let us know!
title: ''
labels: 'enhancement'
assignees: ''
---
<!--
Before you submit, consider asking on Discord/Matrix instead. You'll usually get an answer
sooner, and there are more people there to help!
- Discord: https://discord.gg/cT7ECsZj9w
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org
-->
:bulb: **Idea**
<!-- Share your thoughts; try to be detailed if you can -->
:computer: **Target components**
<!-- Where should this feature/enhancement be added? -->
<!-- e.g. ntfy server, Android app, iOS app, web app -->

View File

@@ -1,21 +0,0 @@
---
name: 🆘 I need help with ...
about: Installing ntfy, configuring the app, etc.
title: ''
labels: 'tech-support'
assignees: ''
---
<!--
STOP!
This is not the right place to ask for help. Consider asking on Discord/Matrix instead.
You'll usually get an answer sooner, and there are more people there to help!
- Discord: https://discord.gg/cT7ECsZj9w
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org
-->

View File

@@ -1,21 +0,0 @@
---
name: ❓ Question
about: Ask a question about ntfy
title: ''
labels: 'question'
assignees: ''
---
<!--
Before you submit, consider asking on Discord/Matrix instead. You'll usually get an answer
sooner, and there are more people there to help!
- Discord: https://discord.gg/cT7ECsZj9w
- Matrix: https://matrix.to/#/#ntfy:matrix.org / https://matrix.to/#/#ntfy-space:matrix.org
-->
:question: **Question**
<!-- Go ahead and ask your question here :) -->

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -1,24 +1,39 @@
name: build
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
-
name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.24.x'
- name: Install node
uses: actions/setup-node@v3
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v2
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- name: Install dependencies
node-version: '17'
-
name: Checkout code
uses: actions/checkout@v2
-
name: Cache Go and npm modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Install dependencies
run: make build-deps-ubuntu
- name: Build all the things
-
name: Build all the things
run: make build
- name: Print build results and checksums
-
name: Print build results and checksums
run: make cli-build-results

View File

@@ -30,7 +30,7 @@ jobs:
run: |
cd build/ntfy-docs.github.io
git config user.name "GitHub Actions Bot"
git config user.email "<actions@github.com>"
git config user.email "<>"
git add docs/
git commit -m "Updated docs"
git push origin main

View File

@@ -7,28 +7,44 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
-
name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.24.x'
- name: Install node
uses: actions/setup-node@v3
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v2
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- name: Docker login
node-version: '17'
-
name: Checkout code
uses: actions/checkout@v2
-
name: Cache Go and npm modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Docker login
uses: docker/login-action@v2
with:
username: ${{ github.repository_owner }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Install dependencies
-
name: Install dependencies
run: make build-deps-ubuntu
- name: Build and publish
-
name: Build and publish
run: make release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Print build results and checksums
-
name: Print build results and checksums
run: make cli-build-results

View File

@@ -1,30 +1,48 @@
name: test
on: [ push, pull_request ]
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
-
name: Install Go
uses: actions/setup-go@v2
with:
go-version: '1.24.x'
- name: Install node
uses: actions/setup-node@v3
go-version: '1.19.x'
-
name: Install node
uses: actions/setup-node@v2
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: './web/package-lock.json'
- name: Install dependencies
node-version: '17'
-
name: Checkout code
uses: actions/checkout@v2
-
name: Cache Go and npm modules
uses: actions/cache@v3
with:
path: |
~/go/pkg/mod
~/go/bin
~/.npm
web/node_modules
key: ${{ runner.os }}-ntfy-${{ hashFiles('**/go.sum', '**/package.lock') }}
restore-keys: ${{ runner.os }}-ntfy-
-
name: Install dependencies
run: make build-deps-ubuntu
- name: Build docs (required for tests)
-
name: Build docs (required for tests)
run: make docs
- name: Build web app (required for tests)
-
name: Build web app (required for tests)
run: make web
- name: Run tests, formatting, vetting and linting
-
name: Run tests, formatting, vetting and linting
run: make check
- name: Run coverage
-
name: Run coverage
run: make coverage
- name: Upload coverage to codecov.io
-
name: Upload coverage to codecov.io
run: make coverage-upload

5
.gitignore vendored
View File

@@ -1,5 +1,4 @@
dist/
dev-dist/
build/
.idea/
.vscode/
@@ -12,7 +11,3 @@ secrets/
*.iml
node_modules/
.DS_Store
__pycache__
web/dev-dist/
venv/
cmd/key-file.yaml

View File

@@ -1,70 +1,76 @@
version: 2
before:
hooks:
- go mod download
- go mod tidy
builds:
- id: ntfy_linux_amd64
-
id: ntfy_linux_amd64
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [ linux ]
goarch: [ amd64 ]
- id: ntfy_linux_armv6
goos: [linux]
goarch: [amd64]
-
id: ntfy_linux_armv6
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [ linux ]
goarch: [ arm ]
goarm: [ 6 ]
- id: ntfy_linux_armv7
goos: [linux]
goarch: [arm]
goarm: [6]
-
id: ntfy_linux_armv7
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [ linux ]
goarch: [ arm ]
goarm: [ 7 ]
- id: ntfy_linux_arm64
goos: [linux]
goarch: [arm]
goarm: [7]
-
id: ntfy_linux_arm64
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=aarch64-linux-gnu-gcc # apt install gcc-aarch64-linux-gnu
tags: [ sqlite_omit_load_extension,osusergo,netgo ]
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [ linux ]
goarch: [ arm64 ]
- id: ntfy_windows_amd64
goos: [linux]
goarch: [arm64]
-
id: ntfy_windows_amd64
binary: ntfy
env:
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
tags: [ noserver ] # don't include server files
tags: [noserver] # don't include server files
ldflags:
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [ windows ]
goarch: [ amd64 ]
- id: ntfy_darwin_all
goos: [windows]
goarch: [amd64]
-
id: ntfy_darwin_all
binary: ntfy
env:
- CGO_ENABLED=0 # explicitly disable, since we don't need go-sqlite3
tags: [ noserver ] # don't include server files
tags: [noserver] # don't include server files
ldflags:
- "-X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [ darwin ]
goarch: [ amd64, arm64 ] # will be combined to "universal binary" (see below)
goos: [darwin]
goarch: [amd64, arm64] # will be combined to "universal binary" (see below)
nfpms:
- package_name: ntfy
-
package_name: ntfy
homepage: https://heckel.io/ntfy
maintainer: Philipp C. Heckel <philipp.heckel@gmail.com>
description: Simple pub-sub notification service
@@ -84,8 +90,6 @@ nfpms:
type: "config|noreplace"
- src: client/ntfy-client.service
dst: /lib/systemd/system/ntfy-client.service
- src: client/user/ntfy-client.service
dst: /lib/systemd/user/ntfy-client.service
- dst: /var/cache/ntfy
type: dir
- dst: /var/cache/ntfy/attachments
@@ -93,15 +97,16 @@ nfpms:
- dst: /var/lib/ntfy
type: dir
- dst: /usr/share/ntfy/logo.png
src: web/public/static/images/ntfy.png
src: web/public/static/img/ntfy.png
scripts:
preinstall: "scripts/preinst.sh"
postinstall: "scripts/postinst.sh"
preremove: "scripts/prerm.sh"
postremove: "scripts/postrm.sh"
archives:
- id: ntfy_linux
ids:
-
id: ntfy_linux
builds:
- ntfy_linux_amd64
- ntfy_linux_armv6
- ntfy_linux_armv7
@@ -114,32 +119,40 @@ archives:
- server/ntfy.service
- client/client.yml
- client/ntfy-client.service
- client/user/ntfy-client.service
- id: ntfy_windows
ids:
replacements:
amd64: x86_64
-
id: ntfy_windows
builds:
- ntfy_windows_amd64
formats: [ zip ]
format: zip
wrap_in_directory: true
files:
- LICENSE
- README.md
- client/client.yml
- id: ntfy_darwin
ids:
replacements:
amd64: x86_64
-
id: ntfy_darwin
builds:
- ntfy_darwin_all
wrap_in_directory: true
files:
- LICENSE
- README.md
- client/client.yml
replacements:
darwin: macOS
universal_binaries:
- id: ntfy_darwin_all
-
id: ntfy_darwin_all
replace: true
name_template: ntfy
checksum:
name_template: 'checksums.txt'
snapshot:
version_template: "{{ .Tag }}-next"
name_template: "{{ .Tag }}-next"
changelog:
sort: asc
filters:
@@ -157,14 +170,14 @@ dockers:
- image_templates:
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
use: buildx
dockerfile: Dockerfile-arm
dockerfile: Dockerfile
goarch: arm64
build_flag_templates:
- "--platform=linux/arm64/v8"
- image_templates:
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
use: buildx
dockerfile: Dockerfile-arm
dockerfile: Dockerfile
goarch: arm
goarm: 7
build_flag_templates:
@@ -172,7 +185,7 @@ dockers:
- image_templates:
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
use: buildx
dockerfile: Dockerfile-arm
dockerfile: Dockerfile
goarch: arm
goarm: 6
build_flag_templates:
@@ -190,15 +203,3 @@ docker_manifests:
- *arm64v8_image
- *armv7_image
- *armv6_image
- name_template: "binwiederhier/ntfy:v{{ .Major }}"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image
- *armv6_image
- name_template: "binwiederhier/ntfy:v{{ .Major }}.{{ .Minor }}"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image
- *armv6_image

View File

@@ -1,16 +1,9 @@
FROM alpine
MAINTAINER Philipp C. Heckel <philipp.heckel@gmail.com>
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
RUN apk add --no-cache tzdata
COPY ntfy /usr/bin
HEALTHCHECK --interval=60s --timeout=10s CMD wget -q --tries=1 http://localhost/v1/health -O - | grep -Eo '"healthy"\s*:\s*true' || exit 1
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]

View File

@@ -1,18 +0,0 @@
FROM alpine
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
# Alpine does not support adding "tzdata" on ARM anymore, see
# https://github.com/binwiederhier/ntfy/issues/894
COPY ntfy /usr/bin
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]

View File

@@ -1,62 +0,0 @@
FROM golang:1.24-bullseye as builder
ARG VERSION=dev
ARG COMMIT=unknown
ARG NODE_MAJOR=18
RUN apt-get update && apt-get install -y \
build-essential ca-certificates curl gnupg \
&& mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" >> /etc/apt/sources.list.d/nodesource.list \
&& apt-get update \
&& apt-get install -y \
python3-pip \
python3-venv \
nodejs \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
ADD Makefile .
# docs
ADD ./requirements.txt .
RUN make docs-deps
ADD ./mkdocs.yml .
ADD ./docs ./docs
RUN make docs-build
# web
ADD ./web/package.json ./web/package-lock.json ./web/
RUN make web-deps
ADD ./web ./web
RUN make web-build
# cli & server
ADD go.mod go.sum main.go ./
ADD ./client ./client
ADD ./cmd ./cmd
ADD ./log ./log
ADD ./server ./server
ADD ./user ./user
ADD ./util ./util
RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server
FROM alpine
ARG VERSION=dev
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
LABEL org.opencontainers.image.title="ntfy"
LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
LABEL org.opencontainers.image.version="$VERSION"
COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
EXPOSE 80/tcp
ENTRYPOINT ["ntfy"]

View File

@@ -1,6 +1,4 @@
MAKEFLAGS := --jobs=1
PYTHON := python3
PIP := pip3
VERSION := $(shell git describe --tag)
COMMIT := $(shell git rev-parse --short HEAD)
@@ -33,16 +31,10 @@ help:
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
@echo
@echo "Build dev Docker:"
@echo " make docker-dev - Build client & server for current architecture using Docker only"
@echo
@echo "Build web app:"
@echo " make web - Build the web app"
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
@echo " make web-lint - Run eslint on the web app"
@echo " make web-fmt - Run prettier on the web app"
@echo " make web-fmt-check - Run prettier on the web app, but don't change anything"
@echo
@echo "Build documentation:"
@echo " make docs - Build the documentation"
@@ -88,45 +80,40 @@ build: web docs cli
update: web-deps-update cli-deps-update docs-deps-update
docker pull alpine
docker-dev:
docker build \
--file ./Dockerfile-build \
--tag binwiederhier/ntfy:$(VERSION) \
--tag binwiederhier/ntfy:dev \
--build-arg VERSION=$(VERSION) \
--build-arg COMMIT=$(COMMIT) \
./
# Ubuntu-specific
build-deps-ubuntu:
sudo apt-get update
sudo apt-get install -y \
sudo apt update
sudo apt install -y \
curl \
gcc-aarch64-linux-gnu \
gcc-arm-linux-gnueabi \
python3 \
python3-venv \
jq
which pip3 || sudo apt-get install -y python3-pip
which pip3 || sudo apt install -y python3-pip
# Documentation
docs: docs-deps docs-build
docs-venv: .PHONY
$(PYTHON) -m venv ./venv
docs-build: .PHONY
@if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
if which python3.8; then \
echo "python3.8 $(shell which mkdocs) build"; \
python3.8 $(shell which mkdocs) build; \
else \
echo "ERROR: Python version too low. mkdocs-material needs >= 3.8"; \
exit 1; \
fi; \
else \
echo "mkdocs build"; \
mkdocs build; \
fi
docs-build: docs-venv
(. venv/bin/activate && $(PYTHON) -m mkdocs build)
docs-deps: docs-venv
(. venv/bin/activate && $(PIP) install -r requirements.txt)
docs-deps: .PHONY
pip3 install -r requirements.txt
docs-deps-update: .PHONY
(. venv/bin/activate && $(PIP) install -r requirements.txt --upgrade)
pip3 install -r requirements.txt --upgrade
# Web app
@@ -140,7 +127,8 @@ web-build:
&& rm -rf ../server/site \
&& mv build ../server/site \
&& rm \
../server/site/config.js
../server/site/config.js \
../server/site/asset-manifest.json
web-deps:
cd web && npm install
@@ -149,37 +137,29 @@ web-deps:
web-deps-update:
cd web && npm update
web-fmt:
cd web && npm run format
web-fmt-check:
cd web && npm run format:check
web-lint:
cd web && npm run lint
# Main server/client build
cli: cli-deps
goreleaser build --snapshot --clean
goreleaser build --snapshot --rm-dist
cli-linux-amd64: cli-deps-static-sites
goreleaser build --snapshot --clean --id ntfy_linux_amd64
goreleaser build --snapshot --rm-dist --id ntfy_linux_amd64
cli-linux-armv6: cli-deps-static-sites cli-deps-gcc-armv6-armv7
goreleaser build --snapshot --clean --id ntfy_linux_armv6
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv6
cli-linux-armv7: cli-deps-static-sites cli-deps-gcc-armv6-armv7
goreleaser build --snapshot --clean --id ntfy_linux_armv7
goreleaser build --snapshot --rm-dist --id ntfy_linux_armv7
cli-linux-arm64: cli-deps-static-sites cli-deps-gcc-arm64
goreleaser build --snapshot --clean --id ntfy_linux_arm64
goreleaser build --snapshot --rm-dist --id ntfy_linux_arm64
cli-windows-amd64: cli-deps-static-sites
goreleaser build --snapshot --clean --id ntfy_windows_amd64
goreleaser build --snapshot --rm-dist --id ntfy_windows_amd64
cli-darwin-all: cli-deps-static-sites
goreleaser build --snapshot --clean --id ntfy_darwin_all
goreleaser build --snapshot --rm-dist --id ntfy_darwin_all
cli-linux-server: cli-deps-static-sites
# This is a target to build the CLI (including the server) manually.
@@ -220,7 +200,7 @@ cli-deps-static-sites:
touch server/docs/index.html server/site/app.html
cli-deps-all:
go install github.com/goreleaser/goreleaser/v2@latest
go install github.com/goreleaser/goreleaser@latest
cli-deps-gcc-armv6-armv7:
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
@@ -232,7 +212,7 @@ cli-deps-update:
go get -u
go install honnef.co/go/tools/cmd/staticcheck@latest
go install golang.org/x/lint/golint@latest
go install github.com/goreleaser/goreleaser/v2@latest
go install github.com/goreleaser/goreleaser@latest
cli-build-results:
cat dist/config.yaml
@@ -246,7 +226,7 @@ cli-build-results:
# Test/check targets
check: test web-fmt-check fmt-check vet web-lint lint staticcheck
check: test fmt-check vet lint staticcheck
test: .PHONY
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
@@ -273,7 +253,7 @@ coverage-upload:
# Lint/formatting targets
fmt: web-fmt
fmt:
gofmt -s -w .
fmt-check:
@@ -297,11 +277,11 @@ staticcheck: .PHONY
# Releasing targets
release: clean cli-deps release-checks docs web check
goreleaser release --clean
release: clean update cli-deps release-checks docs web check
goreleaser release --rm-dist
release-snapshot: clean cli-deps docs web check
goreleaser release --snapshot --clean
release-snapshot: clean update cli-deps docs web check
goreleaser release --snapshot --skip-publish --rm-dist
release-checks:
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))

183
README.md
View File

@@ -1,53 +1,32 @@
<div align="center" markdown="1">
<sup>Special thanks to:</sup>
<br>
<br>
<a href="https://go.warp.dev/ntfy">
<img alt="Warp sponsorship" width="400" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-02.png">
</a>
### [Warp, built for coding with multiple AI agents.](https://go.warp.dev/ntfy)
[Available for MacOS, Linux, & Windows](https://go.warp.dev/ntfy)<br>
</div>
<hr>
![ntfy](web/public/static/images/ntfy.png)
![ntfy](web/public/static/img/ntfy.png)
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
[![Release](https://img.shields.io/github/release/binwiederhier/ntfy.svg?color=success&style=flat-square)](https://github.com/binwiederhier/ntfy/releases/latest)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy/v2)
[![Go Reference](https://pkg.go.dev/badge/heckel.io/ntfy.svg)](https://pkg.go.dev/heckel.io/ntfy)
[![Tests](https://github.com/binwiederhier/ntfy/workflows/test/badge.svg)](https://github.com/binwiederhier/ntfy/actions)
[![Go Report Card](https://goreportcard.com/badge/github.com/binwiederhier/ntfy)](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
[![codecov](https://codecov.io/gh/binwiederhier/ntfy/branch/main/graph/badge.svg?token=A597KQ463G)](https://codecov.io/gh/binwiederhier/ntfy)
[![Discord](https://img.shields.io/discord/874398661709295626?label=Discord)](https://discord.gg/cT7ECsZj9w)
[![Matrix](https://img.shields.io/matrix/ntfy:matrix.org?label=Matrix)](https://matrix.to/#/#ntfy:matrix.org)
[![Matrix space](https://img.shields.io/matrix/ntfy-space:matrix.org?label=Matrix+space)](https://matrix.to/#/#ntfy-space:matrix.org)
[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/ntfy?color=%23317f6f&label=-%20r%2Fntfy&style=social)](https://www.reddit.com/r/ntfy/)
[![Healthcheck](https://healthchecks.io/badge/68b65976-b3b0-4102-aec9-980921/kcoEgrLY.svg)](https://ntfy.statuspage.io/)
[![Gitpod](https://img.shields.io/badge/Contribute%20with-Gitpod-908a85?logo=gitpod)](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
**ntfy** (pronounced "*notify*") is a simple HTTP-based [pub-sub](https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern)
notification service. With ntfy, you can **send notifications to your phone or desktop via scripts** from any computer,
**without having to sign up or pay any fees**. If you'd like to run your own instance of the service, you can easily do
so since ntfy is open source.
**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**.
It's also open source (as you can plainly see) if you want to run your own.
You can access the free version of ntfy at **[ntfy.sh](https://ntfy.sh)**. There is also an [open-source Android app](https://github.com/binwiederhier/ntfy-android)
available on [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/),
as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) available on the [App Store](https://apps.apple.com/us/app/ntfy/id1625396347).
I run a free version of it at **[ntfy.sh](https://ntfy.sh)**. There's also an [open source Android app](https://github.com/binwiederhier/ntfy-android) (see [Google Play](https://play.google.com/store/apps/details?id=io.heckel.ntfy) or [F-Droid](https://f-droid.org/en/packages/io.heckel.ntfy/)), and an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) (see [App Store](https://apps.apple.com/us/app/ntfy/id1625396347)).
<p>
<img src=".github/images/screenshot-curl.png" height="180">
<img src=".github/images/screenshot-web-detail.png" height="180">
<img src=".github/images/screenshot-phone-main.jpg" height="180">
<img src=".github/images/screenshot-phone-detail.jpg" height="180">
<img src=".github/images/screenshot-phone-notification.jpg" height="180">
<img src="web/public/static/img/screenshot-curl.png" height="180">
<img src="web/public/static/img/screenshot-web-detail.png" height="180">
<img src="web/public/static/img/screenshot-phone-main.jpg" height="180">
<img src="web/public/static/img/screenshot-phone-detail.jpg" height="180">
<img src="web/public/static/img/screenshot-phone-notification.jpg" height="180">
</p>
## [ntfy Pro](https://ntfy.sh/app) 💸 🎉
I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self-host, or you want to support the development of
ntfy (→ [Purchase via web app](https://ntfy.sh/app)). You can **buy a plan for as low as $5/month**.
You can also donate via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), and [Liberapay](https://liberapay.com/ntfy).
I would be very humbled by your sponsorship. ❤️
## **[Documentation](https://ntfy.sh/docs/)**
[Getting started](https://ntfy.sh/docs/) |
@@ -56,33 +35,35 @@ I would be very humbled by your sponsorship. ❤️
[Install / Self-hosting](https://ntfy.sh/docs/install/) |
[Building](https://ntfy.sh/docs/develop/)
## Chat/forum
## Chat / forum
There are a few ways to get in touch with me and/or the rest of the community. Feel free to use any of these methods. Whatever
works best for you:
* [Discord server](https://discord.gg/cT7ECsZj9w) - direct chat with the community
* [Matrix room #ntfy](https://matrix.to/#/#ntfy:matrix.org) (+ [Matrix space](https://matrix.to/#/#ntfy-space:matrix.org)) - same chat, bridged from Discord
* [Reddit r/ntfy](https://www.reddit.com/r/ntfy/) - asynchronous forum (_new as of October 2022_)
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
* [Email](https://heckel.io/about) - reach me directly (_I usually prefer the other methods_)
## Announcements/beta testers
## Announcements / beta testers
For announcements of new releases and cutting-edge beta versions, please subscribe to the [ntfy.sh/announcements](https://ntfy.sh/announcements)
topic. If you'd like to test the iOS app, join [TestFlight](https://testflight.apple.com/join/P1fFnAm9). For Android betas,
join Discord/Matrix (I'll eventually make a testing channel in Google Play).
## Contributing
I welcome any and all contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
<a href="https://hosted.weblate.org/engage/ntfy/">
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>
## Sponsors
If you'd like to support the ntfy maintainers, please consider donating to [GitHub Sponsors](https://github.com/sponsors/binwiederhier) or
and [Liberapay](https://liberapay.com/ntfy). We would be humbled if you helped carry the server and developer
account costs. Even small donations are very much appreciated.
Thank you to our commercial sponsors, who help keep the service running and the development going:
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
<a href="https://www.magicbell.com/?utm_source=ntfy"><img src="assets/sponsors/magicbell.png" width="180px"></a>
<a href="https://go.warp.dev/ntfy"><img src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Logos/Warp-Wordmark-Black.png" width="160px"></a>
And a big fat **Thank You** to the individuals who have sponsored ntfy in the past, or are still sponsoring ntfy:
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier),
and [Liberapay](https://liberapay.com/ntfy). I would be humbled if you helped me carry the server and developer
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks already sponsoring ntfy:
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
@@ -136,108 +117,14 @@ And a big fat **Thank You** to the individuals who have sponsored ntfy in the pa
<a href="https://github.com/IanKulin"><img src="https://github.com/IanKulin.png" width="40px" /></a>
<a href="https://github.com/Joachim256"><img src="https://github.com/Joachim256.png" width="40px" /></a>
<a href="https://github.com/overtone1000"><img src="https://github.com/overtone1000.png" width="40px" /></a>
<a href="https://github.com/oakd"><img src="https://github.com/oakd.png" width="40px" /></a>
<a href="https://github.com/KucharczykL"><img src="https://github.com/KucharczykL.png" width="40px" /></a>
<a href="https://github.com/hansbickhofe"><img src="https://github.com/hansbickhofe.png" width="40px" /></a>
<a href="https://github.com/caseodilla"><img src="https://github.com/caseodilla.png" width="40px" /></a>
<a href="https://github.com/0xAF"><img src="https://github.com/0xAF.png" width="40px" /></a>
<a href="https://github.com/soonoo"><img src="https://github.com/soonoo.png" width="40px" /></a>
<a href="https://github.com/nichu42"><img src="https://github.com/nichu42.png" width="40px" /></a>
<a href="https://github.com/samliebow"><img src="https://github.com/samliebow.png" width="40px" /></a>
<a href="https://github.com/johman10"><img src="https://github.com/johman10.png" width="40px" /></a>
<a href="https://github.com/R-Gld"><img src="https://github.com/R-Gld.png" width="40px" /></a>
<a href="https://github.com/FingerlessGlov3s"><img src="https://github.com/FingerlessGlov3s.png" width="40px" /></a>
<a href="https://github.com/Twisterado"><img src="https://github.com/Twisterado.png" width="40px" /></a>
<a href="https://github.com/ScrumpyJack"><img src="https://github.com/ScrumpyJack.png" width="40px" /></a>
<a href="https://github.com/andrejarrell"><img src="https://github.com/andrejarrell.png" width="40px" /></a>
<a href="https://github.com/oaustegard"><img src="https://github.com/oaustegard.png" width="40px" /></a>
<a href="https://github.com/CreativeWarlock"><img src="https://github.com/CreativeWarlock.png" width="40px" /></a>
<a href="https://github.com/darkdragon-001"><img src="https://github.com/darkdragon-001.png" width="40px" /></a>
<a href="https://github.com/jonathan-kosgei"><img src="https://github.com/jonathan-kosgei.png" width="40px" /></a>
<a href="https://github.com/KevinWang15"><img src="https://github.com/KevinWang15.png" width="40px" /></a>
<a href="https://github.com/darkmattercoder"><img src="https://github.com/darkmattercoder.png" width="40px" /></a>
<a href="https://github.com/bmcgonag"><img src="https://github.com/bmcgonag.png" width="40px" /></a>
<a href="https://github.com/skorokithakis"><img src="https://github.com/skorokithakis.png" width="40px" /></a>
<a href="https://github.com/eenturk"><img src="https://github.com/eenturk.png" width="40px" /></a>
<a href="https://github.com/spirossi"><img src="https://github.com/spirossi.png" width="40px" /></a>
<a href="https://github.com/teomarcdhio"><img src="https://github.com/teomarcdhio.png" width="40px" /></a>
<a href="https://github.com/MarcMichalsky"><img src="https://github.com/MarcMichalsky.png" width="40px" /></a>
<a href="https://github.com/LuckVintage"><img src="https://github.com/LuckVintage.png" width="40px" /></a>
<a href="https://github.com/spartan"><img src="https://github.com/spartan.png" width="40px" /></a>
<a href="https://github.com/alexandzors"><img src="https://github.com/alexandzors.png" width="40px" /></a>
<a href="https://github.com/dkramer95"><img src="https://github.com/dkramer95.png" width="40px" /></a>
<a href="https://github.com/YezGotIt"><img src="https://github.com/YezGotIt.png" width="40px" /></a>
<a href="https://github.com/thomasskou"><img src="https://github.com/thomasskou.png" width="40px" /></a>
<a href="https://github.com/surfernv"><img src="https://github.com/surfernv.png" width="40px" /></a>
<a href="https://github.com/richardleach"><img src="https://github.com/richardleach.png" width="40px" /></a>
<a href="https://github.com/bear"><img src="https://github.com/bear.png" width="40px" /></a>
<a href="https://github.com/cminter"><img src="https://github.com/cminter.png" width="40px" /></a>
<a href="https://github.com/bahur142"><img src="https://github.com/bahur142.png" width="40px" /></a>
<a href="https://github.com/pgwiebes"><img src="https://github.com/pgwiebes.png" width="40px" /></a>
<a href="https://github.com/ralhei"><img src="https://github.com/ralhei.png" width="40px" /></a>
<a href="https://github.com/TechMDW"><img src="https://github.com/TechMDW.png" width="40px" /></a>
<a href="https://github.com/ubipo"><img src="https://github.com/ubipo.png" width="40px" /></a>
<a href="https://github.com/tka85"><img src="https://github.com/tka85.png" width="40px" /></a>
<a href="https://github.com/beekeeb"><img src="https://github.com/beekeeb.png" width="40px" /></a>
<a href="https://github.com/Emiliaaah"><img src="https://github.com/Emiliaaah.png" width="40px" /></a>
<a href="https://github.com/zark0s"><img src="https://github.com/zark0s.png" width="40px" /></a>
<a href="https://github.com/tomershvueli"><img src="https://github.com/tomershvueli.png" width="40px" /></a>
<a href="https://github.com/CataIana"><img src="https://github.com/CataIana.png" width="40px" /></a>
<a href="https://github.com/ajay-actuary"><img src="https://github.com/ajay-actuary.png" width="40px" /></a>
<a href="https://github.com/mursec"><img src="https://github.com/mursec.png" width="40px" /></a>
<a href="https://github.com/FrameXX"><img src="https://github.com/FrameXX.png" width="40px" /></a>
<a href="https://github.com/vovayartsev"><img src="https://github.com/vovayartsev.png" width="40px" /></a>
<a href="https://github.com/dwain-lab"><img src="https://github.com/dwain-lab.png" width="40px" /></a>
<a href="https://github.com/brookmg"><img src="https://github.com/brookmg.png" width="40px" /></a>
<a href="https://github.com/siebej"><img src="https://github.com/siebej.png" width="40px" /></a>
<a href="https://github.com/rxsantos"><img src="https://github.com/rxsantos.png" width="40px" /></a>
<a href="https://github.com/hermannx5"><img src="https://github.com/hermannx5.png" width="40px" /></a>
<a href="https://github.com/rwxd"><img src="https://github.com/rwxd.png" width="40px" /></a>
<a href="https://github.com/Integral-Tech"><img src="https://github.com/Integral-Tech.png" width="40px" /></a>
<a href="https://github.com/TheTomik1"><img src="https://github.com/TheTomik1.png" width="40px" /></a>
<a href="https://github.com/dav23r"><img src="https://github.com/dav23r.png" width="40px" /></a>
<a href="https://github.com/stannynuytkens"><img src="https://github.com/stannynuytkens.png" width="40px" /></a>
<a href="https://github.com/danbartram"><img src="https://github.com/danbartram.png" width="40px" /></a>
<a href="https://github.com/arthurgleckler"><img src="https://github.com/arthurgleckler.png" width="40px" /></a>
<a href="https://github.com/tomroth04"><img src="https://github.com/tomroth04.png" width="40px" /></a>
<a href="https://github.com/Circenn5130"><img src="https://github.com/Circenn5130.png" width="40px" /></a>
<a href="https://github.com/jceloria"><img src="https://github.com/jceloria.png" width="40px" /></a>
<a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="40px" /></a>
<a href="https://github.com/PTR-inc"><img src="https://github.com/PTR-inc.png" width="40px" /></a>
<a href="https://github.com/spudooli"><img src="https://github.com/spudooli.png" width="40px" /></a>
<a href="https://github.com/IMarkoMC"><img src="https://github.com/IMarkoMC.png" width="40px" /></a>
<a href="https://github.com/rubund"><img src="https://github.com/rubund.png" width="40px" /></a>
<a href="https://github.com/Riolku"><img src="https://github.com/Riolku.png" width="40px" /></a>
<a href="https://github.com/arnbrhm"><img src="https://github.com/arnbrhm.png" width="40px" /></a>
<a href="https://github.com/herzkerl"><img src="https://github.com/herzkerl.png" width="40px" /></a>
<a href="https://github.com/0x45796164"><img src="https://github.com/0x45796164.png" width="40px" /></a>
<a href="https://github.com/madchr1st"><img src="https://github.com/madchr1st.png" width="40px" /></a>
<a href="https://github.com/avalentic"><img src="https://github.com/avalentic.png" width="40px" /></a>
<a href="https://github.com/TheCraiggers"><img src="https://github.com/TheCraiggers.png" width="40px" /></a>
<a href="https://github.com/sheetd"><img src="https://github.com/sheetd.png" width="40px" /></a>
<a href="https://github.com/dlt-green"><img src="https://github.com/dlt-green.png" width="40px" /></a>
<a href="https://github.com/suhlig"><img src="https://github.com/suhlig.png" width="40px" /></a>
<a href="https://github.com/Proximus888"><img src="https://github.com/Proximus888.png" width="40px" /></a>
<a href="https://github.com/wielandp"><img src="https://github.com/wielandp.png" width="40px" /></a>
<a href="https://github.com/chxseh"><img src="https://github.com/chxseh.png" width="40px" /></a>
<a href="https://github.com/user8446"><img src="https://github.com/user8446.png" width="40px" /></a>
<a href="https://github.com/cdf-eagles"><img src="https://github.com/cdf-eagles.png" width="40px" /></a>
## Contributing
I welcome any contributions. Just create a PR or an issue. For larger features/ideas, please reach out
on Discord/Matrix first to see if I'd accept them. To contribute code, check out the [build instructions](https://ntfy.sh/docs/develop/)
for the server and the Android app. Or, if you'd like to help translate 🇩🇪 🇺🇸 🇧🇬, you can start immediately in
[Hosted Weblate](https://hosted.weblate.org/projects/ntfy/).
I'd also like to thank JetBrains for providing their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/) to me for free,
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
<a href="https://hosted.weblate.org/engage/ntfy/">
<img src="https://hosted.weblate.org/widgets/ntfy/-/multi-blue.svg" alt="Translation status" />
</a>
<a href="https://m.do.co/c/442b929528db"><img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px"></a>
## Code of Conduct
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for
everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity
and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste,
color, religion, or sexual identity and orientation.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
**We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.**
@@ -247,7 +134,7 @@ _Please be sure to read the complete [Code of Conduct](CODE_OF_CONDUCT.md)._
Made with ❤️ by [Philipp C. Heckel](https://heckel.io).
The project is dual licensed under the [Apache License 2.0](LICENSE) and the [GPLv2 License](LICENSE.GPLv2).
Third-party libraries and resources:
Third party libraries and resources:
* [github.com/urfave/cli](https://github.com/urfave/cli) (MIT) is used to drive the CLI
* [Mixkit sounds](https://mixkit.co/free-sound-effects/notification/) (Mixkit Free License) are used as notification sounds
* [Sounds from notificationsounds.com](https://notificationsounds.com) (Creative Commons Attribution) are used as notification sounds
@@ -267,5 +154,3 @@ Third-party libraries and resources:
* [Regex for auto-linking](https://github.com/bryanwoods/autolink-js) (MIT) is used to highlight links (the library is not used)
* [Statically linking go-sqlite3](https://www.arp242.net/static-go.html)
* [Linked tabs in mkdocs](https://facelessuser.github.io/pymdown-extensions/extensions/tabbed/#linked-tabs)
* [webpush-go](https://github.com/SherClockHolmes/webpush-go) (MIT) is used to send web push notifications
* [Sprig](https://github.com/Masterminds/sprig) (MIT) is used to add template parsing functions

View File

@@ -1,10 +0,0 @@
# Security Policy
## Supported Versions
As of today, I only support the latest version of ntfy. Please make sure you stay up-to-date.
## Reporting a Vulnerability
Please report severe security issues privately via ntfy@heckel.io, [Discord](https://discord.gg/cT7ECsZj9w),
or [Matrix](https://matrix.to/#/#ntfy:matrix.org) (my username is `binwiederhier`).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -7,29 +7,27 @@ import (
"encoding/json"
"errors"
"fmt"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"io"
"net/http"
"regexp"
"strings"
"sync"
"time"
)
// Event type constants
const (
// MessageEvent identifies a message event
MessageEvent = "message"
MessageEvent = "message"
KeepaliveEvent = "keepalive"
OpenEvent = "open"
PollRequestEvent = "poll_request"
)
const (
maxResponseBytes = 4096
)
var (
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
)
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
type Client struct {
Messages chan *Message
@@ -98,14 +96,8 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) (*Mess
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
// WithNoFirebase, and the generic WithHeader.
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
topicURL, err := c.expandTopicURL(topic)
if err != nil {
return nil, err
}
req, err := http.NewRequest("POST", topicURL, body)
if err != nil {
return nil, err
}
topicURL := c.expandTopicURL(topic)
req, _ := http.NewRequest("POST", topicURL, body)
for _, option := range options {
if err := option(req); err != nil {
return nil, err
@@ -141,14 +133,11 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
topicURL, err := c.expandTopicURL(topic)
if err != nil {
return nil, err
}
ctx := context.Background()
messages := make([]*Message, 0)
msgChan := make(chan *Message)
errChan := make(chan error)
topicURL := c.expandTopicURL(topic)
log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
options = append(options, WithPoll())
go func() {
@@ -177,18 +166,15 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
// Example:
//
// c := client.New(client.NewConfig())
// subscriptionID, _ := c.Subscribe("mytopic")
// subscriptionID := c.Subscribe("mytopic")
// for m := range c.Messages {
// fmt.Printf("New message: %s", m.Message)
// }
func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) {
topicURL, err := c.expandTopicURL(topic)
if err != nil {
return "", err
}
func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
c.mu.Lock()
defer c.mu.Unlock()
subscriptionID := util.RandomString(10)
topicURL := c.expandTopicURL(topic)
log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
ctx, cancel := context.WithCancel(context.Background())
c.subscriptions[subscriptionID] = &subscription{
@@ -197,7 +183,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, er
cancel: cancel,
}
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
return subscriptionID, nil
return subscriptionID
}
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
@@ -213,16 +199,31 @@ func (c *Client) Unsubscribe(subscriptionID string) {
sub.cancel()
}
func (c *Client) expandTopicURL(topic string) (string, error) {
// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe.
// If there are multiple subscriptions matching the topic, all of them are unsubscribed from.
//
// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
// config (e.g. mytopic -> https://ntfy.sh/mytopic).
func (c *Client) UnsubscribeAll(topic string) {
c.mu.Lock()
defer c.mu.Unlock()
topicURL := c.expandTopicURL(topic)
for _, sub := range c.subscriptions {
if sub.topicURL == topicURL {
delete(c.subscriptions, sub.ID)
sub.cancel()
}
}
}
func (c *Client) expandTopicURL(topic string) string {
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
return topic, nil
return topic
} else if strings.Contains(topic, "/") {
return fmt.Sprintf("https://%s", topic), nil
return fmt.Sprintf("https://%s", topic)
}
if !topicRegex.MatchString(topic) {
return "", fmt.Errorf("invalid topic name: %s", topic)
}
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
}
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {

View File

@@ -5,15 +5,10 @@
#
# default-host: https://ntfy.sh
# Default credentials will be used with "ntfy publish" and "ntfy subscribe" if no other credentials are provided.
# You can set a default token to use or a default user:password combination, but not both. For an empty password,
# use empty double-quotes ("").
# Default username and password will be used with "ntfy publish" if no credentials are provided on command line
# Default username and password will be used with "ntfy subscribe" if no credentials are provided in subscription below
# For an empty password, use empty double-quotes ("")
#
# To override the default user:password combination or default token for a particular subscription (e.g., to send
# no Authorization header), set the user:pass/token for the subscription to empty double-quotes ("").
# default-token:
# default-user:
# default-password:
@@ -21,7 +16,7 @@
# default-command:
# Subscriptions to topics and their actions. This option is primarily used by the systemd service,
# or if you can "ntfy subscribe --from-config" directly.
# or if you cann "ntfy subscribe --from-config" directly.
#
# Example:
# subscribe:
@@ -35,8 +30,6 @@
# command: 'notify-send "$m"'
# user: phill
# password: mypass
# - topic: token_topic
# token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
#
# Variables:
# Variable Aliases Description

View File

@@ -3,9 +3,9 @@ package client_test
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/test"
"os"
"testing"
"time"
@@ -21,7 +21,7 @@ func TestClient_Publish_Subscribe(t *testing.T) {
defer test.StopServer(t, s, port)
c := client.New(newTestConfig(port))
subscriptionID, _ := c.Subscribe("mytopic")
subscriptionID := c.Subscribe("mytopic")
time.Sleep(time.Second)
msg, err := c.Publish("mytopic", "some message")

View File

@@ -2,7 +2,6 @@ package client
import (
"gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/log"
"os"
)
@@ -13,22 +12,17 @@ const (
// Config is the config struct for a Client
type Config struct {
DefaultHost string `yaml:"default-host"`
DefaultUser string `yaml:"default-user"`
DefaultPassword *string `yaml:"default-password"`
DefaultToken string `yaml:"default-token"`
DefaultCommand string `yaml:"default-command"`
Subscribe []Subscribe `yaml:"subscribe"`
}
// Subscribe is the struct for a Subscription within Config
type Subscribe struct {
Topic string `yaml:"topic"`
User *string `yaml:"user"`
Password *string `yaml:"password"`
Token *string `yaml:"token"`
Command string `yaml:"command"`
If map[string]string `yaml:"if"`
DefaultHost string `yaml:"default-host"`
DefaultUser string `yaml:"default-user"`
DefaultPassword *string `yaml:"default-password"`
DefaultCommand string `yaml:"default-command"`
Subscribe []struct {
Topic string `yaml:"topic"`
User string `yaml:"user"`
Password *string `yaml:"password"`
Command string `yaml:"command"`
If map[string]string `yaml:"if"`
} `yaml:"subscribe"`
}
// NewConfig creates a new Config struct for a Client
@@ -37,7 +31,6 @@ func NewConfig() *Config {
DefaultHost: DefaultBaseURL,
DefaultUser: "",
DefaultPassword: nil,
DefaultToken: "",
DefaultCommand: "",
Subscribe: nil,
}
@@ -45,7 +38,6 @@ func NewConfig() *Config {
// LoadConfig loads the Client config from a yaml file
func LoadConfig(filename string) (*Config, error) {
log.Debug("Loading client config from %s", filename)
b, err := os.ReadFile(filename)
if err != nil {
return nil, err

View File

@@ -2,7 +2,7 @@ package client_test
import (
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/client"
"os"
"path/filepath"
"testing"
@@ -37,7 +37,7 @@ subscribe:
require.Equal(t, 4, len(conf.Subscribe))
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
require.Equal(t, "", conf.Subscribe[0].Command)
require.Equal(t, "phil", *conf.Subscribe[0].User)
require.Equal(t, "phil", conf.Subscribe[0].User)
require.Equal(t, "mypass", *conf.Subscribe[0].Password)
require.Equal(t, "echo-this", conf.Subscribe[1].Topic)
require.Equal(t, `echo "Message received: $message"`, conf.Subscribe[1].Command)
@@ -67,7 +67,7 @@ subscribe:
require.Equal(t, 1, len(conf.Subscribe))
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
require.Equal(t, "", conf.Subscribe[0].Command)
require.Equal(t, "phil", *conf.Subscribe[0].User)
require.Equal(t, "phil", conf.Subscribe[0].User)
require.Equal(t, "", *conf.Subscribe[0].Password)
}
@@ -91,7 +91,7 @@ subscribe:
require.Equal(t, 1, len(conf.Subscribe))
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
require.Equal(t, "", conf.Subscribe[0].Command)
require.Equal(t, "phil", *conf.Subscribe[0].User)
require.Equal(t, "phil", conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
}
@@ -113,28 +113,6 @@ subscribe:
require.Equal(t, 1, len(conf.Subscribe))
require.Equal(t, "no-command-with-auth", conf.Subscribe[0].Topic)
require.Equal(t, "", conf.Subscribe[0].Command)
require.Equal(t, "phil", *conf.Subscribe[0].User)
require.Equal(t, "phil", conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
}
func TestConfig_DefaultToken(t *testing.T) {
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(`
default-host: http://localhost
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
subscribe:
- topic: mytopic
`), 0600))
conf, err := client.LoadConfig(filename)
require.Nil(t, err)
require.Equal(t, "http://localhost", conf.DefaultHost)
require.Equal(t, "", conf.DefaultUser)
require.Nil(t, conf.DefaultPassword)
require.Equal(t, "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", conf.DefaultToken)
require.Equal(t, 1, len(conf.Subscribe))
require.Equal(t, "mytopic", conf.Subscribe[0].Topic)
require.Nil(t, conf.Subscribe[0].User)
require.Nil(t, conf.Subscribe[0].Password)
require.Nil(t, conf.Subscribe[0].Token)
}

View File

@@ -2,7 +2,7 @@ package client
import (
"fmt"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"net/http"
"strings"
"time"
@@ -72,17 +72,6 @@ func WithAttach(attach string) PublishOption {
return WithHeader("X-Attach", attach)
}
// WithMarkdown instructs the server to interpret the message body as Markdown
func WithMarkdown() PublishOption {
return WithHeader("X-Markdown", "yes")
}
// WithTemplate instructs the server to use a specific template for the message. If templateName is is "yes" or "1",
// the server will interpret the message and title as a template.
func WithTemplate(templateName string) PublishOption {
return WithHeader("X-Template", templateName)
}
// WithFilename sets a filename for the attachment, and/or forces the HTTP body to interpreted as an attachment
func WithFilename(filename string) PublishOption {
return WithHeader("X-Filename", filename)
@@ -103,11 +92,6 @@ func WithBearerAuth(token string) PublishOption {
return WithHeader("Authorization", fmt.Sprintf("Bearer %s", token))
}
// WithEmptyAuth clears the Authorization header
func WithEmptyAuth() PublishOption {
return RemoveHeader("Authorization")
}
// WithNoCache instructs the server not to cache the message server-side
func WithNoCache() PublishOption {
return WithHeader("X-Cache", "no")
@@ -198,13 +182,3 @@ func WithQueryParam(param, value string) RequestOption {
return nil
}
}
// RemoveHeader is a generic option to remove a header from a request
func RemoveHeader(header string) RequestOption {
return func(r *http.Request) error {
if header != "" {
delete(r.Header, header)
}
return nil
}
}

View File

@@ -1,10 +0,0 @@
[Unit]
Description=ntfy client
After=network.target
[Service]
ExecStart=/usr/bin/ntfy subscribe --config "%h/.config/ntfy/client.yml" --from-config
Restart=on-failure
[Install]
WantedBy=default.target

View File

@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
)
func init() {
@@ -105,10 +105,8 @@ func changeAccess(c *cli.Context, manager *user.Manager, username string, topic
return err
}
u, err := manager.User(username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
} else if u.Role == user.RoleAdmin {
return fmt.Errorf("user %s is an admin user, access control entries have no effect", username)
}
@@ -116,13 +114,13 @@ func changeAccess(c *cli.Context, manager *user.Manager, username string, topic
return err
}
if permission.IsReadWrite() {
fmt.Fprintf(c.App.Writer, "granted read-write access to topic %s\n\n", topic)
fmt.Fprintf(c.App.ErrWriter, "granted read-write access to topic %s\n\n", topic)
} else if permission.IsRead() {
fmt.Fprintf(c.App.Writer, "granted read-only access to topic %s\n\n", topic)
fmt.Fprintf(c.App.ErrWriter, "granted read-only access to topic %s\n\n", topic)
} else if permission.IsWrite() {
fmt.Fprintf(c.App.Writer, "granted write-only access to topic %s\n\n", topic)
fmt.Fprintf(c.App.ErrWriter, "granted write-only access to topic %s\n\n", topic)
} else {
fmt.Fprintf(c.App.Writer, "revoked all access to topic %s\n\n", topic)
fmt.Fprintf(c.App.ErrWriter, "revoked all access to topic %s\n\n", topic)
}
return showUserAccess(c, manager, username)
}
@@ -140,7 +138,7 @@ func resetAllAccess(c *cli.Context, manager *user.Manager) error {
if err := manager.ResetAccess("", ""); err != nil {
return err
}
fmt.Fprintln(c.App.Writer, "reset access for all users")
fmt.Fprintln(c.App.ErrWriter, "reset access for all users")
return nil
}
@@ -148,7 +146,7 @@ func resetUserAccess(c *cli.Context, manager *user.Manager, username string) err
if err := manager.ResetAccess(username, ""); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "reset access for user %s\n\n", username)
fmt.Fprintf(c.App.ErrWriter, "reset access for user %s\n\n", username)
return showUserAccess(c, manager, username)
}
@@ -156,7 +154,7 @@ func resetUserTopicAccess(c *cli.Context, manager *user.Manager, username string
if err := manager.ResetAccess(username, topic); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "reset access for user %s and topic %s\n\n", username, topic)
fmt.Fprintf(c.App.ErrWriter, "reset access for user %s and topic %s\n\n", username, topic)
return showUserAccess(c, manager, username)
}
@@ -177,7 +175,7 @@ func showAllAccess(c *cli.Context, manager *user.Manager) error {
func showUserAccess(c *cli.Context, manager *user.Manager, username string) error {
users, err := manager.User(username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
@@ -195,42 +193,34 @@ func showUsers(c *cli.Context, manager *user.Manager, users []*user.User) error
if u.Tier != nil {
tier = u.Tier.Name
}
provisioned := ""
if u.Provisioned {
provisioned = ", server config"
}
fmt.Fprintf(c.App.Writer, "user %s (role: %s, tier: %s%s)\n", u.Name, u.Role, tier, provisioned)
fmt.Fprintf(c.App.ErrWriter, "user %s (role: %s, tier: %s)\n", u.Name, u.Role, tier)
if u.Role == user.RoleAdmin {
fmt.Fprintf(c.App.Writer, "- read-write access to all topics (admin role)\n")
fmt.Fprintf(c.App.ErrWriter, "- read-write access to all topics (admin role)\n")
} else if len(grants) > 0 {
for _, grant := range grants {
grantProvisioned := ""
if grant.Provisioned {
grantProvisioned = " (server config)"
}
if grant.Permission.IsReadWrite() {
fmt.Fprintf(c.App.Writer, "- read-write access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
} else if grant.Permission.IsRead() {
fmt.Fprintf(c.App.Writer, "- read-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
} else if grant.Permission.IsWrite() {
fmt.Fprintf(c.App.Writer, "- write-only access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
if grant.Allow.IsReadWrite() {
fmt.Fprintf(c.App.ErrWriter, "- read-write access to topic %s\n", grant.TopicPattern)
} else if grant.Allow.IsRead() {
fmt.Fprintf(c.App.ErrWriter, "- read-only access to topic %s\n", grant.TopicPattern)
} else if grant.Allow.IsWrite() {
fmt.Fprintf(c.App.ErrWriter, "- write-only access to topic %s\n", grant.TopicPattern)
} else {
fmt.Fprintf(c.App.Writer, "- no access to topic %s%s\n", grant.TopicPattern, grantProvisioned)
fmt.Fprintf(c.App.ErrWriter, "- no access to topic %s\n", grant.TopicPattern)
}
}
} else {
fmt.Fprintf(c.App.Writer, "- no topic-specific permissions\n")
fmt.Fprintf(c.App.ErrWriter, "- no topic-specific permissions\n")
}
if u.Name == user.Everyone {
access := manager.DefaultAccess()
if access.IsReadWrite() {
fmt.Fprintln(c.App.Writer, "- read-write access to all (other) topics (server config)")
fmt.Fprintln(c.App.ErrWriter, "- read-write access to all (other) topics (server config)")
} else if access.IsRead() {
fmt.Fprintln(c.App.Writer, "- read-only access to all (other) topics (server config)")
fmt.Fprintln(c.App.ErrWriter, "- read-only access to all (other) topics (server config)")
} else if access.IsWrite() {
fmt.Fprintln(c.App.Writer, "- write-only access to all (other) topics (server config)")
fmt.Fprintln(c.App.ErrWriter, "- write-only access to all (other) topics (server config)")
} else {
fmt.Fprintln(c.App.Writer, "- no access to any (other) topics (server config)")
fmt.Fprintln(c.App.ErrWriter, "- no access to any (other) topics (server config)")
}
}
}

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)
@@ -13,9 +13,9 @@ func TestCLI_Access_Show(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, _, stdout, _ := newTestApp()
app, _, _, stderr := newTestApp()
require.Nil(t, runAccessCommand(app, conf))
require.Contains(t, stdout.String(), "user * (role: anonymous, tier: none)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
require.Contains(t, stderr.String(), "user * (role: anonymous, tier: none)\n- no topic-specific permissions\n- no access to any (other) topics (server config)")
}
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
@@ -30,7 +30,7 @@ func TestCLI_Access_Grant_And_Publish(t *testing.T) {
require.Nil(t, runAccessCommand(app, conf, "ben", "sometopic", "read"))
require.Nil(t, runAccessCommand(app, conf, "everyone", "announcements", "read"))
app, _, stdout, _ := newTestApp()
app, _, _, stderr := newTestApp()
require.Nil(t, runAccessCommand(app, conf))
expected := `user phil (role: admin, tier: none)
- read-write access to all topics (admin role)
@@ -41,7 +41,7 @@ user * (role: anonymous, tier: none)
- read-only access to topic announcements
- no access to any (other) topics (server config)
`
require.Equal(t, expected, stdout.String())
require.Equal(t, expected, stderr.String())
// See if access permissions match
app, _, _, _ = newTestApp()

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/log"
"os"
"regexp"
)

View File

@@ -4,8 +4,8 @@ import (
"bytes"
"encoding/json"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"os"
"strings"
"testing"

View File

@@ -5,7 +5,7 @@ import (
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"gopkg.in/yaml.v2"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
"os"
)

View File

@@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"io"
"os"
"os/exec"
@@ -31,8 +31,6 @@ var flagsPublish = append(
&cli.StringFlag{Name: "icon", Aliases: []string{"i"}, EnvVars: []string{"NTFY_ICON"}, Usage: "URL to use as notification icon"},
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
&cli.BoolFlag{Name: "markdown", Aliases: []string{"md"}, EnvVars: []string{"NTFY_MARKDOWN"}, Usage: "Message is formatted as Markdown"},
&cli.StringFlag{Name: "template", Aliases: []string{"tpl"}, EnvVars: []string{"NTFY_TEMPLATE"}, Usage: "use templates to transform JSON message body"},
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
&cli.StringFlag{Name: "email", Aliases: []string{"mail", "e"}, EnvVars: []string{"NTFY_EMAIL"}, Usage: "also send to e-mail address"},
@@ -42,6 +40,7 @@ var flagsPublish = append(
&cli.BoolFlag{Name: "wait-cmd", Aliases: []string{"wait_cmd", "cmd", "done"}, EnvVars: []string{"NTFY_WAIT_CMD"}, Usage: "run command and wait until it finishes before publishing"},
&cli.BoolFlag{Name: "no-cache", Aliases: []string{"no_cache", "C"}, EnvVars: []string{"NTFY_NO_CACHE"}, Usage: "do not cache message server-side"},
&cli.BoolFlag{Name: "no-firebase", Aliases: []string{"no_firebase", "F"}, EnvVars: []string{"NTFY_NO_FIREBASE"}, Usage: "do not forward message to Firebase"},
&cli.BoolFlag{Name: "env-topic", Aliases: []string{"env_topic", "P"}, EnvVars: []string{"NTFY_ENV_TOPIC"}, Usage: "use topic from NTFY_TOPIC env variable"},
&cli.BoolFlag{Name: "quiet", Aliases: []string{"q"}, EnvVars: []string{"NTFY_QUIET"}, Usage: "do not print message"},
)
@@ -70,7 +69,6 @@ Examples:
ntfy pub --icon="http://some.tld/icon.png" 'Icon!' # Send notification with custom icon
ntfy pub --attach="http://some.tld/file.zip" files # Send ZIP archive from URL as attachment
ntfy pub --file=flower.jpg flowers 'Nice!' # Send image.jpg as attachment
echo 'message' | ntfy publish mytopic # Send message from stdin
ntfy pub -u phil:mypass secret Psst # Publish with username/password
ntfy pub --wait-pid 1234 mytopic # Wait for process 1234 to exit before publishing
ntfy pub --wait-cmd mytopic rsync -av ./ /tmp/a # Run command and publish after it completes
@@ -98,8 +96,6 @@ func execPublish(c *cli.Context) error {
icon := c.String("icon")
actions := c.String("actions")
attach := c.String("attach")
markdown := c.Bool("markdown")
template := c.String("template")
filename := c.String("filename")
file := c.String("file")
email := c.String("email")
@@ -145,12 +141,6 @@ func execPublish(c *cli.Context) error {
if attach != "" {
options = append(options, client.WithAttach(attach))
}
if markdown {
options = append(options, client.WithMarkdown())
}
if template != "" {
options = append(options, client.WithTemplate(template))
}
if filename != "" {
options = append(options, client.WithFilename(filename))
}
@@ -165,7 +155,8 @@ func execPublish(c *cli.Context) error {
}
if token != "" {
options = append(options, client.WithBearerAuth(token))
} else if user != "" {
}
if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
@@ -181,8 +172,6 @@ func execPublish(c *cli.Context) error {
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
}
options = append(options, client.WithBasicAuth(user, pass))
} else if conf.DefaultToken != "" {
options = append(options, client.WithBearerAuth(conf.DefaultToken))
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
}
@@ -260,15 +249,6 @@ func parseTopicMessageCommand(c *cli.Context) (topic string, message string, com
if c.String("message") != "" {
message = c.String("message")
}
if message == "" && isStdinRedirected() {
var data []byte
data, err = io.ReadAll(io.LimitReader(c.App.Reader, 1024*1024))
if err != nil {
log.Debug("Failed to read from stdin: %s", err.Error())
return
}
message = strings.TrimSpace(string(data))
}
return
}
@@ -327,12 +307,3 @@ func runAndWaitForCommand(command []string) (message string, err error) {
log.Debug("Command succeeded after %s: %s", runtime, prettyCmd)
return fmt.Sprintf("Command succeeded after %s: %s", runtime, prettyCmd), nil
}
func isStdinRedirected() bool {
stat, err := os.Stdin.Stat()
if err != nil {
log.Debug("Failed to stat stdin: %s", err.Error())
return false
}
return (stat.Mode() & os.ModeCharDevice) == 0
}

View File

@@ -3,13 +3,10 @@ package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/v2/util"
"net/http"
"net/http/httptest"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
@@ -133,11 +130,11 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
require.Equal(t, `command failed: does-not-exist-no-really "really though", error: exec: "does-not-exist-no-really": executable file not found in $PATH`, err.Error())
// Tests with NTFY_TOPIC set ////
t.Setenv("NTFY_TOPIC", topic)
require.Nil(t, os.Setenv("NTFY_TOPIC", topic))
// Test: Successful command with NTFY_TOPIC
app, _, stdout, _ = newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--cmd", "echo", "hi there"}))
require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--cmd", "echo", "hi there"}))
m = toMessage(t, stdout.String())
require.Equal(t, "mytopic", m.Topic)
@@ -146,155 +143,7 @@ func TestCLI_Publish_Wait_PID_And_Cmd(t *testing.T) {
require.Nil(t, sleep.Start())
go sleep.Wait() // Must be called to release resources
app, _, stdout, _ = newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
require.Nil(t, app.Run([]string{"ntfy", "publish", "--env-topic", "--wait-pid", strconv.Itoa(sleep.Process.Pid)}))
m = toMessage(t, stdout.String())
require.Regexp(t, `Process with PID \d+ exited after .+ms`, m.Message)
}
func TestCLI_Publish_Default_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
m := toMessage(t, stdout.String())
require.Equal(t, "triggered", m.Message)
}
func TestCLI_Publish_Default_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "mytopic", "triggered"}))
m := toMessage(t, stdout.String())
require.Equal(t, "triggered", m.Message)
}
func TestCLI_Publish_Default_UserPass_CLI_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
m := toMessage(t, stdout.String())
require.Equal(t, "triggered", m.Message)
}
func TestCLI_Publish_Default_Token_CLI_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
m := toMessage(t, stdout.String())
require.Equal(t, "triggered", m.Message)
}
func TestCLI_Publish_Default_Token_CLI_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_FAKETOKEN01234567890FAKETOKEN
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic", "triggered"}))
m := toMessage(t, stdout.String())
require.Equal(t, "triggered", m.Message)
}
func TestCLI_Publish_Default_UserPass_CLI_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: fakepass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "publish", "--config=" + filename, "--user", "philipp:mypass", "mytopic", "triggered"}))
m := toMessage(t, stdout.String())
require.Equal(t, "triggered", m.Message)
}
func TestCLI_Publish_Token_And_UserPass(t *testing.T) {
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "publish", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
require.Error(t, err)
require.Equal(t, "cannot set both --user and --token", err.Error())
}

View File

@@ -5,67 +5,65 @@ package cmd
import (
"errors"
"fmt"
"github.com/stripe/stripe-go/v74"
"heckel.io/ntfy/user"
"io/fs"
"math"
"net"
"net/netip"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
"heckel.io/ntfy/log"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/payments"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/server"
"heckel.io/ntfy/util"
)
func init() {
commands = append(commands, cmdServe)
}
const (
defaultServerConfigFile = "/etc/ntfy/server.yml"
)
var flagsServe = append(
append([]cli.Flag{}, flagsDefault...),
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, Usage: "config file"},
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "base-url", Aliases: []string{"base_url", "B"}, EnvVars: []string{"NTFY_BASE_URL"}, Usage: "externally visible base URL for this host (e.g. https://ntfy.sh)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used as HTTP listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used as HTTPS listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-http", Aliases: []string{"listen_http", "l"}, EnvVars: []string{"NTFY_LISTEN_HTTP"}, Value: server.DefaultListenHTTP, Usage: "ip:port used to as HTTP listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-https", Aliases: []string{"listen_https", "L"}, EnvVars: []string{"NTFY_LISTEN_HTTPS"}, Usage: "ip:port used to as HTTPS listen address"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "listen-unix", Aliases: []string{"listen_unix", "U"}, EnvVars: []string{"NTFY_LISTEN_UNIX"}, Usage: "listen on unix socket path"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "listen-unix-mode", Aliases: []string{"listen_unix_mode"}, EnvVars: []string{"NTFY_LISTEN_UNIX_MODE"}, DefaultText: "system default", Usage: "file permissions of unix socket, e.g. 0700"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "key-file", Aliases: []string{"key_file", "K"}, EnvVars: []string{"NTFY_KEY_FILE"}, Usage: "private key file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cert-file", Aliases: []string{"cert_file", "E"}, EnvVars: []string{"NTFY_CERT_FILE"}, Usage: "certificate file, if listen-https is set"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "firebase-key-file", Aliases: []string{"firebase_key_file", "F"}, EnvVars: []string{"NTFY_FIREBASE_KEY_FILE"}, Usage: "Firebase credentials file; if set additionally publish to FCM topic"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-file", Aliases: []string{"cache_file", "C"}, EnvVars: []string{"NTFY_CACHE_FILE"}, Usage: "cache file used for message caching"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: util.FormatDuration(server.DefaultCacheDuration), Usage: "buffer messages for this time to allow `since` requests"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-duration", Aliases: []string{"cache_duration", "b"}, EnvVars: []string{"NTFY_CACHE_DURATION"}, Value: server.DefaultCacheDuration, Usage: "buffer messages for this time to allow `since` requests"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "cache-batch-size", Aliases: []string{"cache_batch_size"}, EnvVars: []string{"NTFY_BATCH_SIZE"}, Usage: "max size of messages to batch together when writing to message cache (if zero, writes are synchronous)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Value: util.FormatDuration(server.DefaultCacheBatchTimeout), Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "cache-batch-timeout", Aliases: []string{"cache_batch_timeout"}, EnvVars: []string{"NTFY_CACHE_BATCH_TIMEOUT"}, Usage: "timeout for batched async writes to the message cache (if zero, writes are synchronous)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "cache-startup-queries", Aliases: []string{"cache_startup_queries"}, EnvVars: []string{"NTFY_CACHE_STARTUP_QUERIES"}, Usage: "queries run when the cache database is initialized"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-startup-queries", Aliases: []string{"auth_startup_queries"}, EnvVars: []string{"NTFY_AUTH_STARTUP_QUERIES"}, Usage: "queries run when the auth database is initialized"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-users", Aliases: []string{"auth_users"}, EnvVars: []string{"NTFY_AUTH_USERS"}, Usage: "pre-provisioned declarative users"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-access", Aliases: []string{"auth_access"}, EnvVars: []string{"NTFY_AUTH_ACCESS"}, Usage: "pre-provisioned declarative access control entries"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "auth-tokens", Aliases: []string{"auth_tokens"}, EnvVars: []string{"NTFY_AUTH_TOKENS"}, Usage: "pre-provisioned declarative access tokens"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-cache-dir", Aliases: []string{"attachment_cache_dir"}, EnvVars: []string{"NTFY_ATTACHMENT_CACHE_DIR"}, Usage: "cache directory for attached files"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentTotalSizeLimit), Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultAttachmentFileSizeLimit), Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultAttachmentExpiryDuration), Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "template-dir", Aliases: []string{"template_dir"}, EnvVars: []string{"NTFY_TEMPLATE_DIR"}, Value: server.DefaultTemplateDir, Usage: "directory to load named message templates from"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: util.FormatDuration(server.DefaultKeepaliveInterval), Usage: "interval of keepalive messages"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: util.FormatDuration(server.DefaultManagerInterval), Usage: "interval of for message pruning and stats printing"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-total-size-limit", Aliases: []string{"attachment_total_size_limit", "A"}, EnvVars: []string{"NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT"}, DefaultText: "5G", Usage: "limit of the on-disk attachment cache"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "attachment-file-size-limit", Aliases: []string{"attachment_file_size_limit", "Y"}, EnvVars: []string{"NTFY_ATTACHMENT_FILE_SIZE_LIMIT"}, DefaultText: "15M", Usage: "per-file attachment size limit (e.g. 300k, 2M, 100M)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"attachment_expiry_duration", "X"}, EnvVars: []string{"NTFY_ATTACHMENT_EXPIRY_DURATION"}, Value: server.DefaultAttachmentExpiryDuration, DefaultText: "3h", Usage: "duration after which uploaded attachments will be deleted (e.g. 3h, 20h)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "keepalive-interval", Aliases: []string{"keepalive_interval", "k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"manager_interval", "m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
altsrc.NewStringSliceFlag(&cli.StringSliceFlag{Name: "disallowed-topics", Aliases: []string{"disallowed_topics"}, EnvVars: []string{"NTFY_DISALLOWED_TOPICS"}, Usage: "topics that are not allowed to be used"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "/", Usage: "sets root of the web app (e.g. /, or /app), or disables it (disable)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", Aliases: []string{"web_root"}, EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-signup", Aliases: []string{"enable_signup"}, EnvVars: []string{"NTFY_ENABLE_SIGNUP"}, Value: false, Usage: "allows users to sign up via the web app, or API"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-login", Aliases: []string{"enable_login"}, EnvVars: []string{"NTFY_ENABLE_LOGIN"}, Value: false, Usage: "allows users to log in via the web app, or API"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-reservations", Aliases: []string{"enable_reservations"}, EnvVars: []string{"NTFY_ENABLE_RESERVATIONS"}, Value: false, Usage: "allows users to reserve topics (if their tier allows it)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "require-login", Aliases: []string{"require_login"}, EnvVars: []string{"NTFY_REQUIRE_LOGIN"}, Value: false, Usage: "all actions via the web app requires a login"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-base-url", Aliases: []string{"upstream_base_url"}, EnvVars: []string{"NTFY_UPSTREAM_BASE_URL"}, Value: "", Usage: "forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "upstream-access-token", Aliases: []string{"upstream_access_token"}, EnvVars: []string{"NTFY_UPSTREAM_ACCESS_TOKEN"}, Value: "", Usage: "access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", Aliases: []string{"smtp_sender_addr"}, EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", Aliases: []string{"smtp_sender_user"}, EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", Aliases: []string{"smtp_sender_pass"}, EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
@@ -73,41 +71,19 @@ var flagsServe = append(
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-listen", Aliases: []string{"smtp_server_listen"}, EnvVars: []string{"NTFY_SMTP_SERVER_LISTEN"}, Usage: "SMTP server address (ip:port) for incoming emails, e.g. :25"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-domain", Aliases: []string{"smtp_server_domain"}, EnvVars: []string{"NTFY_SMTP_SERVER_DOMAIN"}, Usage: "SMTP domain for incoming e-mail, e.g. ntfy.sh"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-server-addr-prefix", Aliases: []string{"smtp_server_addr_prefix"}, EnvVars: []string{"NTFY_SMTP_SERVER_ADDR_PREFIX"}, Usage: "SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-')"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-account", Aliases: []string{"twilio_account"}, EnvVars: []string{"NTFY_TWILIO_ACCOUNT"}, Usage: "Twilio account SID, used for phone calls, e.g. AC123..."}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-auth-token", Aliases: []string{"twilio_auth_token"}, EnvVars: []string{"NTFY_TWILIO_AUTH_TOKEN"}, Usage: "Twilio auth token"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-phone-number", Aliases: []string{"twilio_phone_number"}, EnvVars: []string{"NTFY_TWILIO_PHONE_NUMBER"}, Usage: "Twilio number to use for outgoing calls"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "twilio-verify-service", Aliases: []string{"twilio_verify_service"}, EnvVars: []string{"NTFY_TWILIO_VERIFY_SERVICE"}, Usage: "Twilio Verify service ID, used for phone number verification"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-size-limit", Aliases: []string{"message_size_limit"}, EnvVars: []string{"NTFY_MESSAGE_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultMessageSizeLimit), Usage: "size limit for the message (see docs for limitations)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "message-delay-limit", Aliases: []string{"message_delay_limit"}, EnvVars: []string{"NTFY_MESSAGE_DELAY_LIMIT"}, Value: util.FormatDuration(server.DefaultMessageDelayMax), Usage: "max duration a message can be scheduled into the future"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "global-topic-limit", Aliases: []string{"global_topic_limit", "T"}, EnvVars: []string{"NTFY_GLOBAL_TOPIC_LIMIT"}, Value: server.DefaultTotalTopicLimit, Usage: "total number of topics allowed"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-subscription-limit", Aliases: []string{"visitor_subscription_limit"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIPTION_LIMIT"}, Value: server.DefaultVisitorSubscriptionLimit, Usage: "number of subscriptions per visitor"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "visitor-subscriber-rate-limiting", Aliases: []string{"visitor_subscriber_rate_limiting"}, EnvVars: []string{"NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING"}, Value: false, Usage: "enables subscriber-based rate limiting"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: util.FormatSize(server.DefaultVisitorAttachmentTotalSizeLimit), Usage: "total storage limit used for attachments per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-total-size-limit", Aliases: []string{"visitor_attachment_total_size_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT"}, Value: "100M", Usage: "total storage limit used for attachments per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-attachment-daily-bandwidth-limit", Aliases: []string{"visitor_attachment_daily_bandwidth_limit"}, EnvVars: []string{"NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT"}, Value: "500M", Usage: "total daily attachment download/upload bandwidth limit per visitor"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-request-limit-burst", Aliases: []string{"visitor_request_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_BURST"}, Value: server.DefaultVisitorRequestLimitBurst, Usage: "initial limit of requests per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorRequestLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-request-limit-replenish", Aliases: []string{"visitor_request_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_REPLENISH"}, Value: server.DefaultVisitorRequestLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-request-limit-exempt-hosts", Aliases: []string{"visitor_request_limit_exempt_hosts"}, EnvVars: []string{"NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS"}, Value: "", Usage: "hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-message-daily-limit", Aliases: []string{"visitor_message_daily_limit"}, EnvVars: []string{"NTFY_VISITOR_MESSAGE_DAILY_LIMIT"}, Value: server.DefaultVisitorMessageDailyLimit, Usage: "max messages per visitor per day, derived from request limit if unset"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-email-limit-burst", Aliases: []string{"visitor_email_limit_burst"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_BURST"}, Value: server.DefaultVisitorEmailLimitBurst, Usage: "initial limit of e-mails per visitor"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: util.FormatDuration(server.DefaultVisitorEmailLimitReplenish), Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv4", Aliases: []string{"visitor_prefix_bits_ipv4"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV4"}, Value: server.DefaultVisitorPrefixBitsIPv4, Usage: "number of bits of the IPv4 address to use for rate limiting (default: 32, full address)"}),
altsrc.NewIntFlag(&cli.IntFlag{Name: "visitor-prefix-bits-ipv6", Aliases: []string{"visitor_prefix_bits_ipv6"}, EnvVars: []string{"NTFY_VISITOR_PREFIX_BITS_IPV6"}, Value: server.DefaultVisitorPrefixBitsIPv6, Usage: "number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-forwarded-header", Aliases: []string{"proxy_forwarded_header"}, EnvVars: []string{"NTFY_PROXY_FORWARDED_HEADER"}, Value: "X-Forwarded-For", Usage: "use specified header to determine visitor IP address (for rate limiting)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "proxy-trusted-hosts", Aliases: []string{"proxy_trusted_hosts"}, EnvVars: []string{"NTFY_PROXY_TRUSTED_HOSTS"}, Value: "", Usage: "comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header"}),
altsrc.NewDurationFlag(&cli.DurationFlag{Name: "visitor-email-limit-replenish", Aliases: []string{"visitor_email_limit_replenish"}, EnvVars: []string{"NTFY_VISITOR_EMAIL_LIMIT_REPLENISH"}, Value: server.DefaultVisitorEmailLimitReplenish, Usage: "interval at which burst limit is replenished (one per x)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "behind-proxy", Aliases: []string{"behind_proxy", "P"}, EnvVars: []string{"NTFY_BEHIND_PROXY"}, Value: false, Usage: "if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-secret-key", Aliases: []string{"stripe_secret_key"}, EnvVars: []string{"NTFY_STRIPE_SECRET_KEY"}, Value: "", Usage: "key used for the Stripe API communication, this enables payments"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "stripe-webhook-key", Aliases: []string{"stripe_webhook_key"}, EnvVars: []string{"NTFY_STRIPE_WEBHOOK_KEY"}, Value: "", Usage: "key required to validate the authenticity of incoming webhooks from Stripe"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "billing-contact", Aliases: []string{"billing_contact"}, EnvVars: []string{"NTFY_BILLING_CONTACT"}, Value: "", Usage: "e-mail or website to display in upgrade dialog (only if payments are enabled)"}),
altsrc.NewBoolFlag(&cli.BoolFlag{Name: "enable-metrics", Aliases: []string{"enable_metrics"}, EnvVars: []string{"NTFY_ENABLE_METRICS"}, Value: false, Usage: "if set, Prometheus metrics are exposed via the /metrics endpoint"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "metrics-listen-http", Aliases: []string{"metrics_listen_http"}, EnvVars: []string{"NTFY_METRICS_LISTEN_HTTP"}, Usage: "ip:port used to expose the metrics endpoint (implicitly enables metrics)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "profile-listen-http", Aliases: []string{"profile_listen_http"}, EnvVars: []string{"NTFY_PROFILE_LISTEN_HTTP"}, Usage: "ip:port used to expose the profiling endpoints (implicitly enables profiling)"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-public-key", Aliases: []string{"web_push_public_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PUBLIC_KEY"}, Usage: "public key used for web push notifications"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-private-key", Aliases: []string{"web_push_private_key"}, EnvVars: []string{"NTFY_WEB_PUSH_PRIVATE_KEY"}, Usage: "private key used for web push notifications"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-file", Aliases: []string{"web_push_file"}, EnvVars: []string{"NTFY_WEB_PUSH_FILE"}, Usage: "file used to store web push subscriptions"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-email-address", Aliases: []string{"web_push_email_address"}, EnvVars: []string{"NTFY_WEB_PUSH_EMAIL_ADDRESS"}, Usage: "e-mail address of sender, required to use browser push services"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-startup-queries", Aliases: []string{"web_push_startup_queries"}, EnvVars: []string{"NTFY_WEB_PUSH_STARTUP_QUERIES"}, Usage: "queries run when the web push database is initialized"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-duration", Aliases: []string{"web_push_expiry_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryDuration), Usage: "automatically expire unused subscriptions after this time"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "web-push-expiry-warning-duration", Aliases: []string{"web_push_expiry_warning_duration"}, EnvVars: []string{"NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION"}, Value: util.FormatDuration(server.DefaultWebPushExpiryWarningDuration), Usage: "send web push warning notification after this time before expiring unused subscriptions"}),
)
var cmdServe = &cli.Command{
@@ -135,7 +111,7 @@ func execServe(c *cli.Context) error {
// Read all the options
config := c.String("config")
baseURL := strings.TrimSuffix(c.String("base-url"), "/")
baseURL := c.String("base-url")
listenHTTP := c.String("listen-http")
listenHTTPS := c.String("listen-https")
listenUnix := c.String("listen-unix")
@@ -143,39 +119,26 @@ func execServe(c *cli.Context) error {
keyFile := c.String("key-file")
certFile := c.String("cert-file")
firebaseKeyFile := c.String("firebase-key-file")
webPushPrivateKey := c.String("web-push-private-key")
webPushPublicKey := c.String("web-push-public-key")
webPushFile := c.String("web-push-file")
webPushEmailAddress := c.String("web-push-email-address")
webPushStartupQueries := c.String("web-push-startup-queries")
webPushExpiryDurationStr := c.String("web-push-expiry-duration")
webPushExpiryWarningDurationStr := c.String("web-push-expiry-warning-duration")
cacheFile := c.String("cache-file")
cacheDurationStr := c.String("cache-duration")
cacheDuration := c.Duration("cache-duration")
cacheStartupQueries := c.String("cache-startup-queries")
cacheBatchSize := c.Int("cache-batch-size")
cacheBatchTimeoutStr := c.String("cache-batch-timeout")
cacheBatchTimeout := c.Duration("cache-batch-timeout")
authFile := c.String("auth-file")
authStartupQueries := c.String("auth-startup-queries")
authDefaultAccess := c.String("auth-default-access")
authUsersRaw := c.StringSlice("auth-users")
authAccessRaw := c.StringSlice("auth-access")
authTokensRaw := c.StringSlice("auth-tokens")
attachmentCacheDir := c.String("attachment-cache-dir")
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
attachmentExpiryDurationStr := c.String("attachment-expiry-duration")
templateDir := c.String("template-dir")
keepaliveIntervalStr := c.String("keepalive-interval")
managerIntervalStr := c.String("manager-interval")
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
keepaliveInterval := c.Duration("keepalive-interval")
managerInterval := c.Duration("manager-interval")
disallowedTopics := c.StringSlice("disallowed-topics")
webRoot := c.String("web-root")
enableSignup := c.Bool("enable-signup")
enableLogin := c.Bool("enable-login")
requireLogin := c.Bool("require-login")
enableReservations := c.Bool("enable-reservations")
upstreamBaseURL := c.String("upstream-base-url")
upstreamAccessToken := c.String("upstream-access-token")
smtpSenderAddr := c.String("smtp-sender-addr")
smtpSenderUser := c.String("smtp-sender-user")
smtpSenderPass := c.String("smtp-sender-pass")
@@ -183,108 +146,23 @@ func execServe(c *cli.Context) error {
smtpServerListen := c.String("smtp-server-listen")
smtpServerDomain := c.String("smtp-server-domain")
smtpServerAddrPrefix := c.String("smtp-server-addr-prefix")
twilioAccount := c.String("twilio-account")
twilioAuthToken := c.String("twilio-auth-token")
twilioPhoneNumber := c.String("twilio-phone-number")
twilioVerifyService := c.String("twilio-verify-service")
messageSizeLimitStr := c.String("message-size-limit")
messageDelayLimitStr := c.String("message-delay-limit")
totalTopicLimit := c.Int("global-topic-limit")
visitorSubscriptionLimit := c.Int("visitor-subscription-limit")
visitorSubscriberRateLimiting := c.Bool("visitor-subscriber-rate-limiting")
visitorAttachmentTotalSizeLimitStr := c.String("visitor-attachment-total-size-limit")
visitorAttachmentDailyBandwidthLimitStr := c.String("visitor-attachment-daily-bandwidth-limit")
visitorRequestLimitBurst := c.Int("visitor-request-limit-burst")
visitorRequestLimitReplenishStr := c.String("visitor-request-limit-replenish")
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
visitorRequestLimitExemptHosts := util.SplitNoEmpty(c.String("visitor-request-limit-exempt-hosts"), ",")
visitorMessageDailyLimit := c.Int("visitor-message-daily-limit")
visitorEmailLimitBurst := c.Int("visitor-email-limit-burst")
visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
visitorPrefixBitsIPv4 := c.Int("visitor-prefix-bits-ipv4")
visitorPrefixBitsIPv6 := c.Int("visitor-prefix-bits-ipv6")
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
behindProxy := c.Bool("behind-proxy")
proxyForwardedHeader := c.String("proxy-forwarded-header")
proxyTrustedHosts := util.SplitNoEmpty(c.String("proxy-trusted-hosts"), ",")
stripeSecretKey := c.String("stripe-secret-key")
stripeWebhookKey := c.String("stripe-webhook-key")
billingContact := c.String("billing-contact")
metricsListenHTTP := c.String("metrics-listen-http")
enableMetrics := c.Bool("enable-metrics") || metricsListenHTTP != ""
profileListenHTTP := c.String("profile-listen-http")
// Convert durations
cacheDuration, err := util.ParseDuration(cacheDurationStr)
if err != nil {
return fmt.Errorf("invalid cache duration: %s", cacheDurationStr)
}
cacheBatchTimeout, err := util.ParseDuration(cacheBatchTimeoutStr)
if err != nil {
return fmt.Errorf("invalid cache batch timeout: %s", cacheBatchTimeoutStr)
}
attachmentExpiryDuration, err := util.ParseDuration(attachmentExpiryDurationStr)
if err != nil {
return fmt.Errorf("invalid attachment expiry duration: %s", attachmentExpiryDurationStr)
}
keepaliveInterval, err := util.ParseDuration(keepaliveIntervalStr)
if err != nil {
return fmt.Errorf("invalid keepalive interval: %s", keepaliveIntervalStr)
}
managerInterval, err := util.ParseDuration(managerIntervalStr)
if err != nil {
return fmt.Errorf("invalid manager interval: %s", managerIntervalStr)
}
messageDelayLimit, err := util.ParseDuration(messageDelayLimitStr)
if err != nil {
return fmt.Errorf("invalid message delay limit: %s", messageDelayLimitStr)
}
visitorRequestLimitReplenish, err := util.ParseDuration(visitorRequestLimitReplenishStr)
if err != nil {
return fmt.Errorf("invalid visitor request limit replenish: %s", visitorRequestLimitReplenishStr)
}
visitorEmailLimitReplenish, err := util.ParseDuration(visitorEmailLimitReplenishStr)
if err != nil {
return fmt.Errorf("invalid visitor email limit replenish: %s", visitorEmailLimitReplenishStr)
}
webPushExpiryDuration, err := util.ParseDuration(webPushExpiryDurationStr)
if err != nil {
return fmt.Errorf("invalid web push expiry duration: %s", webPushExpiryDurationStr)
}
webPushExpiryWarningDuration, err := util.ParseDuration(webPushExpiryWarningDurationStr)
if err != nil {
return fmt.Errorf("invalid web push expiry warning duration: %s", webPushExpiryWarningDurationStr)
}
// Convert sizes to bytes
messageSizeLimit, err := util.ParseSize(messageSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid message size limit: %s", messageSizeLimitStr)
}
attachmentTotalSizeLimit, err := util.ParseSize(attachmentTotalSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid attachment total size limit: %s", attachmentTotalSizeLimitStr)
}
attachmentFileSizeLimit, err := util.ParseSize(attachmentFileSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid attachment file size limit: %s", attachmentFileSizeLimitStr)
}
visitorAttachmentTotalSizeLimit, err := util.ParseSize(visitorAttachmentTotalSizeLimitStr)
if err != nil {
return fmt.Errorf("invalid visitor attachment total size limit: %s", visitorAttachmentTotalSizeLimitStr)
}
visitorAttachmentDailyBandwidthLimit, err := util.ParseSize(visitorAttachmentDailyBandwidthLimitStr)
if err != nil {
return fmt.Errorf("invalid visitor attachment daily bandwidth limit: %s", visitorAttachmentDailyBandwidthLimitStr)
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
}
// Check values
if firebaseKeyFile != "" && !util.FileExists(firebaseKeyFile) {
return errors.New("if set, FCM key file must exist")
} else if firebaseKeyFile != "" && !server.FirebaseAvailable {
return errors.New("cannot set firebase-key-file, support for Firebase is not available (nofirebase)")
} else if webPushPublicKey != "" && (webPushPrivateKey == "" || webPushFile == "" || webPushEmailAddress == "" || baseURL == "") {
return errors.New("if web push is enabled, web-push-private-key, web-push-public-key, web-push-file, web-push-email-address, and base-url should be set. run 'ntfy webpush keys' to generate keys")
} else if keepaliveInterval < 5*time.Second {
return errors.New("keepalive interval cannot be lower than five seconds")
} else if managerInterval < 5*time.Second {
@@ -297,21 +175,18 @@ func execServe(c *cli.Context) error {
return errors.New("if set, certificate file must exist")
} else if listenHTTPS != "" && (keyFile == "" || certFile == "") {
return errors.New("if listen-https is set, both key-file and cert-file must be set")
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderFrom == "") {
return errors.New("if smtp-sender-addr is set, base-url, and smtp-sender-from must also be set")
} else if smtpSenderAddr != "" && (baseURL == "" || smtpSenderUser == "" || smtpSenderPass == "" || smtpSenderFrom == "") {
return errors.New("if smtp-sender-addr is set, base-url, smtp-sender-user, smtp-sender-pass and smtp-sender-from must also be set")
} else if smtpServerListen != "" && smtpServerDomain == "" {
return errors.New("if smtp-server-listen is set, smtp-server-domain must also be set")
} else if attachmentCacheDir != "" && baseURL == "" {
return errors.New("if attachment-cache-dir is set, base-url must also be set")
} else if baseURL != "" {
u, err := url.Parse(baseURL)
if err != nil {
return fmt.Errorf("if set, base-url must be a valid URL, e.g. https://ntfy.mydomain.com: %v", err)
} else if u.Scheme != "http" && u.Scheme != "https" {
return errors.New("if set, base-url must be a valid URL starting with http:// or https://, e.g. https://ntfy.mydomain.com")
} else if u.Path != "" {
return fmt.Errorf("if set, base-url must not have a path (%s), as hosting ntfy on a sub-path is not supported, e.g. https://ntfy.mydomain.com", u.Path)
}
} else if baseURL != "" && !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
return errors.New("if set, base-url must start with http:// or https://")
} else if baseURL != "" && strings.HasSuffix(baseURL, "/") {
return errors.New("if set, base-url must not end with a slash (/)")
} else if !util.Contains([]string{"app", "home", "disable"}, webRoot) {
return errors.New("if set, web-root must be 'home' or 'app'")
} else if upstreamBaseURL != "" && !strings.HasPrefix(upstreamBaseURL, "http://") && !strings.HasPrefix(upstreamBaseURL, "https://") {
return errors.New("if set, upstream-base-url must start with http:// or https://")
} else if upstreamBaseURL != "" && strings.HasSuffix(upstreamBaseURL, "/") {
@@ -320,93 +195,62 @@ func execServe(c *cli.Context) error {
return errors.New("if upstream-base-url is set, base-url must also be set")
} else if upstreamBaseURL != "" && baseURL != "" && baseURL == upstreamBaseURL {
return errors.New("base-url and upstream-base-url cannot be identical, you'll likely want to set upstream-base-url to https://ntfy.sh, see https://ntfy.sh/docs/config/#ios-instant-notifications")
} else if authFile == "" && (enableSignup || enableLogin || requireLogin || enableReservations || stripeSecretKey != "") {
return errors.New("cannot set enable-signup, enable-login, require-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
} else if authFile == "" && (enableSignup || enableLogin || enableReservations || stripeSecretKey != "") {
return errors.New("cannot set enable-signup, enable-login, enable-reserve-topics, or stripe-secret-key if auth-file is not set")
} else if enableSignup && !enableLogin {
return errors.New("cannot set enable-signup without also setting enable-login")
} else if requireLogin && !enableLogin {
return errors.New("cannot set require-login without also setting enable-login")
} else if !payments.Available && (stripeSecretKey != "" || stripeWebhookKey != "") {
return errors.New("cannot set stripe-secret-key or stripe-webhook-key, support for payments is not available in this build (nopayments)")
} else if stripeSecretKey != "" && (stripeWebhookKey == "" || baseURL == "") {
return errors.New("if stripe-secret-key is set, stripe-webhook-key and base-url must also be set")
} else if twilioAccount != "" && (twilioAuthToken == "" || twilioPhoneNumber == "" || twilioVerifyService == "" || baseURL == "" || authFile == "") {
return errors.New("if twilio-account is set, twilio-auth-token, twilio-phone-number, twilio-verify-service, base-url, and auth-file must also be set")
} else if messageSizeLimit > server.DefaultMessageSizeLimit {
log.Warn("message-size-limit is greater than 4K, this is not recommended and largely untested, and may lead to issues with some clients")
if messageSizeLimit > 5*1024*1024 {
return errors.New("message-size-limit cannot be higher than 5M")
}
} else if !server.WebPushAvailable && (webPushPrivateKey != "" || webPushPublicKey != "" || webPushFile != "") {
return errors.New("cannot enable WebPush, support is not available in this build (nowebpush)")
} else if webPushExpiryWarningDuration > 0 && webPushExpiryWarningDuration > webPushExpiryDuration {
return errors.New("web push expiry warning duration cannot be higher than web push expiry duration")
} else if behindProxy && proxyForwardedHeader == "" {
return errors.New("if behind-proxy is set, proxy-forwarded-header must also be set")
} else if visitorPrefixBitsIPv4 < 1 || visitorPrefixBitsIPv4 > 32 {
return errors.New("visitor-prefix-bits-ipv4 must be between 1 and 32")
} else if visitorPrefixBitsIPv6 < 1 || visitorPrefixBitsIPv6 > 128 {
return errors.New("visitor-prefix-bits-ipv6 must be between 1 and 128")
}
// Backwards compatibility
if webRoot == "app" {
webRoot = "/"
} else if webRoot == "home" {
webRoot = "/app"
} else if webRoot == "disable" {
webRoot = ""
} else if !strings.HasPrefix(webRoot, "/") {
webRoot = "/" + webRoot
}
webRootIsApp := webRoot == "app"
enableWeb := webRoot != "disable"
// Convert default auth permission, read provisioned users
// Default auth permissions
authDefault, err := user.ParsePermission(authDefaultAccess)
if err != nil {
return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
authUsers, err := parseUsers(authUsersRaw)
if err != nil {
return err
}
authAccess, err := parseAccess(authUsers, authAccessRaw)
if err != nil {
return err
}
authTokens, err := parseTokens(authUsers, authTokensRaw)
if err != nil {
return err
}
// Special case: Unset default
if listenHTTP == "-" {
listenHTTP = ""
}
// Convert sizes to bytes
attachmentTotalSizeLimit, err := parseSize(attachmentTotalSizeLimitStr, server.DefaultAttachmentTotalSizeLimit)
if err != nil {
return err
}
attachmentFileSizeLimit, err := parseSize(attachmentFileSizeLimitStr, server.DefaultAttachmentFileSizeLimit)
if err != nil {
return err
}
visitorAttachmentTotalSizeLimit, err := parseSize(visitorAttachmentTotalSizeLimitStr, server.DefaultVisitorAttachmentTotalSizeLimit)
if err != nil {
return err
}
visitorAttachmentDailyBandwidthLimit, err := parseSize(visitorAttachmentDailyBandwidthLimitStr, server.DefaultVisitorAttachmentDailyBandwidthLimit)
if err != nil {
return err
} else if visitorAttachmentDailyBandwidthLimit > math.MaxInt {
return fmt.Errorf("config option visitor-attachment-daily-bandwidth-limit must be lower than %d", math.MaxInt)
}
// Resolve hosts
visitorRequestLimitExemptPrefixes := make([]netip.Prefix, 0)
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
for _, host := range visitorRequestLimitExemptHosts {
prefixes, err := parseIPHostPrefix(host)
ips, err := parseIPHostPrefix(host)
if err != nil {
log.Warn("cannot resolve host %s: %s, ignoring visitor request exemption", host, err.Error())
continue
}
visitorRequestLimitExemptPrefixes = append(visitorRequestLimitExemptPrefixes, prefixes...)
}
// Parse trusted prefixes
trustedProxyPrefixes := make([]netip.Prefix, 0)
for _, host := range proxyTrustedHosts {
prefixes, err := parseIPHostPrefix(host)
if err != nil {
return fmt.Errorf("cannot resolve trusted proxy host %s: %s", host, err.Error())
}
trustedProxyPrefixes = append(trustedProxyPrefixes, prefixes...)
visitorRequestLimitExemptIPs = append(visitorRequestLimitExemptIPs, ips...)
}
// Stripe things
if stripeSecretKey != "" {
payments.Setup(stripeSecretKey)
stripe.Key = stripeSecretKey
}
// Add default forbidden topics
@@ -431,20 +275,15 @@ func execServe(c *cli.Context) error {
conf.AuthFile = authFile
conf.AuthStartupQueries = authStartupQueries
conf.AuthDefault = authDefault
conf.AuthUsers = authUsers
conf.AuthAccess = authAccess
conf.AuthTokens = authTokens
conf.AttachmentCacheDir = attachmentCacheDir
conf.AttachmentTotalSizeLimit = attachmentTotalSizeLimit
conf.AttachmentFileSizeLimit = attachmentFileSizeLimit
conf.AttachmentExpiryDuration = attachmentExpiryDuration
conf.TemplateDir = templateDir
conf.KeepaliveInterval = keepaliveInterval
conf.ManagerInterval = managerInterval
conf.DisallowedTopics = disallowedTopics
conf.WebRoot = webRoot
conf.WebRootIsApp = webRootIsApp
conf.UpstreamBaseURL = upstreamBaseURL
conf.UpstreamAccessToken = upstreamAccessToken
conf.SMTPSenderAddr = smtpSenderAddr
conf.SMTPSenderUser = smtpSenderUser
conf.SMTPSenderPass = smtpSenderPass
@@ -452,45 +291,23 @@ func execServe(c *cli.Context) error {
conf.SMTPServerListen = smtpServerListen
conf.SMTPServerDomain = smtpServerDomain
conf.SMTPServerAddrPrefix = smtpServerAddrPrefix
conf.TwilioAccount = twilioAccount
conf.TwilioAuthToken = twilioAuthToken
conf.TwilioPhoneNumber = twilioPhoneNumber
conf.TwilioVerifyService = twilioVerifyService
conf.MessageSizeLimit = int(messageSizeLimit)
conf.MessageDelayMax = messageDelayLimit
conf.TotalTopicLimit = totalTopicLimit
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
conf.VisitorSubscriberRateLimiting = visitorSubscriberRateLimiting
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
conf.VisitorAttachmentDailyBandwidthLimit = visitorAttachmentDailyBandwidthLimit
conf.VisitorRequestLimitBurst = visitorRequestLimitBurst
conf.VisitorRequestLimitReplenish = visitorRequestLimitReplenish
conf.VisitorRequestExemptPrefixes = visitorRequestLimitExemptPrefixes
conf.VisitorRequestExemptIPAddrs = visitorRequestLimitExemptIPs
conf.VisitorMessageDailyLimit = visitorMessageDailyLimit
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
conf.VisitorPrefixBitsIPv4 = visitorPrefixBitsIPv4
conf.VisitorPrefixBitsIPv6 = visitorPrefixBitsIPv6
conf.BehindProxy = behindProxy
conf.ProxyForwardedHeader = proxyForwardedHeader
conf.ProxyTrustedPrefixes = trustedProxyPrefixes
conf.StripeSecretKey = stripeSecretKey
conf.StripeWebhookKey = stripeWebhookKey
conf.BillingContact = billingContact
conf.EnableWeb = enableWeb
conf.EnableSignup = enableSignup
conf.EnableLogin = enableLogin
conf.RequireLogin = requireLogin
conf.EnableReservations = enableReservations
conf.EnableMetrics = enableMetrics
conf.MetricsListenHTTP = metricsListenHTTP
conf.ProfileListenHTTP = profileListenHTTP
conf.WebPushPrivateKey = webPushPrivateKey
conf.WebPushPublicKey = webPushPublicKey
conf.WebPushFile = webPushFile
conf.WebPushEmailAddress = webPushEmailAddress
conf.WebPushStartupQueries = webPushStartupQueries
conf.WebPushExpiryDuration = webPushExpiryDuration
conf.WebPushExpiryWarningDuration = webPushExpiryWarningDuration
conf.Version = c.App.Version
// Set up hot-reloading of config
@@ -499,14 +316,25 @@ func execServe(c *cli.Context) error {
// Run server
s, err := server.New(conf)
if err != nil {
log.Fatal("%s", err.Error())
log.Fatal(err.Error())
} else if err := s.Run(); err != nil {
log.Fatal("%s", err.Error())
log.Fatal(err.Error())
}
log.Info("Exiting.")
return nil
}
func parseSize(s string, defaultValue int64) (v int64, err error) {
if s == "" {
return defaultValue, nil
}
v, err = util.ParseSize(s)
if err != nil {
return 0, err
}
return v, nil
}
func sigHandlerConfigReload(config string) {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGHUP)
@@ -524,7 +352,7 @@ func sigHandlerConfigReload(config string) {
}
func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
// Try parsing as prefix, e.g. 10.0.1.0/24 or 2001:db8::/32
// Try parsing as prefix, e.g. 10.0.1.0/24
prefix, err := netip.ParsePrefix(host)
if err == nil {
prefixes = append(prefixes, prefix.Masked())
@@ -548,112 +376,6 @@ func parseIPHostPrefix(host string) (prefixes []netip.Prefix, err error) {
return
}
func parseUsers(usersRaw []string) ([]*user.User, error) {
users := make([]*user.User, 0)
for _, userLine := range usersRaw {
parts := strings.Split(userLine, ":")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid auth-users: %s, expected format: 'name:hash:role'", userLine)
}
username := strings.TrimSpace(parts[0])
passwordHash := strings.TrimSpace(parts[1])
role := user.Role(strings.TrimSpace(parts[2]))
if !user.AllowedUsername(username) {
return nil, fmt.Errorf("invalid auth-users: %s, username invalid", userLine)
} else if err := user.ValidPasswordHash(passwordHash, user.DefaultUserPasswordBcryptCost); err != nil {
return nil, fmt.Errorf("invalid auth-users: %s, password hash invalid, %s", userLine, err.Error())
} else if !user.AllowedRole(role) {
return nil, fmt.Errorf("invalid auth-users: %s, role %s is not allowed, allowed roles are 'admin' or 'user'", userLine, role)
}
users = append(users, &user.User{
Name: username,
Hash: passwordHash,
Role: role,
Provisioned: true,
})
}
return users, nil
}
func parseAccess(users []*user.User, accessRaw []string) (map[string][]*user.Grant, error) {
access := make(map[string][]*user.Grant)
for _, accessLine := range accessRaw {
parts := strings.Split(accessLine, ":")
if len(parts) != 3 {
return nil, fmt.Errorf("invalid auth-access: %s, expected format: 'user:topic:permission'", accessLine)
}
username := strings.TrimSpace(parts[0])
if username == userEveryone {
username = user.Everyone
}
u, exists := util.Find(users, func(u *user.User) bool {
return u.Name == username
})
if username != user.Everyone {
if !exists {
return nil, fmt.Errorf("invalid auth-access: %s, user %s is not provisioned", accessLine, username)
} else if !user.AllowedUsername(username) {
return nil, fmt.Errorf("invalid auth-access: %s, username %s invalid", accessLine, username)
} else if u.Role != user.RoleUser {
return nil, fmt.Errorf("invalid auth-access: %s, user %s is not a regular user, only regular users can have ACL entries", accessLine, username)
}
}
topic := strings.TrimSpace(parts[1])
if !user.AllowedTopicPattern(topic) {
return nil, fmt.Errorf("invalid auth-access: %s, topic pattern %s invalid", accessLine, topic)
}
permission, err := user.ParsePermission(strings.TrimSpace(parts[2]))
if err != nil {
return nil, fmt.Errorf("invalid auth-access: %s, permission %s invalid, %s", accessLine, parts[2], err.Error())
}
if _, exists := access[username]; !exists {
access[username] = make([]*user.Grant, 0)
}
access[username] = append(access[username], &user.Grant{
TopicPattern: topic,
Permission: permission,
Provisioned: true,
})
}
return access, nil
}
func parseTokens(users []*user.User, tokensRaw []string) (map[string][]*user.Token, error) {
tokens := make(map[string][]*user.Token)
for _, tokenLine := range tokensRaw {
parts := strings.Split(tokenLine, ":")
if len(parts) < 2 || len(parts) > 3 {
return nil, fmt.Errorf("invalid auth-tokens: %s, expected format: 'user:token[:label]'", tokenLine)
}
username := strings.TrimSpace(parts[0])
_, exists := util.Find(users, func(u *user.User) bool {
return u.Name == username
})
if !exists {
return nil, fmt.Errorf("invalid auth-tokens: %s, user %s is not provisioned", tokenLine, username)
} else if !user.AllowedUsername(username) {
return nil, fmt.Errorf("invalid auth-tokens: %s, username %s invalid", tokenLine, username)
}
token := strings.TrimSpace(parts[1])
if !user.ValidToken(token) {
return nil, fmt.Errorf("invalid auth-tokens: %s, token %s invalid, use 'ntfy token generate' to generate a random token", tokenLine, token)
}
var label string
if len(parts) > 2 {
label = parts[2]
}
if _, exists := tokens[username]; !exists {
tokens[username] = make([]*user.Token, 0)
}
tokens[username] = append(tokens[username], &user.Token{
Value: token,
Label: label,
Provisioned: true,
})
}
return tokens, nil
}
func reloadLogLevel(inputSource altsrc.InputSourceContext) error {
newLevelStr, err := inputSource.String("log-level")
if err != nil {

View File

@@ -12,461 +12,13 @@ import (
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/client"
"heckel.io/ntfy/test"
"heckel.io/ntfy/util"
)
func TestParseUsers_Success(t *testing.T) {
tests := []struct {
name string
input []string
expected []*user.User
}{
{
name: "single user",
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
expected: []*user.User{
{
Name: "alice",
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
Role: user.RoleUser,
Provisioned: true,
},
},
},
{
name: "multiple users with different roles",
input: []string{
"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user",
"bob:$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq:admin",
},
expected: []*user.User{
{
Name: "alice",
Hash: "$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S",
Role: user.RoleUser,
Provisioned: true,
},
{
Name: "bob",
Hash: "$2a$10$jIcuBWcbxd6oW1aPvoJ5iOShzu3/UJ2kSxKbTZtDypG06nBflQagq",
Role: user.RoleAdmin,
Provisioned: true,
},
},
},
{
name: "empty input",
input: []string{},
expected: []*user.User{},
},
{
name: "user with special characters in name",
input: []string{"alice.test+123@example.com:$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe:user"},
expected: []*user.User{
{
Name: "alice.test+123@example.com",
Hash: "$2a$10$RYUYAsl5zOnAIp6fH7BPX.Eug0rUfEUk92r8WiVusb0VK.vGojWBe",
Role: user.RoleUser,
Provisioned: true,
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseUsers(tt.input)
require.NoError(t, err)
require.Len(t, result, len(tt.expected))
for i, expectedUser := range tt.expected {
assert.Equal(t, expectedUser.Name, result[i].Name)
assert.Equal(t, expectedUser.Hash, result[i].Hash)
assert.Equal(t, expectedUser.Role, result[i].Role)
assert.Equal(t, expectedUser.Provisioned, result[i].Provisioned)
}
})
}
}
func TestParseUsers_Errors(t *testing.T) {
tests := []struct {
name string
input []string
error string
}{
{
name: "invalid format - too few parts",
input: []string{"alice:hash"},
error: "invalid auth-users: alice:hash, expected format: 'name:hash:role'",
},
{
name: "invalid format - too many parts",
input: []string{"alice:hash:role:extra"},
error: "invalid auth-users: alice:hash:role:extra, expected format: 'name:hash:role'",
},
{
name: "invalid username",
input: []string{"alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
error: "invalid auth-users: alice@#$%:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
},
{
name: "invalid password hash - wrong prefix",
input: []string{"alice:plaintext:user"},
error: "invalid auth-users: alice:plaintext:user, password hash invalid, password hash must be a bcrypt hash, use 'ntfy user hash' to generate",
},
{
name: "invalid role",
input: []string{"alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid"},
error: "invalid auth-users: alice:$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:invalid, role invalid is not allowed, allowed roles are 'admin' or 'user'",
},
{
name: "empty username",
input: []string{":$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user"},
error: "invalid auth-users: :$2a$10$320YlQeaMghYZsvtu9jzfOQZS32FysWY/T9qu5NWqcIh.DN.u5P5S:user, username invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseUsers(tt.input)
require.Error(t, err)
require.Nil(t, result)
assert.Contains(t, err.Error(), tt.error)
})
}
}
func TestParseAccess_Success(t *testing.T) {
users := []*user.User{
{Name: "alice", Role: user.RoleUser},
{Name: "bob", Role: user.RoleUser},
}
tests := []struct {
name string
users []*user.User
input []string
expected map[string][]*user.Grant
}{
{
name: "single access entry",
users: users,
input: []string{"alice:mytopic:read-write"},
expected: map[string][]*user.Grant{
"alice": {
{
TopicPattern: "mytopic",
Permission: user.PermissionReadWrite,
Provisioned: true,
},
},
},
},
{
name: "multiple access entries for same user",
users: users,
input: []string{
"alice:topic1:read-only",
"alice:topic2:write-only",
},
expected: map[string][]*user.Grant{
"alice": {
{
TopicPattern: "topic1",
Permission: user.PermissionRead,
Provisioned: true,
},
{
TopicPattern: "topic2",
Permission: user.PermissionWrite,
Provisioned: true,
},
},
},
},
{
name: "access for everyone",
users: users,
input: []string{"everyone:publictopic:read-only"},
expected: map[string][]*user.Grant{
user.Everyone: {
{
TopicPattern: "publictopic",
Permission: user.PermissionRead,
Provisioned: true,
},
},
},
},
{
name: "wildcard topic pattern",
users: users,
input: []string{"alice:topic*:read-write"},
expected: map[string][]*user.Grant{
"alice": {
{
TopicPattern: "topic*",
Permission: user.PermissionReadWrite,
Provisioned: true,
},
},
},
},
{
name: "empty input",
users: users,
input: []string{},
expected: map[string][]*user.Grant{},
},
{
name: "deny-all permission",
users: users,
input: []string{"alice:secretopic:deny-all"},
expected: map[string][]*user.Grant{
"alice": {
{
TopicPattern: "secretopic",
Permission: user.PermissionDenyAll,
Provisioned: true,
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseAccess(tt.users, tt.input)
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseAccess_Errors(t *testing.T) {
users := []*user.User{
{Name: "alice", Role: user.RoleUser},
{Name: "admin", Role: user.RoleAdmin},
}
tests := []struct {
name string
users []*user.User
input []string
error string
}{
{
name: "invalid format - too few parts",
users: users,
input: []string{"alice:topic"},
error: "invalid auth-access: alice:topic, expected format: 'user:topic:permission'",
},
{
name: "invalid format - too many parts",
users: users,
input: []string{"alice:topic:read:extra"},
error: "invalid auth-access: alice:topic:read:extra, expected format: 'user:topic:permission'",
},
{
name: "user not provisioned",
users: users,
input: []string{"charlie:topic:read"},
error: "invalid auth-access: charlie:topic:read, user charlie is not provisioned",
},
{
name: "admin user cannot have ACL entries",
users: users,
input: []string{"admin:topic:read"},
error: "invalid auth-access: admin:topic:read, user admin is not a regular user, only regular users can have ACL entries",
},
{
name: "invalid topic pattern",
users: users,
input: []string{"alice:topic-with-invalid-chars!:read"},
error: "invalid auth-access: alice:topic-with-invalid-chars!:read, topic pattern topic-with-invalid-chars! invalid",
},
{
name: "invalid permission",
users: users,
input: []string{"alice:topic:invalid-permission"},
error: "invalid auth-access: alice:topic:invalid-permission, permission invalid-permission invalid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseAccess(tt.users, tt.input)
require.Error(t, err)
require.Nil(t, result)
assert.Contains(t, err.Error(), tt.error)
})
}
}
func TestParseTokens_Success(t *testing.T) {
users := []*user.User{
{Name: "alice"},
{Name: "bob"},
}
tests := []struct {
name string
users []*user.User
input []string
expected map[string][]*user.Token
}{
{
name: "single token without label",
users: users,
input: []string{"alice:tk_abcdefghijklmnopqrstuvwxyz123"},
expected: map[string][]*user.Token{
"alice": {
{
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
Label: "",
Provisioned: true,
},
},
},
},
{
name: "single token with label",
users: users,
input: []string{"alice:tk_abcdefghijklmnopqrstuvwxyz123:My Phone"},
expected: map[string][]*user.Token{
"alice": {
{
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
Label: "My Phone",
Provisioned: true,
},
},
},
},
{
name: "multiple tokens for same user",
users: users,
input: []string{
"alice:tk_abcdefghijklmnopqrstuvwxyz123:Phone",
"alice:tk_zyxwvutsrqponmlkjihgfedcba987:Laptop",
},
expected: map[string][]*user.Token{
"alice": {
{
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
Label: "Phone",
Provisioned: true,
},
{
Value: "tk_zyxwvutsrqponmlkjihgfedcba987",
Label: "Laptop",
Provisioned: true,
},
},
},
},
{
name: "tokens for multiple users",
users: users,
input: []string{
"alice:tk_abcdefghijklmnopqrstuvwxyz123:Phone",
"bob:tk_zyxwvutsrqponmlkjihgfedcba987:Tablet",
},
expected: map[string][]*user.Token{
"alice": {
{
Value: "tk_abcdefghijklmnopqrstuvwxyz123",
Label: "Phone",
Provisioned: true,
},
},
"bob": {
{
Value: "tk_zyxwvutsrqponmlkjihgfedcba987",
Label: "Tablet",
Provisioned: true,
},
},
},
},
{
name: "empty input",
users: users,
input: []string{},
expected: map[string][]*user.Token{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseTokens(tt.users, tt.input)
require.NoError(t, err)
assert.Equal(t, tt.expected, result)
})
}
}
func TestParseTokens_Errors(t *testing.T) {
users := []*user.User{
{Name: "alice"},
}
tests := []struct {
name string
users []*user.User
input []string
error string
}{
{
name: "invalid format - too few parts",
users: users,
input: []string{"alice"},
error: "invalid auth-tokens: alice, expected format: 'user:token[:label]'",
},
{
name: "invalid format - too many parts",
users: users,
input: []string{"alice:token:label:extra:parts"},
error: "invalid auth-tokens: alice:token:label:extra:parts, expected format: 'user:token[:label]'",
},
{
name: "user not provisioned",
users: users,
input: []string{"charlie:tk_abcdefghijklmnopqrstuvwxyz123"},
error: "invalid auth-tokens: charlie:tk_abcdefghijklmnopqrstuvwxyz123, user charlie is not provisioned",
},
{
name: "invalid token format",
users: users,
input: []string{"alice:invalid-token"},
error: "invalid auth-tokens: alice:invalid-token, token invalid-token invalid, use 'ntfy token generate' to generate a random token",
},
{
name: "token too short",
users: users,
input: []string{"alice:tk_short"},
error: "invalid auth-tokens: alice:tk_short, token tk_short invalid, use 'ntfy token generate' to generate a random token",
},
{
name: "token without prefix",
users: users,
input: []string{"alice:abcdefghijklmnopqrstuvwxyz12345"},
error: "invalid auth-tokens: alice:abcdefghijklmnopqrstuvwxyz12345, token abcdefghijklmnopqrstuvwxyz12345 invalid, use 'ntfy token generate' to generate a random token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := parseTokens(tt.users, tt.input)
require.Error(t, err)
require.Nil(t, result)
assert.Contains(t, err.Error(), tt.error)
})
}
func init() {
rand.Seed(time.Now().UnixMilli())
}
func TestCLI_Serve_Unix_Curl(t *testing.T) {

View File

@@ -4,9 +4,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/client"
"heckel.io/ntfy/v2/log"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/client"
"heckel.io/ntfy/log"
"heckel.io/ntfy/util"
"os"
"os/exec"
"os/user"
@@ -30,7 +30,6 @@ var flagsSubscribe = append(
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, Usage: "client config file"},
&cli.StringFlag{Name: "since", Aliases: []string{"s"}, Usage: "return events since `SINCE` (Unix timestamp, or all)"},
&cli.StringFlag{Name: "user", Aliases: []string{"u"}, EnvVars: []string{"NTFY_USER"}, Usage: "username[:password] used to auth against the server"},
&cli.StringFlag{Name: "token", Aliases: []string{"k"}, EnvVars: []string{"NTFY_TOKEN"}, Usage: "access token used to auth against the server"},
&cli.BoolFlag{Name: "from-config", Aliases: []string{"from_config", "C"}, Usage: "read subscriptions from config file (service mode)"},
&cli.BoolFlag{Name: "poll", Aliases: []string{"p"}, Usage: "return events and exit, do not listen for new events"},
&cli.BoolFlag{Name: "scheduled", Aliases: []string{"sched", "S"}, Usage: "also return scheduled/delayed events"},
@@ -72,7 +71,7 @@ ntfy subscribe TOPIC COMMAND
$NTFY_TITLE $title, $t Message title
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
$NTFY_RAW $raw Raw JSON message
$NTFY_RAW $raw Raw JSON message
Examples:
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
@@ -98,18 +97,11 @@ func execSubscribe(c *cli.Context) error {
cl := client.New(conf)
since := c.String("since")
user := c.String("user")
token := c.String("token")
poll := c.Bool("poll")
scheduled := c.Bool("scheduled")
fromConfig := c.Bool("from-config")
topic := c.Args().Get(0)
command := c.Args().Get(1)
// Checks
if user != "" && token != "" {
return errors.New("cannot set both --user and --token")
}
if !fromConfig {
conf.Subscribe = nil // wipe if --from-config not passed
}
@@ -117,9 +109,7 @@ func execSubscribe(c *cli.Context) error {
if since != "" {
options = append(options, client.WithSince(since))
}
if token != "" {
options = append(options, client.WithBearerAuth(token))
} else if user != "" {
if user != "" {
var pass string
parts := strings.SplitN(user, ":", 2)
if len(parts) == 2 {
@@ -135,10 +125,9 @@ func execSubscribe(c *cli.Context) error {
fmt.Fprintf(c.App.ErrWriter, "\r%s\r", strings.Repeat(" ", 20))
}
options = append(options, client.WithBasicAuth(user, pass))
} else if conf.DefaultToken != "" {
options = append(options, client.WithBearerAuth(conf.DefaultToken))
} else if conf.DefaultUser != "" && conf.DefaultPassword != nil {
options = append(options, client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword))
}
if poll {
options = append(options, client.WithPoll())
}
if scheduled {
options = append(options, client.WithScheduled())
@@ -156,9 +145,6 @@ func execSubscribe(c *cli.Context) error {
func doPoll(c *cli.Context, cl *client.Client, conf *client.Config, topic, command string, options ...client.SubscribeOption) error {
for _, s := range conf.Subscribe { // may be nil
if auth := maybeAddAuthHeader(s, conf); auth != nil {
options = append(options, auth)
}
if err := doPollSingle(c, cl, s.Topic, s.Command, options...); err != nil {
return err
}
@@ -189,15 +175,22 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
for filter, value := range s.If {
topicOptions = append(topicOptions, client.WithFilter(filter, value))
}
if auth := maybeAddAuthHeader(s, conf); auth != nil {
topicOptions = append(topicOptions, auth)
var user string
var password *string
if s.User != "" {
user = s.User
} else if conf.DefaultUser != "" {
user = conf.DefaultUser
}
subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...)
if err != nil {
return err
if s.Password != nil {
password = s.Password
} else if conf.DefaultPassword != nil {
password = conf.DefaultPassword
}
if user != "" && password != nil {
topicOptions = append(topicOptions, client.WithBasicAuth(user, *password))
}
subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
if s.Command != "" {
cmds[subscriptionID] = s.Command
} else if conf.DefaultCommand != "" {
@@ -207,10 +200,7 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
}
}
if topic != "" {
subscriptionID, err := cl.Subscribe(topic, options...)
if err != nil {
return err
}
subscriptionID := cl.Subscribe(topic, options...)
cmds[subscriptionID] = command
}
for m := range cl.Messages {
@@ -224,30 +214,6 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
return nil
}
func maybeAddAuthHeader(s client.Subscribe, conf *client.Config) client.SubscribeOption {
// if an explicit empty token or empty user:pass is given, exit without auth
if (s.Token != nil && *s.Token == "") || (s.User != nil && *s.User == "" && s.Password != nil && *s.Password == "") {
return client.WithEmptyAuth()
}
// check for subscription token then subscription user:pass
if s.Token != nil && *s.Token != "" {
return client.WithBearerAuth(*s.Token)
}
if s.User != nil && *s.User != "" && s.Password != nil {
return client.WithBasicAuth(*s.User, *s.Password)
}
// if no subscription token nor subscription user:pass, check for default token then default user:pass
if conf.DefaultToken != "" {
return client.WithBearerAuth(conf.DefaultToken)
}
if conf.DefaultUser != "" && conf.DefaultPassword != nil {
return client.WithBasicAuth(conf.DefaultUser, *conf.DefaultPassword)
}
return nil
}
func printMessageOrRunCommand(c *cli.Context, m *client.Message, command string) {
if command != "" {
runCommand(c, command, m)
@@ -310,43 +276,28 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
if filename != "" {
return client.LoadConfig(filename)
}
configFile, err := defaultClientConfigFile()
if err != nil {
log.Warn("Could not determine default client config file: %s", err.Error())
} else {
if s, _ := os.Stat(configFile); s != nil {
return client.LoadConfig(configFile)
}
log.Debug("Config file %s not found", configFile)
configFile := defaultClientConfigFile()
if s, _ := os.Stat(configFile); s != nil {
return client.LoadConfig(configFile)
}
log.Debug("Loading default config")
return client.NewConfig(), nil
}
//lint:ignore U1000 Conditionally used in different builds
func defaultClientConfigFileUnix() (string, error) {
u, err := user.Current()
if err != nil {
return "", fmt.Errorf("could not determine current user: %w", err)
}
func defaultClientConfigFileUnix() string {
u, _ := user.Current()
configFile := clientRootConfigFileUnixAbsolute
if u.Uid != "0" {
homeDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("could not determine user config dir: %w", err)
}
return filepath.Join(homeDir, clientUserConfigFileUnixRelative), nil
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
}
return configFile, nil
return configFile
}
//lint:ignore U1000 Conditionally used in different builds
func defaultClientConfigFileWindows() (string, error) {
homeDir, err := os.UserConfigDir()
if err != nil {
return "", fmt.Errorf("could not determine user config dir: %w", err)
}
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative), nil
func defaultClientConfigFileWindows() string {
homeDir, _ := os.UserConfigDir()
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
}
func logMessagePrefix(m *client.Message) string {

View File

@@ -11,6 +11,6 @@ var (
scriptLauncher = []string{"sh", "-c"}
)
func defaultClientConfigFile() (string, error) {
func defaultClientConfigFile() string {
return defaultClientConfigFileUnix()
}

View File

@@ -1,417 +0,0 @@
package cmd
import (
"fmt"
"github.com/stretchr/testify/require"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestCLI_Subscribe_Default_UserPass_Subscription_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
subscribe:
- topic: mytopic
user: philipp
password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_FAKETOKEN01234567890FAKETOKEN
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_UserPass_Subscription_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: fake
default-password: password
subscribe:
- topic: mytopic
user: philipp
password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_Empty(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
subscribe:
- topic: mytopic
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_UserPass_Subscription_Empty(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
subscribe:
- topic: mytopic
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Empty_Subscription_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Empty_Subscription_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
subscribe:
- topic: mytopic
user: philipp
password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_CLI_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_FAKETOKEN0123456789FAKETOKEN
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_CLI_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass", "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_Token_Subscription_Token_CLI_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_FAKETOKEN01234567890FAKETOKEN
subscribe:
- topic: mytopic
token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "--user", "philipp:mypass"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Token_And_UserPass(t *testing.T) {
app, _, _, _ := newTestApp()
err := app.Run([]string{"ntfy", "subscribe", "--poll", "--token", "tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", "--user", "philipp:mypass", "mytopic", "triggered"})
require.Error(t, err)
require.Equal(t, "cannot set both --user and --token", err.Error())
}
func TestCLI_Subscribe_Default_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Bearer tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Default_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "Basic cGhpbGlwcDpteXBhc3M=", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename, "mytopic"}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Override_Default_UserPass_With_Empty_UserPass(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-user: philipp
default-password: mypass
subscribe:
- topic: mytopic
user: ""
password: ""
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}
func TestCLI_Subscribe_Override_Default_Token_With_Empty_Token(t *testing.T) {
message := `{"id":"RXIQBFaieLVr","time":124,"expires":1124,"event":"message","topic":"mytopic","message":"triggered"}`
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
require.Equal(t, "/mytopic/json", r.URL.Path)
require.Equal(t, "", r.Header.Get("Authorization"))
w.WriteHeader(http.StatusOK)
w.Write([]byte(message))
}))
defer server.Close()
filename := filepath.Join(t.TempDir(), "client.yml")
require.Nil(t, os.WriteFile(filename, []byte(fmt.Sprintf(`
default-host: %s
default-token: tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2
subscribe:
- topic: mytopic
token: ""
`, server.URL)), 0600))
app, _, stdout, _ := newTestApp()
require.Nil(t, app.Run([]string{"ntfy", "subscribe", "--poll", "--from-config", "--config=" + filename}))
require.Equal(t, message, strings.TrimSpace(stdout.String()))
}

View File

@@ -13,6 +13,6 @@ var (
scriptLauncher = []string{"sh", "-c"}
)
func defaultClientConfigFile() (string, error) {
func defaultClientConfigFile() string {
return defaultClientConfigFileUnix()
}

View File

@@ -10,6 +10,6 @@ var (
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
)
func defaultClientConfigFile() (string, error) {
func defaultClientConfigFile() string {
return defaultClientConfigFileWindows()
}

View File

@@ -6,8 +6,9 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"time"
)
func init() {
@@ -16,13 +17,12 @@ func init() {
const (
defaultMessageLimit = 5000
defaultMessageExpiryDuration = "12h"
defaultMessageExpiryDuration = 12 * time.Hour
defaultEmailLimit = 20
defaultCallLimit = 0
defaultReservationLimit = 3
defaultAttachmentFileSizeLimit = "15M"
defaultAttachmentTotalSizeLimit = "100M"
defaultAttachmentExpiryDuration = "6h"
defaultAttachmentExpiryDuration = 6 * time.Hour
defaultAttachmentBandwidthLimit = "1G"
)
@@ -47,16 +47,14 @@ var cmdTier = &cli.Command{
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Usage: "tier name"},
&cli.Int64Flag{Name: "message-limit", Value: defaultMessageLimit, Usage: "daily message limit"},
&cli.StringFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
&cli.DurationFlag{Name: "message-expiry-duration", Value: defaultMessageExpiryDuration, Usage: "duration after which messages are deleted"},
&cli.Int64Flag{Name: "email-limit", Value: defaultEmailLimit, Usage: "daily email limit"},
&cli.Int64Flag{Name: "call-limit", Value: defaultCallLimit, Usage: "daily phone call limit"},
&cli.Int64Flag{Name: "reservation-limit", Value: defaultReservationLimit, Usage: "topic reservation limit"},
&cli.StringFlag{Name: "attachment-file-size-limit", Value: defaultAttachmentFileSizeLimit, Usage: "per-attachment file size limit"},
&cli.StringFlag{Name: "attachment-total-size-limit", Value: defaultAttachmentTotalSizeLimit, Usage: "total size limit of attachments for the user"},
&cli.StringFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"},
&cli.DurationFlag{Name: "attachment-expiry-duration", Value: defaultAttachmentExpiryDuration, Usage: "duration after which attachments are deleted"},
&cli.StringFlag{Name: "attachment-bandwidth-limit", Value: defaultAttachmentBandwidthLimit, Usage: "daily bandwidth limit for attachment uploads/downloads"},
&cli.StringFlag{Name: "stripe-monthly-price-id", Usage: "Monthly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.StringFlag{Name: "stripe-yearly-price-id", Usage: "Yearly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.BoolFlag{Name: "ignore-exists", Usage: "if the tier already exists, perform no action and exit"},
},
Description: `Add a new tier to the ntfy user database.
@@ -91,16 +89,14 @@ Examples:
Flags: []cli.Flag{
&cli.StringFlag{Name: "name", Usage: "tier name"},
&cli.Int64Flag{Name: "message-limit", Usage: "daily message limit"},
&cli.StringFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
&cli.DurationFlag{Name: "message-expiry-duration", Usage: "duration after which messages are deleted"},
&cli.Int64Flag{Name: "email-limit", Usage: "daily email limit"},
&cli.Int64Flag{Name: "call-limit", Usage: "daily phone call limit"},
&cli.Int64Flag{Name: "reservation-limit", Usage: "topic reservation limit"},
&cli.StringFlag{Name: "attachment-file-size-limit", Usage: "per-attachment file size limit"},
&cli.StringFlag{Name: "attachment-total-size-limit", Usage: "total size limit of attachments for the user"},
&cli.StringFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"},
&cli.DurationFlag{Name: "attachment-expiry-duration", Usage: "duration after which attachments are deleted"},
&cli.StringFlag{Name: "attachment-bandwidth-limit", Usage: "daily bandwidth limit for attachment uploads/downloads"},
&cli.StringFlag{Name: "stripe-monthly-price-id", Usage: "Monthly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.StringFlag{Name: "stripe-yearly-price-id", Usage: "Yearly Stripe price ID for paid tiers (e.g. price_12345)"},
&cli.StringFlag{Name: "stripe-price-id", Usage: "Stripe price ID for paid tiers (e.g. price_12345)"},
},
Description: `Updates a tier to change the limits.
@@ -114,8 +110,7 @@ Examples:
ntfy tier change --name="Pro" pro # Update the name of an existing tier
ntfy tier change \ # Update multiple limits and fields
--message-expiry-duration=24h \
--stripe-monthly-price-id=price_1234 \
--stripe-monthly-price-id=price_5678 \
--stripe-price-id=price_1234 \
pro
`,
},
@@ -171,10 +166,6 @@ func execTierAdd(c *cli.Context) error {
return errors.New("tier code expected, type 'ntfy tier add --help' for help")
} else if !user.AllowedTier(code) {
return errors.New("tier code must consist only of numbers and letters")
} else if c.String("stripe-monthly-price-id") != "" && c.String("stripe-yearly-price-id") == "" {
return errors.New("if stripe-monthly-price-id is set, stripe-yearly-price-id must also be set")
} else if c.String("stripe-monthly-price-id") == "" && c.String("stripe-yearly-price-id") != "" {
return errors.New("if stripe-yearly-price-id is set, stripe-monthly-price-id must also be set")
}
manager, err := createUserManager(c)
if err != nil {
@@ -182,7 +173,7 @@ func execTierAdd(c *cli.Context) error {
}
if tier, _ := manager.Tier(code); tier != nil {
if c.Bool("ignore-exists") {
fmt.Fprintf(c.App.Writer, "tier %s already exists (exited successfully)\n", code)
fmt.Fprintf(c.App.ErrWriter, "tier %s already exists (exited successfully)\n", code)
return nil
}
return fmt.Errorf("tier %s already exists", code)
@@ -191,10 +182,6 @@ func execTierAdd(c *cli.Context) error {
if name == "" {
name = code
}
messageExpiryDuration, err := util.ParseDuration(c.String("message-expiry-duration"))
if err != nil {
return err
}
attachmentFileSizeLimit, err := util.ParseSize(c.String("attachment-file-size-limit"))
if err != nil {
return err
@@ -207,25 +194,19 @@ func execTierAdd(c *cli.Context) error {
if err != nil {
return err
}
attachmentExpiryDuration, err := util.ParseDuration(c.String("attachment-expiry-duration"))
if err != nil {
return err
}
tier := &user.Tier{
ID: "", // Generated
Code: code,
Name: name,
MessageLimit: c.Int64("message-limit"),
MessageExpiryDuration: messageExpiryDuration,
MessageExpiryDuration: c.Duration("message-expiry-duration"),
EmailLimit: c.Int64("email-limit"),
CallLimit: c.Int64("call-limit"),
ReservationLimit: c.Int64("reservation-limit"),
AttachmentFileSizeLimit: attachmentFileSizeLimit,
AttachmentTotalSizeLimit: attachmentTotalSizeLimit,
AttachmentExpiryDuration: attachmentExpiryDuration,
AttachmentExpiryDuration: c.Duration("attachment-expiry-duration"),
AttachmentBandwidthLimit: attachmentBandwidthLimit,
StripeMonthlyPriceID: c.String("stripe-monthly-price-id"),
StripeYearlyPriceID: c.String("stripe-yearly-price-id"),
StripePriceID: c.String("stripe-price-id"),
}
if err := manager.AddTier(tier); err != nil {
return err
@@ -234,7 +215,7 @@ func execTierAdd(c *cli.Context) error {
if err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "tier added\n\n")
fmt.Fprintf(c.App.ErrWriter, "tier added\n\n")
printTier(c, tier)
return nil
}
@@ -263,17 +244,11 @@ func execTierChange(c *cli.Context) error {
tier.MessageLimit = c.Int64("message-limit")
}
if c.IsSet("message-expiry-duration") {
tier.MessageExpiryDuration, err = util.ParseDuration(c.String("message-expiry-duration"))
if err != nil {
return err
}
tier.MessageExpiryDuration = c.Duration("message-expiry-duration")
}
if c.IsSet("email-limit") {
tier.EmailLimit = c.Int64("email-limit")
}
if c.IsSet("call-limit") {
tier.CallLimit = c.Int64("call-limit")
}
if c.IsSet("reservation-limit") {
tier.ReservationLimit = c.Int64("reservation-limit")
}
@@ -290,10 +265,7 @@ func execTierChange(c *cli.Context) error {
}
}
if c.IsSet("attachment-expiry-duration") {
tier.AttachmentExpiryDuration, err = util.ParseDuration(c.String("attachment-expiry-duration"))
if err != nil {
return err
}
tier.AttachmentExpiryDuration = c.Duration("attachment-expiry-duration")
}
if c.IsSet("attachment-bandwidth-limit") {
tier.AttachmentBandwidthLimit, err = util.ParseSize(c.String("attachment-bandwidth-limit"))
@@ -301,21 +273,13 @@ func execTierChange(c *cli.Context) error {
return err
}
}
if c.IsSet("stripe-monthly-price-id") {
tier.StripeMonthlyPriceID = c.String("stripe-monthly-price-id")
}
if c.IsSet("stripe-yearly-price-id") {
tier.StripeYearlyPriceID = c.String("stripe-yearly-price-id")
}
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID == "" {
return errors.New("if stripe-monthly-price-id is set, stripe-yearly-price-id must also be set")
} else if tier.StripeMonthlyPriceID == "" && tier.StripeYearlyPriceID != "" {
return errors.New("if stripe-yearly-price-id is set, stripe-monthly-price-id must also be set")
if c.IsSet("stripe-price-id") {
tier.StripePriceID = c.String("stripe-price-id")
}
if err := manager.UpdateTier(tier); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "tier updated\n\n")
fmt.Fprintf(c.App.ErrWriter, "tier updated\n\n")
printTier(c, tier)
return nil
}
@@ -335,7 +299,7 @@ func execTierDel(c *cli.Context) error {
if err := manager.RemoveTier(code); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "tier %s removed\n", code)
fmt.Fprintf(c.App.ErrWriter, "tier %s removed\n", code)
return nil
}
@@ -355,20 +319,19 @@ func execTierList(c *cli.Context) error {
}
func printTier(c *cli.Context, tier *user.Tier) {
prices := "(none)"
if tier.StripeMonthlyPriceID != "" && tier.StripeYearlyPriceID != "" {
prices = fmt.Sprintf("%s / %s", tier.StripeMonthlyPriceID, tier.StripeYearlyPriceID)
stripePriceID := tier.StripePriceID
if stripePriceID == "" {
stripePriceID = "(none)"
}
fmt.Fprintf(c.App.Writer, "tier %s (id: %s)\n", tier.Code, tier.ID)
fmt.Fprintf(c.App.Writer, "- Name: %s\n", tier.Name)
fmt.Fprintf(c.App.Writer, "- Message limit: %d\n", tier.MessageLimit)
fmt.Fprintf(c.App.Writer, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
fmt.Fprintf(c.App.Writer, "- Email limit: %d\n", tier.EmailLimit)
fmt.Fprintf(c.App.Writer, "- Phone call limit: %d\n", tier.CallLimit)
fmt.Fprintf(c.App.Writer, "- Reservation limit: %d\n", tier.ReservationLimit)
fmt.Fprintf(c.App.Writer, "- Attachment file size limit: %s\n", util.FormatSizeHuman(tier.AttachmentFileSizeLimit))
fmt.Fprintf(c.App.Writer, "- Attachment total size limit: %s\n", util.FormatSizeHuman(tier.AttachmentTotalSizeLimit))
fmt.Fprintf(c.App.Writer, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
fmt.Fprintf(c.App.Writer, "- Attachment daily bandwidth limit: %s\n", util.FormatSizeHuman(tier.AttachmentBandwidthLimit))
fmt.Fprintf(c.App.Writer, "- Stripe prices (monthly/yearly): %s\n", prices)
fmt.Fprintf(c.App.ErrWriter, "tier %s (id: %s)\n", tier.Code, tier.ID)
fmt.Fprintf(c.App.ErrWriter, "- Name: %s\n", tier.Name)
fmt.Fprintf(c.App.ErrWriter, "- Message limit: %d\n", tier.MessageLimit)
fmt.Fprintf(c.App.ErrWriter, "- Message expiry duration: %s (%d seconds)\n", tier.MessageExpiryDuration.String(), int64(tier.MessageExpiryDuration.Seconds()))
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
fmt.Fprintf(c.App.ErrWriter, "- Reservation limit: %d\n", tier.ReservationLimit)
fmt.Fprintf(c.App.ErrWriter, "- Attachment file size limit: %s\n", util.FormatSize(tier.AttachmentFileSizeLimit))
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSize(tier.AttachmentTotalSizeLimit))
fmt.Fprintf(c.App.ErrWriter, "- Attachment expiry duration: %s (%d seconds)\n", tier.AttachmentExpiryDuration.String(), int64(tier.AttachmentExpiryDuration.Seconds()))
fmt.Fprintf(c.App.ErrWriter, "- Attachment daily bandwidth limit: %s\n", util.FormatSize(tier.AttachmentBandwidthLimit))
fmt.Fprintf(c.App.ErrWriter, "- Stripe price: %s\n", stripePriceID)
}

View File

@@ -3,8 +3,8 @@ package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"testing"
)
@@ -12,46 +12,45 @@ func TestCLI_Tier_AddListChangeDelete(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, _, stdout, _ := newTestApp()
app, _, _, stderr := newTestApp()
require.Nil(t, runTierCommand(app, conf, "add", "--name", "Pro", "--message-limit", "1234", "pro"))
require.Contains(t, stdout.String(), "tier added\n\ntier pro (id: ti_")
require.Contains(t, stderr.String(), "tier added\n\ntier pro (id: ti_")
err := runTierCommand(app, conf, "add", "pro")
require.NotNil(t, err)
require.Equal(t, "tier pro already exists", err.Error())
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "list"))
require.Contains(t, stdout.String(), "tier pro (id: ti_")
require.Contains(t, stdout.String(), "- Name: Pro")
require.Contains(t, stdout.String(), "- Message limit: 1234")
require.Contains(t, stderr.String(), "tier pro (id: ti_")
require.Contains(t, stderr.String(), "- Name: Pro")
require.Contains(t, stderr.String(), "- Message limit: 1234")
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "change",
"--message-limit=999",
"--message-expiry-duration=2d",
"--message-expiry-duration=99h",
"--email-limit=91",
"--reservation-limit=98",
"--attachment-file-size-limit=100m",
"--attachment-expiry-duration=1d",
"--attachment-expiry-duration=7h",
"--attachment-total-size-limit=10G",
"--attachment-bandwidth-limit=100G",
"--stripe-monthly-price-id=price_991",
"--stripe-yearly-price-id=price_992",
"--stripe-price-id=price_991",
"pro",
))
require.Contains(t, stdout.String(), "- Message limit: 999")
require.Contains(t, stdout.String(), "- Message expiry duration: 48h")
require.Contains(t, stdout.String(), "- Email limit: 91")
require.Contains(t, stdout.String(), "- Reservation limit: 98")
require.Contains(t, stdout.String(), "- Attachment file size limit: 100.0 MB")
require.Contains(t, stdout.String(), "- Attachment expiry duration: 24h")
require.Contains(t, stdout.String(), "- Attachment total size limit: 10.0 GB")
require.Contains(t, stdout.String(), "- Stripe prices (monthly/yearly): price_991 / price_992")
require.Contains(t, stderr.String(), "- Message limit: 999")
require.Contains(t, stderr.String(), "- Message expiry duration: 99h")
require.Contains(t, stderr.String(), "- Email limit: 91")
require.Contains(t, stderr.String(), "- Reservation limit: 98")
require.Contains(t, stderr.String(), "- Attachment file size limit: 100.0 MB")
require.Contains(t, stderr.String(), "- Attachment expiry duration: 7h")
require.Contains(t, stderr.String(), "- Attachment total size limit: 10.0 GB")
require.Contains(t, stderr.String(), "- Stripe price: price_991")
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTierCommand(app, conf, "remove", "pro"))
require.Contains(t, stdout.String(), "tier pro removed")
require.Contains(t, stderr.String(), "tier pro removed")
}
func runTierCommand(app *cli.App, conf *server.Config, args ...string) error {

View File

@@ -6,8 +6,8 @@ import (
"errors"
"fmt"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/user"
"heckel.io/ntfy/util"
"net/netip"
"time"
)
@@ -72,15 +72,6 @@ Example:
This is a server-only command. It directly reads from user.db as defined in the server config
file server.yml. The command only works if 'auth-file' is properly defined.`,
},
{
Name: "generate",
Usage: "Generates a random token",
Action: execTokenGenerate,
Description: `Randomly generate a token to be used in provisioned tokens.
This command only generates the token value, but does not persist it anywhere.
The output can be used in the 'auth-tokens' config option.`,
},
},
Description: `Manage access tokens for individual users.
@@ -121,19 +112,19 @@ func execTokenAdd(c *cli.Context) error {
return err
}
u, err := manager.User(username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
}
token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified(), false)
token, err := manager.CreateToken(u.ID, label, expires, netip.IPv4Unspecified())
if err != nil {
return err
}
if expires.Unix() == 0 {
fmt.Fprintf(c.App.Writer, "token %s created for user %s, never expires\n", token.Value, u.Name)
fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, never expires\n", token.Value, u.Name)
} else {
fmt.Fprintf(c.App.Writer, "token %s created for user %s, expires %v\n", token.Value, u.Name, expires.Format(time.UnixDate))
fmt.Fprintf(c.App.ErrWriter, "token %s created for user %s, expires %v\n", token.Value, u.Name, expires.Format(time.UnixDate))
}
return nil
}
@@ -150,7 +141,7 @@ func execTokenDel(c *cli.Context) error {
return err
}
u, err := manager.User(username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
@@ -158,7 +149,7 @@ func execTokenDel(c *cli.Context) error {
if err := manager.RemoveToken(u.ID, token); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "token %s for user %s removed\n", token, username)
fmt.Fprintf(c.App.ErrWriter, "token %s for user %s removed\n", token, username)
return nil
}
@@ -174,7 +165,7 @@ func execTokenList(c *cli.Context) error {
var users []*user.User
if username != "" {
u, err := manager.User(username)
if errors.Is(err, user.ErrUserNotFound) {
if err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
} else if err != nil {
return err
@@ -192,15 +183,15 @@ func execTokenList(c *cli.Context) error {
if err != nil {
return err
} else if len(tokens) == 0 && username != "" {
fmt.Fprintf(c.App.Writer, "user %s has no access tokens\n", username)
fmt.Fprintf(c.App.ErrWriter, "user %s has no access tokens\n", username)
return nil
} else if len(tokens) == 0 {
continue
}
usersWithTokens++
fmt.Fprintf(c.App.Writer, "user %s\n", u.Name)
fmt.Fprintf(c.App.ErrWriter, "user %s\n", u.Name)
for _, t := range tokens {
var label, expires, provisioned string
var label, expires string
if t.Label != "" {
label = fmt.Sprintf(" (%s)", t.Label)
}
@@ -209,19 +200,11 @@ func execTokenList(c *cli.Context) error {
} else {
expires = fmt.Sprintf("expires %s", t.Expires.Format(time.RFC822))
}
if t.Provisioned {
provisioned = " (server config)"
}
fmt.Fprintf(c.App.Writer, "- %s%s, %s, accessed from %s at %s%s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822), provisioned)
fmt.Fprintf(c.App.ErrWriter, "- %s%s, %s, accessed from %s at %s\n", t.Value, label, expires, t.LastOrigin.String(), t.LastAccess.Format(time.RFC822))
}
}
if usersWithTokens == 0 {
fmt.Fprintf(c.App.Writer, "no users with tokens\n")
fmt.Fprintf(c.App.ErrWriter, "no users with tokens\n")
}
return nil
}
func execTokenGenerate(c *cli.Context) error {
fmt.Fprintln(c.App.Writer, user.GenerateToken())
return nil
}

View File

@@ -4,8 +4,8 @@ import (
"fmt"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"regexp"
"testing"
)
@@ -14,28 +14,28 @@ func TestCLI_Token_AddListRemove(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stdout.String(), "user phil added with role user")
require.Contains(t, stderr.String(), "user phil added with role user")
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "add", "phil"))
require.Regexp(t, `token tk_.+ created for user phil, never expires`, stdout.String())
require.Regexp(t, `token tk_.+ created for user phil, never expires`, stderr.String())
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "list", "phil"))
require.Regexp(t, `user phil\n- tk_.+, never expires, accessed from 0.0.0.0 at .+`, stdout.String())
require.Regexp(t, `user phil\n- tk_.+, never expires, accessed from 0.0.0.0 at .+`, stderr.String())
re := regexp.MustCompile(`tk_\w+`)
token := re.FindString(stdout.String())
token := re.FindString(stderr.String())
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "remove", "phil", token))
require.Regexp(t, fmt.Sprintf("token %s for user phil removed", token), stdout.String())
require.Regexp(t, fmt.Sprintf("token %s for user phil removed", token), stderr.String())
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runTokenCommand(app, conf, "list"))
require.Equal(t, "no users with tokens\n", stdout.String())
require.Equal(t, "no users with tokens\n", stderr.String())
}
func runTokenCommand(app *cli.App, conf *server.Config, args ...string) error {

View File

@@ -6,14 +6,13 @@ import (
"crypto/subtle"
"errors"
"fmt"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/user"
"os"
"strings"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
"heckel.io/ntfy/v2/util"
"heckel.io/ntfy/util"
)
const (
@@ -26,7 +25,7 @@ func init() {
var flagsUser = append(
append([]cli.Flag{}, flagsDefault...),
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: server.DefaultConfigFile, DefaultText: server.DefaultConfigFile, Usage: "config file"},
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-file", Aliases: []string{"auth_file", "H"}, EnvVars: []string{"NTFY_AUTH_FILE"}, Usage: "auth database file used for access control"}),
altsrc.NewStringFlag(&cli.StringFlag{Name: "auth-default-access", Aliases: []string{"auth_default_access", "p"}, EnvVars: []string{"NTFY_AUTH_DEFAULT_ACCESS"}, Value: "read-write", Usage: "default permissions if no matching entries in the auth database are found"}),
)
@@ -43,7 +42,7 @@ var cmdUser = &cli.Command{
Name: "add",
Aliases: []string{"a"},
Usage: "Adds a new user",
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD_HASH=... ntfy user add [--role=admin|user] USERNAME",
UsageText: "ntfy user add [--role=admin|user] USERNAME\nNTFY_PASSWORD=... ntfy user add [--role=admin|user] USERNAME",
Action: execUserAdd,
Flags: []cli.Flag{
&cli.StringFlag{Name: "role", Aliases: []string{"r"}, Value: string(user.RoleUser), Usage: "user role"},
@@ -56,13 +55,12 @@ granted otherwise by the auth-default-access setting). An admin user has read an
topics.
Examples:
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
NTFY_PASSWORD_HASH=... ntfy user add phil # Add user, using env variable to set password hash (for scripts)
ntfy user add phil # Add regular user phil
ntfy user add --role=admin phil # Add admin user phil
NTFY_PASSWORD=... ntfy user add phil # Add user, using env variable to set password (for scripts)
You may set the NTFY_PASSWORD environment variable to pass the password, or NTFY_PASSWORD_HASH to pass
directly the bcrypt hash. This is useful if you are creating users via scripts.
You may set the NTFY_PASSWORD environment variable to pass the password. This is useful if
you are creating users via scripts.
`,
},
{
@@ -81,7 +79,7 @@ Example:
Name: "change-pass",
Aliases: []string{"chp"},
Usage: "Changes a user's password",
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME\nNTFY_PASSWORD_HASH=... ntfy user change-pass USERNAME",
UsageText: "ntfy user change-pass USERNAME\nNTFY_PASSWORD=... ntfy user change-pass USERNAME",
Action: execUserChangePass,
Description: `Change the password for the given user.
@@ -91,10 +89,10 @@ it twice.
Example:
ntfy user change-pass phil
NTFY_PASSWORD=.. ntfy user change-pass phil
NTFY_PASSWORD_HASH=.. ntfy user change-pass phil
You may set the NTFY_PASSWORD environment variable to pass the new password or NTFY_PASSWORD_HASH to pass
directly the bcrypt hash. This is useful if you are updating users via scripts.
You may set the NTFY_PASSWORD environment variable to pass the new password. This is
useful if you are updating users via scripts.
`,
},
{
@@ -133,22 +131,6 @@ as messages per day, attachment file sizes, etc.
Example:
ntfy user change-tier phil pro # Change tier to "pro" for user "phil"
ntfy user change-tier phil - # Remove tier from user "phil" entirely
`,
},
{
Name: "hash",
Usage: "Create password hash for a predefined user",
UsageText: "ntfy user hash",
Action: execUserHash,
Description: `Asks for a password and creates a bcrypt password hash.
This command is useful to create a password hash for a user, which can then be used
for predefined users in the server config file, in auth-users.
Example:
$ ntfy user hash
(asks for password and confirmation)
$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C
`,
},
{
@@ -192,12 +174,7 @@ variable to pass the new password. This is useful if you are creating/updating u
func execUserAdd(c *cli.Context) error {
username := c.Args().Get(0)
role := user.Role(c.String("role"))
password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH")
if !hashed {
password = os.Getenv("NTFY_PASSWORD")
}
password := os.Getenv("NTFY_PASSWORD")
if username == "" {
return errors.New("username expected, type 'ntfy user add --help' for help")
} else if username == userEveryone || username == user.Everyone {
@@ -211,7 +188,7 @@ func execUserAdd(c *cli.Context) error {
}
if user, _ := manager.User(username); user != nil {
if c.Bool("ignore-exists") {
fmt.Fprintf(c.App.Writer, "user %s already exists (exited successfully)\n", username)
fmt.Fprintf(c.App.ErrWriter, "user %s already exists (exited successfully)\n", username)
return nil
}
return fmt.Errorf("user %s already exists", username)
@@ -221,12 +198,13 @@ func execUserAdd(c *cli.Context) error {
if err != nil {
return err
}
password = p
}
if err := manager.AddUser(username, password, role, hashed); err != nil {
if err := manager.AddUser(username, password, role); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "user %s added with role %s\n", username, role)
fmt.Fprintf(c.App.ErrWriter, "user %s added with role %s\n", username, role)
return nil
}
@@ -241,23 +219,19 @@ func execUserDel(c *cli.Context) error {
if err != nil {
return err
}
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
if _, err := manager.User(username); err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if err := manager.RemoveUser(username); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "user %s removed\n", username)
fmt.Fprintf(c.App.ErrWriter, "user %s removed\n", username)
return nil
}
func execUserChangePass(c *cli.Context) error {
username := c.Args().Get(0)
password, hashed := os.LookupEnv("NTFY_PASSWORD_HASH")
if !hashed {
password = os.Getenv("NTFY_PASSWORD")
}
password := os.Getenv("NTFY_PASSWORD")
if username == "" {
return errors.New("username expected, type 'ntfy user change-pass --help' for help")
} else if username == userEveryone || username == user.Everyone {
@@ -267,7 +241,7 @@ func execUserChangePass(c *cli.Context) error {
if err != nil {
return err
}
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
if _, err := manager.User(username); err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if password == "" {
@@ -276,10 +250,10 @@ func execUserChangePass(c *cli.Context) error {
return err
}
}
if err := manager.ChangePassword(username, password, hashed); err != nil {
if err := manager.ChangePassword(username, password); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "changed password for user %s\n", username)
fmt.Fprintf(c.App.ErrWriter, "changed password for user %s\n", username)
return nil
}
@@ -295,26 +269,13 @@ func execUserChangeRole(c *cli.Context) error {
if err != nil {
return err
}
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
if _, err := manager.User(username); err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if err := manager.ChangeRole(username, role); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "changed role for user %s to %s\n", username, role)
return nil
}
func execUserHash(c *cli.Context) error {
password, err := readPasswordAndConfirm(c)
if err != nil {
return err
}
hash, err := user.HashPassword(password)
if err != nil {
return fmt.Errorf("failed to hash password: %w", err)
}
fmt.Fprintln(c.App.Writer, hash)
fmt.Fprintf(c.App.ErrWriter, "changed role for user %s to %s\n", username, role)
return nil
}
@@ -332,19 +293,19 @@ func execUserChangeTier(c *cli.Context) error {
if err != nil {
return err
}
if _, err := manager.User(username); errors.Is(err, user.ErrUserNotFound) {
if _, err := manager.User(username); err == user.ErrUserNotFound {
return fmt.Errorf("user %s does not exist", username)
}
if tier == tierReset {
if err := manager.ResetTier(username); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "removed tier from user %s\n", username)
fmt.Fprintf(c.App.ErrWriter, "removed tier from user %s\n", username)
} else {
if err := manager.ChangeTier(username, tier); err != nil {
return err
}
fmt.Fprintf(c.App.Writer, "changed tier for user %s to %s\n", username, tier)
fmt.Fprintf(c.App.ErrWriter, "changed tier for user %s to %s\n", username, tier)
}
return nil
}
@@ -374,15 +335,7 @@ func createUserManager(c *cli.Context) (*user.Manager, error) {
if err != nil {
return nil, errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
}
authConfig := &user.Config{
Filename: authFile,
StartupQueries: authStartupQueries,
DefaultAccess: authDefault,
ProvisionEnabled: false, // Hack: Do not re-provision users on manager initialization
BcryptCost: user.DefaultUserPasswordBcryptCost,
QueueWriterInterval: user.DefaultUserStatsQueueWriterInterval,
}
return user.NewManager(authConfig)
return user.NewManager(authFile, authStartupQueries, authDefault, user.DefaultUserPasswordBcryptCost, user.DefaultUserStatsQueueWriterInterval)
}
func readPasswordAndConfirm(c *cli.Context) (string, error) {
@@ -390,8 +343,6 @@ func readPasswordAndConfirm(c *cli.Context) (string, error) {
password, err := util.ReadPassword(c.App.Reader)
if err != nil {
return "", err
} else if len(password) == 0 {
return "", errors.New("password cannot be empty")
}
fmt.Fprintf(c.App.ErrWriter, "\r%s\rconfirm: ", strings.Repeat(" ", 25))
confirm, err := util.ReadPassword(c.App.Reader)

View File

@@ -3,9 +3,9 @@ package cmd
import (
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
"heckel.io/ntfy/v2/test"
"heckel.io/ntfy/v2/user"
"heckel.io/ntfy/server"
"heckel.io/ntfy/test"
"heckel.io/ntfy/user"
"os"
"path/filepath"
"testing"
@@ -15,20 +15,20 @@ func TestCLI_User_Add(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stdout.String(), "user phil added with role user")
require.Contains(t, stderr.String(), "user phil added with role user")
}
func TestCLI_User_Add_Exists(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stdout.String(), "user phil added with role user")
require.Contains(t, stderr.String(), "user phil added with role user")
app, stdin, _, _ = newTestApp()
stdin.WriteString("mypass\nmypass")
@@ -41,10 +41,10 @@ func TestCLI_User_Add_Admin(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
defer test.StopServer(t, s, port)
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "--role=admin", "phil"))
require.Contains(t, stdout.String(), "user phil added with role admin")
require.Contains(t, stderr.String(), "user phil added with role admin")
}
func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
@@ -60,27 +60,19 @@ func TestCLI_User_Add_Password_Mismatch(t *testing.T) {
func TestCLI_User_ChangePass(t *testing.T) {
s, conf, port := newTestServerWithAuth(t)
conf.AuthUsers = []*user.User{
{Name: "philuser", Hash: "$2a$10$U4WSIYY6evyGmZaraavM2e2JeVG6EMGUKN1uUwufUeeRd4Jpg6cGC", Role: user.RoleUser}, // philuser:philpass
}
defer test.StopServer(t, s, port)
// Add user
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stdout.String(), "user phil added with role user")
require.Contains(t, stderr.String(), "user phil added with role user")
// Change pass
app, stdin, stdout, _ = newTestApp()
app, stdin, _, stderr = newTestApp()
stdin.WriteString("newpass\nnewpass")
require.Nil(t, runUserCommand(app, conf, "change-pass", "phil"))
require.Contains(t, stdout.String(), "changed password for user phil")
// Cannot change provisioned user's pass
app, stdin, _, _ = newTestApp()
stdin.WriteString("newpass\nnewpass")
require.Error(t, runUserCommand(app, conf, "change-pass", "philuser"))
require.Contains(t, stderr.String(), "changed password for user phil")
}
func TestCLI_User_ChangeRole(t *testing.T) {
@@ -88,15 +80,15 @@ func TestCLI_User_ChangeRole(t *testing.T) {
defer test.StopServer(t, s, port)
// Add user
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stdout.String(), "user phil added with role user")
require.Contains(t, stderr.String(), "user phil added with role user")
// Change role
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runUserCommand(app, conf, "change-role", "phil", "admin"))
require.Contains(t, stdout.String(), "changed role for user phil to admin")
require.Contains(t, stderr.String(), "changed role for user phil to admin")
}
func TestCLI_User_Delete(t *testing.T) {
@@ -104,15 +96,15 @@ func TestCLI_User_Delete(t *testing.T) {
defer test.StopServer(t, s, port)
// Add user
app, stdin, stdout, _ := newTestApp()
app, stdin, _, stderr := newTestApp()
stdin.WriteString("mypass\nmypass")
require.Nil(t, runUserCommand(app, conf, "add", "phil"))
require.Contains(t, stdout.String(), "user phil added with role user")
require.Contains(t, stderr.String(), "user phil added with role user")
// Delete user
app, _, stdout, _ = newTestApp()
app, _, _, stderr = newTestApp()
require.Nil(t, runUserCommand(app, conf, "del", "phil"))
require.Contains(t, stdout.String(), "user phil removed")
require.Contains(t, stderr.String(), "user phil removed")
// Delete user again (does not exist)
app, _, _, _ = newTestApp()

View File

@@ -1,69 +0,0 @@
//go:build !noserver && !nowebpush
package cmd
import (
"fmt"
"os"
"github.com/SherClockHolmes/webpush-go"
"github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc"
)
var flagsWebPush = append(
[]cli.Flag{},
altsrc.NewStringFlag(&cli.StringFlag{Name: "output-file", Aliases: []string{"f"}, Usage: "write VAPID keys to this file"}),
)
func init() {
commands = append(commands, cmdWebPush)
}
var cmdWebPush = &cli.Command{
Name: "webpush",
Usage: "Generate keys, in the future manage web push subscriptions",
UsageText: "ntfy webpush [keys]",
Category: categoryServer,
Subcommands: []*cli.Command{
{
Action: generateWebPushKeys,
Name: "keys",
Usage: "Generate VAPID keys to enable browser background push notifications",
UsageText: "ntfy webpush keys",
Category: categoryServer,
Flags: flagsWebPush,
},
},
}
func generateWebPushKeys(c *cli.Context) error {
privateKey, publicKey, err := webpush.GenerateVAPIDKeys()
if err != nil {
return err
}
if outputFile := c.String("output-file"); outputFile != "" {
contents := fmt.Sprintf(`---
web-push-public-key: %s
web-push-private-key: %s
`, publicKey, privateKey)
err = os.WriteFile(outputFile, []byte(contents), 0660)
if err != nil {
return err
}
_, err = fmt.Fprintf(c.App.Writer, "Web Push keys written to %s.\n", outputFile)
} else {
_, err = fmt.Fprintf(c.App.Writer, `Web Push keys generated. Add the following lines to your config file:
web-push-public-key: %s
web-push-private-key: %s
web-push-file: /var/cache/ntfy/webpush.db # or similar
web-push-email-address: <email address>
See https://ntfy.sh/docs/config/#web-push for details.
`, publicKey, privateKey)
}
return err
}

View File

@@ -1,34 +0,0 @@
package cmd
import (
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v2"
"heckel.io/ntfy/v2/server"
)
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
app, _, stdout, _ := newTestApp()
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys"))
require.Contains(t, stdout.String(), "Web Push keys generated.")
}
func TestCLI_WebPush_WriteKeysToFile(t *testing.T) {
tempDir := t.TempDir()
t.Chdir(tempDir)
app, _, stdout, _ := newTestApp()
require.Nil(t, runWebPushCommand(app, server.NewConfig(), "keys", "--output-file=key-file.yaml"))
require.Contains(t, stdout.String(), "Web Push keys written to key-file.yaml")
require.FileExists(t, filepath.Join(tempDir, "key-file.yaml"))
}
func runWebPushCommand(app *cli.App, conf *server.Config, args ...string) error {
webPushArgs := []string{
"ntfy",
"--log-level=ERROR",
"webpush",
}
return app.Run(append(webPushArgs, args...))
}

View File

@@ -1,3 +1,4 @@
version: "2.1"
services:
ntfy:
image: binwiederhier/ntfy
@@ -13,3 +14,4 @@ services:
ports:
- 80:80
restart: unless-stopped

View File

@@ -1,50 +0,0 @@
{% extends "base.html" %}
{% block announce %}
<style>
div[data-md-component="announce"] {
z-index: 10;
}
div[data-md-component="announce"] a {
color: white;
}
div[data-md-component="announce"] a:hover, div[data-md-component="announce"] a:focus {
transition: ease-in 150ms;
color: #ccc;
}
div[data-md-component="announce"] .md-banner__button {
color: #ccc;
}
div[data-md-component="announce"] .md-banner.hidden {
display: none;
}
div[data-md-component="announce"] .twemoji {
margin-top: 2px;
}
</style>
<button id="announce-bar-close" class="md-banner__button md-icon" aria-label="Don't show this again">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41Z"></path>
</svg>
</button>
If you like ntfy, please consider sponsoring me via <a target="_blank" href="https://github.com/sponsors/binwiederhier"><strong>GitHub Sponsors</strong></a>
or <a target="_blank" href="https://en.liberapay.com/ntfy/"><strong>Liberapay</strong></a>
<svg xmlns="http://www.w3.org/2000/svg" role="img" viewBox="0 0 36 36" class="twemoji md-footer-custom-text">
<path fill="#DD2E44" d="M35.885 11.833c0-5.45-4.418-9.868-9.867-9.868-3.308 0-6.227 1.633-8.018 4.129-1.791-2.496-4.71-4.129-8.017-4.129-5.45 0-9.868 4.417-9.868 9.868 0 .772.098 1.52.266 2.241C1.751 22.587 11.216 31.568 18 34.034c6.783-2.466 16.249-11.447 17.617-19.959.17-.721.268-1.469.268-2.242z"/>
</svg>, or subscribing to <a target="_blank" href="https://ntfy.sh/app"><strong>ntfy Pro</strong></a>.
<script>
announceBarKey = 'announce-bar-closed-sponsor';
document.getElementById('announce-bar-close').addEventListener('click', (e) => {
localStorage.setItem(announceBarKey, 'true');
document.querySelector('div[data-md-component="announce"] .md-banner').style.display = 'none';
});
if (localStorage.getItem(announceBarKey) === 'true') {
document.querySelector('div[data-md-component="announce"] .md-banner').style.display = 'none';
}
</script>
{% endblock %}

View File

@@ -18,13 +18,13 @@ get a list of [command line options](#command-line-options).
## Example config
!!! info
Definitely check out the **[server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)** file. It contains examples and detailed descriptions of all the settings.
You may also want to look at how ntfy.sh is configured in the [ntfy-ansible](https://github.com/binwiederhier/ntfy-ansible) repository.
Definitely check out the **[server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml)** file.
It contains examples and detailed descriptions of all the settings.
The most basic settings are `base-url` (the external URL of the ntfy server), the HTTP/HTTPS listen address (`listen-http`
and `listen-https`), and socket path (`listen-unix`). All the other things are additional features.
Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
Here are a few working sample configs:
=== "server.yml (HTTP-only, with cache + attachments)"
``` yaml
@@ -44,15 +44,6 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
attachment-cache-dir: "/var/cache/ntfy/attachments"
```
=== "server.yml (behind proxy, with cache + attachments)"
``` yaml
base-url: "http://ntfy.example.com"
listen-http: ":2586"
cache-file: "/var/cache/ntfy/cache.db"
attachment-cache-dir: "/var/cache/ntfy/attachments"
behind-proxy: true
```
=== "server.yml (ntfy.sh config)"
``` yaml
# All the things: Behind a proxy, Firebase, cache, attachments,
@@ -74,57 +65,6 @@ Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
keepalive-interval: "45s"
```
Alternatively, you can also use command line arguments or environment variables to configure the server. Here's an example
using Docker Compose (i.e. `docker-compose.yml`):
=== "Docker Compose (w/ auth, cache, attachments)"
``` yaml
services:
ntfy:
image: binwiederhier/ntfy
restart: unless-stopped
environment:
NTFY_BASE_URL: http://ntfy.example.com
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_AUTH_USERS: 'phil:$$2a$$10$$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin' # Must escape '$' as '$$'
NTFY_BEHIND_PROXY: true
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: true
volumes:
- ./:/var/lib/ntfy
ports:
- 80:80
command: serve
```
=== "Docker Compose (w/ auth, cache, web push, iOS)"
``` yaml
services:
ntfy:
image: binwiederhier/ntfy
restart: unless-stopped
environment:
NTFY_BASE_URL: http://ntfy.example.com
NTFY_CACHE_FILE: /var/lib/ntfy/cache.db
NTFY_AUTH_FILE: /var/lib/ntfy/auth.db
NTFY_AUTH_DEFAULT_ACCESS: deny-all
NTFY_BEHIND_PROXY: true
NTFY_ATTACHMENT_CACHE_DIR: /var/lib/ntfy/attachments
NTFY_ENABLE_LOGIN: true
NTFY_UPSTREAM_BASE_URL: https://ntfy.sh
NTFY_WEB_PUSH_PUBLIC_KEY: <public_key>
NTFY_WEB_PUSH_PRIVATE_KEY: <private_key>
NTFY_WEB_PUSH_FILE: /var/lib/ntfy/webpush.db
NTFY_WEB_PUSH_EMAIL_ADDRESS: <email>
volumes:
- ./:/var/lib/ntfy
ports:
- 8093:80
command: serve
```
## Message cache
If desired, ntfy can temporarily keep notifications in an in-memory or an on-disk cache. Caching messages for a short period
of time is important to allow [phones](subscribe/phone.md) and other devices with brittle Internet connections to be able to retrieve
@@ -189,31 +129,19 @@ ntfy's auth is implemented with a simple [SQLite](https://www.sqlite.org/)-based
(`user` and `admin`) and per-topic `read` and `write` permissions using an [access control list (ACL)](https://en.wikipedia.org/wiki/Access-control_list).
Access control entries can be applied to users as well as the special everyone user (`*`), which represents anonymous API access.
To set up auth, **configure the following options**:
To set up auth, simply **configure the following two options**:
* `auth-file` is the user/access database; it is created automatically if it doesn't already exist; suggested
location `/var/lib/ntfy/user.db` (easiest if deb/rpm package is used)
* `auth-default-access` defines the default/fallback access if no access control entry is found; it can be
set to `read-write` (default), `read-only`, `write-only` or `deny-all`. **If you are setting up a private instance,
you'll want to set this to `deny-all`** (see [private instance example](#example-private-instance)).
set to `read-write` (default), `read-only`, `write-only` or `deny-all`.
Once configured, you can use
- the `ntfy user` command and the `auth-users` config option to [add or modify users](#users-and-roles)
- the `ntfy access` command and the `auth-access` option to [modify the access control list](#access-control-list-acl)
and topic patterns, and
- the `ntfy token` command and the `auth-tokens` config option to [manage access tokens](#access-tokens) for users.
These commands **directly edit the auth database** (as defined in `auth-file`), so they only work on the server,
and only if the user accessing them has the right permissions.
Once configured, you can use the `ntfy user` command to [add or modify users](#users-and-roles), and the `ntfy access` command
lets you [modify the access control list](#access-control-list-acl) for specific users and topic patterns. Both of these
commands **directly edit the auth database** (as defined in `auth-file`), so they only work on the server, and only if the user
accessing them has the right permissions.
### Users and roles
Users can be added to the ntfy user database in two different ways
* [Using the CLI](#users-via-the-cli): Using the `ntfy user` command, you can manually add/update/remove users.
* [In the config](#users-via-the-config): You can provision users in the `server.yml` file via `auth-users` key.
#### Users via the CLI
The `ntfy user` command allows you to add/remove/change users in the ntfy user database, as well as change
passwords or roles (`user` or `admin`). In practice, you'll often just create one admin
user with `ntfy user add --role=admin ...` and be done with all this (see [example below](#example-private-instance)).
@@ -234,54 +162,12 @@ ntfy user del phil # Delete user phil
ntfy user change-pass phil # Change password for user phil
ntfy user change-role phil admin # Make user phil an admin
ntfy user change-tier phil pro # Change phil's tier to "pro"
ntfy user hash # Generate password hash, use with auth-users config option
```
#### Users via the config
As an alternative to manually creating users via the `ntfy user` CLI command, you can provision users declaratively in
the `server.yml` file by adding them to the `auth-users` array. This is useful for general admins, or if you'd like to
deploy your ntfy server via Docker/Ansible without manually editing the database.
The `auth-users` option is a list of users that are automatically created/updated when the server starts. Users
previously defined in the config but later removed will be deleted. Each entry is defined in the format `<username>:<password-hash>:<role>`.
Here's an example with two users: `phil` is an admin, `ben` is a regular user.
=== "Declarative users in /etc/ntfy/server.yml"
``` yaml
auth-file: "/var/lib/ntfy/user.db"
auth-users:
- "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin"
- "ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user"
```
=== "Declarative users via env variables"
```
# Comma-separated list, use single quotes to avoid issues with the bcrypt hash
NTFY_AUTH_FILE='/var/lib/ntfy/user.db'
NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user'
```
The password hash can be created using `ntfy user hash` or an [online bcrypt generator](https://bcrypt-generator.com/) (though
note that you're putting your password in an untrusted website).
!!! important
Users added declaratively via the config file are marked in the database as "provisioned users". Removing users
from the config file will **delete them from the database** the next time ntfy is restarted.
Also, users that were originally manually created will be "upgraded" to be provisioned users if they are added to
the config. Adding a user manually, then adding it to the config, and then removing it from the config will hence
lead to the **deletion of that user**.
### Access control list (ACL)
The access control list (ACL) **manages access to topics for non-admin users, and for anonymous access (`everyone`/`*`)**.
Each entry represents the access permissions for a user to a specific topic or topic pattern. Entries can be created in
two different ways:
Each entry represents the access permissions for a user to a specific topic or topic pattern.
* [Using the CLI](#acl-entries-via-the-cli): Using the `ntfy access` command, you can manually edit the access control list.
* [In the config](#acl-entries-via-the-config): You can provision ACL entries in the `server.yml` file via `auth-access` key.
#### ACL entries via the CLI
The ACL can be displayed or modified with the `ntfy access` command:
```
@@ -337,51 +223,6 @@ User `ben` has three topic-specific entries. He can read, but not write to topic
to topic `garagedoor` and all topics starting with the word `alerts` (wildcards). Clients that are not authenticated
(called `*`/`everyone`) only have read access to the `announcements` and `server-stats` topics.
#### ACL entries via the config
As an alternative to manually creating ACL entries via the `ntfy access` CLI command, you can provision access control
entries declaratively in the `server.yml` file by adding them to the `auth-access` array, similar to the `auth-users`
option (see [users via the config](#users-via-the-config).
The `auth-access` option is a list of access control entries that are automatically created/updated when the server starts.
When entries are removed, they are deleted from the database. Each entry is defined in the format `<username>:<topic-pattern>:<access>`.
The `<username>` can be any existing, provisioned user as defined in the `auth-users` section (see [users via the config](#users-via-the-config)),
or `everyone`/`*` for anonymous access. The `<topic-pattern>` can be a specific topic name or a pattern with wildcards (`*`). The
`<access>` can be one of the following:
* `read-write` or `rw`: Allows both publishing to and subscribing to the topic
* `read-only`, `read`, or `ro`: Allows only subscribing to the topic
* `write-only`, `write`, or `wo`: Allows only publishing to the topic
* `deny-all`, `deny`, or `none`: Denies all access to the topic
Here's an example with several ACL entries:
=== "Declarative ACL entries in /etc/ntfy/server.yml"
``` yaml
auth-file: "/var/lib/ntfy/user.db"
auth-users:
- "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user"
- "ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user"
auth-access:
- "phil:mytopic:rw"
- "ben:alerts-*:rw"
- "ben:system-logs:ro"
- "*:announcements:ro" # or: "everyone:announcements,ro"
```
=== "Declarative ACL entries via env variables"
```
# Comma-separated list
NTFY_AUTH_FILE='/var/lib/ntfy/user.db'
NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:user,ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user'
NTFY_AUTH_ACCESS='phil:mytopic:rw,ben:alerts-*:rw,ben:system-logs:ro,*:announcements:ro'
```
In this example, the `auth-users` section defines two users, `phil` and `ben`. The `auth-access` section defines
access control entries for these users. `phil` has read-write access to the topic `mytopic`, while `ben` has read-write
access to all topics starting with `alerts-` and read-only access to the topic `system-logs`. The last entry allows
anonymous users (i.e. clients that do not authenticate) to read the `announcements` topic.
### Access tokens
In addition to username/password auth, ntfy also provides authentication via access tokens. Access tokens are useful
to avoid having to configure your password across multiple publishing/subscribing applications. For instance, you may
@@ -392,14 +233,8 @@ want to use a dedicated token to publish from your backup host, and one from you
and deleting the account, every action can be performed with a token. Granular access tokens are on the roadmap,
but not yet implemented.
You can create access tokens in two different ways:
* [Using the CLI](#tokens-via-the-cli): Using the `ntfy token` command, you can manually add/update/remove tokens.
* [In the config](#tokens-via-the-config): You can provision access tokens in the `server.yml` file via `auth-tokens` key.
#### Tokens via the CLI
The `ntfy token` command can be used to manage access tokens for users. Tokens can have labels, and they can expire
automatically (or never expire). Each user can have up to 60 tokens (hardcoded).
automatically (or never expire). Each user can have up to 20 tokens (hardcoded).
**Example commands** (type `ntfy token --help` or `ntfy token COMMAND --help` for more details):
```
@@ -408,7 +243,6 @@ ntfy token list phil # Shows list of tokens for user phil
ntfy token add phil # Create token for user phil which never expires
ntfy token add --expires=2d phil # Create token for user phil which expires in 2 days
ntfy token remove phil tk_th2sxr... # Delete token
ntfy token generate # Generate random token, can be used in auth-tokens config option
```
**Creating an access token:**
@@ -416,89 +250,32 @@ ntfy token generate # Generate random token, can be used in aut
$ ntfy token add --expires=30d --label="backups" phil
$ ntfy token list
user phil
- tk_7eevizlsiwf9yi4uxsrs83r4352o0 (backups), expires 15 Mar 23 14:33 EDT, accessed from 0.0.0.0 at 13 Feb 23 13:33 EST
- tk_AgQdq7mVBoFD37zQVN29RhuMzNIz2 (backups), expires 15 Mar 23 14:33 EDT, accessed from 0.0.0.0 at 13 Feb 23 13:33 EST
```
Once an access token is created, you can **use it to authenticate against the ntfy server, e.g. when you publish or
subscribe to topics**. To learn how, check out [authenticate via access tokens](publish.md#access-tokens).
#### Tokens via the config
Access tokens can be pre-provisioned in the `server.yml` configuration file using the `auth-tokens` config option.
This is useful for automated setups, Docker environments, or when you want to define tokens declaratively.
The `auth-tokens` option is a list of access tokens that are automatically created/updated when the server starts.
When entries are removed, they are deleted from the database. Each entry is defined in the format `<username>:<token>[:<label>]`.
The `<username>` must be an existing, provisioned user, as defined in the `auth-users` section (see [users via the config](#users-via-the-config)).
The `<token>` is a valid access token, which must start with `tk_` and be 32 characters long (including the prefix). You can generate
random tokens using the `ntfy token generate` command. The optional `<label>` is a human-readable label for the token,
which can be used to identify it later.
Once configured, these tokens can be used to authenticate API requests just like tokens created via the CLI.
For usage examples, see [authenticate via access tokens](publish.md#access-tokens).
Here's an example:
=== "Declarative tokens in /etc/ntfy/server.yml"
``` yaml
auth-file: "/var/lib/ntfy/user.db"
auth-users:
- "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin"
- "backup-service:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user"
auth-tokens:
- "phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76"
- "backup-service:tk_f099we8uzj7xi5qshzajwp6jffvkz:Backup script"
```
=== "Declarative tokens via env variables"
```
# Comma-separated list
NTFY_AUTH_FILE='/var/lib/ntfy/user.db'
NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,ben:$2a$10$NKbrNb7HPMjtQXWJ0f1pouw03LDLT/WzlO9VAv44x84bRCkh19h6m:user'
NTFY_AUTH_TOKENS='phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76,backup-service:tk_f099we8uzj7xi5qshzajwp6jffvkz:Backup script'
```
In this example, the `auth-users` section defines two users, `phil` and `backup-service`. The `auth-tokens` section
defines access tokens for these users. `phil` has a token `tk_3gd7d2yftt4b8ixyfe9mnmro88o76`, while `backup-service`
has a token `tk_f099we8uzj7xi5qshzajwp6jffvkz` with the label "Backup script".
### Example: Private instance
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`,
and to configure users in the `auth-users` section (see [users via the config](#users-via-the-config)),
access control entries in the `auth-access` section (see [ACL entries via the config](#acl-entries-via-the-config)),
and access tokens in the `auth-tokens` section (see [access tokens via the config](#tokens-via-the-config)).
The easiest way to configure a private instance is to set `auth-default-access` to `deny-all` in the `server.yml`:
Here's an example that defines a single admin user `phil` with the password `mypass`, and a regular user `backup-script`
with the password `backup-script`. The admin user has full access to all topics, while regular user can only
access the `backups` topic with read-write permissions. The `auth-default-access` is set to `deny-all`, which means
that all other users and anonymous access are denied by default.
=== "Config via /etc/ntfy/server.yml"
=== "/etc/ntfy/server.yml"
``` yaml
auth-file: "/var/lib/ntfy/user.db"
auth-default-access: "deny-all"
auth-users:
- "phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin"
- "backup-script:$2a$10$/ehiQt.w7lhTmHXq.RNsOOkIwiPPeWFIzWYO3DRxNixnWKLX8.uj.:user"
auth-access:
- "backup-service:backups:rw"
auth-tokens:
- "phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76:My personal token"
```
=== "Config via env variables"
``` yaml
NTFY_AUTH_FILE='/var/lib/ntfy/user.db'
NTFY_AUTH_DEFAULT_ACCESS='deny-all'
NTFY_AUTH_USERS='phil:$2a$10$YLiO8U21sX1uhZamTLJXHuxgVC0Z/GKISibrKCLohPgtG7yIxSk4C:admin,backup-script:$2a$10$/ehiQt.w7lhTmHXq.RNsOOkIwiPPeWFIzWYO3DRxNixnWKLX8.uj.:user'
NTFY_AUTH_ACCESS='backup-service:backups:rw'
NTFY_AUTH_TOKENS='phil:tk_3gd7d2yftt4b8ixyfe9mnmro88o76:My personal token'
```
After that, simply create an `admin` user:
```
$ ntfy user add --role=admin phil
password: mypass
confirm: mypass
user phil added with role admin
```
Once you've done that, you can publish and subscribe using [Basic Auth](https://en.wikipedia.org/wiki/Basic_access_authentication)
with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password.
Here's a simple example (using the credentials of the `phil` user):
with the given username/password. Be sure to use HTTPS to avoid eavesdropping and exposing your password. Here's a simple example:
=== "Command line (curl)"
```
@@ -567,10 +344,10 @@ Here's a simple example (using the credentials of the `phil` user):
```
### Example: UnifiedPush
[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/developers/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
has anonymous write access to the [topic](https://unifiedpush.org/developers/spec/definitions/#endpoint) used for push messages.
[UnifiedPush](https://unifiedpush.org) requires that the [application server](https://unifiedpush.org/spec/definitions/#application-server) (e.g. Synapse, Fediverse Server, …)
has anonymous write access to the [topic](https://unifiedpush.org/spec/definitions/#endpoint) used for push messages.
The topic names used by UnifiedPush all start with the `up*` prefix. Please refer to the
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users-acl)** for more details.
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users)** for more details.
To enable support for UnifiedPush for private servers (i.e. `auth-default-access: "deny-all"`), you should either
allow anonymous write access for the entire prefix or explicitly per topic:
@@ -681,31 +458,6 @@ $ dig A mx1.ntfy.sh +short
3.139.215.220
```
### Local-only email
If you want to send emails from an internal service on the same network as your ntfy instance, you do not need to
worry about DNS records at all. Define a port for the SMTP server and pick an SMTP server domain (can be
anything).
=== "/etc/ntfy/server.yml"
``` yaml
smtp-server-listen: ":25"
smtp-server-domain: "example.com"
smtp-server-addr-prefix: "ntfy-" # optional
```
Then, in the email settings of your internal service, set the SMTP server address to the IP address of your
ntfy instance. Set the port to the value you defined in `smtp-server-listen`. Leave any username and password
fields empty. In the "From" address, pick anything (e.g., "alerts@ntfy.sh"); the value doesn't matter.
In the "To" address, put in an email address that follows this pattern: `[topic]@[smtp-server-domain]` (or
`[smtp-server-addr-prefix][topic]@[smtp-server-domain]` if you set `smtp-server-addr-prefix`).
So if you used `example.com` as the SMTP server domain, and you want to send a message to the `email-alerts`
topic, set the "To" address to `email-alerts@example.com`. If the topic has access restrictions, you will need
to include an access token in the "To" address, such as `email-alerts+tk_AbC123dEf456@example.com`.
If the internal service lets you use define an email "Subject", it will become the title of the notification.
The body of the email will become the message of the notification.
## 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
@@ -715,91 +467,17 @@ It may be desirable to run ntfy behind a proxy (e.g. nginx, HAproxy or Apache),
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.
### IP-based rate limiting
If you are running ntfy behind a proxy, you should set the `behind-proxy` flag. This will instruct the
[rate limiting](#rate-limiting) logic to use the header configured in `proxy-forwarded-header` (default is `X-Forwarded-For`)
as the primary identifier for a visitor, as opposed to the remote IP address.
[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.
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.
Relevant flags to consider:
* `behind-proxy` makes it so that the real visitor IP address is extracted from the header defined in `proxy-forwarded-header`.
Without this, the remote address of the incoming connection is used (default: `false`).
* `proxy-forwarded-header` is the header to use to identify visitors (default: `X-Forwarded-For`). It may be a single IP address (e.g. `1.2.3.4`),
a comma-separated list of IP addresses (e.g. `1.2.3.4, 5.6.7.8`), or an [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239)-style
header (e.g. `for=1.2.3.4;by=proxy.example.com, for=5.6.7.8`).
* `proxy-trusted-hosts` is a comma-separated list of IP addresses, hosts or CIDRs that are removed from the forwarded header
to determine the real IP address. This is only useful if there are multiple proxies involved that add themselves to
the forwarded header (default: empty).
* `visitor-prefix-bits-ipv4` is the number of bits of the IPv4 address to use for rate limiting (default is `32`, which is the entire
IP address). In IPv4 environments, by default, a visitor's **full IPv4 address** is used as-is for rate limiting. This means that
if someone publishes messages from multiple IP addresses, they will be counted as separate visitors. You can adjust this by setting the `visitor-prefix-bits-ipv4` config option. To group visitors in a /24 subnet and count them as one, for instance,
set it to `24`. In that case, `1.2.3.4` and `1.2.3.99` are treated as the same visitor.
* `visitor-prefix-bits-ipv6` is the number of bits of the IPv6 address to use for rate limiting (default is `64`, which is a /64 subnet).
In IPv6 environments, by default, a visitor's IP address is **truncated to the /64 subnet**, meaning that `2001:db8:25:86:1::1` and
`2001:db8:25:86:2::1` are treated as the same visitor. Use the `visitor-prefix-bits-ipv6` config option to adjust this behavior.
See [IPv6 considerations](#ipv6-considerations) for more details.
=== "/etc/ntfy/server.yml (behind a proxy)"
=== "/etc/ntfy/server.yml"
``` yaml
# Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting
#
# Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set,
# the visitor IP will be 1.2.3.4 (right-most address).
#
# Tell ntfy to use "X-Forwarded-For" to identify visitors
behind-proxy: true
```
=== "/etc/ntfy/server.yml (X-Client-IP header)"
``` yaml
# Tell ntfy to use "X-Client-IP" header to identify visitors for rate limiting
#
# Example: If "X-Client-IP: 9.9.9.9" is set,
# the visitor IP will be 9.9.9.9.
#
behind-proxy: true
proxy-forwarded-header: "X-Client-IP"
```
=== "/etc/ntfy/server.yml (Forwarded header)"
``` yaml
# Tell ntfy to use "Forwarded" header (RFC 7239) to identify visitors for rate limiting
#
# Example: If "Forwarded: for=1.2.3.4;by=proxy.example.com, for=9.9.9.9" is set,
# the visitor IP will be 9.9.9.9.
#
behind-proxy: true
proxy-forwarded-header: "Forwarded"
```
=== "/etc/ntfy/server.yml (multiple proxies)"
``` yaml
# Tell ntfy to use "X-Forwarded-For" header to identify visitors for rate limiting,
# and to strip the IP addresses of the proxies 1.2.3.4 and 1.2.3.5
#
# Example: If "X-Forwarded-For: 9.9.9.9, 1.2.3.4" is set,
# the visitor IP will be 9.9.9.9 (right-most unknown address).
#
behind-proxy: true
proxy-trusted-hosts: "1.2.3.0/24, 1.2.2.2, 2001:db8::/64"
```
=== "/etc/ntfy/server.yml (adjusted IPv4/IPv6 prefixes proxies)"
``` yaml
# Tell ntfy to treat visitors as being in a /24 subnet (IPv4) or /48 subnet (IPv6)
# as one visitor, so that they are counted as one for rate limiting.
#
# Example 1: If 1.2.3.4 and 1.2.3.5 publish a message, the visitor 1.2.3.0 will have
# used 2 messages.
# Example 2: If 2001:db8:2500:1::1 and 2001:db8:2500:2::1 publish a message, the visitor
# 2001:db8:2500:: will have used 2 messages.
#
visitor-prefix-bits-ipv4: 24
visitor-prefix-bits-ipv6: 48
```
### 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).
@@ -868,7 +546,7 @@ or the root domain:
listen 443 ssl http2;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
@@ -935,7 +613,7 @@ or the root domain:
listen 443 ssl http2;
server_name ntfy.sh;
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
# See https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6see https://ssl-config.mozilla.org/#server=nginx&version=1.18.0&config=intermediate&openssl=1.1.1k&hsts=false&ocsp=false&guideline=5.6
ssl_session_timeout 1d;
ssl_session_cache shared:MozSSL:10m; # about 40000 sessions
ssl_session_tickets off;
@@ -971,8 +649,8 @@ or the root domain:
<VirtualHost *:80>
ServerName ntfy.sh
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
# Proxy connections to ntfy (requires "a2enmod proxy")
ProxyPass / http://127.0.0.1:2586/
ProxyPassReverse / http://127.0.0.1:2586/
SetEnv proxy-nokeepalive 1
@@ -980,13 +658,19 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# Enable mod_rewrite (requires "a2enmod rewrite")
RewriteEngine on
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix (requires "a2enmod alias")
<If "%{REQUEST_METHOD} == 'GET'">
RedirectMatch permanent "^/([-_A-Za-z0-9]{0,64})$" "https://%{SERVER_NAME}/$1"
</If>
# it to work with curl without the annoying https:// prefix
RewriteCond %{REQUEST_METHOD} GET
RewriteRule ^/([-_A-Za-z0-9]{0,64})$ https://%{SERVER_NAME}/$1 [R,L]
</VirtualHost>
<VirtualHost *:443>
@@ -997,8 +681,8 @@ or the root domain:
SSLCertificateKeyFile /etc/letsencrypt/live/ntfy.sh/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
# Proxy connections to ntfy (requires "a2enmod proxy proxy_http")
ProxyPass / http://127.0.0.1:2586/ upgrade=websocket
# Proxy connections to ntfy (requires "a2enmod proxy")
ProxyPass / http://127.0.0.1:2586/
ProxyPassReverse / http://127.0.0.1:2586/
SetEnv proxy-nokeepalive 1
@@ -1006,7 +690,14 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# Enable mod_rewrite (requires "a2enmod rewrite")
RewriteEngine on
# WebSockets support (requires "a2enmod rewrite proxy_wstunnel")
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
</VirtualHost>
```
@@ -1014,7 +705,6 @@ or the root domain:
```
# Note that this config is most certainly incomplete. Please help out and let me know what's missing
# via Discord/Matrix or in a GitHub issue.
# Note: Caddy automatically handles both HTTP and WebSockets with reverse_proxy
ntfy.sh, http://nfty.sh {
reverse_proxy 127.0.0.1:2586
@@ -1069,7 +759,6 @@ To configure it, simply set `upstream-base-url` like so:
``` yaml
upstream-base-url: "https://ntfy.sh"
upstream-access-token: "..." # optional, only if rate limits exceeded, or upstream server protected
```
If set, all incoming messages will publish a poll request to the configured upstream server, containing
@@ -1099,59 +788,6 @@ Note that the self-hosted server literally sends the message `New message` for e
may be `Some other message`. This is so that if iOS cannot talk to the self-hosted server (in time, or at all),
it'll show `New message` as a popup.
## Web Push
[Web Push](https://developer.mozilla.org/en-US/docs/Web/API/Push_API) ([RFC8030](https://datatracker.ietf.org/doc/html/rfc8030))
allows ntfy to receive push notifications, even when the ntfy web app (or even the browser, depending on the platform) is closed.
When enabled, the user can enable **background notifications** for their topics in the web app under Settings. Once enabled by the
user, ntfy will forward published messages to the push endpoint (browser-provided, e.g. fcm.googleapis.com), which will then
forward it to the browser.
To configure Web Push, you need to generate and configure a [VAPID](https://datatracker.ietf.org/doc/html/draft-thomson-webpush-vapid) keypair (via `ntfy webpush keys`),
a database to keep track of the browser's subscriptions, and an admin email address (you):
- `web-push-public-key` is the generated VAPID public key, e.g. AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
- `web-push-private-key` is the generated VAPID private key, e.g. AA2BB1234567890abcdefzxcvbnm1234567890
- `web-push-file` is a database file to keep track of browser subscription endpoints, e.g. `/var/cache/ntfy/webpush.db`
- `web-push-email-address` is the admin email address send to the push provider, e.g. `sysadmin@example.com`
- `web-push-startup-queries` is an optional list of queries to run on startup`
- `web-push-expiry-warning-duration` defines the duration after which unused subscriptions are sent a warning (default is `55d`)
- `web-push-expiry-duration` defines the duration after which unused subscriptions will expire (default is `60d`)
Limitations:
- Like foreground browser notifications, background push notifications require the web app to be served over HTTPS. A _valid_
certificate is required, as service workers will not run on origins with untrusted certificates.
- Web Push is only supported for the same server. You cannot use subscribe to web push on a topic on another server. This
is due to a limitation of the Push API, which doesn't allow multiple push servers for the same origin.
To configure VAPID keys, first generate them:
```sh
$ ntfy webpush keys
Web Push keys generated.
...
```
Then copy the generated values into your `server.yml` or use the corresponding environment variables or command line arguments:
```yaml
web-push-public-key: AA1234BBCCddvveekaabcdfqwertyuiopasdfghjklzxcvbnm1234567890
web-push-private-key: AA2BB1234567890abcdefzxcvbnm1234567890
web-push-file: /var/cache/ntfy/webpush.db
web-push-email-address: sysadmin@example.com
```
The `web-push-file` is used to store the push subscriptions. Unused subscriptions will send out a warning after 55 days,
and will automatically expire after 60 days (default). If the gateway returns an error (e.g. 410 Gone when a user has unsubscribed),
subscriptions are also removed automatically.
The web app refreshes subscriptions on start and regularly on an interval, but this file should be persisted across restarts. If the subscription
file is deleted or lost, any web apps that aren't open will not receive new web push notifications until you open then.
Changing your public/private keypair is **not recommended**. Browsers only allow one server identity (public key) per origin, and
if you change them the clients will not be able to subscribe via web push until the user manually clears the notification permission.
## Tiers
ntfy supports associating users to pre-defined tiers. Tiers can be used to grant users higher limits, such as
daily message limits, attachment size, or make it possible for users to reserve topics. If [payments are enabled](#payments),
@@ -1178,7 +814,6 @@ ntfy tier add \
--message-limit=10000 \
--message-expiry-duration=24h \
--email-limit=50 \
--call-limit=10 \
--reservation-limit=10 \
--attachment-file-size-limit=100M \
--attachment-total-size-limit=1G \
@@ -1204,8 +839,6 @@ config options:
enables payments in the ntfy web app (e.g. Upgrade dialog). See [API keys](https://dashboard.stripe.com/apikeys).
* `stripe-webhook-key` is the key required to validate the authenticity of incoming webhooks from Stripe.
Webhooks are essential to keep the local database in sync with the payment provider. See [Webhooks](https://dashboard.stripe.com/webhooks).
* `billing-contact` is an email address or website displayed in the "Upgrade tier" dialog to let people reach
out with billing questions. If unset, nothing will be displayed.
In addition to setting these two options, you also need to define a [Stripe webhook](https://dashboard.stripe.com/webhooks)
for the `customer.subscription.updated` and `customer.subscription.deleted` event, which points
@@ -1216,34 +849,8 @@ Here's an example:
``` yaml
stripe-secret-key: "sk_test_ZmhzZGtmbGhkc2tqZmhzYcO2a2hmbGtnaHNkbGtnaGRsc2hnbG"
stripe-webhook-key: "whsec_ZnNkZnNIRExBSFNES0hBRFNmaHNka2ZsaGR"
billing-contact: "phil@example.com"
```
## Phone calls
ntfy supports phone calls via [Twilio](https://www.twilio.com/) as a call provider. If phone calls are enabled,
users can verify and add a phone number, and then receive phone calls when publishing a message using the `X-Call` header.
See [publishing page](publish.md#phone-calls) for more details.
To enable Twilio integration, sign up with [Twilio](https://www.twilio.com/), purchase a phone number (Toll free numbers
are the easiest), and then configure the following options:
* `twilio-account` is the Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586
* `twilio-auth-token` is the Twilio auth token, e.g. affebeef258625862586258625862586
* `twilio-phone-number` is the outgoing phone number you purchased, e.g. +18775132586
* `twilio-verify-service` is the Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586
After you have configured phone calls, create a [tier](#tiers) with a call limit (e.g. `ntfy tier create --call-limit=10 ...`),
and then assign it to a user. Users may then use the `X-Call` header to receive a phone call when publishing a message.
## Message limits
There are a few message limits that you can configure:
* `message-size-limit` defines the max size of a message body. Please note message sizes >4K are **not recommended,
and largely untested**. The Android/iOS and other clients may not work, or work properly. If FCM and/or APNS is used,
the limit should stay 4K, because their limits are around that size. If you increase this size limit regardless,
FCM and APNS will NOT work for large messages.
* `message-delay-limit` defines the max delay of a message when using the "Delay" header and [scheduled delivery](publish.md#scheduled-delivery).
## Rate limiting
!!! info
Be aware that if you are running ntfy behind a proxy, you must set the `behind-proxy` flag.
@@ -1322,40 +929,6 @@ If this ever happens, there will be a log message that looks something like this
WARN Firebase quota exceeded (likely for topic), temporarily denying Firebase access to visitor
```
### IPv6 considerations
By default, rate limiting for IPv6 is done using the `/64` subnet of the visitor's IPv6 address. This means that all visitors
in the same `/64` subnet are treated as one visitor. This is done to prevent abuse, as IPv6 subnet assignments are typically
much larger than IPv4 subnets (and much cheaper), and it is common for ISPs to assign large subnets to their customers.
Other than that, rate limiting for IPv6 is done the same way as for IPv4, using the visitor's IP address or subnet to identify them.
There are two options to configure the number of bits used for rate limiting (for IPv4 and IPv6):
- `visitor-prefix-bits-ipv4` is number of bits of the IPv4 address to use for rate limiting (default: 32, full address)
- `visitor-prefix-bits-ipv6` is number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet)
### Subscriber-based rate limiting
By default, ntfy puts almost all rate limits on the message publisher, e.g. number of messages, requests, and attachment
size are all based on the visitor who publishes a message. **Subscriber-based rate limiting is a way to use the rate limits
of a topic's subscriber, instead of the limits of the publisher.**
If subscriber-based rate limiting is enabled, **messages published on UnifiedPush topics** (topics starting with `up`, e.g. `up123456789012`)
will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
Once enabled, a client subscribing to UnifiedPush topics via HTTP stream, or websockets, will be automatically registered as
a "rate visitor", i.e. the visitor whose rate limits will be used when publishing on this topic. Note that setting the rate visitor
requires **read-write permission** on the topic.
If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
response if no "rate visitor" has been previously registered. This is to avoid burning the publisher's
`visitor-message-daily-limit`.
To enable subscriber-based rate limiting, set `visitor-subscriber-rate-limiting: true`.
!!! info
Due to a [denial-of-service issue](https://github.com/binwiederhier/ntfy/issues/1048), support for the `Rate-Topics`
header was removed entirely. This is unfortunate, but subscriber-based rate limiting will still work for `up*` topics.
## 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**.
@@ -1494,83 +1067,6 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
maxretry = 10
```
Note that if you run nginx in a container, append `, chain=DOCKER-USER` to the jail.local action. By default, the jail action chain
is `INPUT`, but `FORWARD` is used when using docker networks. `DOCKER-USER`, available when using docker, is part of the `FORWARD`
chain.
The official ntfy.sh server uses fail2ban to ban IPs. Check out ntfy.sh's [Ansible fail2ban role](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban) for details. Ban actors are banned for 1 hour initially, and up to
4 hours at a time for repeated offenses. IPv4 addresses are banned individually, while IPv6 addresses are banned by their `/56` prefix.
## IPv6 support
ntfy fully supports IPv6, though there are a few things to keep in mind.
- **Listening on an IPv6 address**: By default, ntfy listens on `:80` (IPv4-only). If you want to listen on an IPv6 address, you need to
explicitly set the `listen-http` and/or `listen-https` options in your `server.yml` file to an IPv6 address, e.g. `[::]:80`. To listen on
IPv4 and IPv6, you must run ntfy behind a reverse proxy, e.g. `listen :80; listen [::]:80;` in nginx.
- **Rate limiting:** By default, ntfy uses the `/64` subnet of the visitor's IPv6 address for rate limiting. This means that all visitors in the same `/64`
subnet are treated as one visitor. If you want to change this, you can set the `visitor-prefix-bits-ipv6` option in your `server.yml` file to a different
value (e.g. `48` for `/48` subnets). See [IPv6 considerations](#ipv6-considerations) and [IP-based rate limiting](#ip-based-rate-limiting) for more details.
- **Banning IPs with fail2ban:** By default, if you're using the `iptables-multiport` action, fail2ban bans individual IPv4 and IPv6 addresses via `iptables` and `ip6tables`. While this behavior is fine for IPv4, it is not for IPv6, because every host can technically have up to 2^64 addresses. Please ensure that your `actionban` and `actionunban` commands
support IPv6 and also ban the entire prefix (e.g. `/48`). See [Banning bad actors](#banning-bad-actors-fail2ban) for details.
!!! info
The official ntfy.sh server supports IPv6. Check out ntfy.sh's [Ansible repository](https://github.com/binwiederhier/ntfy-ansible) for examples of how to
configure [ntfy](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/ntfy), [nginx](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/nginx) and [fail2ban](https://github.com/binwiederhier/ntfy-ansible/tree/main/roles/fail2ban).
## Health checks
A preliminary health check API endpoint is exposed at `/v1/health`. The endpoint returns a `json` response in the format shown below.
If a non-200 HTTP status code is returned or if the returned `healthy` field is `false` the ntfy service should be considered as unhealthy.
```json
{"healthy":true}
```
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
## Monitoring
If configured, ntfy can expose a `/metrics` endpoint for [Prometheus](https://prometheus.io/), which can then be used to
create dashboards and alerts (e.g. via [Grafana](https://grafana.com/)).
To configure the metrics endpoint, either set `enable-metrics` and/or set the `listen-metrics-http` option to a dedicated
listen address. Metrics may be considered sensitive information, so before you enable them, be sure you know what you are
doing, and/or secure access to the endpoint in your reverse proxy.
- `enable-metrics` enables the /metrics endpoint for the default ntfy server (i.e. HTTP, HTTPS and/or Unix socket)
- `metrics-listen-http` exposes the metrics endpoint via a dedicated `[IP]:port`. If set, this option implicitly
enables metrics as well, e.g. "10.0.1.1:9090" or ":9090"
=== "server.yml (Using default port)"
```yaml
enable-metrics: true
```
=== "server.yml (Using dedicated IP/port)"
```yaml
metrics-listen-http: "10.0.1.1:9090"
```
In Prometheus, an example scrape config would look like this:
=== "prometheus.yml"
```yaml
scrape_configs:
- job_name: "ntfy"
static_configs:
- targets: ["10.0.1.1:9090"]
```
Here's an example Grafana dashboard built from the metrics (see [Grafana JSON on GitHub](https://raw.githubusercontent.com/binwiederhier/ntfy/main/examples/grafana-dashboard/ntfy-grafana.json)):
<figure markdown style="padding-left: 50px; padding-right: 50px">
<a href="../../static/img/grafana-dashboard.png" target="_blank"><img src="../../static/img/grafana-dashboard.png"/></a>
<figcaption>ntfy Grafana dashboard</figcaption>
</figure>
## Profiling
ntfy can expose Go's [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoints to support profiling of the ntfy server.
If enabled, ntfy will listen on a dedicated listen IP/port, which can be accessed via the web browser on `http://<ip>:<port>/debug/pprof/`.
This can be helpful to expose bottlenecks, and visualize call flows. To enable, simply set the `profile-listen-http` config option.
## Logging & debugging
By default, ntfy logs to the console (stderr), with an `info` log level, and in a human-readable text format.
@@ -1649,17 +1145,15 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `listen-unix-mode` | `NTFY_LISTEN_UNIX_MODE` | *file mode* | *system default* | File mode of the Unix socket, e.g. 0700 or 0777 |
| `key-file` | `NTFY_KEY_FILE` | *filename* | - | HTTPS/TLS private key file, only used if `listen-https` is set. |
| `cert-file` | `NTFY_CERT_FILE` | *filename* | - | HTTPS/TLS certificate file, only used if `listen-https` is set. |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM)](#firebase-fcm). |
| `firebase-key-file` | `NTFY_FIREBASE_KEY_FILE` | *filename* | - | If set, also publish messages to a Firebase Cloud Messaging (FCM) topic for your app. This is optional and only required to save battery when using the Android app. See [Firebase (FCM](#firebase-fcm). |
| `cache-file` | `NTFY_CACHE_FILE` | *filename* | - | If set, messages are cached in a local SQLite database instead of only in-memory. This allows for service restarts without losing messages in support of the since= parameter. See [message cache](#message-cache). |
| `cache-duration` | `NTFY_CACHE_DURATION` | *duration* | 12h | Duration for which messages will be buffered before they are deleted. This is required to support the `since=...` and `poll=1` parameter. Set this to `0` to disable the cache entirely. |
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#message-cache) |
| `cache-startup-queries` | `NTFY_CACHE_STARTUP_QUERIES` | *string (SQL queries)* | - | SQL queries to run during database startup; this is useful for tuning and [enabling WAL mode](#wal-for-message-cache) |
| `cache-batch-size` | `NTFY_CACHE_BATCH_SIZE` | *int* | 0 | Max size of messages to batch together when writing to message cache (if zero, writes are synchronous) |
| `cache-batch-timeout` | `NTFY_CACHE_BATCH_TIMEOUT` | *duration* | 0s | Timeout for batched async writes to the message cache (if zero, writes are synchronous) |
| `auth-file` | `NTFY_AUTH_FILE` | *filename* | - | Auth database file used for access control. If set, enables authentication and access control. See [access control](#access-control). |
| `auth-default-access` | `NTFY_AUTH_DEFAULT_ACCESS` | `read-write`, `read-only`, `write-only`, `deny-all` | `read-write` | Default permissions if no matching entries in the auth database are found. Default is `read-write`. |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) |
| `proxy-forwarded-header` | `NTFY_PROXY_FORWARDED_HEADER` | *string* | `X-Forwarded-For` | Use specified header to determine visitor IP address (for rate limiting) |
| `proxy-trusted-hosts` | `NTFY_PROXY_TRUSTED_HOSTS` | *comma-separated host/IP/CIDR list* | - | Comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header |
| `behind-proxy` | `NTFY_BEHIND_PROXY` | *bool* | false | If set, the X-Forwarded-For header is used to determine the visitor IP address instead of the remote address of the connection. |
| `attachment-cache-dir` | `NTFY_ATTACHMENT_CACHE_DIR` | *directory* | - | Cache directory for attached files. To enable attachments, this has to be set. |
| `attachment-total-size-limit` | `NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 5G | Limit of the on-disk attachment cache directory. If the limits is exceeded, new attachments will be rejected. |
| `attachment-file-size-limit` | `NTFY_ATTACHMENT_FILE_SIZE_LIMIT` | *size* | 15M | Per-file attachment size limit (e.g. 300k, 2M, 100M). Larger attachment will be rejected. |
@@ -1671,17 +1165,10 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `smtp-server-listen` | `NTFY_SMTP_SERVER_LISTEN` | `[ip]:port` | - | Defines the IP address and port the SMTP server will listen on, e.g. `:25` or `1.2.3.4:25` |
| `smtp-server-domain` | `NTFY_SMTP_SERVER_DOMAIN` | *domain name* | - | SMTP server e-mail domain, e.g. `ntfy.sh` |
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | *string* | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
| `twilio-account` | `NTFY_TWILIO_ACCOUNT` | *string* | - | Twilio account SID, e.g. AC12345beefbeef67890beefbeef122586 |
| `twilio-auth-token` | `NTFY_TWILIO_AUTH_TOKEN` | *string* | - | Twilio auth token, e.g. affebeef258625862586258625862586 |
| `twilio-phone-number` | `NTFY_TWILIO_PHONE_NUMBER` | *string* | - | Twilio outgoing phone number, e.g. +18775132586 |
| `twilio-verify-service` | `NTFY_TWILIO_VERIFY_SERVICE` | *string* | - | Twilio Verify service SID, e.g. VA12345beefbeef67890beefbeef122586 |
| `keepalive-interval` | `NTFY_KEEPALIVE_INTERVAL` | *duration* | 45s | Interval in which keepalive messages are sent to the client. This is to prevent intermediaries closing the connection for inactivity. Note that the Android app has a hardcoded timeout at 77s, so it should be less than that. |
| `manager-interval` | `NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
| `message-size-limit` | `NTFY_MESSAGE_SIZE_LIMIT` | *size* | 4K | The size limit for the message body. Please note that this is largely untested, and that FCM/APNS have limits around 4KB. If you increase this size limit, FCM and APNS will NOT work for large messages. |
| `message-delay-limit` | `NTFY_MESSAGE_DELAY_LIMIT` | *duration* | 3d | Amount of time a message can be [scheduled](publish.md#scheduled-delivery) into the future when using the `Delay` header |
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
| `upstream-base-url` | `NTFY_UPSTREAM_BASE_URL` | *URL* | `https://ntfy.sh` | Forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers |
| `upstream-access-token` | `NTFY_UPSTREAM_ACCESS_TOKEN` | *string* | `tk_zyYLYj...` | Access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth |
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
| `visitor-attachment-daily-bandwidth-limit` | `NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT` | *size* | 500M | Rate limiting: Total daily attachment download/upload traffic limit per visitor. This is to protect your bandwidth costs from exploding. |
| `visitor-email-limit-burst` | `NTFY_VISITOR_EMAIL_LIMIT_BURST` | *number* | 16 | Rate limiting:Initial limit of e-mails per visitor |
@@ -1689,35 +1176,21 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
| `visitor-message-daily-limit` | `NTFY_VISITOR_MESSAGE_DAILY_LIMIT` | *number* | - | Rate limiting: Allowed number of messages per day per visitor, reset every day at midnight (UTC). By default, this value is unset. |
| `visitor-request-limit-burst` | `NTFY_VISITOR_REQUEST_LIMIT_BURST` | *number* | 60 | Rate limiting: Allowed GET/PUT/POST requests per second, per visitor. This setting is the initial bucket of requests each visitor has |
| `visitor-request-limit-replenish` | `NTFY_VISITOR_REQUEST_LIMIT_REPLENISH` | *duration* | 5s | Rate limiting: Strongly related to `visitor-request-limit-burst`: The rate at which the bucket is refilled |
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP/CIDR list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
| `visitor-request-limit-exempt-hosts` | `NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS` | *comma-separated host/IP list* | - | Rate limiting: List of hostnames and IPs to be exempt from request rate limiting |
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
| `visitor-subscriber-rate-limiting` | `NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING` | *bool* | `false` | Rate limiting: Enables subscriber-based rate limiting |
| `visitor-prefix-bits-ipv4` | `NTFY_VISITOR_PREFIX_BITS_IPV4` | *number* | 32 | Rate limiting: Number of bits to use for IPv4 visitor prefix, e.g. 24 for /24 |
| `visitor-prefix-bits-ipv6` | `NTFY_VISITOR_PREFIX_BITS_IPV6` | *number* | 64 | Rate limiting: Number of bits to use for IPv6 visitor prefix, e.g. 48 for /48 |
| `web-root` | `NTFY_WEB_ROOT` | *path*, e.g. `/` or `/app`, or `disable` | `/` | Sets root of the web app (e.g. /, or /app), or disables it entirely (disable) |
| `web-root` | `NTFY_WEB_ROOT` | `app`, `home` or `disable` | `app` | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable) |
| `enable-signup` | `NTFY_ENABLE_SIGNUP` | *boolean* (`true` or `false`) | `false` | Allows users to sign up via the web app, or API |
| `enable-login` | `NTFY_ENABLE_LOGIN` | *boolean* (`true` or `false`) | `false` | Allows users to log in via the web app, or API |
| `enable-reservations` | `NTFY_ENABLE_RESERVATIONS` | *boolean* (`true` or `false`) | `false` | Allows users to reserve topics (if their tier allows it) |
| `require-login` | `NTFY_REQUIRE_LOGIN` | *boolean* (`true` or `false`) | `false` | All actions via the web app require a login |
| `stripe-secret-key` | `NTFY_STRIPE_SECRET_KEY` | *string* | - | Payments: Key used for the Stripe API communication, this enables payments |
| `stripe-webhook-key` | `NTFY_STRIPE_WEBHOOK_KEY` | *string* | - | Payments: Key required to validate the authenticity of incoming webhooks from Stripe |
| `billing-contact` | `NTFY_BILLING_CONTACT` | *email address* or *website* | - | Payments: Email or website displayed in Upgrade dialog as a billing contact |
| `web-push-public-key` | `NTFY_WEB_PUSH_PUBLIC_KEY` | *string* | - | Web Push: Public Key. Run `ntfy webpush keys` to generate |
| `web-push-private-key` | `NTFY_WEB_PUSH_PRIVATE_KEY` | *string* | - | Web Push: Private Key. Run `ntfy webpush keys` to generate |
| `web-push-file` | `NTFY_WEB_PUSH_FILE` | *string* | - | Web Push: Database file that stores subscriptions |
| `web-push-email-address` | `NTFY_WEB_PUSH_EMAIL_ADDRESS` | *string* | - | Web Push: Sender email address |
| `web-push-startup-queries` | `NTFY_WEB_PUSH_STARTUP_QUERIES` | *string* | - | Web Push: SQL queries to run against subscription database at startup |
| `web-push-expiry-duration` | `NTFY_WEB_PUSH_EXPIRY_DURATION` | *duration* | 60d | Web Push: Duration after which a subscription is considered stale and will be deleted. This is to prevent stale subscriptions. |
| `web-push-expiry-warning-duration` | `NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION` | *duration* | 55d | Web Push: Duration after which a warning is sent to subscribers that their subscription will expire soon. This is to prevent stale subscriptions. |
| `log-format` | `NTFY_LOG_FORMAT` | *string* | `text` | Defines the output format, can be text or json |
| `log-file` | `NTFY_LOG_FILE` | *string* | - | Defines the filename to write logs to. If this is not set, ntfy logs to stderr |
| `log-level` | `NTFY_LOG_LEVEL` | *string* | `info` | Defines the default log level, can be one of trace, debug, info, warn or error |
The format for a *duration* is: `<number>(smhd)`, e.g. 30s, 20m, 1h or 3d.
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
## Command line options
```
$ ntfy serve --help
NAME:
ntfy serve - Run the ntfy server
@@ -1745,36 +1218,35 @@ OPTIONS:
--log-level-overrides value, --log_level_overrides value [ --log-level-overrides value, --log_level_overrides value ] set log level overrides [$NTFY_LOG_LEVEL_OVERRIDES]
--log-format value, --log_format value set log format (default: "text") [$NTFY_LOG_FORMAT]
--log-file value, --log_file value set log file, default is STDOUT [$NTFY_LOG_FILE]
--config value, -c value config file (default: "/etc/ntfy/server.yml") [$NTFY_CONFIG_FILE]
--config value, -c value config file (default: /etc/ntfy/server.yml) [$NTFY_CONFIG_FILE]
--base-url value, --base_url value, -B value externally visible base URL for this host (e.g. https://ntfy.sh) [$NTFY_BASE_URL]
--listen-http value, --listen_http value, -l value ip:port used as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
--listen-https value, --listen_https value, -L value ip:port used as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--listen-http value, --listen_http value, -l value ip:port used to as HTTP listen address (default: ":80") [$NTFY_LISTEN_HTTP]
--listen-https value, --listen_https value, -L value ip:port used to as HTTPS listen address [$NTFY_LISTEN_HTTPS]
--listen-unix value, --listen_unix value, -U value listen on unix socket path [$NTFY_LISTEN_UNIX]
--listen-unix-mode value, --listen_unix_mode value file permissions of unix socket, e.g. 0700 (default: system default) [$NTFY_LISTEN_UNIX_MODE]
--key-file value, --key_file value, -K value private key file, if listen-https is set [$NTFY_KEY_FILE]
--cert-file value, --cert_file value, -E value certificate file, if listen-https is set [$NTFY_CERT_FILE]
--firebase-key-file value, --firebase_key_file value, -F value Firebase credentials file; if set additionally publish to FCM topic [$NTFY_FIREBASE_KEY_FILE]
--cache-file value, --cache_file value, -C value cache file used for message caching [$NTFY_CACHE_FILE]
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: "12h") [$NTFY_CACHE_DURATION]
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: 12h0m0s) [$NTFY_CACHE_DURATION]
--cache-batch-size value, --cache_batch_size value max size of messages to batch together when writing to message cache (if zero, writes are synchronous) (default: 0) [$NTFY_BATCH_SIZE]
--cache-batch-timeout value, --cache_batch_timeout value timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: "0s") [$NTFY_CACHE_BATCH_TIMEOUT]
--cache-batch-timeout value, --cache_batch_timeout value timeout for batched async writes to the message cache (if zero, writes are synchronous) (default: 0s) [$NTFY_CACHE_BATCH_TIMEOUT]
--cache-startup-queries value, --cache_startup_queries value queries run when the cache database is initialized [$NTFY_CACHE_STARTUP_QUERIES]
--auth-file value, --auth_file value, -H value auth database file used for access control [$NTFY_AUTH_FILE]
--auth-startup-queries value, --auth_startup_queries value queries run when the auth database is initialized [$NTFY_AUTH_STARTUP_QUERIES]
--auth-default-access value, --auth_default_access value, -p value default permissions if no matching entries in the auth database are found (default: "read-write") [$NTFY_AUTH_DEFAULT_ACCESS]
--attachment-cache-dir value, --attachment_cache_dir value cache directory for attached files [$NTFY_ATTACHMENT_CACHE_DIR]
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: "5G") [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: "15M") [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: "3h") [$NTFY_ATTACHMENT_EXPIRY_DURATION]
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: "45s") [$NTFY_KEEPALIVE_INTERVAL]
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: "1m") [$NTFY_MANAGER_INTERVAL]
--attachment-total-size-limit value, --attachment_total_size_limit value, -A value limit of the on-disk attachment cache (default: 5G) [$NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT]
--attachment-file-size-limit value, --attachment_file_size_limit value, -Y value per-file attachment size limit (e.g. 300k, 2M, 100M) (default: 15M) [$NTFY_ATTACHMENT_FILE_SIZE_LIMIT]
--attachment-expiry-duration value, --attachment_expiry_duration value, -X value duration after which uploaded attachments will be deleted (e.g. 3h, 20h) (default: 3h) [$NTFY_ATTACHMENT_EXPIRY_DURATION]
--keepalive-interval value, --keepalive_interval value, -k value interval of keepalive messages (default: 45s) [$NTFY_KEEPALIVE_INTERVAL]
--manager-interval value, --manager_interval value, -m value interval of for message pruning and stats printing (default: 1m0s) [$NTFY_MANAGER_INTERVAL]
--disallowed-topics value, --disallowed_topics value [ --disallowed-topics value, --disallowed_topics value ] topics that are not allowed to be used [$NTFY_DISALLOWED_TOPICS]
--web-root value, --web_root value sets root of the web app (e.g. /, or /app), or disables it (disable) (default: "/") [$NTFY_WEB_ROOT]
--web-root value, --web_root value sets web root to landing page (home), web app (app) or disabled (disable) (default: "app") [$NTFY_WEB_ROOT]
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
--enable-login, --enable_login allows users to log in via the web app, or API (default: false) [$NTFY_ENABLE_LOGIN]
--enable-reservations, --enable_reservations allows users to reserve topics (if their tier allows it) (default: false) [$NTFY_ENABLE_RESERVATIONS]
--upstream-base-url value, --upstream_base_url value forward poll request to an upstream server, this is needed for iOS push notifications for self-hosted servers [$NTFY_UPSTREAM_BASE_URL]
--upstream-access-token value, --upstream_access_token value access token to use for the upstream server; needed only if upstream rate limits are exceeded or upstream server requires auth [$NTFY_UPSTREAM_ACCESS_TOKEN]
--smtp-sender-addr value, --smtp_sender_addr value SMTP server address (host:port) for outgoing emails [$NTFY_SMTP_SENDER_ADDR]
--smtp-sender-user value, --smtp_sender_user value SMTP user (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_USER]
--smtp-sender-pass value, --smtp_sender_pass value SMTP password (if e-mail sending is enabled) [$NTFY_SMTP_SENDER_PASS]
@@ -1782,40 +1254,19 @@ OPTIONS:
--smtp-server-listen value, --smtp_server_listen value SMTP server address (ip:port) for incoming emails, e.g. :25 [$NTFY_SMTP_SERVER_LISTEN]
--smtp-server-domain value, --smtp_server_domain value SMTP domain for incoming e-mail, e.g. ntfy.sh [$NTFY_SMTP_SERVER_DOMAIN]
--smtp-server-addr-prefix value, --smtp_server_addr_prefix value SMTP email address prefix for topics to prevent spam (e.g. 'ntfy-') [$NTFY_SMTP_SERVER_ADDR_PREFIX]
--twilio-account value, --twilio_account value Twilio account SID, used for phone calls, e.g. AC123... [$NTFY_TWILIO_ACCOUNT]
--twilio-auth-token value, --twilio_auth_token value Twilio auth token [$NTFY_TWILIO_AUTH_TOKEN]
--twilio-phone-number value, --twilio_phone_number value Twilio number to use for outgoing calls [$NTFY_TWILIO_PHONE_NUMBER]
--twilio-verify-service value, --twilio_verify_service value Twilio Verify service ID, used for phone number verification [$NTFY_TWILIO_VERIFY_SERVICE]
--message-size-limit value, --message_size_limit value size limit for the message (see docs for limitations) (default: "4K") [$NTFY_MESSAGE_SIZE_LIMIT]
--message-delay-limit value, --message_delay_limit value max duration a message can be scheduled into the future (default: "3d") [$NTFY_MESSAGE_DELAY_LIMIT]
--global-topic-limit value, --global_topic_limit value, -T value total number of topics allowed (default: 15000) [$NTFY_GLOBAL_TOPIC_LIMIT]
--visitor-subscription-limit value, --visitor_subscription_limit value number of subscriptions per visitor (default: 30) [$NTFY_VISITOR_SUBSCRIPTION_LIMIT]
--visitor-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
--visitor-attachment-total-size-limit value, --visitor_attachment_total_size_limit value total storage limit used for attachments per visitor (default: "100M") [$NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT]
--visitor-attachment-daily-bandwidth-limit value, --visitor_attachment_daily_bandwidth_limit value total daily attachment download/upload bandwidth limit per visitor (default: "500M") [$NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT]
--visitor-request-limit-burst value, --visitor_request_limit_burst value initial limit of requests per visitor (default: 60) [$NTFY_VISITOR_REQUEST_LIMIT_BURST]
--visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: "5s") [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
--visitor-request-limit-replenish value, --visitor_request_limit_replenish value interval at which burst limit is replenished (one per x) (default: 5s) [$NTFY_VISITOR_REQUEST_LIMIT_REPLENISH]
--visitor-request-limit-exempt-hosts value, --visitor_request_limit_exempt_hosts value hostnames and/or IP addresses of hosts that will be exempt from the visitor request limit [$NTFY_VISITOR_REQUEST_LIMIT_EXEMPT_HOSTS]
--visitor-message-daily-limit value, --visitor_message_daily_limit value max messages per visitor per day, derived from request limit if unset (default: 0) [$NTFY_VISITOR_MESSAGE_DAILY_LIMIT]
--visitor-email-limit-burst value, --visitor_email_limit_burst value initial limit of e-mails per visitor (default: 16) [$NTFY_VISITOR_EMAIL_LIMIT_BURST]
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: "1h") [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--visitor-prefix-bits-ipv4 value, --visitor_prefix_bits_ipv4 value number of bits of the IPv4 address to use for rate limiting (default: 32, full address) (default: 32) [$NTFY_VISITOR_PREFIX_BITS_IPV4]
--visitor-prefix-bits-ipv6 value, --visitor_prefix_bits_ipv6 value number of bits of the IPv6 address to use for rate limiting (default: 64, /64 subnet) (default: 64) [$NTFY_VISITOR_PREFIX_BITS_IPV6]
--behind-proxy, --behind_proxy, -P if set, use forwarded header (e.g. X-Forwarded-For, X-Client-IP) to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--proxy-forwarded-header value, --proxy_forwarded_header value use specified header to determine visitor IP address (for rate limiting) (default: "X-Forwarded-For") [$NTFY_PROXY_FORWARDED_HEADER]
--proxy-trusted-hosts value, --proxy_trusted_hosts value comma-separated list of trusted IP addresses, hosts, or CIDRs to remove from forwarded header [$NTFY_PROXY_TRUSTED_HOSTS]
--visitor-email-limit-replenish value, --visitor_email_limit_replenish value interval at which burst limit is replenished (one per x) (default: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
--behind-proxy, --behind_proxy, -P if set, use X-Forwarded-For header to determine visitor IP address (for rate limiting) (default: false) [$NTFY_BEHIND_PROXY]
--stripe-secret-key value, --stripe_secret_key value key used for the Stripe API communication, this enables payments [$NTFY_STRIPE_SECRET_KEY]
--stripe-webhook-key value, --stripe_webhook_key value key required to validate the authenticity of incoming webhooks from Stripe [$NTFY_STRIPE_WEBHOOK_KEY]
--billing-contact value, --billing_contact value e-mail or website to display in upgrade dialog (only if payments are enabled) [$NTFY_BILLING_CONTACT]
--enable-metrics, --enable_metrics if set, Prometheus metrics are exposed via the /metrics endpoint (default: false) [$NTFY_ENABLE_METRICS]
--metrics-listen-http value, --metrics_listen_http value ip:port used to expose the metrics endpoint (implicitly enables metrics) [$NTFY_METRICS_LISTEN_HTTP]
--profile-listen-http value, --profile_listen_http value ip:port used to expose the profiling endpoints (implicitly enables profiling) [$NTFY_PROFILE_LISTEN_HTTP]
--web-push-public-key value, --web_push_public_key value public key used for web push notifications [$NTFY_WEB_PUSH_PUBLIC_KEY]
--web-push-private-key value, --web_push_private_key value private key used for web push notifications [$NTFY_WEB_PUSH_PRIVATE_KEY]
--web-push-file value, --web_push_file value file used to store web push subscriptions [$NTFY_WEB_PUSH_FILE]
--web-push-email-address value, --web_push_email_address value e-mail address of sender, required to use browser push services [$NTFY_WEB_PUSH_EMAIL_ADDRESS]
--web-push-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
--web-push-expiry-duration value, --web_push_expiry_duration value automatically expire unused subscriptions after this time (default: "60d") [$NTFY_WEB_PUSH_EXPIRY_DURATION]
--web-push-expiry-warning-duration value, --web_push_expiry_warning_duration value send web push warning notification after this time before expiring unused subscriptions (default: "55d") [$NTFY_WEB_PUSH_EXPIRY_WARNING_DURATION]
--help, -h
--help, -h show help (default: false)
```

View File

@@ -1,4 +1,4 @@
# Deprecations and breaking changes
# Deprecation notices
This page is used to list deprecation notices for ntfy. Deprecated commands and options will be
**removed after 1-3 months** from the time they were deprecated. How long the feature is deprecated
before the behavior is changed depends on the severity of the change, and how prominent the feature is.

View File

@@ -16,7 +16,7 @@ server consists of three components:
* **The documentation** is generated by [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/),
which is written in [Python](https://www.python.org/). You'll need Python and MkDocs (via `pip`) only if you want to
build the docs.
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Vite](https://vitejs.dev/)
* **The web app** is written in [React](https://reactjs.org/), using [MUI](https://mui.com/). It uses [Create React App](https://create-react-app.dev/)
to build the production build. If you want to modify the web app, you need [nodejs](https://nodejs.org/en/) (for `npm`)
and install all the 100,000 dependencies (*sigh*).
@@ -163,15 +163,6 @@ $ make release-snapshot
During development, you may want to be more picky and build only certain things. Here are a few examples.
### Build a Docker image only for Linux
This is useful to test the final build with web app, docs, and server without any dependencies locally
``` shell
$ make docker-dev
$ docker run --rm -p 80:80 binwiederhier/ntfy:dev serve
```
### Build the ntfy binary
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
@@ -241,41 +232,6 @@ $ cd web
$ npm start
```
### Testing Web Push locally
Reference: <https://stackoverflow.com/questions/34160509/options-for-testing-service-workers-via-http>
#### With the dev servers
1. Get web push keys `go run main.go webpush keys`
2. Run the server with web push enabled
```sh
go run main.go \
--log-level debug \
serve \
--web-push-public-key KEY \
--web-push-private-key KEY \
--web-push-email-address <email> \
--web-push-file=/tmp/webpush.db
```
3. In `web/public/config.js`:
- Set `base_url` to `http://localhost`, This is required as web push can only be used with the server matching the `base_url`.
- Set the `web_push_public_key` correctly.
4. Run `npm run start`
#### With a built package
1. Run `make web-build`
2. Run the server (step 2 above)
3. Open <http://localhost/>
### Build the docs
The sources for the docs live in `docs/`. Similarly to the web app, you can simply run `make docs` to build the
documentation. As long as you have `mkdocs` installed (see above), this should work fine:
@@ -363,7 +319,7 @@ To build your own version with Firebase, you must:
* And change `app_base_url` in [values.xml](https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/values.xml)
* Then run:
```
# To build an unsigned .apk (app/build/outputs/apk/play/release/*.apk)
# To build an unsigned .apk (app/build/outputs/apk/play/*.apk)
./gradlew assemblePlayRelease
# To build a bundle .aab (app/play/release/*.aab)
@@ -384,7 +340,7 @@ strictly based off of my development on this app. There may be other versions of
### Apple setup
!!! info
Along with this step, the [PLIST Deployment](#plist-config) step is also required
Along with this step, the [PLIST Deployment](#plist-deployment-and-configuration) step is also required
for these changes to take effect in the iOS app.
1. [Create a new key in Apple Developer Member Center](https://developer.apple.com/account/resources/authkeys/add)
@@ -429,7 +385,7 @@ steps:
### XCode setup
1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) to install the
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
`firebase-ios-sdk` in XCode, if it's not already present - you can select any packages in addition to Firebase Core / Firebase Messaging
1. Similarly, install the SQLite.swift package dependency in XCode
1. When running the debug build, ensure XCode is pointed to the connected iOS device - registering for push notifications does not work in the iOS simulators

File diff suppressed because it is too large Load Diff

View File

@@ -16,7 +16,7 @@ I started adding notifications pretty much all of my scripts. Typically, I just
directly to the command I'm running. The following example will either send <i>Laptop backup succeeded</i>
or ⚠️ <i>Laptop backup failed</i> directly to my phone:
``` bash
```
rsync -a root@laptop /backups/laptop \
&& zfs snapshot ... \
&& curl -H prio:low -d "Laptop backup succeeded" ntfy.sh/backups \
@@ -26,17 +26,11 @@ rsync -a root@laptop /backups/laptop \
Here's one for the history books. I desperately want the `github.com/ntfy` organization, but all my tickets with
GitHub have been hopeless. In case it ever becomes available, I want to know immediately.
```
``` cron
# Check github/ntfy user
*/6 * * * * if curl -s https://api.github.com/users/ntfy | grep "Not Found"; then curl -d "github.com/ntfy is available" -H "Tags: tada" -H "Prio: high" ntfy.sh/my-alerts; fi
```
You can also use [`ntfy-run`](https://github.com/quantum5/ntfy-run) to send the output of your cronjob in the
notification, so that you know exactly why it failed:
```
0 0 * * * ntfy-run -n https://ntfy.sh/backups --success-priority low --failure-tags warning ~/backup-computer
```
## Low disk space alerts
Here's a simple cronjob that I use to alert me when the disk space on the root disk is running low. It's simple, but
@@ -141,64 +135,28 @@ You can send a message during a workflow run with curl. Here is an example sendi
${{ secrets.NTFY_URL }}
```
## Changedetection.io
ntfy is an excellent choice for getting notifications when a website has a change sent to your mobile (or desktop),
[changedetection.io](https://changedetection.io) or on GitHub ([dgtlmoon/changedetection.io](https://github.com/dgtlmoon/changedetection.io))
uses [apprise](https://github.com/caronc/apprise) library for notification integrations.
To add any ntfy(s) notification to a website change simply add the [ntfy style URL](https://github.com/caronc/apprise/wiki/Notify_ntfy)
to the notification list.
For example `ntfy://{topic}` or `ntfy://{user}:{password}@{host}:{port}/{topics}`
In your changedetection.io installation, click `Edit` > `Notifications` on a single website watch (or group) then add
the special ntfy Apprise Notification URL to the Notification List.
![ntfy alerts on website change](static/img/cdio-setup.jpg)
## Watchtower (shoutrrr)
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
You can use [shoutrrr](https://github.com/containrrr/shoutrrr) generic webhook support to send
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
Example docker-compose.yml:
``` yaml
services:
watchtower:
image: containrrr/watchtower
environment:
- WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
- WATCHTOWER_NOTIFICATION_URL=ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
- WATCHTOWER_NOTIFICATIONS=shoutrrr
- WATCHTOWER_NOTIFICATION_URL=generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates
```
The environment variable `WATCHTOWER_NOTIFICATION_SKIP_TITLE` is required to prevent Watchtower from [replacing the `title` query parameter](https://containrrr.dev/watchtower/notifications/#settings). If omitted, the provided notification title will not be used.
Or, if you only want to send notifications using shoutrrr:
```
shoutrrr send -u "ntfy://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
Authentication tokens are also supported:
- (Recommended) Ntfy url format (replace the domain, topic and token with your own):
```
ntfy://:TOKEN@DOMAIN/TOPIC
```
- Generic webhook and authorization header using this url format (replace the domain, topic and token with your own):
```
generic+https://DOMAIN/TOPIC?@authorization=Bearer+TOKEN`
shoutrrr send -u "generic+https://ntfy.sh/my_watchtower_topic?title=WatchtowerUpdates" -m "testMessage"
```
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
<!-- Sonarr v4 is in beta as of May 2023, should be updated to remove v3 reference when stable -->
Radarr, Prowlarr, and Sonarr v4 support ntfy natively under Settings > Connect.
Sonarr v3, Readarr, and SABnzbd support custom scripts for downloads, warnings, grabs, etc.
Some simple bash scripts to achieve this are kindly provided in [nickexyz's ntfy-shellscripts repository](https://github.com/nickexyz/ntfy-shellscripts).
It's possible to use custom scripts for all the *arr services, plus SABnzbd. Notifications for downloads, warnings, grabs etc.
Some simple bash scripts to achieve this are kindly provided in [nickexyz's repository](https://github.com/nickexyz/ntfy-shellscripts).
## Node-RED
You can use the HTTP request node to send messages with [Node-RED](https://nodered.org), some examples:
@@ -614,82 +572,4 @@ Example `template.html`:
Add notification on Rundeck (attachment type must be: `Attached as file to email`):
![Rundeck](static/img/rundeck.png)
## Traccar
This will only work on selfhosted [traccar](https://www.traccar.org/) ([Github](https://github.com/traccar/traccar)) instances, as you need to be able to set `sms.http.*` keys, which is not possible through the UI attributes
The easiest way to integrate traccar with ntfy, is to configure ntfy as the SMS provider for your instance. You then can set your ntfy topic as your account's phone number in traccar. Sending the email notifications to ntfy will not work, as ntfy does not support HTML emails.
**Info:** Add a phone number to your traccar account not in device, as otherwise it will not try to send SMS.
**Caution:** JSON publishing is only possible, when POST-ing to the root URL of the ntfy instance. (see [documentation](publish.md#publish-as-json))
```xml
<entry key='sms.http.url'>https://ntfy.sh</entry>
<entry key='sms.http.template'>
{
"topic": "{phone}",
"message": "{message}"
}
</entry>
```
If [access control](config.md#access-control) is enabled, and the target topic does not support anonymous writes, you'll also have to provide an authorization header, for example in form of a privileged token
```xml
<entry key='sms.http.authorization'>Bearer tk_JhbsnoMrgy2FcfHeofv97Pi5uXaZZ</entry>
```
or by simply providing traccar with a valid username/password combination.
```xml
<entry key='sms.http.user'>phil</entry>
<entry key='sms.http.password'>mypass</entry>
```
## Terminal Notifications for Long-Running Commands
This example provides a simple way to send notifications using [ntfy.sh](https://ntfy.sh) when a terminal command completes. It includes success or failure indicators based on the command's exit status.
Store your ntfy.sh bearer token securely if access control is enabled:
```sh
echo "your_bearer_token_here" > ~/.ntfy_token
chmod 600 ~/.ntfy_token
```
Add the following function and alias to your `.bashrc` or `.bash_profile`:
```sh
# Function for alert notifications using ntfy.sh
notify_via_ntfy() {
local exit_status=$? # Capture the exit status before doing anything else
local token=$(< ~/.ntfy_token) # Securely read the token
local status_icon="$([ $exit_status -eq 0 ] && echo magic_wand || echo warning)"
local last_command=$(history | tail -n1 | sed -e 's/^[[:space:]]*[0-9]\{1,\}[[:space:]]*//' -e 's/[;&|][[:space:]]*alert$//')
curl -s -X POST "https://n.example.dev/alerts" \
-H "Authorization: Bearer $token" \
-H "Title: Terminal" \
-H "X-Priority: 3" \
-H "Tags: $status_icon" \
-d "Command: $last_command (Exit: $exit_status)"
echo "Tags: $status_icon"
echo "$last_command (Exit: $exit_status)"
}
# Add an "alert" alias for long running commands using ntfy.sh
alias alert='notify_via_ntfy'
```
Now you can run any long-running command and append `alert` to notify when it completes:
```sh
sleep 10; alert
```
![ntfy notifications on mobile device](static/img/mobile-screenshot-notification.png)
**Notification Sent** with a success 🪄 (`magic_wand`) or failure ⚠️ (`warning`) tag.
To test failure notifications:
```sh
false; alert # Always fails (exit 1)
ls --invalid; alert # Invalid option
cat nonexistent_file; alert # File not found
```

View File

@@ -43,9 +43,9 @@ of the app and [self-host your own ntfy server](install.md).
## How much battery does the Android app use?
If you use the ntfy.sh server, and you don't use the [instant delivery](subscribe/phone.md#instant-delivery) feature,
the Android/iOS app uses no additional battery, since Firebase Cloud Messaging (FCM) is used. If you use your own server,
or you use *instant delivery* (Android only), or install from F-droid ([which does not support FCM](https://f-droid.org/docs/Inclusion_Policy/)),
the app has to maintain a constant connection to the server, which consumes about 0-1% of battery in 17h of use (on my phone).
There has been a ton of testing and improvement around this. I think it's pretty decent now.
or you use *instant delivery* (Android only), the app has to maintain a constant connection to the server, which consumes
about 0-1% of battery in 17h of use (on my phone). There has been a ton of testing and improvement around this. I think it's pretty
decent now.
## Paid plans? I thought it was open source?
All of ntfy will remain open source, with a free software license (Apache 2.0 and GPLv2). If you'd like to self-host, you
@@ -76,29 +76,7 @@ However, if you still want to disable it, you can do so with the `web-root: disa
Think of the ntfy web app like an Android/iOS app. It is freely available and accessible to anyone, yet useless without
a proper backend. So as long as you secure your backend with ACLs, exposing the ntfy web app to the Internet is harmless.
## If topic names are public, could I not just brute force them?
If you don't have [ACLs set up](config.md#access-control), the topic name is your password, it says so everywhere. If you
choose a easy-to-guess/dumb topic name, people will be able to guess it. If you choose a randomly generated topic name,
the topic is as good as a good password.
As for brute forcing: It's not possible to brute force a ntfy server for very long, as you'll get quickly rate limited.
In the default configuration, you'll be able to do 60 requests as a burst, and then 1 request per 10 seconds. Assuming you
choose a random 10 digit topic name using only A-Z, a-z, 0-9, _ and -, there are 64^10 possible topic names. Even if you
could do hundreds of requests per seconds (which you cannot), it would take many years to brute force a topic name.
For ntfy.sh, there's even a fail2ban in place which will ban your IP pretty quickly.
## Where can I donate?
I have just very recently started accepting donations via [GitHub Sponsors](https://github.com/sponsors/binwiederhier).
I would be humbled if you helped me carry the server and developer account costs. Even small donations are very much
appreciated.
## Can I email you? Can I DM you on Discord/Matrix?
While I love chatting on [Discord](https://discord.gg/cT7ECsZj9w), [Matrix](https://matrix.to/#/#ntfy-space:matrix.org),
[Lemmy](https://discuss.ntfy.sh/c/ntfy), or [GitHub](https://github.com/binwiederhier/ntfy/issues), I generally
**do not respond to emails about ntfy or direct messages** about ntfy, unless you are paying for a
[ntfy Pro](https://ntfy.sh/#pricing) plan, or you are inquiring about business opportunities.
I am sorry, but answering individual questions about ntfy on a 1-on-1 basis is not scalable. Answering your questions
in the above-mentioned forums benefits others, since I can link to the discussion at a later point in time, or other users
may be able to help out. I hope you understand.

View File

@@ -1,7 +0,0 @@
import os
import shutil
def on_post_build(config, **kwargs):
site_dir = config["site_dir"]
shutil.copytree("docs/static/fonts", os.path.join(site_dir, "get"))

View File

@@ -3,11 +3,11 @@ ntfy lets you **send push notifications to your phone or desktop via scripts fro
or POST requests. I use it to notify myself when scripts fail, or long-running commands complete.
## Step 1: Get the app
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img width="170" src="static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img width="170" src="static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img width="150" src="static/img/badge-appstore.png"></a>
<a href="https://play.google.com/store/apps/details?id=io.heckel.ntfy"><img src="../../static/img/badge-googleplay.png"></a>
<a href="https://f-droid.org/en/packages/io.heckel.ntfy/"><img src="../../static/img/badge-fdroid.png"></a>
<a href="https://apps.apple.com/us/app/ntfy/id1625396347"><img src="../../static/img/badge-appstore.png"></a>
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play, App Store or F-Droid.
To [receive notifications on your phone](subscribe/phone.md), install the app, either via Google Play or F-Droid.
Once installed, open it and subscribe to a topic of your choosing. Topics don't have to explicitly be created, so just
pick a name and use it later when you [publish a message](publish.md). Note that **topic names are public, so it's wise
to choose something that cannot be guessed easily.**

View File

@@ -14,72 +14,62 @@ We support amd64, armv7 and arm64.
1. Install ntfy using one of the methods described below
2. Then (optionally) edit `/etc/ntfy/server.yml` for the server (Linux only, see [configuration](config.md) or [sample server.yml](https://github.com/binwiederhier/ntfy/blob/main/server/server.yml))
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user), `~/Library/Application Support/ntfy/client.yml` (for the macOS non-root user), or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
3. Or (optionally) create/edit `~/.config/ntfy/client.yml` (for the non-root user) or `/etc/ntfy/client.yml` (for the root user), see [sample client.yml](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml))
To run the ntfy server, then just run `ntfy serve` (or `systemctl start ntfy` when using the deb/rpm).
To send messages, use `ntfy publish`. To subscribe to topics, use `ntfy subscribe` (see [subscribing via CLI](subscribe/cli.md)
for details).
If you like tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU) on YouTube, or
[Alex's Docker-based setup guide](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/). Both are great
resources to get started. _I am not affiliated with Kris or Alex, I just liked their video/post._
## Linux binaries
Please check out the [releases page](https://github.com/binwiederhier/ntfy/releases) for binaries and
deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.tar.gz
tar zxvf ntfy_2.15.0_linux_amd64.tar.gz
sudo cp -a ntfy_2.15.0_linux_amd64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_amd64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_x86_64.tar.gz
tar zxvf ntfy_2.0.0_linux_x86_64.tar.gz
sudo cp -a ntfy_2.0.0_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.tar.gz
tar zxvf ntfy_2.15.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.15.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv6.tar.gz
tar zxvf ntfy_2.0.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.0.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.tar.gz
tar zxvf ntfy_2.15.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.15.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv7.tar.gz
tar zxvf ntfy_2.0.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.0.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.tar.gz
tar zxvf ntfy_2.15.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.15.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_arm64.tar.gz
tar zxvf ntfy_2.0.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.0.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.0.0_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
## Debian/Ubuntu repository
!!! info
As of September 2025, **the official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh/apt)**.
The old repository [archive.heckel.io](https://archive.heckel.io/apt) is still available for now, but will likely
go away soon. I suspect I will phase it out some time in early 2026.
Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4 6B7C CFDB 962D 4F1E C4AF`):
Installation via Debian repository:
=== "x86_64/amd64"
```bash
sudo mkdir -p /etc/apt/keyrings
sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
sudo apt install apt-transport-https
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
| sudo tee /etc/apt/sources.list.d/ntfy.list
sudo sh -c "echo 'deb [arch=amd64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
@@ -89,10 +79,10 @@ Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4
=== "armv7/armhf"
```bash
sudo mkdir -p /etc/apt/keyrings
sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
sudo apt install apt-transport-https
echo "deb [arch=armhf signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
| sudo tee /etc/apt/sources.list.d/ntfy.list
sudo sh -c "echo 'deb [arch=armhf signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
@@ -102,10 +92,10 @@ Installation via Debian/Ubuntu repository (fingerprint `55BA 774A 6F5E E674 31E4
=== "arm64"
```bash
sudo mkdir -p /etc/apt/keyrings
sudo curl -L -o /etc/apt/keyrings/ntfy.gpg https://archive.ntfy.sh/apt/keyring.gpg
curl -fsSL https://archive.heckel.io/apt/pubkey.txt | sudo gpg --dearmor -o /etc/apt/keyrings/archive.heckel.io.gpg
sudo apt install apt-transport-https
echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/ntfy.gpg] https://archive.ntfy.sh/apt stable main" \
| sudo tee /etc/apt/sources.list.d/ntfy.list
sudo sh -c "echo 'deb [arch=arm64 signed-by=/etc/apt/keyrings/archive.heckel.io.gpg] https://archive.heckel.io/apt debian main' \
> /etc/apt/sources.list.d/archive.heckel.io.list"
sudo apt update
sudo apt install ntfy
sudo systemctl enable ntfy
@@ -116,7 +106,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -124,7 +114,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -132,7 +122,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -140,7 +130,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -150,36 +140,34 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
## Arch Linux
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/).
You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download,
build and install ntfy and keep it up to date.
ntfy can be installed using an [AUR package](https://aur.archlinux.org/packages/ntfysh-bin/). You can use an [AUR helper](https://wiki.archlinux.org/title/AUR_helpers) like `paru`, `yay` or others to download, build and install ntfy and keep it up to date.
```
paru -S ntfysh-bin
```
@@ -201,35 +189,30 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_macOS_all.tar.gz),
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz > ntfy_2.15.0_darwin_all.tar.gz
tar zxvf ntfy_2.15.0_darwin_all.tar.gz
sudo cp -a ntfy_2.15.0_darwin_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_macOS_all.tar.gz > ntfy_2.0.0_macOS_all.tar.gz
tar zxvf ntfy_2.0.0_macOS_all.tar.gz
sudo cp -a ntfy_2.0.0_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.15.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.0.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
!!! info
Only the ntfy CLI is supported on macOS. ntfy server is currently not supported, but you can build and run it for
development as well. Check out the [build instructions](develop.md) for details.
## Homebrew
To install the [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) via Homebrew (Linux and macOS),
simply run:
```
brew install ntfy
```
There is a [GitHub issue](https://github.com/binwiederhier/ntfy/issues/286) about making ntfy installable via
[Homebrew](https://brew.sh/). I'll eventually get to that, but I'd also love if somebody else stepped up to do it.
Also, you can build and run the ntfy server on macOS as well, though I don't officially support that.
Check out the [build instructions](develop.md) for details.
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.0.0/ntfy_2.0.0_windows_x86_64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).
@@ -283,8 +266,10 @@ docker run \
serve
```
Using docker-compose with non-root user and healthchecks enabled:
Using docker-compose with non-root user:
```yaml
version: "2.1"
services:
ntfy:
image: binwiederhier/ntfy
@@ -299,14 +284,7 @@ services:
- /etc/ntfy:/etc/ntfy
ports:
- 80:80
healthcheck: # optional: remember to adapt the host:port to your environment
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"]
interval: 60s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
init: true # needed, if healthcheck is used. Prevents zombie processes
```
If using a non-root user when running the docker version, be sure to chown the server.yml, user.db, and cache.db files and attachments directory to the same uid/gid.
@@ -325,6 +303,7 @@ The setup for Kubernetes is very similar to that for Docker, and requires a fair
are a few options to mix and match, including a deployment without a cache file, a stateful set with a persistent cache, and a standalone
unmanned pod.
=== "deployment"
```yaml
apiVersion: apps/v1
@@ -543,7 +522,7 @@ kubectl apply -k /ntfy
cpu: 150m
memory: 150Mi
volumeMounts:
- mountPath: /etc/ntfy
- mountPath: /etc/ntfy/server.yml
subPath: server.yml
name: config-volume # generated vie configMapGenerator from kustomization file
- mountPath: /var/cache/ntfy

View File

@@ -4,21 +4,24 @@ There are quite a few projects that work with ntfy, integrate ntfy, or have been
I've added a ⭐ to projects or posts that have a significant following, or had a lot of interaction by the community.
## Table of Contents
## Public ntfy servers
- [Official integrations](#official-integrations)
- [Integration via HTTP/SMTP/etc.](#integration-via-httpsmtpetc)
- [UnifiedPush integrations](#unifiedpush-integrations)
- [Libraries](#libraries)
- [CLIs + GUIs](#clis-guis)
- [Projects + scripts](#projects-scripts)
- [Blog + forum posts](#blog-forum-posts)
- [Alternative ntfy servers](#alternative-ntfy-servers)
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
ntfy community. Thanks to everyone running a public server. **You guys rock!**
| URL | Country |
|---------------------------------------------------|--------------------|
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
and uptime of third party servers, so use of each server is **at your own discretion**.
## Official integrations
- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
- [Home Assistant](https://www.home-assistant.io/integrations/ntfy) ⭐ - Home Assistant is an open-source platform for automating and controlling smart home devices.
- [Healthchecks.io](https://healthchecks.io/) ⭐ - Online service for monitoring regularly running tasks such as cron jobs
- [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy) ⭐ - Push notifications that work with just about every platform
- [Uptime Kuma](https://uptime.kuma.pet/) ⭐ - A self-hosted monitoring tool
@@ -29,30 +32,9 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [Gatus](https://gatus.io/) ⭐ - Automated service health dashboard
- [Automatisch](https://automatisch.io/) ⭐ - Open source Zapier alternative / workflow automation tool
- [FlexGet](https://flexget.com/Plugins/Notifiers/ntfysh) ⭐ - Multipurpose automation tool for all of your media
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.8/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
- [Netdata](https://learn.netdata.cloud/docs/alerts-and-notifications/notifications/agent-alert-notifications/ntfy) ⭐ - Real-time performance monitoring
- [Deployer](https://github.com/deployphp/deployer) ⭐ - PHP deployment tool
- [Shoutrrr](https://containrrr.dev/shoutrrr/v0.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
- [Scrt.link](https://scrt.link/) - Share a secret
- [Platypush](https://docs.platypush.tech/platypush/plugins/ntfy.html) - Automation platform aimed to run on any device that can run Python
- [diun](https://crazymax.dev/diun/) - Docker Image Update Notifier
- [Cloudron](https://www.cloudron.io/store/sh.ntfy.cloudronapp.html) - Platform that makes it easy to manage web apps on your server
- [Xitoring](https://xitoring.com/docs/notifications/notification-roles/ntfy/) - Server and Uptime monitoring
- [HetrixTools](https://docs.hetrixtools.com/ntfy-sh-notifications/) - Uptime monitoring
- [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage) - Visual data transformation and automation tool
- [Monibot](https://monibot.io/) - Monibot monitors your websites, servers and applications and notifies you if something goes wrong.
- [Miniflux](https://miniflux.app/docs/ntfy.html) - Minimalist and opinionated feed reader
- [Beszel](https://beszel.dev/guide/notifications/ntfy) - Server monitoring platform
## Integration via HTTP/SMTP/etc.
- [Watchtower](https://containrrr.dev/watchtower/) ⭐ - Automating Docker container base image updates (see [integration example](examples.md#watchtower-shoutrrr))
- [Jellyfin](https://jellyfin.org/) ⭐ - The Free Software Media System (see [integration example](examples.md#))
- [Overseerr](https://docs.overseerr.dev/using-overseerr/notifications/webhooks) ⭐ - a request management and media discovery tool for Plex (see [integration example](examples.md#jellyseerroverseerr-webhook))
- [Tautulli](https://github.com/Tautulli/Tautulli) ⭐ - Monitoring and tracking tool for Plex (integration [via webhook](https://github.com/Tautulli/Tautulli/wiki/Notification-Agents-Guide#webhook))
- [Mailrise](https://github.com/YoRyan/mailrise) - An SMTP gateway (integration via [Apprise](https://github.com/caronc/apprise/wiki/Notify_ntfy))
- [Proxmox-Ntfy](https://github.com/qtsone/proxmox-ntfy) - Python script that monitors Proxmox tasks and sends notifications using the Ntfy service.
- [Scrutiny](https://github.com/AnalogJ/scrutiny) - WebUI for smartd S.M.A.R.T monitoring. Scrutiny includes shoutrrr/ntfy integration ([see integration README](https://github.com/AnalogJ/scrutiny?tab=readme-ov-file#notifications))
- [UptimeObserver](https://uptimeobserver.com) - Uptime Monitoring tool for Websites, APIs, SSL Certificates, DNS, Domain Names and Ports. [Integration Guide](https://support.uptimeobserver.com/integrations/ntfy/)
## [UnifiedPush](https://unifiedpush.org/users/apps/) integrations
@@ -76,43 +58,30 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy](https://github.com/jonocarroll/ntfy) - Wraps the ntfy API with pipe-friendly tooling (R)
- [ntfy-for-delphi](https://github.com/hazzelnuts/ntfy-for-delphi) - A friendly library to push instant notifications ntfy (Delphi)
- [ntfy](https://github.com/ffflorian/ntfy) - Send notifications over ntfy (JS)
- [ntfy_dart](https://github.com/jr1221/ntfy_dart) - Dart wrapper around the ntfy API (Dart)
- [gotfy](https://github.com/AnthonyHewins/gotfy) - A Go wrapper for the ntfy API (Go)
- [symfony/ntfy-notifier](https://symfony.com/components/NtfyNotifier) ⭐ - Symfony Notifier integration for ntfy (PHP)
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
- [aiontfy](https://github.com/tr4nt0r/aiontfy) - Asynchronous client library for publishing and subscribing to ntfy (Python)
## CLIs + GUIs
- [ntfy.sh.sh](https://github.com/mininmobile/ntfy.sh.sh) - Run scripts on ntfy.sh events
- [ntfy-desktop](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
- [ntfy-desktop](https://github.com/Aetherinox/ntfy-desktop) - Desktop client for Windows, Linux, and MacOS with push notifications
- [ntfy Desktop client](https://codeberg.org/zvava/ntfy-desktop) - Cross-platform desktop application for ntfy
- [ntfy svelte front-end](https://github.com/novatorem/Ntfy) - Front-end built with svelte
- [wio-ntfy-ticker](https://github.com/nachotp/wio-ntfy-ticker) - Ticker display for a ntfy.sh topic
- [ntfysh-windows](https://github.com/lucas-bortoli/ntfysh-windows) - A ntfy client for Windows Desktop
- [ntfyr](https://github.com/haxwithaxe/ntfyr) - A simple commandline tool to send notifications to ntfy
- [ntfy.py](https://github.com/ioqy/ntfy-client-python) - ntfy.py is a simple nfty.sh client for sending notifications
- [wlzntfy](https://github.com/Walzen-Group/ntfy-toaster) - A minimalistic, receive-only toast notification client for Windows 11
- [Ntfy_CSV_Reminders](https://github.com/thiswillbeyourgithub/Ntfy_CSV_Reminders) - A Python tool that sends random-timing phone notifications for recurring tasks by using daily probability checks based on CSV-defined frequencies.
- [Daily Fact Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy) - Generate [llm](https://github.com/simonw/llm) generated fact every day about any topic you're interested in.
- [ntfyexec](https://github.com/alecthomas/ntfyexec) - Send a notification through ntfy.sh if a command fails
- [Ntfy Desktop](https://github.com/emmaexe/ntfyDesktop) - Fully featured desktop client for Linux, built with Qt and C++.
## Projects + scripts
- [Grafana-to-ntfy](https://github.com/kittyandrew/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Rust)
- [Grafana-ntfy-webhook-integration](https://github.com/academo/grafana-alerting-ntfy-webhook-integration) - Integrates Grafana alerts webhooks (Go)
- [Grafana-to-ntfy](https://gitlab.com/Saibe1111/grafana-to-ntfy) - Grafana-to-ntfy alerts channel (Node Js)
- [ntfy-long-zsh-command](https://github.com/robfox92/ntfy-long-zsh-command) - Notifies you once a long-running command completes (zsh)
- [ntfy-shellscripts](https://github.com/nickexyz/ntfy-shellscripts) - A few scripts for the ntfy project (Shell)
- [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay) - ntfy.sh relay for Alertmanager (Go)
- [QuickStatus](https://github.com/corneliusroot/QuickStatus) - A shell script to alert to any immediate problems upon login (Shell)
- [ntfy.el](https://github.com/shombando/ntfy) - Send notifications from Emacs (Emacs)
- [backup-projects](https://gist.github.com/anthonyaxenov/826ba65abbabd5b00196bc3e6af76002) - Stupidly simple backup script for own projects (Shell)
- [grav-plugin-whistleblower](https://github.com/Himmlisch-Studios/grav-plugin-whistleblower) - Grav CMS plugin to get notifications via ntfy (PHP)
- [ntfy-server-status](https://github.com/filip2cz/ntfy-server-status) - Checking if server is online and reporting through ntfy (C)
- [borg-based backup](https://github.com/davidhi7/backup) - Simple borg-based backup script with notifications based on ntfy.sh or Discord webhooks (Python/Shell)
- [ntfy.sh *arr script](https://github.com/agent-squirrel/nfty-arr-script) - Quick and hacky script to get sonarr/radarr to notify the ntfy.sh service (Shell)
- [website-watcher](https://github.com/muety/website-watcher) - A small tool to watch websites for changes (with XPath support) (Python)
- [siteeagle](https://github.com/tpanum/siteeagle) - A small Python script to monitor websites and notify changes (Python)
- [send_to_phone](https://github.com/whipped-cream/send_to_phone) - Scripts to upload a file to Transfer.sh and ping ntfy with the download link (Python)
- [ntfy Discord bot](https://github.com/R0dn3yS/ntfy-bot) - WIP ntfy discord bot (TypeScript)
@@ -136,107 +105,15 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy_on_a_chip](https://github.com/gergepalfi/ntfy_on_a_chip) - ESP8266 and ESP32 client code to communicate with ntfy
- [ntfy-sdk](https://github.com/yukibtc/ntfy-sdk) - ntfy client library to send notifications (Rust)
- [ntfy_ynh](https://github.com/YunoHost-Apps/ntfy_ynh) - ntfy app for YunoHost
- [woodpecker-ntfy](https://codeberg.org/l-x/woodpecker-ntfy)- Woodpecker CI plugin for sending ntfy notfication from a pipeline (Go)
- [drone-ntfy](https://github.com/Clortox/drone-ntfy) - Drone.io plugin for sending ntfy notifications from a pipeline (Shell)
- [ignition-ntfy-module](https://github.com/Kyvis-Labs/ignition-ntfy-module) - Adds support for sending notifications via a ntfy server to Ignition (Java)
- [maubot-ntfy](https://gitlab.com/999eagle/maubot-ntfy) - Matrix bot to subscribe to ntfy topics and send messages to Matrix (Python)
- [ntfy-wrapper](https://github.com/vict0rsch/ntfy-wrapper) - Wrapper around ntfy (Python)
- [nodebb-plugin-ntfy](https://github.com/NodeBB/nodebb-plugin-ntfy) - Push notifications for NodeBB forums
- [n8n-ntfy](https://github.com/raghavanand98/n8n-ntfy.sh) - n8n community node that lets you use ntfy in your workflows
- [nlog-ntfy](https://github.com/MichelMichels/nlog-ntfy) - Send NLog messages over ntfy (C# / .NET / NLog)
- [helm-charts](https://github.com/sarab97/helm-charts) - Helm charts of some of the selfhosted services, incl. ntfy
- [ntfy_ansible_role](https://github.com/stevenengland/ntfy_ansible_role) (on [Ansible Galaxy](https://galaxy.ansible.com/stevenengland/ntfy)) - Ansible role to install ntfy
- [easy2ntfy](https://github.com/chromoxdor/easy2ntfy) - Gateway for ESPeasy to receive commands through ntfy and using easyfetch (HTML/JS)
- [ntfy_lite](https://github.com/MPI-IS/ntfy_lite) - Minimalist python API for pushing ntfy notifications (Python)
- [notify](https://github.com/guanguans/notify) - 推送通知 (PHP)
- [zpool-events](https://github.com/maglar0/zpool-events) - Notify on ZFS pool events (Python)
- [ntfyd](https://github.com/joachimschmidt557/ntfyd) - ntfy desktop daemon (Zig)
- [ntfy-browser](https://github.com/johman10/ntfy-browser) - browser extension to receive notifications without having the page open (TypeScript)
- [ntfy-electron](https://github.com/xdpirate/ntfy-electron) - Electron wrapper for the ntfy web app (JS)
- [systemd-ntfy-poweronoff](https://github.com/stendler/systemd-ntfy-poweronoff) - Systemd services to send notifications on system startup, shutdown and service failure
- [msgdrop](https://github.com/jbrubake/msgdrop) - Send and receive encrypted messages (Bash)
- [vigilant](https://github.com/VerifiedJoseph/vigilant) - Monitor RSS/ATOM and JSON feeds, and send push notifications on new entries (PHP)
- [ansible-role-ntfy-alertmanager](https://github.com/bleetube/ansible-role-ntfy-alertmanager) - Ansible role to install xenrox/ntfy-alertmanager
- [NtfyMe-Blender](https://github.com/NotNanook/NtfyMe-Blender) - Blender addon to send notifications to NtfyMe (Python)
- [ntfy-ios-url-share](https://www.icloud.com/shortcuts/be8a7f49530c45f79733cfe3e41887e6) - An iOS shortcut that lets you share URLs easily and quickly.
- [ntfy-ios-filesharing](https://www.icloud.com/shortcuts/fe948d151b2e4ae08fb2f9d6b27d680b) - An iOS shortcut that lets you share files from your share feed to a topic of your choice.
- [systemd-ntfy](https://hackage.haskell.org/package/systemd-ntfy) - monitor a set of systemd services an send a notification to ntfy.sh whenever their status changes
- [RouterOS Scripts](https://git.eworm.de/cgit/routeros-scripts/about/) - a collection of scripts for MikroTik RouterOS
- [ntfy-android-builder](https://github.com/TheBlusky/ntfy-android-builder) - Script for building ntfy-android with custom Firebase configuration (Docker/Shell)
- [jetspotter](https://github.com/vvanouytsel/jetspotter) - send notifications when planes are spotted near you (Go)
- [monitoring_ntfy](https://www.drupal.org/project/monitoring_ntfy) - Drupal monitoring Ntfy.sh integration (PHP/Drupal)
- [Notify](https://flathub.org/apps/com.ranfdev.Notify) - Native GTK4 client for ntfy (Rust)
- [notify-via-ntfy](https://exchange.checkmk.com/p/notify-via-ntfy) - Checkmk plugin to send notifications via ntfy (Python)
- [ntfy-java](https://github.com/MaheshBabu11/ntfy-java/) - A Java package to interact with a ntfy server (Java)
- [container-update-check](https://github.com/stendler/container-update-check) - Scripts to check and notify if a podman or docker container image can be updated (Podman/Shell)
- [ignition-combustion-template](https://github.com/stendler/ignition-combustion-template) - Templates and scripts to generate a configuration to automatically setup a system on first boot. Including systemd-ntfy-poweronoff (Shell)
- [ntfy-run](https://github.com/quantum5/ntfy-run) - Tool to run a command, capture its output, and send it to ntfy (Rust)
- [Clipboard IO](https://github.com/jim3692/clipboard-io) - End to end encrypted clipboard
- [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp) - An ntfy MCP server for sending/fetching ntfy notifications to your self-hosted ntfy server from AI Agents (supports secure token auth & more - use with npx or docker!) (Node/Typescript)
- [InvaderInformant](https://github.com/patricksthannon/InvaderInformant) - Script for Mac OS systems that monitors new or dropped connections to your network using ntfy (Shell)
- [NtfyPwsh](https://github.com/ptmorris1/NtfyPwsh) - PowerShell module to help send messages to ntfy (PowerShell)
- [ntfyrr](https://github.com/leukosaima/ntfyrr) - Overseerr and Maintainerr webhook notification to ntfy helper service (C#)
- [ntfy for Sandstorm](https://apps.sandstorm.io/app/c6rk81r4qk6dm3k04x1kxmyccqewhh4npuxeyg1xrpfypn2ddy0h) - ntfy app for the Sandstorm platform
- [ntfy-heartbeat-monitor](https://codeberg.org/RockWolf/ntfy-heartbeat-monitor) - Application for implementing heartbeat monitoring/alerting by utilizing ntfy
- [ntfy-bridge](https://github.com/AlexGaudon/ntfy-bridge) - An application to bridge Discord messages (or webhooks) to ntfy.
- [ntailfy](https://github.com/leukosaima/ntailfy) - ntfy notifications when Tailscale devices connect/disconnect (Go)
## Blog + forum posts
- [Device notifications via HTTP with ntfy](https://alistairshepherd.uk/writing/ntfy/) - alistairshepherd.uk - 6/2025
- [Notifications about (almost) anything with ntfy.sh](https://hamatti.org/posts/notifications-about-almost-anything-with-ntfy-sh/) - hamatti.org - 6/2025
- [I set up a self-hosted notification service for everything, and I'll never look back](https://www.xda-developers.com/set-up-self-hosted-notification-service/) ⭐ - xda-developers.com - 5/2025
- [How to Set Up Ntfy: Self-Hosted Push Notifications Made Easy](https://www.youtube.com/watch?v=wDJDiAYZ3H0) - youtube.com (sass drew) - 1/2025
- [The NTFY is a game-changer FREE solution for IT people](https://www.youtube.com/watch?v=NtlztHT-sRw) - youtube.com (Valters Tech Turf) - 1/2025
- [Notify: A Powerful Tool for Real-Time Notifications (ntfy.sh)](https://www.youtube.com/watch?v=XXTTeVfGBz0) - youtube.com (LinuxCloudHacks) - 12/2025
- [Push notifications with ntfy and n8n](https://www.youtube.com/watch?v=DKG1R3xYvwQ) - youtube.com (Oskar) - 10/2024
- [Setup ntfy for selfhosted notifications with Cloudflare Tunnel](https://medium.com/@svenvanginkel/setup-ntfy-for-selfhosted-notifications-with-cloudflare-tunnel-e342f470177d) - medium.com (Sven van Ginkel) - 10/2024
- [Self-Host NTFY - How It Works & Easy Setup Guide](https://www.youtube.com/watch?v=79wHc_jfrJE) ⭐ - youtube.com (Techdox)- 9/2024
- [ntfy / Emacs Lisp](https://speechcode.com/blog/ntfy/) - speechcode.com - 3/2024
- [Boost Your Productivity with ntfy.sh: The Ultimate Notification Tool for Command-Line Users](https://dev.to/archetypal/boost-your-productivity-with-ntfysh-the-ultimate-notification-tool-for-command-line-users-iil) - dev.to - 3/2024
- [Nextcloud Talk (F-Droid version) notifications using ntfy (ntfy.sh)](https://www.youtube.com/watch?v=0a6PpfN5PD8) - youtube.com - 2/2024
- [ZFS and SMART Warnings via Ntfy](https://rair.dev/zfs-smart-ntfy/) - rair.dev - 2/2024
- [Automating Security Camera Notifications With Home Assistant and Ntfy](https://runtimeterror.dev/automating-camera-notifications-home-assistant-ntfy/) ⭐ - runtimeterror.dev - 2/2024
- [Ntfy: self-hosted notification service](https://medium.com/@williamdonze/ntfy-self-hosted-notification-service-0f3eada6e657) ⭐ - williamdonze.medium.com - 1/2024
- [Lets Supercharge Snowflake Alerts with Cool ntfy Open-source Notifications!](https://sarathi-data-ml-cloud.medium.com/lets-supercharge-snowflake-alerts-with-cool-ntfy-open-source-notifications-296da442c331) - sarathi-data-ml-cloud.medium.com - 1/2024
- [Setting up NTFY with Ngnix-Proxy-Manager, authentication and Ansible notifications](https://random-it-blog.de/rocky-linux/setting-up-ntfy-with-ngnix-proxy-manager-authentication-and-ansible-notifications/) - random-it-blog.de - 12/2023
- [Introducing the Monitoring Ntfy.sh Integration Module: Real-time Notifications for Drupal Monitoring](https://cyberschorsch.dev/drupal/introducing-monitoring-ntfysh-integration-module-real-time-notifications-drupal-monitoring) - cyberschorsch.dev - 11/2023
- [How to install Ntfy.sh on CasaOS using BigBearCasaOS](https://www.youtube.com/watch?v=wSWhtSNwTd8) - youtube.com - 10/2023
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-update-notifications-ntfy/) - rair.dev - 9/2023
- [Easy Push Notifications With ntfy.sh](https://runtimeterror.dev/easy-push-notifications-with-ntfy/) ⭐ - runtimeterror.dev - 9/2023
- [Ntfy: Your Ultimate Push Notification Powerhouse!](https://kkamalesh117.medium.com/ntfy-your-ultimate-push-notification-powerhouse-1968c070f1d1) - kkamalesh117.medium.com - 9/2023
- [Installing Self Host NTFY On Linux Using Docker Container](https://www.pinoylinux.org/topicsplus/containers/installing-self-host-ntfy-on-linux-using-docker-container/) - pinoylinux.org - 9/2023
- [Homelab Notifications with ntfy](https://blog.alexsguardian.net/posts/2023/09/12/selfhosting-ntfy/) ⭐ - alexsguardian.net - 9/2023
- [Why NTFY is the Ultimate Push Notification Tool for Your Needs](https://osintph.medium.com/why-ntfy-is-the-ultimate-push-notification-tool-for-your-needs-e767421c84c5) - osintph.medium.com - 9/2023
- [Supercharge Your Alerts: Ntfy — The Ultimate Push Notification Solution](https://medium.com/spring-boot/supercharge-your-alerts-ntfy-the-ultimate-push-notification-solution-a3dda79651fe) - spring-boot.medium.com - 9/2023
- [Deploy Ntfy using Docker](https://www.linkedin.com/pulse/deploy-ntfy-mohamed-sharfy/) - linkedin.com - 9/2023
- [Send Notifications With Ntfy for New WordPress Posts](https://www.activepieces.com/blog/ntfy-notifications-for-wordpress-new-posts) - activepieces.com - 9/2023
- [Get Ntfy Notifications About New Zendesk Ticket](https://www.activepieces.com/blog/ntfy-notifications-about-new-zendesk-tickets) - activepieces.com - 9/2023
- [Set reminder for recurring events using ntfy & Cron](https://www.youtube.com/watch?v=J3O4aQ-EcYk) - youtube.com - 9/2023
- [ntfy - Installation and full configuration setup](https://www.youtube.com/watch?v=QMy14rGmpFI) - youtube.com - 9/2023
- [How to install Ntfy.sh on Portainer / Docker Compose](https://www.youtube.com/watch?v=utD9GNbAwyg) - youtube.com - 9/2023
- [ntfy - Push-Benachrichtigungen // Push Notifications](https://www.youtube.com/watch?v=LE3vRPPqZOU) - youtube.com - 9/2023
- [Podman Update Notifications via Ntfy](https://rair.dev/podman-upadte-notifications-ntfy/) - rair.dev - 9/2023
- [How to Send Alerts From Raspberry Pi Pico W to a Phone or Tablet](https://www.tomshardware.com/how-to/send-alerts-raspberry-pi-pico-w-to-mobile-device) - tomshardware.com - 8/2023
- [NetworkChunk - how did I NOT know about this?](https://www.youtube.com/watch?v=poDIT2ruQ9M) ⭐ - youtube.com - 8/2023
- [NTFY - Command-Line Notifications](https://academy.networkchuck.com/blog/ntfy/) - academy.networkchuck.com - 8/2023
- [Open Source Push Notifications! Get notified of any event you can imagine. Triggers abound!](https://www.youtube.com/watch?v=WJgwWXt79pE) ⭐ - youtube.com - 8/2023
- [How to install and self host an Ntfy server on Linux](https://linuxconfig.org/how-to-install-and-self-host-an-ntfy-server-on-linux) - linuxconfig.org - 7/2023
- [Basic website monitoring using cronjobs and ntfy.sh](https://burkhardt.dev/2023/website-monitoring-cron-ntfy/) - burkhardt.dev - 6/2023
- [Pingdom alternative in one line of curl through ntfy.sh](https://piqoni.bearblog.dev/uptime-monitoring-in-one-line-of-curl/) - bearblog.dev - 6/2023
- [#OpenSourceDiscovery 78: ntfy.sh](https://opensourcedisc.substack.com/p/opensourcediscovery-78-ntfysh) - opensourcedisc.substack.com - 6/2023
- [ntfy: des notifications instantanées](https://blogmotion.fr/diy/ntfy-notification-push-domotique-20708) - blogmotion.fr - 5/2023
- [桌面通知ntfy](https://www.cnblogs.com/xueweihan/archive/2023/05/04/17370060.html) - cnblogs.com - 5/2023
- [ntfy.sh - Open source push notifications via PUT/POST](https://lobste.rs/s/5drapz/ntfy_sh_open_source_push_notifications) - lobste.rs - 5/2023
- [Install ntfy Inside Docker Container in Linux](https://lindevs.com/install-ntfy-inside-docker-container-in-linux) - lindevs.com - 4/2023
- [ntfy.sh](https://neo-sahara.com/wp/2023/03/25/ntfy-sh/) - neo-sahara.com - 3/2023
- [Using Ntfy to send and receive push notifications - Samuel Rosa de Oliveria - Delphicon 2023](https://www.youtube.com/watch?v=feu0skpI9QI) - youtube.com - 3/2023
- [ntfy: własny darmowy system powiadomień](https://sprawdzone.it/ntfy-wlasny-darmowy-system-powiadomien/) - sprawdzone.it - 3/2023
- [Deploying ntfy on railway](https://www.youtube.com/watch?v=auJICXtxoNA) - youtube.com - 3/2023
- [Start-Job,Variables, and ntfy.sh](https://klingele.dev/2023/03/01/start-jobvariables-and-ntfy-sh/) - klingele.dev - 3/2023
- [enviar notificaciones automáticas usando ntfy.sh](https://osiux.com/2023-02-15-send-automatic-notifications-using-ntfy.html) - osiux.com - 2/2023
- [Carnet IP动态解析以及通过ntfy推送IP信息](https://blog.wslll.cn/index.php/archives/201/) - blog.wslll.cn - 2/2023
- [Open-Source-Brieftaube: ntfy verschickt Push-Meldungen auf Smartphone und PC](https://www.heise.de/news/Open-Source-Brieftaube-ntfy-verschickt-Push-Meldungen-auf-Smartphone-und-PC-7521583.html) ⭐ - heise.de - 2/2023
- [Video: Simple Push Notifications ntfy](https://www.youtube.com/watch?v=u9EcWrsjE20) ⭐ - youtube.com - 2/2023
- [Use ntfy.sh with Home Assistant](https://diecknet.de/en/2023/02/12/ntfy-sh-with-homeassistant/) - diecknet.de - 2/2023
- [On installe Ntfy sur Synology Docker](https://www.maison-et-domotique.com/140356-serveur-notification-jeedom-ntfy-synology-docker/) - maison-et-domotique.co - 1/2023
@@ -245,13 +122,10 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [UnifiedPush: a decentralized, open-source push notification protocol](https://f-droid.org/en/2022/12/18/unifiedpush.html) ⭐ - 12/2022
- [ntfy setup instructions](https://docs.benjamin-altpeter.de/network/vms/1001029-ntfy/) - benjamin-altpeter.de - 12/2022
- [Ntfy Self-Hosted Push Notifications](https://lachlanlife.net/posts/2022-12-ntfy/) - lachlanlife.net - 12/2022
- [NTFY - système de notification hyper simple et complet](https://www.youtube.com/watch?v=UieZYWVVgA4) - youtube.com - 12/2022
- [ntfy.sh](https://paramdeo.com/til/ntfy-sh) - paramdeo.com - 11/2022
- [Using ntfy to warn me when my computer is discharging](https://ulysseszh.github.io/programming/2022/11/28/ntfy-warn-discharge.html) - ulysseszh.github.io - 11/2022
- [Enabling SSH Login Notifications using Ntfy](https://paramdeo.com/blog/enabling-ssh-login-notifications-using-ntfy) - paramdeo.com - 11/2022
- [ntfy - Push Notification Service](https://dizzytech.de/posts/ntfy/) - dizzytech.de - 11/2022
- [Console #132](https://console.substack.com/p/console-132) ⭐ - console.substack.com - 11/2022
- [How to make my phone buzz*](https://evbogue.com/howtomakemyphonebuzz) - evbogue.com - 11/2022
- [MeshCentral - Ntfy Push Notifications ](https://www.youtube.com/watch?v=wyE4rtUd4Bg) - youtube.com - 11/2022
- [Changelog | Tracking layoffs, tech worker demand still high, ntfy, ...](https://changelog.com/news/tracking-layoffs-tech-worker-demand-still-high-ntfy-devenv-markdoc-mike-bifulco-Y1jW) ⭐ - changelog.com - 11/2022
- [Pointer | Issue #367](https://www.pointer.io/archives/a9495a2a6f/) - pointer.io - 11/2022
@@ -289,24 +163,3 @@ I've added a ⭐ to projects or posts that have a significant following, or had
- [ntfy otro sistema de notificaciones pub-sub simple basado en HTTP](https://ugeek.github.io/blog/post/2021-11-05-ntfy-sh-otro-sistema-de-notificaciones-pub-sub-simple-basado-en-http.html) - ugeek.github.io - 11/2021
- [Show HN: A tool to send push notifications to your phone, written in Go](https://news.ycombinator.com/item?id=29715464) ⭐ - news.ycombinator.com - 12/2021
- [Reddit selfhostable post](https://www.reddit.com/r/selfhosted/comments/qxlsm9/my_open_source_notification_android_app_and/) ⭐ - reddit.com - 11/2021
- [ntfy on The Canary in the Cage Podcast](https://odysee.com/@TheCanaryInTheCage:b/The-Canary-in-the-Cage-Episode-42:1?r=4gitYjTacQqPEjf22874USecDQYJ5y5E&t=3062) - odysee.com - 1/2025
- [NtfyPwsh - A PowerShell Module to Send Ntfy Messages](https://ptmorris1.github.io/posts/NtfyPwsh/) - github.io - 5/2025
## Alternative ntfy servers
Here's a list of public ntfy servers. As of right now, there is only one official server. The others are provided by the
ntfy community. Thanks to everyone running a public server. **You guys rock!**
| URL | Country |
|---------------------------------------------------|--------------------|
| [ntfy.sh](https://ntfy.sh/) (*Official*) | 🇺🇸 United States |
| [ntfy.tedomum.net](https://ntfy.tedomum.net/) | 🇫🇷 France |
| [ntfy.jae.fi](https://ntfy.jae.fi/) | 🇫🇮 Finland |
| [ntfy.adminforge.de](https://ntfy.adminforge.de/) | 🇩🇪 Germany |
| [ntfy.envs.net](https://ntfy.envs.net) | 🇩🇪 Germany |
| [ntfy.mzte.de](https://ntfy.mzte.de/) | 🇩🇪 Germany |
| [ntfy.hostux.net](https://ntfy.hostux.net/) | 🇫🇷 France |
| [ntfy.fossman.de](https://ntfy.fossman.de/) | 🇩🇪 Germany |
Please be aware that **server operators can log your messages**. The project also cannot guarantee the reliability
and uptime of third party servers, so use of each server is **at your own discretion**.

View File

@@ -1,5 +1,5 @@
# Known issues
This is an incomplete list of known issues with the ntfy server, web app, Android app, and iOS app. You can find a complete
This is an incomplete list of known issues with the ntfy server, Android app, and iOS app. You can find a complete
list [on GitHub](https://github.com/binwiederhier/ntfy/labels/%F0%9F%AA%B2%20bug), but I thought it may be helpful
to have the prominent ones here to link to.
@@ -8,7 +8,7 @@ For some (many?) users, the iOS app is not refreshing the view when new notifica
swipe down, you do not see the newly arrived messages, even though the popup appeared before.
This is caused by some weirdness between the Notification Service Extension (NSE), SwiftUI and Core Data. I am entirely
clueless on how to fix it, sadly, as it is ephemeral and not clear to me what is causing it.
clueless on how to fix it, sadly, as it is ephemeral and now clear to me what is causing it.
Please send experienced iOS developers my way to help me figure this out.
@@ -26,18 +26,3 @@ Be sure that in your selfhosted server:
* Set `upstream-base-url: "https://ntfy.sh"` (**not your own hostname!**)
* Ensure that the URL you set in `base-url` **matches exactly** what you set the Default Server in iOS to
## iOS app seeing "New message", but not real message content
If you see `New message` notifications on iOS, your iPhone can likely not talk to your self-hosted server. Be sure that
your iOS device and your ntfy server are either on the same network, or that your phone can actually reach the server.
Turn on tracing/debugging on the server (via `log-level: trace` or `log-level: debug`, see [troubleshooting](troubleshooting.md)),
and read docs on [iOS instant notifications](https://docs.ntfy.sh/config/#ios-instant-notifications).
## Safari does not play sounds for web push notifications
Safari does not support playing sounds for web push notifications, and treats them all as silent. This will be fixed with
iOS 17 / Safari 17, which will be released later in 2023.
## PWA on iOS sometimes crashes with an IndexedDB error (see [#787](https://github.com/binwiederhier/ntfy/issues/787))
When resuming the installed PWA from the background, it sometimes crashes with an error from IndexedDB/Dexie, due to a
[WebKit bug]( https://bugs.webkit.org/show_bug.cgi?id=197050). A reload will fix it until a permanent fix is found.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@@ -2,530 +2,6 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
### ntfy server v2.15.0
Released Nov 16, 2025
This release adds a `require-login` flag to topics, which forces users to log in before they can
use the web app. This is useful for self-hosters and will obviously not be enabled on ntfy.sh.
**Features:**
* Add `require-login` flag to redirect to login page if not logged in ([#1434](https://github.com/binwiederhier/ntfy/pull/1434)/[#238](https://github.com/binwiederhier/ntfy/issues/238)/[#1329](https://github.com/binwiederhier/ntfy/pull/1329), thanks to [@theatischbein](https://github.com/theatischbein) for implementing most of this)
**Bug fixes + maintenance:**
* The official ntfy.sh Debian/Ubuntu repository has moved to [archive.ntfy.sh](https://archive.ntfy.sh) ([#1357](https://github.com/binwiederhier/ntfy/issues/1357)/[#1401](https://github.com/binwiederhier/ntfy/issues/1401), thanks to [@skibbipl](https://github.com/skibbipl) and [@lduesing](https://github.com/lduesing) for reporting)
* Add mutex around message cache writes to avoid `database locked` errors ([#1397](https://github.com/binwiederhier/ntfy/pull/1397), [#1391](https://github.com/binwiederhier/ntfy/issues/1391), thanks to [@timofej673](https://github.com/timofej673))
* Add build tags `nopayments`, `nofirebase` and `nowebpush` to allow excluding external dependencies, useful for
packaging in Debian ([#1420](https://github.com/binwiederhier/ntfy/pull/1420), discussion in [#1258](https://github.com/binwiederhier/ntfy/issues/1258), thanks to [@thekhalifa](https://github.com/thekhalifa) for packaging ntfy for Debian/Ubuntu)
* Make copying tokens, phone numbers, etc. possible on HTTP ([#1432](https://github.com/binwiederhier/ntfy/pull/1432)/[#1408](https://github.com/binwiederhier/ntfy/issues/1408)/[#1295](https://github.com/binwiederhier/ntfy/issues/1295), thanks to [@EdwinKM](https://github.com/EdwinKM), [@xxl6097](https://github.com/xxl6097) for reporting)
## ntfy Android app v1.17.13
Released October 21, 2025
This release makes changes to comply with the Google Play policies. See [#1463](https://github.com/binwiederhier/ntfy/issues/1463)
or [ef57cd1](https://github.com/binwiederhier/ntfy-android/commit/ef57cd1374118b3e4d7a7ab496afe337e714fff7) for details.
The policies do not allow directly or indirectly linking to paid plans or donation links that do not go through Google Play.
**Changes:**
- Remove the "Donate" button from menu (all variants)
- Change default display name from "ntfy.sh/mytopic" to "mytopic" (all variants)
- Remove links to ntfy docs and issue tracker (Play variant only)
- Remove how-to links to ntfy.sh in a few places (Play variant only)
- Remove "Copy topic address" from subscription menu (Play variant only)
## ntfy Android app v1.17.8
Released September 23, 2025
This is largely a maintenance update to ensure the SDK is up-to-date.
**Features:**
* Markdown is now rendered if "Markdown: yes" was passed ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@NiNiyas](https://github.com/NiNiyas) for reporting)
* You can now disable UnifiedPush so ntfy does not act as a UnifiedPush distributor ([#646](https://github.com/binwiederhier/ntfy/issues/646), thanks to [@ollien](https://github.com/ollien) for reporting and to [@wunter8](https://github.com/wunter8) for implementing)
**Bug fixes + maintenance:**
* UnifiedPush subscriptions now include the `Rate-Topics` header to facilitate subscriber-based billing ([#652](https://github.com/binwiederhier/ntfy/issues/652), thanks to [@wunter8](https://github.com/wunter8))
* Subscriptions without icons no longer appear to use another subscription's icon ([#634](https://github.com/binwiederhier/ntfy/issues/634), thanks to [@topcaser](https://github.com/topcaser) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
* Bumped all dependencies to the latest versions (no ticket)
## ntfy server v2.14.0
Released August 5, 2025
This release adds support for [declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config). This allows you to define users, ACL entries and tokens in the config file, which is useful for static deployments or deployments that use a configuration management system.
It also adds support for [pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support, as well as advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) functions.
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier), [Liberapay](https://en.liberapay.com/ntfy/), Bitcoin (`1626wjrw3uWk9adyjCfYwafw4sQWujyjn8`), or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy
will always remain open source.
**Features:**
* [Declarative users](config.md#users-via-the-config), [declarative ACL entries](config.md#acl-entries-via-the-config) and [declarative tokens](config.md#tokens-via-the-config) ([#464](https://github.com/binwiederhier/ntfy/issues/464), [#1384](https://github.com/binwiederhier/ntfy/pull/1384), [#1413](https://github.com/binwiederhier/ntfy/pull/1413), thanks to [pinpox](https://github.com/pinpox) for reporting, to [@wunter8](https://github.com/wunter8) for reviewing and implementing parts of it)
* [Pre-defined templates](publish.md#pre-defined-templates) and [custom templates](publish.md#custom-templates) for enhanced JSON webhook support ([#1390](https://github.com/binwiederhier/ntfy/pull/1390))
* Support of advanced [template functions](publish.md#template-functions) based on the [Sprig](https://github.com/Masterminds/sprig) library ([#1121](https://github.com/binwiederhier/ntfy/issues/1121), thanks to [@davidatkinsondoyle](https://github.com/davidatkinsondoyle) for reporting, to [@wunter8](https://github.com/wunter8) for implementing, and to the Sprig team for their work)
## ntfy server v2.13.0
Released July 10, 2025
This is a relatively small release, mainly to support IPv6 and to add more sophisticated
proxy header support. Quick reminder that if you like ntfy, **please consider sponsoring us**
via [GitHub Sponsors](https://github.com/sponsors/binwiederhier) and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app).
ntfy will always remain open source.
**Features:**
* Full [IPv6 support](config.md#ipv6-support) for ntfy and the official ntfy.sh server ([#519](https://github.com/binwiederhier/ntfy/issues/519)/[#1380](https://github.com/binwiederhier/ntfy/pull/1380)/[ansible#4](https://github.com/binwiederhier/ntfy-ansible/pull/4))
* Support `X-Client-IP`, `X-Real-IP`, `Forwarded` headers for [rate limiting](config.md#ip-based-rate-limiting) via `proxy-forwarded-header` and `proxy-trusted-hosts` ([#1360](https://github.com/binwiederhier/ntfy/pull/1360)/[#1252](https://github.com/binwiederhier/ntfy/pull/1252), thanks to [@pixitha](https://github.com/pixitha))
* Add STDIN support for `ntfy publish` ([#1382](https://github.com/binwiederhier/ntfy/pull/1382), thanks to [@srevn](https://github.com/srevn))
**Languages**
* Update new languages from Weblate. Thanks to all the contributors!
* Added Estonian (Esti), Galician (Galego), Romanian (Română), Slovak (Slovenčina) as new languages to the web app
## ntfy server v2.12.0
Released May 29, 2025
This is mainly a maintenance release that updates dependencies, though since it's been over a year, there are a few
new features and bug fixes as well.
Thanks to everyone who contributed to this release, and special thanks to [@wunter8](https://github.com/wunter8) for his continued
user support in Discord/Matrix/GitHub! You rock, man!
**Features:**
* Add username/password auth to email publishing ([#1164](https://github.com/binwiederhier/ntfy/pull/1164), thanks to [@bishtawi](https://github.com/bishtawi))
* Write VAPID keys to file in `ntfy webpush --output-file` ([#1138](https://github.com/binwiederhier/ntfy/pull/1138), thanks to [@nogweii](https://github.com/nogweii))
* Add Docker major/minor version to image tags ([#1271](https://github.com/binwiederhier/ntfy/pull/1271), thanks to [@RoboMagus](https://github.com/RoboMagus))
* Add `latest` subscription param for grabbing just the most recent message ([#1216](https://github.com/binwiederhier/ntfy/pull/1216), thanks to [@wunter8](https://github.com/wunter8))
* Allow using `NTFY_PASSWORD_HASH` in `ntfy user` command instead of raw password ([#1340](https://github.com/binwiederhier/ntfy/pull/1340), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht) for implementing)
* You can now change passwords via `v1/users` API ([#1267](https://github.com/binwiederhier/ntfy/pull/1267), thanks to [@wunter8](https://github.com/wunter8) for implementing)
* Make WebPush subscription warning/expiry configurable, increase default to 55/60 days ([#1212](https://github.com/binwiederhier/ntfy/pull/1212), thanks to [@KuroSetsuna29](https://github.com/KuroSetsuna29))
* Support [systemd user service](https://docs.ntfy.sh/subscribe/cli/#using-the-systemd-service) `ntfy-client.service` ([#1002](https://github.com/binwiederhier/ntfy/pull/1002), thanks to [@dandersch](https://github.com/dandersch))
**Bug fixes + maintenance:**
* Security updates for dependencies and Docker images ([#1341](https://github.com/binwiederhier/ntfy/pull/1341))
* Upgrade to Vite 6 ([#1342](https://github.com/binwiederhier/ntfy/pull/1342), thanks Dependabot)
* Fix iOS delivery issues for read-protected topics ([#1207](https://github.com/binwiederhier/ntfy/pull/1287), thanks a lot to [@barart](https://github.com/barart)!)
* Add `Date` header to outgoing emails to avoid rejection ([#1141](https://github.com/binwiederhier/ntfy/pull/1141), thanks to [@pcouy](https://github.com/pcouy))
* Fix IP address parsing when behind a proxy ([#1266](https://github.com/binwiederhier/ntfy/pull/1266), thanks to [@mmatuska](https://github.com/mmatuska))
* Make sure UnifiedPush messages are not treated as attachments ([#1312](https://github.com/binwiederhier/ntfy/pull/1312), thanks to [@vkrause](https://github.com/vkrause))
* Add OCI image version to Docker image ([#1307](https://github.com/binwiederhier/ntfy/pull/1307), thanks to [@jlssmt](https://github.com/jlssmt))
* WebSocket returning incorrect HTTP error code ([#1338](https://github.com/binwiederhier/ntfy/pull/1338) / [#1337](https://github.com/binwiederhier/ntfy/pull/1337), thanks to [@wunter8](https://github.com/wunter8) for debugging and implementing)
* Make Markdown in the web app scrollable horizontally ([#1262](https://github.com/binwiederhier/ntfy/pull/1262), thanks to [@rake5k](https://github.com/rake5k) for fixing)
* Make sure WebPush subscription topics are actually deleted (no ticket)
* Increase the number of access tokens per user to 60 ([#1308](https://github.com/binwiederhier/ntfy/issues/1308))
* Allow specifying `cache` and `firebase` via JSON publishing ([#1119](https://github.com/binwiederhier/ntfy/issues/1119)/[#1123](https://github.com/binwiederhier/ntfy/pull/1123), thanks to [@stendler](https://github.com/stendler))
**Documentation:**
* Lots of new integrations and projects. Amazing!
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
* [UptimeObserver](https://uptimeobserver.com)
* [alertmanager-ntfy-relay](https://github.com/therobbielee/alertmanager-ntfy-relay)
* [Monibot](https://monibot.io/)
* [Daily_Fact_Ntfy](https://github.com/thiswillbeyourgithub/Daily_Fact_Ntfy)
* [EasyMorph](https://help.easymorph.com/doku.php?id=transformations:sendntfymessage)
* [ntfy-run](https://github.com/quantum5/ntfy-run)
* [Clipboard IO](https://github.com/jim3692/clipboard-io)
* [ntfy-me-mcp](https://github.com/gitmotion/ntfy-me-mcp)
* [InvaderInformant](https://github.com/patricksthannon/InvaderInformant)
* Various docs updates ([#1161](https://github.com/binwiederhier/ntfy/pull/1161), thanks to [@OneWeekNotice](https://github.com/OneWeekNotice))
* Typo in config docs ([#1177](https://github.com/binwiederhier/ntfy/pull/1177), thanks to [@hoho4190](https://github.com/hoho4190))
* Typo in CLI docs ([#1172](https://github.com/binwiederhier/ntfy/pull/1172), thanks to [@anirvan](https://github.com/anirvan))
* Correction about MacroDroid ([#1137](https://github.com/binwiederhier/ntfy/pull/1137), thanks to [@ShlomoCode](https://github.com/ShlomoCode))
* Note about fail2ban in Docker ([#1175](https://github.com/binwiederhier/ntfy/pull/1175)), thanks to [@Measurity](https://github.com/Measurity))
* Lots of other tiny docs updates, thanks to everyone who contributed!
**Languages**
* Update new languages from Weblate. Thanks to all the contributors!
* Added Tamil (தமிழ்) as a new language to the web app
## ntfy server v2.11.0
Released May 13, 2024
This is a tiny release that fixes a database index issue that caused performance issues on ntfy.sh. It also fixes a bug
in the rate visitor logic that caused rate visitors to be assigned to seemingly random topics. Nothing major this time.
❤️ Quick reminder that if you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app). ntfy will always remain open source.
**Bug fixes + maintenance:**
* Re-add database index `idx_topic` to the `messages` table to fix performance issues on ntfy.sh (no ticket, big thanks to [@tcaputi](https://github.com/tcaputi) for finding this issue)
* Do not set rate visitor for non-eligible topics (no ticket)
* Do not cache `config.js` ([#1098](https://github.com/binwiederhier/ntfy/pull/1098), thanks to [@wunter8](https://github.com/wunter8))
## ntfy server v2.10.0
Released Mar 27, 2024
This release adds support for **message templating** in the ntfy server, which allows you to include a message and/or
title template that will be filled with values from a JSON body (e.g. `curl -gd '{"alert":"Disk space low"}' "ntfy.sh/mytopic?tpl=1&m={{.alert}}"`).
This is great for services that let you specify a webhook URL but do not let you change the webhook body (such as GitHub, or Grafana).
**Features:**
* [Message templating](publish.md#message-templating): You can now include a message and/or title template that will be filled with values from a JSON body ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
## ntfy server v2.9.0
Released Mar 7, 2024
A small release after a long pause (lots of day job work). This release adds for **larger messages** and **longer
message delays** in scheduled delivery messages. The web app also now supports pasting images from the clipboard. Other
than that, only a few bug fixes and documentation updates, and a teeny tiny breaking change 😬.
!!! info
⚠️ **Breaking change**: The `Rate-Topics` header was removed due to a [DoS issue](https://github.com/binwiederhier/ntfy/issues/1048). This only affects
installations with `visitor-subscriber-rate-limiting: true`, which is not the default and likely very rarely used.
Normally I'd never remove a feature, but this is a security issue, and likely affects almost nobody.
**Features:**
* Support for larger message delays with `message-delay-limit` (see [message limits](config.md#message-limits), [#1050](https://github.com/binwiederhier/ntfy/pull/1050)/[#1019](https://github.com/binwiederhier/ntfy/issues/1019), thanks to [@MrChadMWood](https://github.com/MrChadMWood) for reporting)
* Support for larger message body sizes with `message-size-limit` (use at your own risk, see [message limits](config.md#message-limits), [#836](https://github.com/binwiederhier/ntfy/pull/836)/[#1050](https://github.com/binwiederhier/ntfy/pull/1050), thanks to [@zhzy0077](https://github.com/zhzy0077) for implementing this, and to [@nkjshlsqja7331](https://github.com/nkjshlsqja7331) for reporting)
* Web app: You can now paste images into the message bar or publish dialog ([#963](https://github.com/binwiederhier/ntfy/pull/963)/[#572](https://github.com/binwiederhier/ntfy/issues/572), thanks to [@cmj2002](https://github.com/cmj2002) for implementing, and [@rounakdatta](https://github.com/rounakdatta) for reporting)
**Bug fixes + maintenance:**
* ⚠️ Remove `Rate-Topics` header due to DoS security issue if `visitor-subscriber-rate-limiting: true` ([#1048](https://github.com/binwiederhier/ntfy/issues/1048))
**Documentation:**
* Remove `mkdocs-simple-hooks` ([#1016](https://github.com/binwiederhier/ntfy/pull/1016), thanks to [@Tom-Hubrecht](https://github.com/Tom-Hubrecht))
* Update Watchtower example ([#1014](https://github.com/binwiederhier/ntfy/pull/1014), thanks to [@lennart-m](https://github.com/lennart-m))
* Fix dead links ([#1022](https://github.com/binwiederhier/ntfy/pull/1022), thanks to [@DerRockWolf](https://github.com/DerRockWolf))
* PowerShell file upload example ([#1004](https://github.com/binwiederhier/ntfy/pull/1004), thanks to [@YMan84](https://github.com/YMan84))
## ntfy iOS app v1.3
Released Nov 26, 2023
This release (hopefully) fixes the issues with the iOS UI not updating properly when new notifications arrive, as well
as notifications not being received (anymore) after previously working. Both issues have been annoying and known bugs
for a long time, and I hope that they are finally fixed.
Many thanks to [@tcaputi](https://github.com/tcaputi) for fixing the issues, and to the anonymous donor for sponsoring these fixes.
**Bug fixes:**
* UI not updating properly ([#267](https://github.com/binwiederhier/ntfy/issues/267)/[#402](https://github.com/binwiederhier/ntfy/issues/402), thanks to [@tcaputi](https://github.com/tcaputi))
## ntfy server v2.8.0
Released November 19, 2023
This release brings a handful of random bug fixes: two unrelated access control list fixes, a fix around web app crashes
for languages with underscores in the language code (e.g. `zh_Hant`, `zh_Hans`, `pt_BR`, ...), a workaround for the
`Priority` header (often used in Cloudflare setups), and support among others support for HTML-only emails (finally),
web app crash fixes
**Bug fixes + maintenance:**
* Support for HTML-only emails ([#690](https://github.com/binwiederhier/ntfy/issues/690)/[#693](https://github.com/binwiederhier/ntfy/pull/693), thanks to [@teastrainer](https://github.com/teastrainer) and [@CrazyWolf13](https://github.com/CrazyWolf13) for reporting)
* Fix ACL issue with topic patterns containing underscores ([#840](https://github.com/binwiederhier/ntfy/issues/840), thanks to [@Joe-0237](https://github.com/Joe-0237) for reporting)
* Fix ACL issue with order of read/write rules ([#914](https://github.com/binwiederhier/ntfy/issues/914)/[#917](https://github.com/binwiederhier/ntfy/pull/917), thanks to [@sandman7920](https://github.com/sandman7920))
* Re-add `tzdata` to Docker images for amd64 image ([#894](https://github.com/binwiederhier/ntfy/issues/894), [#307](https://github.com/binwiederhier/ntfy/pull/307))
* Add special logic to ignore `Priority` header if it resembles an RFC 9218 value ([#851](https://github.com/binwiederhier/ntfy/pull/851)/[#895](https://github.com/binwiederhier/ntfy/pull/895), thanks to [@gusdleon](https://github.com/gusdleon), see also [#351](https://github.com/binwiederhier/ntfy/issues/351), [#353](https://github.com/binwiederhier/ntfy/issues/353), [#461](https://github.com/binwiederhier/ntfy/issues/461))
* PWA: hide install prompt on macOS 14 Safari ([#899](https://github.com/binwiederhier/ntfy/pull/899), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
* Fix web app crash in Edge for languages with underline in locale ([#922](https://github.com/binwiederhier/ntfy/pull/922)/[#912](https://github.com/binwiederhier/ntfy/issues/912)/[#852](https://github.com/binwiederhier/ntfy/issues/852), thanks to [@imkero](https://github.com/imkero))
**Additional languages:**
* Finnish (thanks to [@Seppo](https://hosted.weblate.org/user/Seppo/))
## ntfy server v2.7.0
Released August 17, 2023
This release ships Markdown support for the web app (not in the Android app yet), and adds support for
right-to-left languages (RTL) in the web app. It also fixes a few issues around date/time formatting,
internationalization support, a CLI auth bug.
Furthermore, it fixes a security issue around access tokens getting erroneously deleted for other users
in a specific scenario. This was a denial-of-service-type security issue, since it **effectively allowed a
single user to deny access to all other users of a ntfy instance**. Please note that while tokens were
erroneously deleted, **nobody but the token owner ever had access to it.** Please refer to [the ticket](https://github.com/binwiederhier/ntfy/issues/838)
for details. **Please upgrade your ntfy instance if you run a multi-user system.**
**Features:**
* Add support for [Markdown formatting](publish.md#markdown-formatting) in web app ([#310](https://github.com/binwiederhier/ntfy/issues/310), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
* Add support for right-to-left languages (RTL) in the web app ([#663](https://github.com/binwiederhier/ntfy/issues/663), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Security:** ⚠️
* Fixes issue with access tokens getting deleted ([#838](https://github.com/binwiederhier/ntfy/issues/838))
**Bug fixes + maintenance:**
* Fix issues with date/time with different locales ([#700](https://github.com/binwiederhier/ntfy/issues/700), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Re-init i18n on each service worker message to avoid missing translations ([#817](https://github.com/binwiederhier/ntfy/pull/817), thanks to [@nihalgonsalves](https://github.com/nihalgonsalves))
* You can now unset the default user:pass/token in `client.yml` for an individual subscription to remove the Authorization header ([#829](https://github.com/binwiederhier/ntfy/issues/829), thanks to [@tomeon](https://github.com/tomeon) for reporting and to [@wunter8](https://github.com/wunter8) for fixing)
**Documentation:**
* Update docs for Apache config ([#819](https://github.com/binwiederhier/ntfy/pull/819), thanks to [@nisbet-hubbard](https://github.com/nisbet-hubbard))
## ntfy server v2.6.2
Released June 30, 2023
With this release, the ntfy web app now contains a **[progressive web app](subscribe/pwa.md) (PWA)
with Web Push support**, which means you'll be able to **install the ntfy web app on your desktop or phone** similar
to a native app (__even on iOS!__ 🥳). Installing the PWA gives ntfy web its own launcher, a standalone window,
push notifications, and an app badge with the unread notification count. Note that for self-hosted servers,
[Web Push](config.md#web-push) must be configured.
On top of that, this release also brings **dark mode** 🧛🌙 to the web app.
🙏 A huge thanks for this release goes to [@nimbleghost](https://github.com/nimbleghost), for basically implementing the
Web Push / PWA and dark mode feature by himself. I'm really grateful for your contributions.
❤️ If you like ntfy, **please consider sponsoring us** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), or buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
if you use promo code `MYTOPIC`). ntfy will always remain open source.
**Features:**
* The web app now supports Web Push, and is installable as a [progressive web app (PWA)](https://docs.ntfy.sh/subscribe/pwa/) on Chrome, Edge, Android, and iOS ([#751](https://github.com/binwiederhier/ntfy/pull/751), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Support for dark mode in the web app ([#206](https://github.com/binwiederhier/ntfy/issues/206), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Bug fixes:**
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
* Newly created access tokens are now lowercase only to fully support `<topic>+<token>@<domain>` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting)
* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost))
* The .2 release fixes issues with the service worker in Firefox and adds automatic service worker updates ([#795](https://github.com/binwiederhier/ntfy/pull/795), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Maintenance:**
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Changes in tarball/zip naming:**
Due to a [change in GoReleaser](https://goreleaser.com/deprecations/#archivesreplacements), some of the binary release
archives now have slightly different names. My apologies if this causes issues in the downstream projects that use ntfy:
- `ntfy_v${VERSION}_windows_x86_64.zip` -> `ntfy_v${VERSION}_windows_amd64.zip`
- `ntfy_v${VERSION}_linux_x86_64.tar.gz` -> `ntfy_v${VERSION}_linux_amd64.tar.gz`
- `ntfy_v${VERSION}_macOS_all.tar.gz` -> `ntfy_v${VERSION}_darwin_all.tar.gz`
## ntfy server v2.5.0
Released May 18, 2023
This release brings a number of new features, including support for text-to-speech style [phone calls](publish.md#phone-calls),
an admin API to manage users and ACL (currently in beta, and hence undocumented), and support for authorized access to
upstream servers via the `upstream-access-token` config option.
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app) (20% off
if you use promo code `MYTOPIC`). ntfy will always remain open source.
**Features:**
* Support for text-to-speech style [phone calls](publish.md#phone-calls) using the `X-Call` header (no ticket)
* Admin API to manage users and ACL, `v1/users` + `v1/users/access` (intentionally undocumented as of now, [#722](https://github.com/binwiederhier/ntfy/issues/722), thanks to [@CreativeWarlock](https://github.com/CreativeWarlock) for sponsoring this ticket)
* Added `upstream-access-token` config option to allow authorized access to upstream servers (no ticket)
**Bug fixes + maintenance:**
* Removed old ntfy website from ntfy entirely (no ticket)
* Make emoji lookup for emails more efficient ([#725](https://github.com/binwiederhier/ntfy/pull/725), thanks to [@adamantike](https://github.com/adamantike))
* Fix potential subscriber ID clash ([#712](https://github.com/binwiederhier/ntfy/issues/712), thanks to [@peterbourgon](https://github.com/peterbourgon) for reporting, and [@dropdevrahul](https://github.com/dropdevrahul) for fixing)
* Support for `quoted-printable` in incoming emails ([#719](https://github.com/binwiederhier/ntfy/pull/719), thanks to [@Aerion](https://github.com/Aerion))
* Attachments with filenames that are downloaded using a browser will now download with the proper filename ([#726](https://github.com/binwiederhier/ntfy/issues/726), thanks to [@un99known99](https://github.com/un99known99) for reporting, and [@wunter8](https://github.com/wunter8) for fixing)
* Fix web app i18n issue in account preferences ([#730](https://github.com/binwiederhier/ntfy/issues/730), thanks to [@codebude](https://github.com/codebude) for reporting)
## ntfy server v2.4.0
Released Apr 26, 2023
This release adds a tiny `v1/stats` endpoint to expose how many messages have been published, and adds support to encode the `X-Title`,
`X-Message` and `X-Tags` header as RFC 2047. It's a pretty small release, and mainly enables the release of the new ntfy.sh website.
❤️ If you like ntfy, **please consider sponsoring me** via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/), or by buying a [paid plan via the web app](https://ntfy.sh/app). ntfy
will always remain open source.
**Features:**
* [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) can now be installed via Homebrew (thanks to [@Moulick](https://github.com/Moulick))
* Added `v1/stats` endpoint to expose messages stats (no ticket)
* Support [RFC 2047](https://datatracker.ietf.org/doc/html/rfc2047#section-2) encoded headers (no ticket, honorable mention to [mqttwarn](https://github.com/jpmens/mqttwarn/pull/638) and [@amotl](https://github.com/amotl))
**Bug fixes + maintenance:**
* Hide country flags on Windows ([#606](https://github.com/binwiederhier/ntfy/issues/606), thanks to [@cmeis](https://github.com/cmeis) for reporting, and to [@pokej6](https://github.com/pokej6) for fixing it)
* `ntfy sub` now uses default auth credentials as defined in `client.yml` ([#698](https://github.com/binwiederhier/ntfy/issues/698), thanks to [@CrimsonFez](https://github.com/CrimsonFez) for reporting, and to [@wunter8](https://github.com/wunter8) for fixing it)
**Documentation:**
* Updated PowerShell examples ([#697](https://github.com/binwiederhier/ntfy/pull/697), thanks to [@Natfan](https://github.com/Natfan))
**Additional languages:**
* Swedish (thanks to [@hellbown](https://hosted.weblate.org/user/Shjosan/))
## ntfy server v2.3.1
Released March 30, 2023
This release disables server-initiated polling of iOS devices entirely, thereby eliminating the thundering herd problem
on ntfy.sh that we observe every 20 minutes. The polling was never strictly necessary, and has actually caused duplicate
delivery issues as well, so disabling it should not have any negative effects. iOS users, please reach out via Discord
or Matrix if there are issues.
**Bug fixes + maintenance:**
* Disable iOS polling entirely ([#677](https://github.com/binwiederhier/ntfy/issues/677)/[#509](https://github.com/binwiederhier/ntfy/issues/509))
## ntfy server v2.3.0
Released March 29, 2023
This release primarily fixes an issue with delayed messages, and it adds support for Go's profiler (if enabled), which
will allow investigating usage spikes in more detail. There will likely be a follow-up release this week to fix the
actual spikes [caused by iOS devices](https://github.com/binwiederhier/ntfy/issues/677).
**Features:**
* ntfy now supports Go's `pprof` profiler, if enabled (relates to [#677](https://github.com/binwiederhier/ntfy/issues/677))
**Bug fixes + maintenance:**
* Fix delayed message sending from authenticated users ([#679](https://github.com/binwiederhier/ntfy/issues/679))
* Fixed plural for Polish and other translations ([#678](https://github.com/binwiederhier/ntfy/pull/678), thanks to [@bmoczulski](https://github.com/bmoczulski))
## ntfy server v2.2.0
Released March 17, 2023
With this release, ntfy is now able to expose metrics via a `/metrics` endpoint for [Prometheus](https://prometheus.io/), if enabled.
The endpoint exposes about 20 different counters and gauges, from the number of published messages and emails, to active subscribers,
visitors and topics. If you'd like more metrics, pop in the Discord/Matrix or file an issue on GitHub.
On top of this, you can now use access tokens in the ntfy CLI (defined in the `client.yml` file), fixed a bug in `ntfy subscribe`,
removed the dependency on Google Fonts, and more.
🔥 Reminder: Purchase one of three **ntfy Pro plans** for **50% off** for a limited time (if you use promo code `MYTOPIC`).
ntfy Pro gives you higher rate limits and lets you reserve topic names. [Buy through web app](https://ntfy.sh/app).
❤️ If you don't need ntfy Pro, please consider sponsoring ntfy via [GitHub Sponsors](https://github.com/sponsors/binwiederhier)
and [Liberapay](https://en.liberapay.com/ntfy/). ntfy will stay open source forever.
**Features:**
* Monitoring: ntfy now exposes a `/metrics` endpoint for [Prometheus](https://prometheus.io/) if [configured](config.md#monitoring) ([#210](https://github.com/binwiederhier/ntfy/issues/210), thanks to [@rogeliodh](https://github.com/rogeliodh) for reporting)
* You can now use tokens in `client.yml` for publishing and subscribing ([#653](https://github.com/binwiederhier/ntfy/issues/653), thanks to [@wunter8](https://github.com/wunter8))
**Bug fixes + maintenance:**
* `ntfy sub --poll --from-config` will now include authentication headers from client.yml (if applicable) ([#658](https://github.com/binwiederhier/ntfy/issues/658), thanks to [@wunter8](https://github.com/wunter8))
* Docs: Removed dependency on Google Fonts in docs ([#554](https://github.com/binwiederhier/ntfy/issues/554), thanks to [@bt90](https://github.com/bt90) for reporting, and [@ozskywalker](https://github.com/ozskywalker) for implementing)
* Increase allowed auth failure attempts per IP address to 30 (no ticket)
* Web app: Increase maximum incremental backoff retry interval to 2 minutes (no ticket)
**Documentation:**
* Make query parameter description more clear ([#630](https://github.com/binwiederhier/ntfy/issues/630), thanks to [@bbaa-bbaa](https://github.com/bbaa-bbaa) for reporting, and to [@wunter8](https://github.com/wunter8) for a fix)
## ntfy server v2.1.2
Released March 4, 2023
This is a hotfix release, mostly to combat the ridiculous amount of Matrix requests with invalid/dead pushkeys, and the
corresponding HTTP 507 responses the ntfy.sh server is sending out. We're up to >600k HTTP 507 responses per day 🤦. This
release solves this issue by rejecting Matrix pushkeys, if nobody has subscribed to the corresponding topic for 12 hours.
The release furthermore reverts the default rate limiting behavior for UnifiedPush to be publisher-based, and introduces
a flag to enable [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting) for high volume servers.
**Features:**
* Support SMTP servers without auth ([#645](https://github.com/binwiederhier/ntfy/issues/645), thanks to [@Sharknoon](https://github.com/Sharknoon) for reporting)
**Bug fixes + maintenance:**
* Token auth doesn't work if default user credentials are defined in `client.yml` ([#650](https://github.com/binwiederhier/ntfy/issues/650), thanks to [@Xinayder](https://github.com/Xinayder))
* Add `visitor-subscriber-rate-limiting` flag to allow enabling [subscriber-based rate limiting](config.md#subscriber-based-rate-limiting) (off by default now, [#649](https://github.com/binwiederhier/ntfy/issues/649)/[#655](https://github.com/binwiederhier/ntfy/pull/655), thanks to [@barathrm](https://github.com/barathrm) for reporting, and to [@karmanyaahm](https://github.com/karmanyaahm) and [@p1gp1g](https://github.com/p1gp1g) for help with the design)
* Reject Matrix pushkey after 12 hours of inactivity on a topic, if `visitor-subscriber-rate-limiting` is enabled ([#643](https://github.com/binwiederhier/ntfy/pull/643), thanks to [@karmanyaahm](https://github.com/karmanyaahm) and [@p1gp1g](https://github.com/p1gp1g) for help with the design)
**Additional languages:**
* Danish (thanks to [@Andersbiha](https://hosted.weblate.org/user/Andersbiha/))
## ntfy server v2.1.1
Released March 1, 2023
This is a tiny release with a few bug fixes, but it's big for me personally. After almost three months of work,
**today I am finally launching the paid plans on ntfy.sh** 🥳 🎉.
You are now able to purchase one of three plans that'll give you **higher rate limits** (messages, emails, attachment sizes, ...),
as well as the ability to **reserve topic names** for your personal use, while at the same time supporting me and the
ntfy open source project ❤️. You can check out the pricing, and [purchase plans through the web app](https://ntfy.sh/app) (use
promo code `MYTOPIC` for a **50% discount**, limited time only).
And as I've said many times: Do not worry. **ntfy will always stay open source**, and that includes all features. There
are no closed-source features. So if you'd like to run your own server, you can!
**Bug fixes + maintenance:**
* Fix panic when using Firebase without users ([#641](https://github.com/binwiederhier/ntfy/issues/641), thanks to [u/heavybell](https://www.reddit.com/user/heavybell/) for reporting)
* Remove health check from `Dockerfile` and [document it](config.md#health-checks) ([#635](https://github.com/binwiederhier/ntfy/issues/635), thanks to [@Andersbiha](https://github.com/Andersbiha))
* Upgrade dialog: Disable submit button for free tier (no ticket)
* Allow multiple `log-level-overrides` on the same field (no ticket)
* Actually remove `ntfy publish --env-topic` flag (as per [deprecations](deprecations.md), no ticket)
* Added `billing-contact` config option (no ticket)
## ntfy server v2.1.0
Released February 25, 2023
This release changes the way UnifiedPush (UP) topics are rate limited from publisher-based rate limiting to subscriber-based
rate limiting. This allows UP application servers to send higher volumes, since the subscribers carry the rate limits.
However, it also means that UP clients have to subscribe to a topic first before they are allowed to publish. If they do
no, clients will receive an HTTP 507 response from the server.
We also fixed another issue with UnifiedPush: Some Mastodon servers were sending unsupported `Authorization` headers,
which ntfy rejected with an HTTP 401. We now ignore unsupported header values.
As of this release, ntfy also supports sending emails to protected topics, and it ships code to support annual billing
cycles (not live yet).
As part of this release, I also enabled sign-up and login (free accounts only), and I also started reducing the rate
limits for anonymous & free users a bit. With the next release and the launch of the paid plan, I'll reduce the limits
a bit more. For 90% of users, you should not feel the difference.
**Features:**
* UnifiedPush: Subscriber-based rate limiting for `up*` topics ([#584](https://github.com/binwiederhier/ntfy/pull/584)/[#609](https://github.com/binwiederhier/ntfy/pull/609)/[#633](https://github.com/binwiederhier/ntfy/pull/633), thanks to [@karmanyaahm](https://github.com/karmanyaahm))
* Support for publishing to protected topics via email with access tokens ([#612](https://github.com/binwiederhier/ntfy/pull/621), thanks to [@tamcore](https://github.com/tamcore))
* Support for base64-encoded and nested multipart emails ([#610](https://github.com/binwiederhier/ntfy/issues/610), thanks to [@Robert-litts](https://github.com/Robert-litts))
* Payments: Add support for annual billing intervals (no ticket)
**Bug fixes + maintenance:**
* Web: Do not disable "Reserve topic" checkbox for admins (no ticket, thanks to @xenrox for reporting)
* UnifiedPush: Treat non-Basic/Bearer `Authorization` header like header was not sent ([#629](https://github.com/binwiederhier/ntfy/issues/629), thanks to [@Boebbele](https://github.com/Boebbele) and [@S1m](https://github.com/S1m) for reporting)
**Documentation:**
* Added example for [Traccar](https://ntfy.sh/docs/examples/#traccar) ([#631](https://github.com/binwiederhier/ntfy/pull/631), thanks to [tamcore](https://github.com/tamcore))
**Additional languages:**
* Arabic (thanks to [@ButterflyOfFire](https://hosted.weblate.org/user/ButterflyOfFire/))
## ntfy server v2.0.1
Released February 17, 2023
This is a quick bugfix release to address a panic that happens when `attachment-cache-dir` is not set.
**Bug fixes + maintenance:**
* Avoid panic in manager when `attachment-cache-dir` is not set ([#617](https://github.com/binwiederhier/ntfy/issues/617), thanks to [@ksurl](https://github.com/ksurl))
* Ensure that calls to standard logger `log.Println` also output JSON (no ticket)
## ntfy server v2.0.0
Released February 16, 2023
@@ -588,11 +64,6 @@ going. It'll only make ntfy better.
* User account signup, login, topic reservations, access tokens, tiers etc. ([#522](https://github.com/binwiederhier/ntfy/issues/522))
* `OPTIONS` method calls are not serviced when the UI is disabled ([#598](https://github.com/binwiederhier/ntfy/issues/598), thanks to [@enticedwanderer](https://github.com/enticedwanderer) for reporting)
**Special thanks:**
A big Thank-you goes to everyone who tested the user account and payments work. I very much appreciate all the feedback,
suggestions, and bug reports. Thank you, @nwithan8, @deadcade, @xenrox, @cmeis, @wunter8 and the others who I forgot.
## ntfy server v1.31.0
Released February 14, 2023
@@ -624,6 +95,11 @@ breaking-change upgrade, which required some work to get working again.
* Portuguese (thanks to [@ssantos](https://hosted.weblate.org/user/ssantos/))
**Special thanks:**
A big Thank-you goes to everyone who tested the user account and payments work. I very much appreciate all the feedback,
suggestions, and bug reports. Thank you, @nwithan8, @deadcade, and @xenrox.
## ntfy server v1.30.1
Released December 23, 2022 🎅
@@ -834,7 +310,7 @@ minute or so, due to competing stats gathering (personal installations will like
**Features:**
* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#message-cache) (no ticket)
* Add `cache-startup-queries` option to allow custom [SQLite performance tuning](config.md#wal-for-message-cache) (no ticket)
* ntfy CLI can now [wait for a command or PID](subscribe/cli.md#wait-for-pidcommand) before publishing ([#263](https://github.com/binwiederhier/ntfy/issues/263), thanks to the [original ntfy](https://github.com/dschep/ntfy) for the idea)
* Trace: Log entire HTTP request to simplify debugging (no ticket)
* Allow setting user password via `NTFY_PASSWORD` env variable ([#327](https://github.com/binwiederhier/ntfy/pull/327), thanks to [@Kenix3](https://github.com/Kenix3))
@@ -1508,14 +984,10 @@ Released Dec 28, 2021
**Features & bug fixes:**
* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
* Fixing the Santa bug #65
## Older releases
For older releases, check out the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
## Not released yet
_Nothing to see, move along ..._

Binary file not shown.

Binary file not shown.

View File

@@ -2,15 +2,13 @@
--md-primary-fg-color: #338574;
--md-primary-fg-color--light: #338574;
--md-primary-fg-color--dark: #338574;
--md-footer-bg-color: #353744;
--md-text-font: "Roboto";
--md-code-font: "Roboto Mono";
}
.md-header__button.md-logo :is(img, svg) {
width: unset !important;
}
.md-header__topic:first-child {
font-weight: 400;
}
@@ -71,18 +69,7 @@ figure video {
}
.remove-md-box td {
padding: 0 10px;
}
.emoji-table .c {
vertical-align: middle !important;
}
.emoji-table .e {
font-size: 2.5em;
padding: 0 2px !important;
text-align: center !important;
vertical-align: middle !important;
padding: 0 10px
}
/* Lightbox; thanks to https://yossiabramov.com/blog/vanilla-js-lightbox */
@@ -160,57 +147,3 @@ figure video {
.lightbox .close-lightbox:hover::before {
background-color: #fff;
}
/* roboto-300 - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 300;
src: url('../fonts/roboto-v30-latin-300.woff2') format('woff2');
}
/* roboto-regular - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 400;
src: url('../fonts/roboto-v30-latin-regular.woff2') format('woff2');
}
/* roboto-italic - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: italic;
font-weight: 400;
src: url('../fonts/roboto-v30-latin-italic.woff2') format('woff2');
}
/* roboto-500 - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 500;
src: url('../fonts/roboto-v30-latin-500.woff2') format('woff2');
}
/* roboto-700 - latin */
@font-face {
font-display: swap;
font-family: 'Roboto';
font-style: normal;
font-weight: 700;
src: url('../fonts/roboto-v30-latin-700.woff2') format('woff2');
}
/* roboto-mono - latin */
@font-face {
font-display: swap;
font-family: 'Roboto Mono';
font-style: normal;
font-weight: 400;
src: url('../fonts/roboto-mono-v22-latin-regular.woff2') format('woff2');
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

BIN
docs/static/img/favicon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 334 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Some files were not shown because too many files have changed in this diff Show More