mirror of
https://github.com/binwiederhier/ntfy.git
synced 2026-01-19 00:27:25 +01:00
Compare commits
292 Commits
acl-unders
...
templating
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9247dac50d | ||
|
|
de65d07518 | ||
|
|
1966f80855 | ||
|
|
4b2e38320d | ||
|
|
83356f565e | ||
|
|
7fd5f0b29d | ||
|
|
03737dbf5c | ||
|
|
867cf28080 | ||
|
|
b2eb5b94bd | ||
|
|
df7d6baec5 | ||
|
|
4c0ec3f75b | ||
|
|
12bbe9a1ae | ||
|
|
7936c38feb | ||
|
|
d0beaa900f | ||
|
|
0ab6171962 | ||
|
|
b7c2898b9c | ||
|
|
d155ebb3a4 | ||
|
|
3d5bb92774 | ||
|
|
a2f1a45097 | ||
|
|
b43fff7c7e | ||
|
|
b186c7a324 | ||
|
|
e50b6f4075 | ||
|
|
e5342d5eca | ||
|
|
a25fc1daaa | ||
|
|
6e75108aa9 | ||
|
|
0735a5cb48 | ||
|
|
088aede833 | ||
|
|
2119954f67 | ||
|
|
220b012ae3 | ||
|
|
e1278a5e92 | ||
|
|
0ae62d36d2 | ||
|
|
70729edb2b | ||
|
|
de2f7d3e9b | ||
|
|
8c69234e28 | ||
|
|
a9b7c4530b | ||
|
|
76fc015775 | ||
|
|
ef302d22a9 | ||
|
|
7ab8ef73a3 | ||
|
|
7616b24e30 | ||
|
|
05c1b264f2 | ||
|
|
35ccfdb8d8 | ||
|
|
d744204052 | ||
|
|
6c3aca4cd6 | ||
|
|
d54db74f5a | ||
|
|
4e1980c2cc | ||
|
|
40f990fffe | ||
|
|
8931f25ac5 | ||
|
|
94f60fb5b8 | ||
|
|
01b397a31a | ||
|
|
f2cd1edc57 | ||
|
|
243123fd7e | ||
|
|
36b33030f3 | ||
|
|
17709f2fb7 | ||
|
|
a8c17c1856 | ||
|
|
8ac920d28c | ||
|
|
239c620707 | ||
|
|
f2e600d681 | ||
|
|
6486db99fa | ||
|
|
766ca05e15 | ||
|
|
6c41f69db7 | ||
|
|
cae696c323 | ||
|
|
42dc8bc3f5 | ||
|
|
aecf0a5f25 | ||
|
|
896a1b007f | ||
|
|
fc5973751a | ||
|
|
9ca5133c76 | ||
|
|
509f59ac3c | ||
|
|
9d8482a119 | ||
|
|
34343fae02 | ||
|
|
1ff6ecf5d8 | ||
|
|
f2f7ad8253 | ||
|
|
78de5d4866 | ||
|
|
9449b0b875 | ||
|
|
b86c50c60a | ||
|
|
34f90facb2 | ||
|
|
99a0c72d49 | ||
|
|
c4d9e397ab | ||
|
|
3f49f51847 | ||
|
|
1d2b759dc0 | ||
|
|
8e62d2af6f | ||
|
|
7dfcda9306 | ||
|
|
4cfc64e528 | ||
|
|
020968961e | ||
|
|
2cfe18c9c8 | ||
|
|
39dc90e795 | ||
|
|
fe4101d26c | ||
|
|
80861d33b4 | ||
|
|
1383b41062 | ||
|
|
99c03b0e83 | ||
|
|
bb4b5d2bc8 | ||
|
|
4916806298 | ||
|
|
f350b81da4 | ||
|
|
1e306e08c9 | ||
|
|
518505fa9d | ||
|
|
1f4a0601f8 | ||
|
|
0347fdc192 | ||
|
|
d207f3ca26 | ||
|
|
f2d6f09671 | ||
|
|
0f44d20da5 | ||
|
|
aaf53d5d3f | ||
|
|
154e3945c9 | ||
|
|
0a82729650 | ||
|
|
5876c923a2 | ||
|
|
a2b56154a7 | ||
|
|
4b1468cfd8 | ||
|
|
00fe639a95 | ||
|
|
94781c89f3 | ||
|
|
6e2f9a5bdd | ||
|
|
a3312f69fb | ||
|
|
87bcf59bd4 | ||
|
|
4817e33942 | ||
|
|
141cbad5ad | ||
|
|
aaa4976c7d | ||
|
|
2525ef71a9 | ||
|
|
75a2cb9236 | ||
|
|
6b82cb2b7a | ||
|
|
260e24f68b | ||
|
|
3c8fabe409 | ||
|
|
1c3ed3ea40 | ||
|
|
497f45e5cd | ||
|
|
f9a411c307 | ||
|
|
7d755ce604 | ||
|
|
f64dbcb6b2 | ||
|
|
0b41356179 | ||
|
|
0928d99813 | ||
|
|
e724aace49 | ||
|
|
b65044712b | ||
|
|
22f48c5ad3 | ||
|
|
1b4d55fb30 | ||
|
|
ad8b22482b | ||
|
|
780c149c81 | ||
|
|
859a4e4f79 | ||
|
|
d0c2b00fbf | ||
|
|
37091f25a8 | ||
|
|
7f1855ad4d | ||
|
|
b42958de9f | ||
|
|
73eaf7b8b6 | ||
|
|
52361a1c48 | ||
|
|
9b48d674b4 | ||
|
|
c0fab933a5 | ||
|
|
df6d760844 | ||
|
|
c6b6c81c83 | ||
|
|
c2d6e0e316 | ||
|
|
5acc6ad0c4 | ||
|
|
a533f352b2 | ||
|
|
262bb723d9 | ||
|
|
a97b6de37e | ||
|
|
9f738e4a85 | ||
|
|
8895bd77c1 | ||
|
|
404fd4c720 | ||
|
|
058de2d761 | ||
|
|
16d490474d | ||
|
|
bd2088c480 | ||
|
|
c42f6289f6 | ||
|
|
92cfa4040b | ||
|
|
3f3af275e7 | ||
|
|
28c653043e | ||
|
|
abe7275f0c | ||
|
|
d4af2be7a0 | ||
|
|
8dd4c3e3c0 | ||
|
|
af25f164ed | ||
|
|
64ede0f11c | ||
|
|
d3565c9b87 | ||
|
|
c332c132fa | ||
|
|
b3534aecda | ||
|
|
8e04912201 | ||
|
|
909f9b3d24 | ||
|
|
cad38573d7 | ||
|
|
a3663e43e4 | ||
|
|
6d451785f0 | ||
|
|
7791901b2d | ||
|
|
2afe1fbeed | ||
|
|
e2097e856e | ||
|
|
03e7a3ea65 | ||
|
|
27f8cc0e52 | ||
|
|
1aa82ff06a | ||
|
|
0ff1f6520a | ||
|
|
ff2a354333 | ||
|
|
543709336c | ||
|
|
afd6d2e0ee | ||
|
|
32efbd5823 | ||
|
|
6dbdabf9fd | ||
|
|
75d57b9f04 | ||
|
|
554547b431 | ||
|
|
b811da6b83 | ||
|
|
ca6bc1dcb0 | ||
|
|
7c3fd42a86 | ||
|
|
04f12d1e2f | ||
|
|
c6b8ea90b7 | ||
|
|
7f8fb8d571 | ||
|
|
f8cfb084e0 | ||
|
|
70b084457a | ||
|
|
6c12244587 | ||
|
|
e7c0365079 | ||
|
|
43b11de596 | ||
|
|
ef45ea5a50 | ||
|
|
483edb70bf | ||
|
|
7516d25bc6 | ||
|
|
2f2918bd3b | ||
|
|
7a5572ad7c | ||
|
|
73d2b3363b | ||
|
|
5c9cebf059 | ||
|
|
ba0cc7fbf9 | ||
|
|
b7f37138f8 | ||
|
|
53a451671c | ||
|
|
65dff6e8e3 | ||
|
|
03a2de961d | ||
|
|
b94310a4cc | ||
|
|
9c594da847 | ||
|
|
93e62de3d2 | ||
|
|
a3efbb3466 | ||
|
|
aaf01b98d2 | ||
|
|
af037b9d70 | ||
|
|
5dafd7e4a7 | ||
|
|
e2b5f4a9fb | ||
|
|
2e58f0db10 | ||
|
|
26b31acbae | ||
|
|
66e96244ef | ||
|
|
4dc0183901 | ||
|
|
d33eded060 | ||
|
|
5913142389 | ||
|
|
66ef28c2e2 | ||
|
|
19c30fc411 | ||
|
|
50bed826d0 | ||
|
|
b5851dd6d4 | ||
|
|
ff1ee7d292 | ||
|
|
9455428048 | ||
|
|
0f919f3d49 | ||
|
|
d556a675e9 | ||
|
|
bfc1fa5181 | ||
|
|
d9387dac99 | ||
|
|
4818ee57b6 | ||
|
|
addb5efebb | ||
|
|
8adb9ee633 | ||
|
|
418fc98d1a | ||
|
|
beffe4a1f2 | ||
|
|
ef15b44a1b | ||
|
|
bc802bfc77 | ||
|
|
d10a5df3df | ||
|
|
b05d27ce45 | ||
|
|
e61c9fdde9 | ||
|
|
d2e2791729 | ||
|
|
68a7756621 | ||
|
|
42063cbd5c | ||
|
|
a407a2e0f8 | ||
|
|
6ec1ccf7a3 | ||
|
|
044f4182d0 | ||
|
|
bae30d79c9 | ||
|
|
25a60969fb | ||
|
|
528a67722b | ||
|
|
d29dc95962 | ||
|
|
fc3d4dcf5e | ||
|
|
3d4218324f | ||
|
|
6a10bac017 | ||
|
|
f6fbb45978 | ||
|
|
dee16f543d | ||
|
|
9959d1aa43 | ||
|
|
76146c4e74 | ||
|
|
8a8023fcf8 | ||
|
|
4b0d1e448d | ||
|
|
6748a2f2f3 | ||
|
|
4c4d772a5f | ||
|
|
85740d810b | ||
|
|
2305ebca24 | ||
|
|
59bf388534 | ||
|
|
3066b95a6d | ||
|
|
1bd77a83bd | ||
|
|
d0b7336da7 | ||
|
|
c80f71bd9b | ||
|
|
15fa3b7d9f | ||
|
|
e2d7f2cf29 | ||
|
|
3031fb910f | ||
|
|
d999dbe0a0 | ||
|
|
60d5e66e34 | ||
|
|
c6964502c4 | ||
|
|
ca2633ff82 | ||
|
|
a1625c7f15 | ||
|
|
30a913c05c | ||
|
|
1d02933481 | ||
|
|
62c2ec0614 | ||
|
|
45ca20dec9 | ||
|
|
de362d2322 | ||
|
|
115e6e9cf8 | ||
|
|
f17538b7df | ||
|
|
6f68c8cd1f | ||
|
|
f565302a0f | ||
|
|
6a3f169a47 | ||
|
|
6bd8875375 | ||
|
|
02dd72ba57 | ||
|
|
63629efae7 | ||
|
|
3691e59af1 | ||
|
|
d88dbbc90f |
24
.github/workflows/build.yaml
vendored
24
.github/workflows/build.yaml
vendored
@@ -1,30 +1,24 @@
|
||||
name: build
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Install Go
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20.x'
|
||||
-
|
||||
name: Install node
|
||||
go-version: '1.22.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
-
|
||||
name: Install dependencies
|
||||
- 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
|
||||
|
||||
25
.github/workflows/release.yaml
vendored
25
.github/workflows/release.yaml
vendored
@@ -7,35 +7,28 @@ jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Install Go
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20.x'
|
||||
-
|
||||
name: Install node
|
||||
go-version: '1.22.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
-
|
||||
name: Docker login
|
||||
- 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
|
||||
|
||||
33
.github/workflows/test.yaml
vendored
33
.github/workflows/test.yaml
vendored
@@ -1,39 +1,30 @@
|
||||
name: test
|
||||
on: [push, pull_request]
|
||||
on: [ push, pull_request ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout code
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Install Go
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: '1.20.x'
|
||||
-
|
||||
name: Install node
|
||||
go-version: '1.22.x'
|
||||
- name: Install node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: '18'
|
||||
node-version: '20'
|
||||
cache: 'npm'
|
||||
cache-dependency-path: './web/package-lock.json'
|
||||
-
|
||||
name: Install dependencies
|
||||
- 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
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -13,4 +13,5 @@ secrets/
|
||||
node_modules/
|
||||
.DS_Store
|
||||
__pycache__
|
||||
web/dev-dist/
|
||||
web/dev-dist/
|
||||
venv/
|
||||
|
||||
@@ -164,14 +164,14 @@ dockers:
|
||||
- image_templates:
|
||||
- &arm64v8_image "binwiederhier/ntfy:{{ .Tag }}-arm64v8"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm64
|
||||
build_flag_templates:
|
||||
- "--platform=linux/arm64/v8"
|
||||
- image_templates:
|
||||
- &armv7_image "binwiederhier/ntfy:{{ .Tag }}-armv7"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm
|
||||
goarm: 7
|
||||
build_flag_templates:
|
||||
@@ -179,7 +179,7 @@ dockers:
|
||||
- image_templates:
|
||||
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
|
||||
use: buildx
|
||||
dockerfile: Dockerfile
|
||||
dockerfile: Dockerfile-arm
|
||||
goarch: arm
|
||||
goarm: 6
|
||||
build_flag_templates:
|
||||
|
||||
@@ -9,6 +9,7 @@ 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
|
||||
|
||||
EXPOSE 80/tcp
|
||||
|
||||
18
Dockerfile-arm
Normal file
18
Dockerfile-arm
Normal file
@@ -0,0 +1,18 @@
|
||||
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"]
|
||||
@@ -1,14 +1,20 @@
|
||||
FROM golang:1.20-bullseye as builder
|
||||
FROM golang:1.22-bullseye as builder
|
||||
|
||||
ARG VERSION=dev
|
||||
ARG COMMIT=unknown
|
||||
ARG NODE_MAJOR=18
|
||||
|
||||
RUN apt-get update
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash
|
||||
RUN apt-get install -y \
|
||||
build-essential \
|
||||
nodejs \
|
||||
python3-pip
|
||||
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 .
|
||||
@@ -19,7 +25,7 @@ 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
|
||||
|
||||
42
Makefile
42
Makefile
@@ -1,4 +1,6 @@
|
||||
MAKEFLAGS := --jobs=1
|
||||
PYTHON := python3
|
||||
PIP := pip3
|
||||
VERSION := $(shell git describe --tag)
|
||||
COMMIT := $(shell git rev-parse --short HEAD)
|
||||
|
||||
@@ -39,8 +41,8 @@ help:
|
||||
@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-format - Run prettier on the web app"
|
||||
@echo " make web-format-check - Run prettier on the web app, but don't change anything"
|
||||
@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"
|
||||
@@ -95,6 +97,7 @@ docker-dev:
|
||||
--build-arg COMMIT=$(COMMIT) \
|
||||
./
|
||||
|
||||
|
||||
# Ubuntu-specific
|
||||
|
||||
build-deps-ubuntu:
|
||||
@@ -103,32 +106,27 @@ build-deps-ubuntu:
|
||||
curl \
|
||||
gcc-aarch64-linux-gnu \
|
||||
gcc-arm-linux-gnueabi \
|
||||
python3 \
|
||||
python3-venv \
|
||||
jq
|
||||
which pip3 || sudo apt-get install -y python3-pip
|
||||
|
||||
|
||||
# Documentation
|
||||
|
||||
docs: docs-deps docs-build
|
||||
|
||||
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-venv: .PHONY
|
||||
$(PYTHON) -m venv ./venv
|
||||
|
||||
docs-deps: .PHONY
|
||||
pip3 install -r requirements.txt
|
||||
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-update: .PHONY
|
||||
pip3 install -r requirements.txt --upgrade
|
||||
(. venv/bin/activate && $(PIP) install -r requirements.txt --upgrade)
|
||||
|
||||
|
||||
# Web app
|
||||
@@ -151,10 +149,10 @@ web-deps:
|
||||
web-deps-update:
|
||||
cd web && npm update
|
||||
|
||||
web-format:
|
||||
web-fmt:
|
||||
cd web && npm run format
|
||||
|
||||
web-format-check:
|
||||
web-fmt-check:
|
||||
cd web && npm run format:check
|
||||
|
||||
web-lint:
|
||||
@@ -248,7 +246,7 @@ cli-build-results:
|
||||
|
||||
# Test/check targets
|
||||
|
||||
check: test web-format-check fmt-check vet web-lint lint staticcheck
|
||||
check: test web-fmt-check fmt-check vet web-lint lint staticcheck
|
||||
|
||||
test: .PHONY
|
||||
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
|
||||
@@ -275,7 +273,7 @@ coverage-upload:
|
||||
|
||||
# Lint/formatting targets
|
||||
|
||||
fmt:
|
||||
fmt: web-fmt
|
||||
gofmt -s -w .
|
||||
|
||||
fmt-check:
|
||||
|
||||
60
README.md
60
README.md
@@ -2,14 +2,13 @@
|
||||
|
||||
# ntfy.sh | Send push notifications to your phone or desktop via PUT/POST
|
||||
[](https://github.com/binwiederhier/ntfy/releases/latest)
|
||||
[](https://pkg.go.dev/heckel.io/ntfy)
|
||||
[](https://pkg.go.dev/heckel.io/ntfy/v2)
|
||||
[](https://github.com/binwiederhier/ntfy/actions)
|
||||
[](https://goreportcard.com/report/github.com/binwiederhier/ntfy)
|
||||
[](https://codecov.io/gh/binwiederhier/ntfy)
|
||||
[](https://discord.gg/cT7ECsZj9w)
|
||||
[](https://matrix.to/#/#ntfy:matrix.org)
|
||||
[](https://matrix.to/#/#ntfy-space:matrix.org)
|
||||
[](https://discuss.ntfy.sh/c/ntfy)
|
||||
[](https://ntfy.statuspage.io/)
|
||||
[](https://gitpod.io/#https://github.com/binwiederhier/ntfy)
|
||||
|
||||
@@ -18,7 +17,7 @@ notification service. With ntfy, you can **send notifications to your phone or d
|
||||
**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.
|
||||
|
||||
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)
|
||||
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).
|
||||
|
||||
@@ -31,7 +30,10 @@ as well as an [open source iOS app](https://github.com/binwiederhier/ntfy-ios) a
|
||||
</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 $3.33/month** (if you use promo code `MYTOPIC`, limited time only). 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. ❤️
|
||||
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/)**
|
||||
|
||||
@@ -41,22 +43,21 @@ I now offer paid plans for [ntfy.sh](https://ntfy.sh/) if you don't want to self
|
||||
[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
|
||||
* [Lemmy discussion board](https://discuss.ntfy.sh/c/ntfy) - asynchronous forum (_new as of June 2023_)
|
||||
* [GitHub issues](https://github.com/binwiederhier/ntfy/issues) - questions, features, bugs
|
||||
|
||||
## 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
|
||||
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/).
|
||||
@@ -68,7 +69,7 @@ for the server and the Android app. Or, if you'd like to help translate 🇩🇪
|
||||
## Sponsors
|
||||
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:
|
||||
account costs. Even small donations are very much appreciated. A big fat **Thank You** to the folks who have sponsored ntfy in the past, or are still sponsoring ntfy:
|
||||
|
||||
<a href="https://github.com/neutralinsomniac"><img src="https://github.com/neutralinsomniac.png" width="40px" /></a>
|
||||
<a href="https://github.com/aspyct"><img src="https://github.com/aspyct.png" width="40px" /></a>
|
||||
@@ -146,6 +147,45 @@ account costs. Even small donations are very much appreciated. A big fat **Thank
|
||||
<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>
|
||||
|
||||
I'd also like to thank JetBrains for their awesome [IntelliJ IDEA](https://www.jetbrains.com/idea/),
|
||||
and [DigitalOcean](https://m.do.co/c/442b929528db) (*referral link*) for supporting the project:
|
||||
@@ -163,7 +203,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
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
@@ -3,9 +3,9 @@ package client_test
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -2,6 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"gopkg.in/yaml.v2"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"os"
|
||||
)
|
||||
|
||||
@@ -44,6 +45,7 @@ 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
|
||||
|
||||
@@ -2,7 +2,7 @@ package client_test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -2,7 +2,7 @@ package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -4,12 +4,13 @@ import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCLI_Access_Show(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
@@ -19,6 +20,7 @@ func TestCLI_Access_Show(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCLI_Access_Grant_And_Publish(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, conf, port := newTestServerWithAuth(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"os"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"gopkg.in/yaml.v2"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"os"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
)
|
||||
|
||||
func TestNewYamlSourceFromFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
filename := filepath.Join(t.TempDir(), "server.yml")
|
||||
contents := `
|
||||
# Normal options
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
@@ -3,8 +3,8 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||
t.Parallel()
|
||||
testMessage := util.RandomString(10)
|
||||
app, _, _, _ := newTestApp()
|
||||
require.Nil(t, app.Run([]string{"ntfy", "publish", "ntfytest", "ntfy unit test " + testMessage}))
|
||||
@@ -35,6 +36,7 @@ func TestCLI_Publish_Subscribe_Poll_Real_Server(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, port := test.StartServer(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||
@@ -51,6 +53,7 @@ func TestCLI_Publish_Subscribe_Poll(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCLI_Publish_All_The_Things(t *testing.T) {
|
||||
t.Parallel()
|
||||
s, port := test.StartServer(t)
|
||||
defer test.StopServer(t, s, port)
|
||||
topic := fmt.Sprintf("http://127.0.0.1:%d/mytopic", port)
|
||||
|
||||
166
cmd/serve.go
166
cmd/serve.go
@@ -6,23 +6,22 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/stripe/stripe-go/v74"
|
||||
"heckel.io/ntfy/user"
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"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/server"
|
||||
"heckel.io/ntfy/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -35,7 +34,7 @@ const (
|
||||
|
||||
var flagsServe = append(
|
||||
append([]cli.Flag{}, flagsDefault...),
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: defaultServerConfigFile, DefaultText: defaultServerConfigFile, Usage: "config file"},
|
||||
&cli.StringFlag{Name: "config", Aliases: []string{"c"}, EnvVars: []string{"NTFY_CONFIG_FILE"}, Value: 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"}),
|
||||
@@ -45,19 +44,19 @@ var flagsServe = append(
|
||||
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.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.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.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.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-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.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.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"}, 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.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: "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.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.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"}),
|
||||
@@ -76,16 +75,18 @@ var flagsServe = append(
|
||||
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.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-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-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.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-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.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.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.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.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.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"}),
|
||||
@@ -126,7 +127,7 @@ func execServe(c *cli.Context) error {
|
||||
|
||||
// Read all the options
|
||||
config := c.String("config")
|
||||
baseURL := c.String("base-url")
|
||||
baseURL := strings.TrimSuffix(c.String("base-url"), "/")
|
||||
listenHTTP := c.String("listen-http")
|
||||
listenHTTPS := c.String("listen-https")
|
||||
listenUnix := c.String("listen-unix")
|
||||
@@ -140,19 +141,19 @@ func execServe(c *cli.Context) error {
|
||||
webPushEmailAddress := c.String("web-push-email-address")
|
||||
webPushStartupQueries := c.String("web-push-startup-queries")
|
||||
cacheFile := c.String("cache-file")
|
||||
cacheDuration := c.Duration("cache-duration")
|
||||
cacheDurationStr := c.String("cache-duration")
|
||||
cacheStartupQueries := c.String("cache-startup-queries")
|
||||
cacheBatchSize := c.Int("cache-batch-size")
|
||||
cacheBatchTimeout := c.Duration("cache-batch-timeout")
|
||||
cacheBatchTimeoutStr := c.String("cache-batch-timeout")
|
||||
authFile := c.String("auth-file")
|
||||
authStartupQueries := c.String("auth-startup-queries")
|
||||
authDefaultAccess := c.String("auth-default-access")
|
||||
attachmentCacheDir := c.String("attachment-cache-dir")
|
||||
attachmentTotalSizeLimitStr := c.String("attachment-total-size-limit")
|
||||
attachmentFileSizeLimitStr := c.String("attachment-file-size-limit")
|
||||
attachmentExpiryDuration := c.Duration("attachment-expiry-duration")
|
||||
keepaliveInterval := c.Duration("keepalive-interval")
|
||||
managerInterval := c.Duration("manager-interval")
|
||||
attachmentExpiryDurationStr := c.String("attachment-expiry-duration")
|
||||
keepaliveIntervalStr := c.String("keepalive-interval")
|
||||
managerIntervalStr := c.String("manager-interval")
|
||||
disallowedTopics := c.StringSlice("disallowed-topics")
|
||||
webRoot := c.String("web-root")
|
||||
enableSignup := c.Bool("enable-signup")
|
||||
@@ -171,17 +172,19 @@ func execServe(c *cli.Context) error {
|
||||
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")
|
||||
visitorRequestLimitReplenish := c.Duration("visitor-request-limit-replenish")
|
||||
visitorRequestLimitReplenishStr := c.String("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")
|
||||
visitorEmailLimitReplenish := c.Duration("visitor-email-limit-replenish")
|
||||
visitorEmailLimitReplenishStr := c.String("visitor-email-limit-replenish")
|
||||
behindProxy := c.Bool("behind-proxy")
|
||||
stripeSecretKey := c.String("stripe-secret-key")
|
||||
stripeWebhookKey := c.String("stripe-webhook-key")
|
||||
@@ -190,6 +193,64 @@ func execServe(c *cli.Context) error {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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")
|
||||
@@ -213,10 +274,15 @@ func execServe(c *cli.Context) error {
|
||||
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 != "" && !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 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 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, "/") {
|
||||
@@ -233,6 +299,11 @@ func execServe(c *cli.Context) error {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// Backwards compatibility
|
||||
@@ -257,26 +328,6 @@ func execServe(c *cli.Context) error {
|
||||
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
|
||||
visitorRequestLimitExemptIPs := make([]netip.Prefix, 0)
|
||||
for _, host := range visitorRequestLimitExemptHosts {
|
||||
@@ -337,6 +388,8 @@ func execServe(c *cli.Context) error {
|
||||
conf.TwilioAuthToken = twilioAuthToken
|
||||
conf.TwilioPhoneNumber = twilioPhoneNumber
|
||||
conf.TwilioVerifyService = twilioVerifyService
|
||||
conf.MessageSizeLimit = int(messageSizeLimit)
|
||||
conf.MessageDelayMax = messageDelayLimit
|
||||
conf.TotalTopicLimit = totalTopicLimit
|
||||
conf.VisitorSubscriptionLimit = visitorSubscriptionLimit
|
||||
conf.VisitorAttachmentTotalSizeLimit = visitorAttachmentTotalSizeLimit
|
||||
@@ -379,17 +432,6 @@ func execServe(c *cli.Context) error {
|
||||
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)
|
||||
|
||||
@@ -12,15 +12,11 @@ import (
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
func TestCLI_Serve_Unix_Curl(t *testing.T) {
|
||||
sockFile := filepath.Join(t.TempDir(), "ntfy.sock")
|
||||
configFile := newEmptyFile(t) // Avoid issues with existing server.yml file on system
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/client"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/client"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
@@ -310,28 +310,43 @@ func loadConfig(c *cli.Context) (*client.Config, error) {
|
||||
if filename != "" {
|
||||
return client.LoadConfig(filename)
|
||||
}
|
||||
configFile := defaultClientConfigFile()
|
||||
if s, _ := os.Stat(configFile); s != nil {
|
||||
return client.LoadConfig(configFile)
|
||||
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)
|
||||
}
|
||||
log.Debug("Loading default config")
|
||||
return client.NewConfig(), nil
|
||||
}
|
||||
|
||||
//lint:ignore U1000 Conditionally used in different builds
|
||||
func defaultClientConfigFileUnix() string {
|
||||
u, _ := user.Current()
|
||||
func defaultClientConfigFileUnix() (string, error) {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine current user: %w", err)
|
||||
}
|
||||
configFile := clientRootConfigFileUnixAbsolute
|
||||
if u.Uid != "0" {
|
||||
homeDir, _ := os.UserConfigDir()
|
||||
return filepath.Join(homeDir, clientUserConfigFileUnixRelative)
|
||||
homeDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("could not determine user config dir: %w", err)
|
||||
}
|
||||
return filepath.Join(homeDir, clientUserConfigFileUnixRelative), nil
|
||||
}
|
||||
return configFile
|
||||
return configFile, nil
|
||||
}
|
||||
|
||||
//lint:ignore U1000 Conditionally used in different builds
|
||||
func defaultClientConfigFileWindows() string {
|
||||
homeDir, _ := os.UserConfigDir()
|
||||
return filepath.Join(homeDir, clientUserConfigFileWindowsRelative)
|
||||
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 logMessagePrefix(m *client.Message) string {
|
||||
|
||||
@@ -11,6 +11,6 @@ var (
|
||||
scriptLauncher = []string{"sh", "-c"}
|
||||
)
|
||||
|
||||
func defaultClientConfigFile() string {
|
||||
func defaultClientConfigFile() (string, error) {
|
||||
return defaultClientConfigFileUnix()
|
||||
}
|
||||
|
||||
@@ -13,6 +13,6 @@ var (
|
||||
scriptLauncher = []string{"sh", "-c"}
|
||||
)
|
||||
|
||||
func defaultClientConfigFile() string {
|
||||
func defaultClientConfigFile() (string, error) {
|
||||
return defaultClientConfigFileUnix()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,6 @@ var (
|
||||
scriptLauncher = []string{"cmd.exe", "/Q", "/C"}
|
||||
)
|
||||
|
||||
func defaultClientConfigFile() string {
|
||||
func defaultClientConfigFile() (string, error) {
|
||||
return defaultClientConfigFileWindows()
|
||||
}
|
||||
|
||||
10
cmd/tier.go
10
cmd/tier.go
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -366,9 +366,9 @@ func printTier(c *cli.Context, tier *user.Tier) {
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Email limit: %d\n", tier.EmailLimit)
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Phone call limit: %d\n", tier.CallLimit)
|
||||
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 file size limit: %s\n", util.FormatSizeHuman(tier.AttachmentFileSizeLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Attachment total size limit: %s\n", util.FormatSizeHuman(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, "- Attachment daily bandwidth limit: %s\n", util.FormatSizeHuman(tier.AttachmentBandwidthLimit))
|
||||
fmt.Fprintf(c.App.ErrWriter, "- Stripe prices (monthly/yearly): %s\n", prices)
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ package cmd
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
"github.com/urfave/cli/v2/altsrc"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -198,7 +198,6 @@ func execUserAdd(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password = p
|
||||
}
|
||||
if err := manager.AddUser(username, password, role); err != nil {
|
||||
@@ -343,6 +342,8 @@ 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)
|
||||
|
||||
@@ -3,9 +3,9 @@ package cmd
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/test"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"heckel.io/ntfy/v2/test"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
)
|
||||
|
||||
func TestCLI_WebPush_GenerateKeys(t *testing.T) {
|
||||
|
||||
143
docs/config.md
143
docs/config.md
@@ -24,7 +24,7 @@ get a list of [command line options](#command-line-options).
|
||||
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:
|
||||
Here are a few working sample configs using a `/etc/ntfy/server.yml` file:
|
||||
|
||||
=== "server.yml (HTTP-only, with cache + attachments)"
|
||||
``` yaml
|
||||
@@ -73,6 +73,58 @@ Here are a few working sample configs:
|
||||
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
|
||||
version: '3'
|
||||
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
|
||||
volumes:
|
||||
- ./:/var/lib/ntfy
|
||||
ports:
|
||||
- 80:80
|
||||
command: serve
|
||||
```
|
||||
|
||||
=== "Docker Compose (w/ auth, cache, web push, iOS)"
|
||||
``` yaml
|
||||
version: '3'
|
||||
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
|
||||
@@ -352,10 +404,10 @@ with the given username/password. Be sure to use HTTPS to avoid eavesdropping an
|
||||
```
|
||||
|
||||
### Example: UnifiedPush
|
||||
[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.
|
||||
[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.
|
||||
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)** for more details.
|
||||
**[UnifiedPush documentation](https://unifiedpush.org/users/distributors/ntfy/#limit-access-to-some-users-acl)** 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:
|
||||
@@ -466,6 +518,31 @@ $ 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
|
||||
@@ -918,6 +995,15 @@ are the easiest), and then configure the following options:
|
||||
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.
|
||||
@@ -1001,20 +1087,23 @@ By default, ntfy puts almost all rate limits on the message publisher, e.g. numb
|
||||
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 enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
|
||||
to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
|
||||
publishers (e.g. Matrix/Mastodon servers) are allowed to send.
|
||||
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 may send a `Rate-Topics: <topic1>,<topic2>,...` header when subscribing to topics via
|
||||
HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
|
||||
to use when publishing on this topic. Note that setting the rate visitor requires **read-write permission** on 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.
|
||||
|
||||
UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to an `HTTP 507 Insufficient Storage`
|
||||
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**.
|
||||
@@ -1155,10 +1244,10 @@ and [here](https://easyengine.io/tutorials/nginx/block-wp-login-php-bruteforce-a
|
||||
|
||||
## 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 `health` field is `false` the ntfy service should be considered as unhealthy.
|
||||
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
|
||||
{"health":true}
|
||||
{"healthy":true}
|
||||
```
|
||||
|
||||
See [Installation for Docker](install.md#docker) for an example of how this could be used in a `docker-compose` environment.
|
||||
@@ -1311,6 +1400,8 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||
| `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 |
|
||||
@@ -1337,7 +1428,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||
| `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 |
|
||||
|
||||
The format for a *duration* is: `<number>(smh)`, e.g. 30s, 20m or 1h.
|
||||
The format for a *duration* is: `<number>(smhd)`, e.g. 30s, 20m, 1h or 3d.
|
||||
The format for a *size* is: `<number>(GMK)`, e.g. 1G, 200M or 4000k.
|
||||
|
||||
## Command line options
|
||||
@@ -1369,7 +1460,7 @@ 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]
|
||||
@@ -1379,19 +1470,19 @@ OPTIONS:
|
||||
--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: 12h0m0s) [$NTFY_CACHE_DURATION]
|
||||
--cache-duration since, --cache_duration since, -b since buffer messages for this time to allow since requests (default: "12h") [$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: 1m0s) [$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: "1m") [$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]
|
||||
--enable-signup, --enable_signup allows users to sign up via the web app, or API (default: false) [$NTFY_ENABLE_SIGNUP]
|
||||
@@ -1410,16 +1501,18 @@ OPTIONS:
|
||||
--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-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: 1h0m0s) [$NTFY_VISITOR_EMAIL_LIMIT_REPLENISH]
|
||||
--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-subscriber-rate-limiting, --visitor_subscriber_rate_limiting enables subscriber-based rate limiting (default: false) [$NTFY_VISITOR_SUBSCRIBER_RATE_LIMITING]
|
||||
--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]
|
||||
@@ -1432,6 +1525,6 @@ OPTIONS:
|
||||
--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-startup-queries value, --web_push_startup_queries value queries run when the web push database is initialized [$NTFY_WEB_PUSH_STARTUP_QUERIES]
|
||||
--help, -h show help
|
||||
```
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Deprecation notices
|
||||
# Deprecations and breaking changes
|
||||
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.
|
||||
|
||||
@@ -363,7 +363,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/*.apk)
|
||||
# To build an unsigned .apk (app/build/outputs/apk/play/release/*.apk)
|
||||
./gradlew assemblePlayRelease
|
||||
|
||||
# To build a bundle .aab (app/play/release/*.aab)
|
||||
@@ -429,7 +429,7 @@ steps:
|
||||
|
||||
### XCode setup
|
||||
|
||||
1. Follow step 4 of [https://firebase.google.com/docs/ios/setup](Add Firebase to your Apple project) to install the
|
||||
1. Follow step 4 of [Add Firebase to your Apple project](https://firebase.google.com/docs/ios/setup) 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
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
<!-- This file was generated by scripts/emoji-convert.sh -->
|
||||
|
||||
You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
||||
You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
||||
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
|
||||
[tagging and emojis page](../publish/#tags-emojis).
|
||||
[tagging and emojis page](publish.md#tags-emojis).
|
||||
|
||||
<table class="remove-md-box emoji-table"><tr>
|
||||
|
||||
|
||||
@@ -135,6 +135,21 @@ 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.
|
||||
|
||||

|
||||
|
||||
## Watchtower (shoutrrr)
|
||||
You can use [shoutrrr](https://containrrr.dev/shoutrrr/latest/services/ntfy/) to send
|
||||
[Watchtower](https://github.com/containrrr/watchtower/) notifications to your ntfy topic.
|
||||
@@ -147,14 +162,23 @@ services:
|
||||
image: containrrr/watchtower
|
||||
environment:
|
||||
- WATCHTOWER_NOTIFICATIONS=shoutrrr
|
||||
- WATCHTOWER_NOTIFICATION_SKIP_TITLE=True
|
||||
- WATCHTOWER_NOTIFICATION_URL=ntfy://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 via the 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`
|
||||
```
|
||||
|
||||
## Sonarr, Radarr, Lidarr, Readarr, Prowlarr, SABnzbd
|
||||
|
||||
<!-- Sonarr v4 is in beta as of May 2023, should be updated to remove v3 reference when stable -->
|
||||
|
||||
12
docs/faq.md
12
docs/faq.md
@@ -76,6 +76,18 @@ 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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
def copy_fonts(config, **kwargs):
|
||||
site_dir = config['site_dir']
|
||||
shutil.copytree('docs/static/fonts', os.path.join(site_dir, 'get'))
|
||||
|
||||
def on_post_build(config, **kwargs):
|
||||
site_dir = config["site_dir"]
|
||||
shutil.copytree("docs/static/fonts", os.path.join(site_dir, "get"))
|
||||
|
||||
@@ -3,9 +3,9 @@ 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 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>
|
||||
<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 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
|
||||
|
||||
@@ -14,14 +14,15 @@ 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) 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), `~/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))
|
||||
|
||||
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 video tutorials, check out :simple-youtube: [Kris Occhipinti's ntfy install guide](https://www.youtube.com/watch?v=bZzqrX05mNU).
|
||||
It's short and to the point. _I am not affiliated with Kris, I just liked the video._
|
||||
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
|
||||
@@ -29,37 +30,37 @@ deb/rpm packages.
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.tar.gz
|
||||
tar zxvf ntfy_2.9.0_linux_amd64.tar.gz
|
||||
sudo cp -a ntfy_2.9.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.tar.gz
|
||||
tar zxvf ntfy_2.9.0_linux_armv6.tar.gz
|
||||
sudo cp -a ntfy_2.9.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.tar.gz
|
||||
tar zxvf ntfy_2.9.0_linux_armv7.tar.gz
|
||||
sudo cp -a ntfy_2.9.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.7.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.7.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.tar.gz
|
||||
tar zxvf ntfy_2.9.0_linux_arm64.tar.gz
|
||||
sudo cp -a ntfy_2.9.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.9.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||
sudo ntfy serve
|
||||
```
|
||||
|
||||
@@ -109,7 +110,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_amd64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -117,7 +118,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv6"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv6.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv6.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -125,7 +126,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "armv7/armhf"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_armv7.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_armv7.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -133,7 +134,7 @@ Manually installing the .deb file:
|
||||
|
||||
=== "arm64"
|
||||
```bash
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_arm64.deb
|
||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.deb
|
||||
sudo dpkg -i ntfy_*.deb
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
@@ -143,28 +144,28 @@ Manually installing the .deb file:
|
||||
|
||||
=== "x86_64/amd64"
|
||||
```bash
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.7.0/ntfy_2.7.0_linux_amd64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.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.7.0/ntfy_2.7.0_linux_armv6.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.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.7.0/ntfy_2.7.0_linux_armv7.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.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.7.0/ntfy_2.7.0_linux_arm64.rpm
|
||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_linux_arm64.rpm
|
||||
sudo systemctl enable ntfy
|
||||
sudo systemctl start ntfy
|
||||
```
|
||||
@@ -194,18 +195,18 @@ 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.7.0/ntfy_2.7.0_darwin_all.tar.gz),
|
||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_darwin_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.7.0/ntfy_2.7.0_darwin_all.tar.gz > ntfy_2.7.0_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.7.0_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.7.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_darwin_all.tar.gz > ntfy_2.9.0_darwin_all.tar.gz
|
||||
tar zxvf ntfy_2.9.0_darwin_all.tar.gz
|
||||
sudo cp -a ntfy_2.9.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||
mkdir ~/Library/Application\ Support/ntfy
|
||||
cp ntfy_2.7.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
cp ntfy_2.9.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||
ntfy --help
|
||||
```
|
||||
|
||||
@@ -223,7 +224,7 @@ brew install ntfy
|
||||
|
||||
## 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.7.0/ntfy_2.7.0_windows_amd64.zip),
|
||||
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.9.0/ntfy_2.9.0_windows_amd64.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).
|
||||
|
||||
@@ -6,6 +6,7 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
|
||||
## Official integrations
|
||||
|
||||
- [changedetection.io](https://changedetection.io) ⭐ - Website change detection and notification
|
||||
- [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
|
||||
@@ -16,13 +17,15 @@ 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.7/services/ntfy/) ⭐ - Notification library for gophers and their furry friends.
|
||||
- [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
|
||||
- [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
|
||||
|
||||
## Integration via HTTP/SMTP/etc.
|
||||
|
||||
@@ -56,7 +59,8 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [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)
|
||||
- [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)
|
||||
|
||||
## CLIs + GUIs
|
||||
|
||||
@@ -80,7 +84,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [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)
|
||||
@@ -127,10 +130,50 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [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)
|
||||
|
||||
## Blog + forum posts
|
||||
|
||||
- [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 - 9/2021
|
||||
- [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
|
||||
- [Let’s Supercharge Snowflake Alerts with Cool ntfy Open-source Notifications!](https://sarathi-data-ml-cloud.medium.com/lets-supercharge-snowflake-alerts-with-cool-ntfy-open-source-notifications-296da442c331) - sarathi-data-ml-cloud.medium.com - 1/2024
|
||||
- [Setting up NTFY with Ngnix-Proxy-Manager, authentication and Ansible notifications](https://random-it-blog.de/rocky-linux/setting-up-ntfy-with-ngnix-proxy-manager-authentication-and-ansible-notifications/) - random-it-blog.de - 12/2023
|
||||
- [Introducing the Monitoring Ntfy.sh Integration Module: Real-time Notifications for Drupal Monitoring](https://cyberschorsch.dev/drupal/introducing-monitoring-ntfysh-integration-module-real-time-notifications-drupal-monitoring) - cyberschorsch.dev - 11/2023
|
||||
- [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
|
||||
@@ -199,7 +242,6 @@ I've added a ⭐ to projects or posts that have a significant following, or had
|
||||
- [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
|
||||
|
||||
|
||||
## 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
|
||||
@@ -214,6 +256,7 @@ ntfy community. Thanks to everyone running a public server. **You guys rock!**
|
||||
| [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**.
|
||||
|
||||
@@ -27,11 +27,12 @@ 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
|
||||
|
||||
## Firefox on Android not automatically subscribing to web push (see [#789](https://github.com/binwiederhier/ntfy/issues/789))
|
||||
ntfy defaults to web-push based subscriptions when installed as a [progressive web app](./subscribe/pwa.md). Firefox
|
||||
Android has an [open bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1796434) where it reports the PWA mode incorrectly.
|
||||
This causes ntfy to not automatically subscribe to web push, and requires you to go to the ntfy Settings page to enable
|
||||
it manually.
|
||||
## 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
|
||||
|
||||
@@ -457,6 +457,7 @@ You can set the priority with the header `X-Priority` (or any of its aliases: `P
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
$Request = @{
|
||||
Method = 'POST'
|
||||
URI = "https://ntfy.sh/phil_alerts"
|
||||
Headers = @{
|
||||
Priority = "5"
|
||||
@@ -737,9 +738,8 @@ Usage is pretty straight forward. You can set the delivery time using the `X-Del
|
||||
`3h`, `2 days`), or a natural language time string (e.g. `10am`, `8:30pm`, `tomorrow, 3pm`, `Tuesday, 7am`,
|
||||
[and more](https://github.com/olebedev/when)).
|
||||
|
||||
As of today, the minimum delay you can set is **10 seconds** and the maximum delay is **3 days**. This can currently
|
||||
not be configured otherwise ([let me know](https://github.com/binwiederhier/ntfy/issues) if you'd like to change
|
||||
these limits).
|
||||
As of today, the minimum delay you can set is **10 seconds** and the maximum delay is **3 days**. This can be configured
|
||||
with the `message-delay-limit` option).
|
||||
|
||||
For the purposes of [message caching](config.md#message-cache), scheduled messages are kept in the cache until 12 hours
|
||||
after they were delivered (or whatever the server-side cache duration is set to). For instance, if a message is scheduled
|
||||
@@ -1033,7 +1033,7 @@ is the only required one:
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
URI = "https://ntfy.sh"
|
||||
Body = @{
|
||||
Body = ConvertTo-JSON @{
|
||||
Topic = "mytopic"
|
||||
Title = "Low disk space alert"
|
||||
Message = "Disk space is low at 5.1 GB"
|
||||
@@ -1042,7 +1042,7 @@ is the only required one:
|
||||
FileName = "diskspace.jpg"
|
||||
Tags = @("warning", "cd")
|
||||
Click = "https://homecamera.lan/xasds1h2xsSsa/"
|
||||
Actions = ConvertTo-JSON @(
|
||||
Actions = @(
|
||||
@{
|
||||
Action = "view"
|
||||
Label = "Admin panel"
|
||||
@@ -1130,7 +1130,7 @@ As of today, the following actions are supported:
|
||||
when the action button is tapped (only supported on Android)
|
||||
* [`http`](#send-http-request): Sends HTTP POST/GET/PUT request when the action button is tapped
|
||||
|
||||
Here's an example of what that a notification with actions can look like:
|
||||
Here's an example of what a notification with actions can look like:
|
||||
|
||||
<figure markdown>
|
||||
{ width=500 }
|
||||
@@ -1919,10 +1919,10 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
URI = "https://ntfy.sh"
|
||||
Body = @{
|
||||
Body = ConvertTo-Json -Depth 3 @{
|
||||
Topic = "wifey"
|
||||
Message = "Your wife requested you send a picture of yourself."
|
||||
Actions = ConvertTo-Json -Depth 3 @(
|
||||
Actions = @(
|
||||
@{
|
||||
Action = "broadcast"
|
||||
Label = "Take picture"
|
||||
@@ -2072,7 +2072,7 @@ Here's an example using the [`X-Actions` header](#using-a-header):
|
||||
'method' => 'POST',
|
||||
'header' =>
|
||||
"Content-Type: text/plain\r\n" .
|
||||
"Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}",
|
||||
'Actions: http, Close door, https://api.mygarage.lan/, method=PUT, headers.Authorization=Bearer zAzsx1sk.., body={\"action\": \"close\"}',
|
||||
'content' => 'Garage door has been open for 15 minutes. Close it?'
|
||||
]
|
||||
]));
|
||||
@@ -2199,10 +2199,10 @@ And the same example using [JSON publishing](#publish-as-json):
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
URI = "https://ntfy.sh"
|
||||
Body = @{
|
||||
Body = ConvertTo-Json -Depth 3 @{
|
||||
Topic = "myhome"
|
||||
Message = "Garage door has been open for 15 minutes. Close it?"
|
||||
Actions = ConvertTo-Json -Depth 3 @(
|
||||
Actions = @(
|
||||
@{
|
||||
Action = "http"
|
||||
Label = "Close door"
|
||||
@@ -2287,7 +2287,7 @@ You can define which URL to open when a notification is clicked. This may be use
|
||||
to a Zabbix alert or a transaction that you'd like to provide the deep-link for. Tapping the notification will open
|
||||
the web browser (or the app) and open the website.
|
||||
|
||||
To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its aliase `Click`).
|
||||
To define a click action for the notification, pass a URL as the value of the `X-Click` header (or its alias `Click`).
|
||||
If you pass a website URL (`http://` or `https://`) the web browser will open. If you pass another URI that can be handled
|
||||
by another app, the responsible app may open.
|
||||
|
||||
@@ -2439,6 +2439,17 @@ Here's an example showing how to upload an image:
|
||||
http.DefaultClient.Do(req)
|
||||
```
|
||||
|
||||
=== "PowerShell"
|
||||
``` powershell
|
||||
$Request = @{
|
||||
Method = "POST"
|
||||
Uri = "ntfy.sh/flowers"
|
||||
InFile = "flower.jpg"
|
||||
Headers = @{"Filename" = "flower.jpg"}
|
||||
}
|
||||
Invoke-RestMethod @Request
|
||||
```
|
||||
|
||||
=== "Python"
|
||||
``` python
|
||||
requests.put("https://ntfy.sh/flowers",
|
||||
@@ -3546,6 +3557,34 @@ ntfy server plays the role of the Push Gateway, as well as the Push Provider. Un
|
||||
!!! info
|
||||
This is not a generic Matrix Push Gateway. It only works in combination with UnifiedPush and ntfy.
|
||||
|
||||
### Message and Title Templates
|
||||
Some services let you specify a webhook URL but do not let you modify the webhook body (e.g., Grafana). Instead of using a separate
|
||||
bridge program to parse the webhook body into the format ntfy expects, you can include a templated message and/or a templated title
|
||||
which will be populated based on the fields of the webhook body (so long as the webhook body is valid JSON).
|
||||
|
||||
Enable templating by setting the `X-Template` header (or its aliases `Template` or `tpl`) to "yes". Then, include templates
|
||||
in your message and/or title (no other fields can be filled with a template at this time) by including paths to the
|
||||
appropriate JSON fields surrounded by `${` and `}`. See an example below.
|
||||
See [GJSON docs](https://github.com/tidwall/gjson/blob/master/SYNTAX.md) for supported JSON path syntax.
|
||||
[https://gjson.dev/](https://gjson.dev/) is a great resource for testing your templates.
|
||||
|
||||
=== "HTTP"
|
||||
``` http
|
||||
POST /mytopic HTTP/1.1
|
||||
Host: ntfy.sh
|
||||
X-Message: Error message: ${error.desc}
|
||||
X-Title: ${hostname}: A ${error.level} error has occurred
|
||||
X-Template: yes
|
||||
|
||||
{"hostname": "philipp-pc", "error": {"level": "severe", "desc": "Disk has run out of space"}}
|
||||
```
|
||||
|
||||
The example above would send a notification with a title "philipp-pc: A severe error has occurred" and a message "Error message: Disk has run out of space".
|
||||
|
||||
For Grafana webhooks, you might find it helpful to use the headers `X-Title: Grafana alert: ${title}` and `X-Message: ${message}`.
|
||||
Alternatively, you can include the params in the webhook URL. For example, by
|
||||
appending `?template=yes&title=Grafana alert: ${title}&message=${message}` to the URL.
|
||||
|
||||
## Public topics
|
||||
Obviously all topics on ntfy.sh are public, but there are a few designated topics that are used in examples, and topics
|
||||
that you can use to try out what [authentication and access control](#authentication) looks like.
|
||||
|
||||
@@ -2,6 +2,61 @@
|
||||
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.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
|
||||
|
||||
@@ -110,7 +165,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
|
||||
## 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 suport to encode the `X-Title`,
|
||||
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)
|
||||
@@ -1273,7 +1328,7 @@ Released Dec 28, 2021
|
||||
|
||||
**Features & bug fixes:**
|
||||
|
||||
* [Publish messages via e-mail](ntfy.sh/docs/publish/#e-mail-publishing) #66
|
||||
* [Publish messages via e-mail](publish.md#e-mail-publishing) #66
|
||||
* Server-side work to support [unifiedpush.org](https://unifiedpush.org) #64
|
||||
* Fixing the Santa bug #65
|
||||
|
||||
@@ -1283,11 +1338,11 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
||||
|
||||
## Not released yet
|
||||
|
||||
### ntfy server v2.8.0 (UNRELEASED)
|
||||
### ntfy server v2.9.1 (UNRELEASED)
|
||||
|
||||
**Bug fixes + maintenance:**
|
||||
**Features:**
|
||||
|
||||
* 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)
|
||||
* You can now include a message and/or title template that will be filled with values from a JSON body, great for services that let you specify a webhook URL but do not let you change the webhook body (such as Grafana). ([#724](https://github.com/binwiederhier/ntfy/issues/724), thanks to [@wunter8](https://github.com/wunter8) for implementing)
|
||||
|
||||
### ntfy Android app v1.16.1 (UNRELEASED)
|
||||
|
||||
|
||||
BIN
docs/static/img/cdio-setup.jpg
vendored
Normal file
BIN
docs/static/img/cdio-setup.jpg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
BIN
docs/static/img/pwa-install-macos-safari-add-to-dock.png
vendored
Normal file
BIN
docs/static/img/pwa-install-macos-safari-add-to-dock.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 162 KiB |
@@ -190,9 +190,10 @@ format. Keepalive messages are sent as empty lines.
|
||||
|
||||
## WebSockets
|
||||
You may also subscribe to topics via [WebSockets](https://en.wikipedia.org/wiki/WebSocket), which is also widely
|
||||
supported in many languages. Most notably, WebSockets are natively supported in JavaScript. On the command line,
|
||||
I recommend [websocat](https://github.com/vi/websocat), a fantastic tool similar to `socat` or `curl`, but specifically
|
||||
for WebSockets.
|
||||
supported in many languages. Most notably, WebSockets are natively supported in JavaScript. You may also want to
|
||||
check out the [full example on GitHub](https://github.com/binwiederhier/ntfy/tree/main/examples/web-example-websocket).
|
||||
On the command line, I recommend [websocat](https://github.com/vi/websocat), a fantastic tool similar to `socat`
|
||||
or `curl`, but specifically for WebSockets.
|
||||
|
||||
The WebSockets endpoint is available at `<topic>/ws` and returns messages as JSON objects similar to the
|
||||
[JSON stream endpoint](#subscribe-as-json-stream).
|
||||
|
||||
@@ -10,7 +10,7 @@ to topics via the ntfy CLI. The CLI is included in the same `ntfy` binary that c
|
||||
## Install + configure
|
||||
To install the ntfy CLI, simply **follow the steps outlined on the [install page](../install.md)**. The ntfy server and
|
||||
client are the same binary, so it's all very convenient. After installing, you can (optionally) configure the client
|
||||
by creating `~/.config/ntfy/client.yml` (for the non-root user), or `/etc/ntfy/client.yml` (for the root user). You
|
||||
by creating `~/.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). You
|
||||
can find a [skeleton config](https://github.com/binwiederhier/ntfy/blob/main/client/client.yml) on GitHub.
|
||||
|
||||
If you just want to use [ntfy.sh](https://ntfy.sh), you don't have to change anything. If you **self-host your own server**,
|
||||
|
||||
@@ -26,6 +26,13 @@ app drawer:
|
||||
<a href="../../static/img/pwa-badge.png"><img src="../../static/img/pwa-badge.png"/></a>
|
||||
</div>
|
||||
|
||||
### Safari on macOS
|
||||
To install and register the web app via Safari, click on the Share menu and click Add to Dock. You need to be on macOS Sonoma (14) or higher.
|
||||
|
||||
<div id="pwa-screenshots-safari-desktop" class="screenshots">
|
||||
<a href="../../static/img/pwa-install-macos-safari-add-to-dock.png"><img src="../../static/img/pwa-install-macos-safari-add-to-dock.png"/></a>
|
||||
</div>
|
||||
|
||||
### Chrome/Firefox on Android
|
||||
For Chrome on Android, either click the "Add to Home Screen" banner at the bottom of the screen, or select "Install app"
|
||||
in the menu, and then click "Install" in the popup menu. After installation, you can find the app in your app drawer,
|
||||
|
||||
56
examples/web-example-websocket/example-ws.html
Normal file
56
examples/web-example-websocket/example-ws.html
Normal file
@@ -0,0 +1,56 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>ntfy.sh: WebSocket Example</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<style>
|
||||
body { font-size: 1.2em; line-height: 130%; }
|
||||
#events { font-family: monospace; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>ntfy.sh: WebSocket Example</h1>
|
||||
<p>
|
||||
This is an example showing how to use <a href="https://ntfy.sh">ntfy.sh</a> with
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket">WebSocket</a>.<br/>
|
||||
This example doesn't need a server. You can just save the HTML page and run it from anywhere.
|
||||
</p>
|
||||
<button id="publishButton">Send test notification</button>
|
||||
<p><b>Log:</b></p>
|
||||
<div id="events"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const publishURL = `https://ntfy.sh/example`;
|
||||
const subscribeURL = `wss://ntfy.sh/example/ws`;
|
||||
const events = document.getElementById('events');
|
||||
const websocket = new WebSocket(subscribeURL);
|
||||
|
||||
// Publish button
|
||||
document.getElementById("publishButton").onclick = () => {
|
||||
fetch(publishURL, {
|
||||
method: 'POST', // works with PUT as well, though that sends an OPTIONS request too!
|
||||
body: `It is ${new Date().toString()}. This is a test.`
|
||||
})
|
||||
};
|
||||
|
||||
// Incoming events
|
||||
websocket.onopen = () => {
|
||||
let event = document.createElement('div');
|
||||
event.innerHTML = `WebSocket connected to ${subscribeURL}`;
|
||||
events.appendChild(event);
|
||||
};
|
||||
websocket.onerror = (e) => {
|
||||
let event = document.createElement('div');
|
||||
event.innerHTML = `WebSocket error: Failed to connect to ${subscribeURL}`;
|
||||
events.appendChild(event);
|
||||
};
|
||||
websocket.onmessage = (e) => {
|
||||
let event = document.createElement('div');
|
||||
event.innerHTML = e.data;
|
||||
events.appendChild(event);
|
||||
};
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
105
go.mod
105
go.mod
@@ -1,25 +1,27 @@
|
||||
module heckel.io/ntfy
|
||||
module heckel.io/ntfy/v2
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
toolchain go1.21.3
|
||||
|
||||
require (
|
||||
cloud.google.com/go/firestore v1.12.0 // indirect
|
||||
cloud.google.com/go/storage v1.32.0 // indirect
|
||||
cloud.google.com/go/firestore v1.15.0 // indirect
|
||||
cloud.google.com/go/storage v1.39.1 // indirect
|
||||
github.com/BurntSushi/toml v1.3.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/emersion/go-smtp v0.18.0
|
||||
github.com/gabriel-vasile/mimetype v1.4.2
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/mattn/go-sqlite3 v1.14.17
|
||||
github.com/gabriel-vasile/mimetype v1.4.3
|
||||
github.com/gorilla/websocket v1.5.1
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/olebedev/when v1.0.0
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/urfave/cli/v2 v2.25.7
|
||||
golang.org/x/crypto v0.12.0
|
||||
golang.org/x/oauth2 v0.11.0 // indirect
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/term v0.11.0
|
||||
golang.org/x/time v0.3.0
|
||||
google.golang.org/api v0.138.0
|
||||
github.com/stretchr/testify v1.8.4
|
||||
github.com/urfave/cli/v2 v2.27.1
|
||||
golang.org/x/crypto v0.21.0
|
||||
golang.org/x/oauth2 v0.18.0 // indirect
|
||||
golang.org/x/sync v0.6.0
|
||||
golang.org/x/term v0.18.0
|
||||
golang.org/x/time v0.5.0
|
||||
google.golang.org/api v0.170.0
|
||||
gopkg.in/yaml.v2 v2.4.0
|
||||
)
|
||||
|
||||
@@ -28,53 +30,64 @@ replace github.com/emersion/go-smtp => github.com/emersion/go-smtp v0.17.0 // Pi
|
||||
require github.com/pkg/errors v0.9.1 // indirect
|
||||
|
||||
require (
|
||||
firebase.google.com/go/v4 v4.12.0
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0
|
||||
github.com/prometheus/client_golang v1.16.0
|
||||
firebase.google.com/go/v4 v4.13.0
|
||||
github.com/SherClockHolmes/webpush-go v1.3.0
|
||||
github.com/microcosm-cc/bluemonday v1.0.26
|
||||
github.com/prometheus/client_golang v1.19.0
|
||||
github.com/stripe/stripe-go/v74 v74.30.0
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.110.7 // indirect
|
||||
cloud.google.com/go/compute v1.23.0 // indirect
|
||||
cloud.google.com/go v0.112.1 // indirect
|
||||
cloud.google.com/go/compute v1.25.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.3 // indirect
|
||||
cloud.google.com/go/iam v1.1.2 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.1 // indirect
|
||||
cloud.google.com/go/iam v1.1.7 // indirect
|
||||
cloud.google.com/go/longrunning v0.5.6 // indirect
|
||||
github.com/AlekSi/pointer v1.2.0 // indirect
|
||||
github.com/MicahParks/keyfunc v1.9.0 // indirect
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead // indirect
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-logr/logr v1.4.1 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/s2a-go v0.1.5 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/google/s2a-go v0.1.7 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.4.0 // indirect
|
||||
github.com/prometheus/common v0.44.0 // indirect
|
||||
github.com/prometheus/procfs v0.11.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.50.0 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/stretchr/objx v0.5.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
golang.org/x/net v0.14.0 // indirect
|
||||
golang.org/x/sys v0.11.0 // indirect
|
||||
golang.org/x/text v0.12.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.4 // indirect
|
||||
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect
|
||||
google.golang.org/grpc v1.57.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
|
||||
go.opentelemetry.io/otel v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.24.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.24.0 // indirect
|
||||
golang.org/x/net v0.22.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/appengine v1.6.8 // indirect
|
||||
google.golang.org/appengine/v2 v2.0.5 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect
|
||||
google.golang.org/grpc v1.62.1 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
256
go.sum
256
go.sum
@@ -1,21 +1,20 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o=
|
||||
cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI=
|
||||
cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY=
|
||||
cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM=
|
||||
cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM=
|
||||
cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4=
|
||||
cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU=
|
||||
cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls=
|
||||
cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
|
||||
cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA=
|
||||
cloud.google.com/go/firestore v1.12.0 h1:aeEA/N7DW7+l2u5jtkO8I0qv0D95YwjggD8kUHrTHO4=
|
||||
cloud.google.com/go/firestore v1.12.0/go.mod h1:b38dKhgzlmNNGTNZZwe7ZRFEuRab1Hay3/DBsIGKKy4=
|
||||
cloud.google.com/go/iam v1.1.2 h1:gacbrBdWcoVmGLozRuStX45YKvJtzIjJdAolzUs1sm4=
|
||||
cloud.google.com/go/iam v1.1.2/go.mod h1:A5avdyVL2tCppe4unb0951eI9jreack+RJ0/d+KUZOU=
|
||||
cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tErFDWI=
|
||||
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
|
||||
cloud.google.com/go/storage v1.32.0 h1:5w6DxEGOnktmJHarxAOUywxVW9lbNWIzlzzUltG/3+o=
|
||||
cloud.google.com/go/storage v1.32.0/go.mod h1:Hhh/dogNRGca7IWv1RC2YqEn0c0G77ctA/OxflYkiD8=
|
||||
firebase.google.com/go/v4 v4.12.0 h1:I6dCkcWUMFNkFdWgzlf8SLWecQnKdFgJhMv5fT9l1qI=
|
||||
firebase.google.com/go/v4 v4.12.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
|
||||
cloud.google.com/go/firestore v1.15.0 h1:/k8ppuWOtNuDHt2tsRV42yI21uaGnKDEQnRFeBpbFF8=
|
||||
cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk=
|
||||
cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM=
|
||||
cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA=
|
||||
cloud.google.com/go/longrunning v0.5.6 h1:xAe8+0YaWoCKr9t1+aWe+OeQgN/iJK1fEgZSXmjuEaE=
|
||||
cloud.google.com/go/longrunning v0.5.6/go.mod h1:vUaDrWYOMKRuhiv6JBnn49YxCPz2Ayn9GqyjaBT8/mA=
|
||||
cloud.google.com/go/storage v1.39.1 h1:MvraqHKhogCOTXTlct/9C3K3+Uy2jBmFYb3/Sp6dVtY=
|
||||
cloud.google.com/go/storage v1.39.1/go.mod h1:xK6xZmxZmo+fyP7+DEF6FhNc24/JAe95OLyOHCXFH1o=
|
||||
firebase.google.com/go/v4 v4.13.0 h1:meFz9nvDNh/FDyrEykoAzSfComcQbmnQSjoHrePRqeI=
|
||||
firebase.google.com/go/v4 v4.13.0/go.mod h1:e1/gaR6EnbQfsmTnAMx1hnz+ninJIrrr/RAh59Tpfn8=
|
||||
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
|
||||
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
@@ -23,42 +22,41 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
|
||||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/MicahParks/keyfunc v1.9.0 h1:lhKd5xrFHLNOWrDc4Tyb/Q1AJ4LCzQ48GVJyVIID3+o=
|
||||
github.com/MicahParks/keyfunc v1.9.0/go.mod h1:IdnCilugA0O/99dW+/MkvlyrsX8+L8+x95xuVNtM5jw=
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0 h1:sGv0/ZWCvb1HUH+izLqrb2i68HuqD/0Y+AmGQfyqKJA=
|
||||
github.com/SherClockHolmes/webpush-go v1.2.0/go.mod h1:w6X47YApe/B9wUz2Wh8xukxlyupaxSSEbu6yKJcHN2w=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/SherClockHolmes/webpush-go v1.3.0 h1:CAu3FvEE9QS4drc3iKNgpBWFfGqNthKlZhp5QpYnu6k=
|
||||
github.com/SherClockHolmes/webpush-go v1.3.0/go.mod h1:AxRHmJuYwKGG1PVgYzToik1lphQvDnqFYDqimHvwhIw=
|
||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=
|
||||
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
|
||||
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43 h1:hH4PQfOndHDlpzYfLAAfl63E8Le6F2+EL/cdhlkyRJY=
|
||||
github.com/emersion/go-sasl v0.0.0-20231106173351-e73c9f7bad43/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
|
||||
github.com/emersion/go-smtp v0.17.0 h1:tq90evlrcyqRfE6DSXaWVH54oX6OuZOQECEmhWBMEtI=
|
||||
github.com/emersion/go-smtp v0.17.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
|
||||
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||
@@ -70,21 +68,18 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
@@ -92,132 +87,154 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
|
||||
github.com/google/s2a-go v0.1.5 h1:8IYp3w9nysqv3JH+NJgXJzGbDHzLOTj43BmSkp+O7qg=
|
||||
github.com/google/s2a-go v0.1.5/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A=
|
||||
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
|
||||
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
|
||||
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5 h1:UR4rDjcgpgEnqpIEvkiqTYKBCKLNmlge2eVjoZfySzM=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.5/go.mod h1:RxW0N9901Cko1VOCW3SXCpWP+mlIEkk2tP7jnHy9a3w=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
|
||||
github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA=
|
||||
github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
|
||||
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
|
||||
github.com/olebedev/when v1.0.0 h1:T2DZCj8HxUhOVxcqaLOmzuTr+iZLtMHsZEim7mjIA2w=
|
||||
github.com/olebedev/when v1.0.0/go.mod h1:T0THb4kP9D3NNqlvCwIG4GyUioTAzEhB4RNVzig/43E=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8=
|
||||
github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc=
|
||||
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY=
|
||||
github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU=
|
||||
github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY=
|
||||
github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY=
|
||||
github.com/prometheus/procfs v0.11.1 h1:xRC8Iq1yyca5ypa9n1EZnWZkt7dwcoRPQwX/5gwaUuI=
|
||||
github.com/prometheus/procfs v0.11.1/go.mod h1:eesXgaPo1q7lBpVMoMy0ZOFTth9hBn4W/y0/p/ScXhY=
|
||||
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||
github.com/prometheus/common v0.50.0 h1:YSZE6aa9+luNa2da6/Tik0q0A5AbR+U003TItK57CPQ=
|
||||
github.com/prometheus/common v0.50.0/go.mod h1:wHFBCEVWVmHMUpg7pYcOm2QUR/ocQdYSJVQJKnHc3xQ=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
|
||||
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
|
||||
github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
|
||||
github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||
github.com/tidwall/gjson v1.17.1 h1:wlYEnwqAHgzmhNUFfw7Xalt2JzQvsMx2Se4PcoFCT/U=
|
||||
github.com/tidwall/gjson v1.17.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/urfave/cli/v2 v2.27.1 h1:8xSQ6szndafKVRmfyeUMxkNUJQMjL1F2zmsZ+qHpfho=
|
||||
github.com/urfave/cli/v2 v2.27.1/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
|
||||
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913/go.mod h1:4aEEwZQutDLsQv2Deui4iYQ6DWTxR14g6m8Wv88+Xqk=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
|
||||
go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
|
||||
go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
|
||||
golang.org/x/crypto v0.0.0-20190131182504-b8fe1690c613/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg=
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
|
||||
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
|
||||
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
|
||||
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
|
||||
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw=
|
||||
go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc=
|
||||
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
|
||||
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
|
||||
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.11.0 h1:vPL4xzxBM4niKCW6g9whtaWVXTJf1U5e4aZxxFx/gbU=
|
||||
golang.org/x/oauth2 v0.11.0/go.mod h1:LdF7O/8bLR/qWK9DrpXmbHLTouvRHK0SgJl0GmDBchk=
|
||||
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
|
||||
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
|
||||
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
|
||||
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
@@ -225,39 +242,35 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/api v0.138.0 h1:K/tVp05MxNVbHShRw9m7e9VJGdagNeTdMzqPH7AUqr0=
|
||||
google.golang.org/api v0.138.0/go.mod h1:4xyob8CxC+0GChNBvEUAk8VBKNvYOTWM9T3v3UfRxuY=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
|
||||
google.golang.org/api v0.170.0 h1:zMaruDePM88zxZBG+NG8+reALO2rfLhe/JShitLyT48=
|
||||
google.golang.org/api v0.170.0/go.mod h1:/xql9M2btF85xac/VAm4PsLMTLVGUOpq4BE9R8jyNy8=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
|
||||
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/appengine/v2 v2.0.4 h1:aAAPYixP9EfTJjNO6F46afaxp+jfzb0VgwVjMeLBtF4=
|
||||
google.golang.org/appengine/v2 v2.0.4/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
|
||||
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
|
||||
google.golang.org/appengine/v2 v2.0.5 h1:4C+F3Cd3L2nWEfSmFEZDPjQvDwL8T0YCeZBysZifP3k=
|
||||
google.golang.org/appengine/v2 v2.0.5/go.mod h1:WoEXGoXNfa0mLvaH5sV3ZSGXwVmy8yf7Z1JKf3J3wLI=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 h1:Iveh6tGCJkHAjJgEqUQYGDGgbwmhjoAOz8kO/ajxefY=
|
||||
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 h1:WGq4lvB/mlicysM/dUT3SBvijH4D3sm/Ny1A4wmt2CI=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878/go.mod h1:KjSP20unUpOx5kyQUFa7k4OJg0qeJ7DEZflGDu2p6Bk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 h1:lv6/DhyiFFGsmzxbsUUTOkN29II+zeWHxvT8Lpdxsv0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M=
|
||||
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237 h1:PgNlNSx2Nq2/j4juYzQBG0/Zdr+WP4z5N01Vk4VYBCY=
|
||||
google.golang.org/genproto v0.0.0-20240318140521-94a12d6c2237/go.mod h1:9sVD8c25Af3p0rGs7S7LLsxWKFiJt/65LdSyqXBkX/Y=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
|
||||
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
|
||||
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
|
||||
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
|
||||
google.golang.org/grpc v1.57.0 h1:kfzNeI/klCGD2YPMUlaGNT3pxvYfga7smW3Vth8Zsiw=
|
||||
google.golang.org/grpc v1.57.0/go.mod h1:Sd+9RMTACXwmub0zcNY2c4arhtrbBYD1AUHI/dt16Mo=
|
||||
google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
|
||||
google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
@@ -270,12 +283,11 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
|
||||
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -3,7 +3,7 @@ package log
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
2
main.go
2
main.go
@@ -3,7 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/urfave/cli/v2"
|
||||
"heckel.io/ntfy/cmd"
|
||||
"heckel.io/ntfy/v2/cmd"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
56
mkdocs.yml
56
mkdocs.yml
@@ -64,40 +64,40 @@ markdown_extensions:
|
||||
- attr_list
|
||||
- md_in_html
|
||||
- pymdownx.emoji:
|
||||
emoji_index: !!python/name:materialx.emoji.twemoji
|
||||
emoji_generator: !!python/name:materialx.emoji.to_svg
|
||||
emoji_index: !!python/name:material.extensions.emoji.twemoji
|
||||
emoji_generator: !!python/name:material.extensions.emoji.to_svg
|
||||
|
||||
hooks:
|
||||
- docs/hooks.py
|
||||
|
||||
plugins:
|
||||
- search
|
||||
- minify:
|
||||
minify_html: true
|
||||
- mkdocs-simple-hooks:
|
||||
hooks:
|
||||
on_post_build: "docs.hooks:copy_fonts"
|
||||
|
||||
nav:
|
||||
- "Getting started": index.md
|
||||
- "Publishing":
|
||||
- "Sending messages": publish.md
|
||||
- "Subscribing":
|
||||
- "From your phone": subscribe/phone.md
|
||||
- "From the Web app": subscribe/web.md
|
||||
- "From the Desktop": subscribe/pwa.md
|
||||
- "From the CLI": subscribe/cli.md
|
||||
- "Using the API": subscribe/api.md
|
||||
- "Self-hosting":
|
||||
- "Installation": install.md
|
||||
- "Configuration": config.md
|
||||
- "Other things":
|
||||
- "FAQs": faq.md
|
||||
- "Examples": examples.md
|
||||
- "Integrations + projects": integrations.md
|
||||
- "Release notes": releases.md
|
||||
- "Emojis 🥳 🎉": emojis.md
|
||||
- "Troubleshooting": troubleshooting.md
|
||||
- "Known issues": known-issues.md
|
||||
- "Deprecation notices": deprecations.md
|
||||
- "Development": develop.md
|
||||
- "Privacy policy": privacy.md
|
||||
- "Getting started": index.md
|
||||
- "Publishing":
|
||||
- "Sending messages": publish.md
|
||||
- "Subscribing":
|
||||
- "From your phone": subscribe/phone.md
|
||||
- "From the Web app": subscribe/web.md
|
||||
- "From the Desktop": subscribe/pwa.md
|
||||
- "From the CLI": subscribe/cli.md
|
||||
- "Using the API": subscribe/api.md
|
||||
- "Self-hosting":
|
||||
- "Installation": install.md
|
||||
- "Configuration": config.md
|
||||
- "Other things":
|
||||
- "FAQs": faq.md
|
||||
- "Examples": examples.md
|
||||
- "Integrations + projects": integrations.md
|
||||
- "Release notes": releases.md
|
||||
- "Emojis 🥳 🎉": emojis.md
|
||||
- "Troubleshooting": troubleshooting.md
|
||||
- "Known issues": known-issues.md
|
||||
- "Deprecation notices": deprecations.md
|
||||
- "Development": develop.md
|
||||
- "Privacy policy": privacy.md
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# The documentation uses 'mkdocs', which is written in Python
|
||||
mkdocs-material
|
||||
mkdocs-minify-plugin
|
||||
mkdocs-simple-hooks
|
||||
|
||||
@@ -25,9 +25,9 @@ elif [[ "$1" == *.md ]]; then
|
||||
|
||||
<!-- This file was generated by scripts/emoji-convert.sh -->
|
||||
|
||||
You can [tag messages](../publish/#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
||||
You can [tag messages](publish.md#tags-emojis) with emojis 🥳 🎉 and other relevant strings. Matching tags are automatically
|
||||
converted to emojis. This is a reference of all supported emojis. To learn more about the feature, please refer to the
|
||||
[tagging and emojis page](../publish/#tags-emojis).
|
||||
[tagging and emojis page](publish.md#tags-emojis).
|
||||
|
||||
<table class=\"remove-md-box emoji-table\"><tr>
|
||||
" > "$1"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -5,18 +5,19 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
)
|
||||
|
||||
// Defines default config settings (excluding limits, see below)
|
||||
const (
|
||||
DefaultListenHTTP = ":80"
|
||||
DefaultCacheDuration = 12 * time.Hour
|
||||
DefaultCacheBatchTimeout = time.Duration(0)
|
||||
DefaultKeepaliveInterval = 45 * time.Second // Not too frequently to save battery (Android read timeout used to be 77s!)
|
||||
DefaultManagerInterval = time.Minute
|
||||
DefaultDelayedSenderInterval = 10 * time.Second
|
||||
DefaultMinDelay = 10 * time.Second
|
||||
DefaultMaxDelay = 3 * 24 * time.Hour
|
||||
DefaultMessageDelayMin = 10 * time.Second
|
||||
DefaultMessageDelayMax = 3 * 24 * time.Hour
|
||||
DefaultFirebaseKeepaliveInterval = 3 * time.Hour // ~control topic (Android), not too frequently to save battery
|
||||
DefaultFirebasePollInterval = 20 * time.Minute // ~poll topic (iOS), max. 2-3 times per hour (see docs)
|
||||
DefaultFirebaseQuotaExceededPenaltyDuration = 10 * time.Minute // Time that over-users are locked out of Firebase if it returns "quota exceeded"
|
||||
@@ -34,7 +35,7 @@ const (
|
||||
// - total topic limit: max number of topics overall
|
||||
// - various attachment limits
|
||||
const (
|
||||
DefaultMessageLengthLimit = 4096 // Bytes
|
||||
DefaultMessageSizeLimit = 4096 // Bytes; note that FCM/APNS have a limit of ~4 KB for the entire message
|
||||
DefaultTotalTopicLimit = 15000
|
||||
DefaultAttachmentTotalSizeLimit = int64(5 * 1024 * 1024 * 1024) // 5 GB
|
||||
DefaultAttachmentFileSizeLimit = int64(15 * 1024 * 1024) // 15 MB
|
||||
@@ -122,9 +123,9 @@ type Config struct {
|
||||
MetricsEnable bool
|
||||
MetricsListenHTTP string
|
||||
ProfileListenHTTP string
|
||||
MessageLimit int
|
||||
MinDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
MessageDelayMin time.Duration
|
||||
MessageDelayMax time.Duration
|
||||
MessageSizeLimit int
|
||||
TotalTopicLimit int
|
||||
TotalAttachmentSizeLimit int64
|
||||
VisitorSubscriptionLimit int
|
||||
@@ -211,9 +212,9 @@ func NewConfig() *Config {
|
||||
TwilioPhoneNumber: "",
|
||||
TwilioVerifyBaseURL: "https://verify.twilio.com", // Override for tests
|
||||
TwilioVerifyService: "",
|
||||
MessageLimit: DefaultMessageLengthLimit,
|
||||
MinDelay: DefaultMinDelay,
|
||||
MaxDelay: DefaultMaxDelay,
|
||||
MessageSizeLimit: DefaultMessageSizeLimit,
|
||||
MessageDelayMin: DefaultMessageDelayMin,
|
||||
MessageDelayMax: DefaultMessageDelayMax,
|
||||
TotalTopicLimit: DefaultTotalTopicLimit,
|
||||
TotalAttachmentSizeLimit: 0,
|
||||
VisitorSubscriptionLimit: DefaultVisitorSubscriptionLimit,
|
||||
|
||||
@@ -2,7 +2,7 @@ package server_test
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package server
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -117,6 +117,8 @@ var (
|
||||
errHTTPBadRequestWebPushSubscriptionInvalid = &errHTTP{40038, http.StatusBadRequest, "invalid request: web push payload malformed", "", nil}
|
||||
errHTTPBadRequestWebPushEndpointUnknown = &errHTTP{40039, http.StatusBadRequest, "invalid request: web push endpoint unknown", "", nil}
|
||||
errHTTPBadRequestWebPushTopicCountTooHigh = &errHTTP{40040, http.StatusBadRequest, "invalid request: too many web push topic subscriptions", "", nil}
|
||||
errHTTPBadRequestTemplatedMessageTooLarge = &errHTTP{40041, http.StatusBadRequest, "invalid request: message or title is too large after replacing template", "", nil}
|
||||
errHTTPBadRequestTemplatedMessageNotJSON = &errHTTP{40042, http.StatusBadRequest, "invalid request: message body must be JSON if templating is enabled", "", nil}
|
||||
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
|
||||
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}
|
||||
|
||||
@@ -3,8 +3,8 @@ package server
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/gorilla/websocket"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3" // SQLite driver
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
133
server/server.go
133
server/server.go
@@ -29,10 +29,11 @@ import (
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/tidwall/gjson"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
// Server is the main server, providing the UI and API for ntfy
|
||||
@@ -109,6 +110,8 @@ var (
|
||||
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
||||
urlRegex = regexp.MustCompile(`^https?://`)
|
||||
phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`)
|
||||
templateVarRegex = regexp.MustCompile(`\${([^}]+)}`)
|
||||
templateVarFormat = "${%s}"
|
||||
|
||||
//go:embed site
|
||||
webFs embed.FS
|
||||
@@ -123,12 +126,12 @@ var (
|
||||
|
||||
const (
|
||||
firebaseControlTopic = "~control" // See Android if changed
|
||||
firebasePollTopic = "~poll" // See iOS if changed
|
||||
firebasePollTopic = "~poll" // See iOS if changed (DISABLED for now)
|
||||
emptyMessageBody = "triggered" // Used if message body is empty
|
||||
newMessageBody = "New message" // Used in poll requests as generic message
|
||||
defaultAttachmentMessage = "You received a file: %s" // Used if message body is empty, and there is an attachment
|
||||
encodingBase64 = "base64" // Used mainly for binary UnifiedPush messages
|
||||
jsonBodyBytesLimit = 16384 // Max number of bytes for a JSON request body
|
||||
jsonBodyBytesLimit = 32768 // Max number of bytes for a request bodys (unless MessageLimit is higher)
|
||||
unifiedPushTopicPrefix = "up" // Temporarily, we rate limit all "up*" topics based on the subscriber
|
||||
unifiedPushTopicLength = 14 // Length of UnifiedPush topics, including the "up" part
|
||||
messagesHistoryMax = 10 // Number of message count values to keep in memory
|
||||
@@ -673,7 +676,7 @@ func (s *Server) handleFile(w http.ResponseWriter, r *http.Request, v *visitor)
|
||||
// - avoid abuse (e.g. 1 uploader, 1k downloaders)
|
||||
// - and also uses the higher bandwidth limits of a paying user
|
||||
m, err := s.messageCache.Message(messageID)
|
||||
if err == errMessageNotFound {
|
||||
if errors.Is(err, errMessageNotFound) {
|
||||
if s.config.CacheBatchTimeout > 0 {
|
||||
// Strange edge case: If we immediately after upload request the file (the web app does this for images),
|
||||
// and messages are persisted asynchronously, retry fetching from the database
|
||||
@@ -733,18 +736,18 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := util.Peek(r.Body, s.config.MessageLimit)
|
||||
body, err := util.Peek(r.Body, s.config.MessageSizeLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m := newDefaultMessage(t.ID, "")
|
||||
cache, firebase, email, call, unifiedpush, e := s.parsePublishParams(r, m)
|
||||
cache, firebase, email, call, template, unifiedpush, e := s.parsePublishParams(r, m)
|
||||
if e != nil {
|
||||
return nil, e.With(t)
|
||||
}
|
||||
if unifiedpush && s.config.VisitorSubscriberRateLimiting && t.RateVisitor() == nil {
|
||||
// UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting (see
|
||||
// Rate-Topics header). The 5xx response is because some app servers (in particular Mastodon) will remove
|
||||
// UnifiedPush clients must subscribe before publishing to allow proper subscriber-based rate limiting.
|
||||
// The 5xx response is because some app servers (in particular Mastodon) will remove
|
||||
// the subscription as invalid if any 400-499 code (except 429/408) is returned.
|
||||
// See https://github.com/mastodon/mastodon/blob/730bb3e211a84a2f30e3e2bbeae3f77149824a68/app/workers/web/push_notification_worker.rb#L35-L46
|
||||
return nil, errHTTPInsufficientStorageUnifiedPush.With(t)
|
||||
@@ -769,7 +772,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
|
||||
if cache {
|
||||
m.Expires = time.Unix(m.Time, 0).Add(v.Limits().MessageExpiryDuration).Unix()
|
||||
}
|
||||
if err := s.handlePublishBody(r, v, m, body, unifiedpush); err != nil {
|
||||
if err := s.handlePublishBody(r, v, m, body, template, unifiedpush); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if m.Message == "" {
|
||||
@@ -872,7 +875,7 @@ func (s *Server) sendToFirebase(v *visitor, m *message) {
|
||||
logvm(v, m).Tag(tagFirebase).Debug("Publishing to Firebase")
|
||||
if err := s.firebaseClient.Send(v, m); err != nil {
|
||||
minc(metricFirebasePublishedFailure)
|
||||
if err == errFirebaseTemporarilyBanned {
|
||||
if errors.Is(err, errFirebaseTemporarilyBanned) {
|
||||
logvm(v, m).Tag(tagFirebase).Err(err).Debug("Unable to publish to Firebase: %v", err.Error())
|
||||
} else {
|
||||
logvm(v, m).Tag(tagFirebase).Err(err).Warn("Unable to publish to Firebase: %v", err.Error())
|
||||
@@ -924,7 +927,7 @@ func (s *Server) forwardPollRequest(v *visitor, m *message) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, unifiedpush bool, err *errHTTP) {
|
||||
func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, firebase bool, email, call string, template bool, unifiedpush bool, err *errHTTP) {
|
||||
cache = readBoolParam(r, true, "x-cache", "cache")
|
||||
firebase = readBoolParam(r, true, "x-firebase", "firebase")
|
||||
m.Title = readParam(r, "x-title", "title", "t")
|
||||
@@ -940,7 +943,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
}
|
||||
if attach != "" {
|
||||
if !urlRegex.MatchString(attach) {
|
||||
return false, false, "", "", false, errHTTPBadRequestAttachmentURLInvalid
|
||||
return false, false, "", "", false, false, errHTTPBadRequestAttachmentURLInvalid
|
||||
}
|
||||
m.Attachment.URL = attach
|
||||
if m.Attachment.Name == "" {
|
||||
@@ -958,19 +961,19 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
}
|
||||
if icon != "" {
|
||||
if !urlRegex.MatchString(icon) {
|
||||
return false, false, "", "", false, errHTTPBadRequestIconURLInvalid
|
||||
return false, false, "", "", false, false, errHTTPBadRequestIconURLInvalid
|
||||
}
|
||||
m.Icon = icon
|
||||
}
|
||||
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
|
||||
if s.smtpSender == nil && email != "" {
|
||||
return false, false, "", "", false, errHTTPBadRequestEmailDisabled
|
||||
return false, false, "", "", false, false, errHTTPBadRequestEmailDisabled
|
||||
}
|
||||
call = readParam(r, "x-call", "call")
|
||||
if call != "" && (s.config.TwilioAccount == "" || s.userManager == nil) {
|
||||
return false, false, "", "", false, errHTTPBadRequestPhoneCallsDisabled
|
||||
return false, false, "", "", false, false, errHTTPBadRequestPhoneCallsDisabled
|
||||
} else if call != "" && !isBoolValue(call) && !phoneNumberRegex.MatchString(call) {
|
||||
return false, false, "", "", false, errHTTPBadRequestPhoneNumberInvalid
|
||||
return false, false, "", "", false, false, errHTTPBadRequestPhoneNumberInvalid
|
||||
}
|
||||
messageStr := strings.ReplaceAll(readParam(r, "x-message", "message", "m"), "\\n", "\n")
|
||||
if messageStr != "" {
|
||||
@@ -979,27 +982,27 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
var e error
|
||||
m.Priority, e = util.ParsePriority(readParam(r, "x-priority", "priority", "prio", "p"))
|
||||
if e != nil {
|
||||
return false, false, "", "", false, errHTTPBadRequestPriorityInvalid
|
||||
return false, false, "", "", false, false, errHTTPBadRequestPriorityInvalid
|
||||
}
|
||||
m.Tags = readCommaSeparatedParam(r, "x-tags", "tags", "tag", "ta")
|
||||
delayStr := readParam(r, "x-delay", "delay", "x-at", "at", "x-in", "in")
|
||||
if delayStr != "" {
|
||||
if !cache {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayNoCache
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCache
|
||||
}
|
||||
if email != "" {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayNoEmail // we cannot store the email address (yet)
|
||||
}
|
||||
if call != "" {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayNoCall // we cannot store the phone number (yet)
|
||||
}
|
||||
delay, err := util.ParseFutureTime(delayStr, time.Now())
|
||||
if err != nil {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayCannotParse
|
||||
} else if delay.Unix() < time.Now().Add(s.config.MinDelay).Unix() {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayTooSmall
|
||||
} else if delay.Unix() > time.Now().Add(s.config.MaxDelay).Unix() {
|
||||
return false, false, "", "", false, errHTTPBadRequestDelayTooLarge
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayCannotParse
|
||||
} else if delay.Unix() < time.Now().Add(s.config.MessageDelayMin).Unix() {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayTooSmall
|
||||
} else if delay.Unix() > time.Now().Add(s.config.MessageDelayMax).Unix() {
|
||||
return false, false, "", "", false, false, errHTTPBadRequestDelayTooLarge
|
||||
}
|
||||
m.Time = delay.Unix()
|
||||
}
|
||||
@@ -1007,13 +1010,14 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
if actionsStr != "" {
|
||||
m.Actions, e = parseActions(actionsStr)
|
||||
if e != nil {
|
||||
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
|
||||
return false, false, "", "", false, false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
|
||||
}
|
||||
}
|
||||
contentType, markdown := readParam(r, "content-type", "content_type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
|
||||
if markdown || strings.ToLower(contentType) == "text/markdown" {
|
||||
m.ContentType = "text/markdown"
|
||||
}
|
||||
template = readBoolParam(r, false, "x-template", "template", "tpl")
|
||||
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
|
||||
if unifiedpush {
|
||||
firebase = false
|
||||
@@ -1025,7 +1029,7 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
cache = false
|
||||
email = ""
|
||||
}
|
||||
return cache, firebase, email, call, unifiedpush, nil
|
||||
return cache, firebase, email, call, template, unifiedpush, nil
|
||||
}
|
||||
|
||||
// handlePublishBody consumes the PUT/POST body and decides whether the body is an attachment or the message.
|
||||
@@ -1033,16 +1037,18 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
|
||||
// 1. curl -X POST -H "Poll: 1234" ntfy.sh/...
|
||||
// If a message is flagged as poll request, the body does not matter and is discarded
|
||||
// 2. curl -T somebinarydata.bin "ntfy.sh/mytopic?up=1"
|
||||
// If body is binary, encode as base64, if not do not encode
|
||||
// If UnifiedPush is enabled, encode as base64 if body is binary, and do not trim
|
||||
// 3. curl -H "Attach: http://example.com/file.jpg" ntfy.sh/mytopic
|
||||
// Body must be a message, because we attached an external URL
|
||||
// 4. curl -T short.txt -H "Filename: short.txt" ntfy.sh/mytopic
|
||||
// Body must be attachment, because we passed a filename
|
||||
// 5. curl -T file.txt ntfy.sh/mytopic
|
||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||
// 5. curl -H "Template: yes" -T file.txt ntfy.sh/mytopic
|
||||
// If templating is enabled, read up to 32k and treat message body as JSON
|
||||
// 6. curl -T file.txt ntfy.sh/mytopic
|
||||
// If file.txt is > message limit, treat it as an attachment
|
||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, unifiedpush bool) error {
|
||||
// If file.txt is <= 4096 (message limit) and valid UTF-8, treat it as a message
|
||||
// 7. curl -T file.txt ntfy.sh/mytopic
|
||||
// In all other cases, mostly if file.txt is > message limit, treat it as an attachment
|
||||
func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser, template, unifiedpush bool) error {
|
||||
if m.Event == pollRequestEvent { // Case 1
|
||||
return s.handleBodyDiscard(body)
|
||||
} else if unifiedpush {
|
||||
@@ -1051,10 +1057,12 @@ func (s *Server) handlePublishBody(r *http.Request, v *visitor, m *message, body
|
||||
return s.handleBodyAsTextMessage(m, body) // Case 3
|
||||
} else if m.Attachment != nil && m.Attachment.Name != "" {
|
||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 4
|
||||
} else if template {
|
||||
return s.handleBodyAsTemplatedTextMessage(m, body) // Case 5
|
||||
} else if !body.LimitReached && utf8.Valid(body.PeekedBytes) {
|
||||
return s.handleBodyAsTextMessage(m, body) // Case 5
|
||||
return s.handleBodyAsTextMessage(m, body) // Case 6
|
||||
}
|
||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 6
|
||||
return s.handleBodyAsAttachment(r, v, m, body) // Case 7
|
||||
}
|
||||
|
||||
func (s *Server) handleBodyDiscard(body *util.PeekedReadCloser) error {
|
||||
@@ -1086,6 +1094,35 @@ func (s *Server) handleBodyAsTextMessage(m *message, body *util.PeekedReadCloser
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) handleBodyAsTemplatedTextMessage(m *message, body *util.PeekedReadCloser) error {
|
||||
body, err := util.Peek(body, jsonBodyBytesLimit)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if body.LimitReached {
|
||||
return errHTTPEntityTooLargeJSONBody
|
||||
}
|
||||
peekedBody := strings.TrimSpace(string(body.PeekedBytes))
|
||||
if !gjson.Valid(peekedBody) {
|
||||
return errHTTPBadRequestTemplatedMessageNotJSON
|
||||
}
|
||||
m.Message = replaceGJSONTemplate(m.Message, peekedBody)
|
||||
m.Title = replaceGJSONTemplate(m.Title, peekedBody)
|
||||
if len(m.Message) > s.config.MessageSizeLimit {
|
||||
return errHTTPBadRequestTemplatedMessageTooLarge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceGJSONTemplate(template string, source string) string {
|
||||
matches := templateVarRegex.FindAllStringSubmatch(template, -1)
|
||||
for _, m := range matches {
|
||||
if result := gjson.Get(source, m[1]); result.Exists() {
|
||||
template = strings.ReplaceAll(template, fmt.Sprintf(templateVarFormat, m[1]), result.String())
|
||||
}
|
||||
}
|
||||
return template
|
||||
}
|
||||
|
||||
func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message, body *util.PeekedReadCloser) error {
|
||||
if s.fileCache == nil || s.config.BaseURL == "" || s.config.AttachmentCacheDir == "" {
|
||||
return errHTTPBadRequestAttachmentsDisallowed.With(m)
|
||||
@@ -1128,7 +1165,7 @@ func (s *Server) handleBodyAsAttachment(r *http.Request, v *visitor, m *message,
|
||||
util.NewFixedLimiter(vinfo.Stats.AttachmentTotalSizeRemaining),
|
||||
}
|
||||
m.Attachment.Size, err = s.fileCache.Write(m.ID, body, limiters...)
|
||||
if err == util.ErrLimitReached {
|
||||
if errors.Is(err, util.ErrLimitReached) {
|
||||
return errHTTPEntityTooLargeAttachment.With(m)
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -1182,7 +1219,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
|
||||
poll, since, scheduled, filters, err := parseSubscribeParams(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1212,7 +1249,7 @@ func (s *Server) handleSubscribeHTTP(w http.ResponseWriter, r *http.Request, v *
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
|
||||
if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
||||
@@ -1278,7 +1315,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
poll, since, scheduled, filters, rateTopics, err := parseSubscribeParams(r)
|
||||
poll, since, scheduled, filters, err := parseSubscribeParams(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1364,7 +1401,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||
}
|
||||
return conn.WriteJSON(msg)
|
||||
}
|
||||
if err := s.maybeSetRateVisitors(r, v, topics, rateTopics); err != nil {
|
||||
if err := s.maybeSetRateVisitors(r, v, topics); err != nil {
|
||||
return err
|
||||
}
|
||||
w.Header().Set("Access-Control-Allow-Origin", s.config.AccessControlAllowOrigin) // CORS, allow cross-origin requests
|
||||
@@ -1397,7 +1434,7 @@ func (s *Server) handleSubscribeWS(w http.ResponseWriter, r *http.Request, v *vi
|
||||
return err
|
||||
}
|
||||
|
||||
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, rateTopics []string, err error) {
|
||||
func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, scheduled bool, filters *queryFilter, err error) {
|
||||
poll = readBoolParam(r, false, "x-poll", "poll", "po")
|
||||
scheduled = readBoolParam(r, false, "x-scheduled", "scheduled", "sched")
|
||||
since, err = parseSince(r, poll)
|
||||
@@ -1408,7 +1445,6 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
rateTopics = readCommaSeparatedParam(r, "x-rate-topics", "rate-topics")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1420,9 +1456,8 @@ func parseSubscribeParams(r *http.Request) (poll bool, since sinceMarker, schedu
|
||||
// - or the topic is reserved, and v.user is the owner
|
||||
// - or the topic is not reserved, and v.user has write access
|
||||
//
|
||||
// Note: This TEMPORARILY also registers all topics starting with "up" (= UnifiedPush). This is to ease the transition
|
||||
// until the Android app will send the "Rate-Topics" header.
|
||||
func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic, rateTopics []string) error {
|
||||
// This only applies to UnifiedPush topics ("up...").
|
||||
func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*topic) error {
|
||||
// Bail out if not enabled
|
||||
if !s.config.VisitorSubscriberRateLimiting {
|
||||
return nil
|
||||
@@ -1431,7 +1466,7 @@ func (s *Server) maybeSetRateVisitors(r *http.Request, v *visitor, topics []*top
|
||||
// Make a list of topics that we'll actually set the RateVisitor on
|
||||
eligibleRateTopics := make([]*topic, 0)
|
||||
for _, t := range topics {
|
||||
if (strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength) || util.Contains(rateTopics, t.ID) {
|
||||
if strings.HasPrefix(t.ID, unifiedPushTopicPrefix) && len(t.ID) == unifiedPushTopicLength {
|
||||
eligibleRateTopics = append(eligibleRateTopics, t)
|
||||
}
|
||||
}
|
||||
@@ -1756,7 +1791,7 @@ func (s *Server) sendDelayedMessage(v *visitor, m *message) error {
|
||||
// before passing it on to the next handler. This is meant to be used in combination with handlePublish.
|
||||
func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageLimit*2, false) // 2x to account for JSON format overhead
|
||||
m, err := readJSONWithLimit[publishMessage](r.Body, s.config.MessageSizeLimit*2, false) // 2x to account for JSON format overhead
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1814,7 +1849,7 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
|
||||
|
||||
func (s *Server) transformMatrixJSON(next handleFunc) handleFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageLimit)
|
||||
newRequest, err := newRequestFromMatrixJSON(r, s.config.BaseURL, s.config.MessageSizeLimit)
|
||||
if err != nil {
|
||||
logvr(v, r).Tag(tagMatrix).Err(err).Debug("Invalid Matrix request")
|
||||
if e, ok := err.(*errMatrixPushkeyRejected); ok {
|
||||
|
||||
@@ -236,6 +236,16 @@
|
||||
# upstream-base-url:
|
||||
# upstream-access-token:
|
||||
|
||||
# Configures message-specific limits
|
||||
#
|
||||
# - message-size-limit defines the max size of a message body. Please note message sizes >4K are NOT RECOMMENDED,
|
||||
# and largely untested. 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.
|
||||
#
|
||||
# message-size-limit: "4k"
|
||||
# message-delay-limit: "3d"
|
||||
|
||||
# Rate limiting: Total number of topics before the server rejects new topics.
|
||||
#
|
||||
# global-topic-limit: 15000
|
||||
@@ -277,15 +287,14 @@
|
||||
|
||||
# Rate limiting: Enable subscriber-based rate limiting (mostly used for UnifiedPush)
|
||||
#
|
||||
# If enabled, subscribers may opt to have published messages counted against their own rate limits, as opposed
|
||||
# to the publisher's rate limits. This is especially useful to increase the amount of messages that high-volume
|
||||
# publishers (e.g. Matrix/Mastodon servers) are allowed to send.
|
||||
# If subscriber-based rate limiting is enabled, messages published on UnifiedPush topics** (topics starting with "up")
|
||||
# will be counted towards the "rate visitor" of the topic. A "rate visitor" is the first subscriber to the topic.
|
||||
#
|
||||
# Once enabled, a client may send a "Rate-Topics: <topic1>,<topic2>,..." header when subscribing to topics via
|
||||
# HTTP stream, or websockets, thereby registering itself as the "rate visitor", i.e. the visitor whose rate limits
|
||||
# to use when publishing on this topic. Note: Setting the rate visitor requires READ-WRITE permission on 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.
|
||||
#
|
||||
# UnifiedPush only: If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
|
||||
# If this setting is enabled, publishing to UnifiedPush topics will lead to a HTTP 507 response if
|
||||
# no "rate visitor" has been previously registered. This is to avoid burning the publisher's "visitor-message-daily-limit".
|
||||
#
|
||||
# visitor-subscriber-rate-limiting: false
|
||||
|
||||
@@ -2,9 +2,9 @@ package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strings"
|
||||
|
||||
@@ -3,9 +3,9 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
@@ -718,11 +718,11 @@ func TestAccount_Reservation_Delete_Messages_And_Attachments(t *testing.T) {
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "starter",
|
||||
MessageLimit: 10,
|
||||
MessageSizeLimit: 10,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "pro",
|
||||
MessageLimit: 20,
|
||||
MessageSizeLimit: 20,
|
||||
}))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "starter"))
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"heckel.io/ntfy/user"
|
||||
"errors"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -45,7 +46,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
||||
return errHTTPBadRequest.Wrap("username invalid, or password missing")
|
||||
}
|
||||
u, err := s.userManager.User(req.Username)
|
||||
if err != nil && err != user.ErrUserNotFound {
|
||||
if err != nil && !errors.Is(err, user.ErrUserNotFound) {
|
||||
return err
|
||||
} else if u != nil {
|
||||
return errHTTPConflictUserExists
|
||||
@@ -53,7 +54,7 @@ func (s *Server) handleUsersAdd(w http.ResponseWriter, r *http.Request, v *visit
|
||||
var tier *user.Tier
|
||||
if req.Tier != "" {
|
||||
tier, err = s.userManager.Tier(req.Tier)
|
||||
if err == user.ErrTierNotFound {
|
||||
if errors.Is(err, user.ErrTierNotFound) {
|
||||
return errHTTPBadRequestTierInvalid
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -76,7 +77,7 @@ func (s *Server) handleUsersDelete(w http.ResponseWriter, r *http.Request, v *vi
|
||||
return err
|
||||
}
|
||||
u, err := s.userManager.User(req.Username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return errHTTPBadRequestUserNotFound
|
||||
} else if err != nil {
|
||||
return err
|
||||
@@ -98,7 +99,7 @@ func (s *Server) handleAccessAllow(w http.ResponseWriter, r *http.Request, v *vi
|
||||
return err
|
||||
}
|
||||
_, err = s.userManager.User(req.Username)
|
||||
if err == user.ErrUserNotFound {
|
||||
if errors.Is(err, user.ErrUserNotFound) {
|
||||
return errHTTPBadRequestUserNotFound
|
||||
} else if err != nil {
|
||||
return err
|
||||
|
||||
@@ -2,8 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"firebase.google.com/go/v4/messaging"
|
||||
"fmt"
|
||||
"google.golang.org/api/option"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -3,7 +3,7 @@ package server
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
type contextKey int
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
"github.com/stripe/stripe-go/v74/price"
|
||||
"github.com/stripe/stripe-go/v74/subscription"
|
||||
"github.com/stripe/stripe-go/v74/webhook"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
||||
@@ -6,8 +6,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stripe/stripe-go/v74"
|
||||
"golang.org/x/time/rate"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/netip"
|
||||
"path/filepath"
|
||||
|
||||
@@ -3,13 +3,13 @@ package server
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -329,6 +329,27 @@ func TestServer_PublishPriority(t *testing.T) {
|
||||
require.Equal(t, 40007, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func TestServer_PublishPriority_SpecialHTTPHeader(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||
"Priority": "u=4",
|
||||
"X-Priority": "5",
|
||||
})
|
||||
require.Equal(t, 5, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "POST", "/mytopic?priority=4", "test", map[string]string{
|
||||
"Priority": "u=9",
|
||||
})
|
||||
require.Equal(t, 4, toMessage(t, response.Body.String()).Priority)
|
||||
|
||||
response = request(t, s, "POST", "/mytopic", "test", map[string]string{
|
||||
"p": "2",
|
||||
"priority": "u=9, i",
|
||||
})
|
||||
require.Equal(t, 2, toMessage(t, response.Body.String()).Priority)
|
||||
}
|
||||
|
||||
func TestServer_PublishGETOnlyOneTopic(t *testing.T) {
|
||||
// This tests a bug that allowed publishing topics with a comma in the name (no ticket)
|
||||
|
||||
@@ -491,6 +512,8 @@ func TestServer_PublishAtAndPrune(t *testing.T) {
|
||||
messages := toMessages(t, response.Body.String())
|
||||
require.Equal(t, 1, len(messages)) // Not affected by pruning
|
||||
require.Equal(t, "a message", messages[0].Message)
|
||||
|
||||
time.Sleep(time.Second) // FIXME CI failing not sure why
|
||||
}
|
||||
|
||||
func TestServer_PublishAndMultiPoll(t *testing.T) {
|
||||
@@ -1323,9 +1346,7 @@ func TestServer_PublishUnifiedPushBinary_AndPoll(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Register a UnifiedPush subscriber
|
||||
response := request(t, s, "GET", "/up123456789012/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "up123456789012",
|
||||
})
|
||||
response := request(t, s, "GET", "/up123456789012/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Publish message to topic
|
||||
@@ -1356,9 +1377,7 @@ func TestServer_PublishUnifiedPushBinary_Truncated(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Register a UnifiedPush subscriber
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "mytopic",
|
||||
})
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Publish message to topic
|
||||
@@ -1377,9 +1396,7 @@ func TestServer_PublishUnifiedPushText(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
// Register a UnifiedPush subscriber
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "mytopic",
|
||||
})
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
// Publish UnifiedPush text message
|
||||
@@ -1411,9 +1428,7 @@ func TestServer_MatrixGateway_Discovery_Failure_Unconfigured(t *testing.T) {
|
||||
func TestServer_MatrixGateway_Push_Success(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "mytopic", // Register first!
|
||||
})
|
||||
response := request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
|
||||
notification := `{"notification":{"devices":[{"pushkey":"http://127.0.0.1:12345/mytopic?up=1"}]}}`
|
||||
@@ -2243,16 +2258,14 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
|
||||
c.VisitorSubscriberRateLimiting = true
|
||||
s := newTestServer(t, c)
|
||||
|
||||
// "Register" visitor 1.2.3.4 to topic "subscriber1topic" as a rate limit visitor
|
||||
// "Register" visitor 1.2.3.4 to topic "upAAAAAAAAAAAA" as a rate limit visitor
|
||||
subscriber1Fn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
}
|
||||
rr := request(t, s, "GET", "/subscriber1topic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "subscriber1topic",
|
||||
}, subscriber1Fn)
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriber1Fn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
require.Equal(t, "1.2.3.4", s.topics["subscriber1topic"].rateVisitor.ip.String())
|
||||
require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
|
||||
|
||||
// "Register" visitor 8.7.7.1 to topic "up012345678912" as a rate limit visitor (implicitly via topic name)
|
||||
subscriber2Fn := func(r *http.Request) {
|
||||
@@ -2266,10 +2279,10 @@ func TestServer_SubscriberRateLimiting_Success(t *testing.T) {
|
||||
// Publish 2 messages to "subscriber1topic" as visitor 9.9.9.9. It'd be 3 normally, but the
|
||||
// GET request before is also counted towards the request limiter.
|
||||
for i := 0; i < 2; i++ {
|
||||
rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
|
||||
rr := request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
}
|
||||
rr = request(t, s, "PUT", "/subscriber1topic", "some message", nil)
|
||||
rr = request(t, s, "PUT", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 429, rr.Code)
|
||||
|
||||
// Publish another 2 messages to "up012345678912" as visitor 9.9.9.9
|
||||
@@ -2302,14 +2315,12 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
|
||||
// Subscriber rate limiting is disabled!
|
||||
|
||||
// Registering visitor 1.2.3.4 to topic has no effect
|
||||
rr := request(t, s, "GET", "/subscriber1topic/json?poll=1", "", map[string]string{
|
||||
"Rate-Topics": "subscriber1topic",
|
||||
}, func(r *http.Request) {
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
require.Nil(t, s.topics["subscriber1topic"].rateVisitor)
|
||||
require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
|
||||
// Registering visitor 8.7.7.1 to topic has no effect
|
||||
rr = request(t, s, "GET", "/up012345678912/json?poll=1", "", nil, func(r *http.Request) {
|
||||
@@ -2319,7 +2330,7 @@ func TestServer_SubscriberRateLimiting_NotEnabled_Failed(t *testing.T) {
|
||||
require.Equal(t, "", rr.Body.String())
|
||||
require.Nil(t, s.topics["up012345678912"].rateVisitor)
|
||||
|
||||
// Publish 3 messages to "subscriber1topic" as visitor 9.9.9.9
|
||||
// Publish 3 messages to "upAAAAAAAAAAAA" as visitor 9.9.9.9
|
||||
for i := 0; i < 3; i++ {
|
||||
rr := request(t, s, "PUT", "/subscriber1topic", "some message", nil)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
@@ -2392,80 +2403,30 @@ func TestServer_SubscriberRateLimiting_VisitorExpiration(t *testing.T) {
|
||||
subscriberFn := func(r *http.Request) {
|
||||
r.RemoteAddr = "1.2.3.4"
|
||||
}
|
||||
rr := request(t, s, "GET", "/mytopic/json?poll=1", "", map[string]string{
|
||||
"rate-topics": "mytopic",
|
||||
}, subscriberFn)
|
||||
rr := request(t, s, "GET", "/upAAAAAAAAAAAA/json?poll=1", "", nil, subscriberFn)
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "1.2.3.4", s.topics["mytopic"].rateVisitor.ip.String())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["mytopic"].rateVisitor)
|
||||
require.Equal(t, "1.2.3.4", s.topics["upAAAAAAAAAAAA"].rateVisitor.ip.String())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
|
||||
// Publish message, observe rate visitor tokens being decreased
|
||||
response := request(t, s, "POST", "/mytopic", "some message", nil)
|
||||
response := request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, int64(0), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
|
||||
require.Equal(t, int64(1), s.topics["mytopic"].rateVisitor.messagesLimiter.Value())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["mytopic"].rateVisitor)
|
||||
require.Equal(t, int64(1), s.topics["upAAAAAAAAAAAA"].rateVisitor.messagesLimiter.Value())
|
||||
require.Equal(t, s.visitors["ip:1.2.3.4"], s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
|
||||
// Expire visitor
|
||||
s.visitors["ip:1.2.3.4"].seen = time.Now().Add(-1 * 25 * time.Hour)
|
||||
s.pruneVisitors()
|
||||
|
||||
// Publish message again, observe that rateVisitor is not used anymore and is reset
|
||||
response = request(t, s, "POST", "/mytopic", "some message", nil)
|
||||
response = request(t, s, "POST", "/upAAAAAAAAAAAA", "some message", nil)
|
||||
require.Equal(t, 200, response.Code)
|
||||
require.Equal(t, int64(1), s.visitors["ip:9.9.9.9"].messagesLimiter.Value())
|
||||
require.Nil(t, s.topics["mytopic"].rateVisitor)
|
||||
require.Nil(t, s.topics["upAAAAAAAAAAAA"].rateVisitor)
|
||||
require.Nil(t, s.visitors["ip:1.2.3.4"])
|
||||
}
|
||||
|
||||
func TestServer_SubscriberRateLimiting_ProtectedTopics(t *testing.T) {
|
||||
c := newTestConfigWithAuthFile(t)
|
||||
c.AuthDefault = user.PermissionDenyAll
|
||||
c.VisitorSubscriberRateLimiting = true
|
||||
s := newTestServer(t, c)
|
||||
|
||||
// Create some ACLs
|
||||
require.Nil(t, s.userManager.AddTier(&user.Tier{
|
||||
Code: "test",
|
||||
MessageLimit: 5,
|
||||
}))
|
||||
require.Nil(t, s.userManager.AddUser("ben", "ben", user.RoleUser))
|
||||
require.Nil(t, s.userManager.ChangeTier("ben", "test"))
|
||||
require.Nil(t, s.userManager.AllowAccess("ben", "announcements", user.PermissionReadWrite))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "announcements", user.PermissionRead))
|
||||
require.Nil(t, s.userManager.AllowAccess(user.Everyone, "public_topic", user.PermissionReadWrite))
|
||||
|
||||
require.Nil(t, s.userManager.AddUser("phil", "phil", user.RoleUser))
|
||||
require.Nil(t, s.userManager.ChangeTier("phil", "test"))
|
||||
require.Nil(t, s.userManager.AddReservation("phil", "reserved-for-phil", user.PermissionReadWrite))
|
||||
|
||||
// Set rate visitor as user "phil" on topic
|
||||
// - "reserved-for-phil": Allowed, because I am the owner
|
||||
// - "public_topic": Allowed, because it has read-write permissions for everyone
|
||||
// - "announcements": NOT allowed, because it has read-only permissions for everyone
|
||||
rr := request(t, s, "GET", "/reserved-for-phil,public_topic,announcements/json?poll=1", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("phil", "phil"),
|
||||
"Rate-Topics": "reserved-for-phil,public_topic,announcements",
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "phil", s.topics["reserved-for-phil"].rateVisitor.user.Name)
|
||||
require.Equal(t, "phil", s.topics["public_topic"].rateVisitor.user.Name)
|
||||
require.Nil(t, s.topics["announcements"].rateVisitor)
|
||||
|
||||
// Set rate visitor as user "ben" on topic
|
||||
// - "reserved-for-phil": NOT allowed, because I am not the owner
|
||||
// - "public_topic": Allowed, because it has read-write permissions for everyone
|
||||
// - "announcements": Allowed, because I have read-write permissions
|
||||
rr = request(t, s, "GET", "/reserved-for-phil,public_topic,announcements/json?poll=1", "", map[string]string{
|
||||
"Authorization": util.BasicAuth("ben", "ben"),
|
||||
"Rate-Topics": "reserved-for-phil,public_topic,announcements",
|
||||
})
|
||||
require.Equal(t, 200, rr.Code)
|
||||
require.Equal(t, "phil", s.topics["reserved-for-phil"].rateVisitor.user.Name)
|
||||
require.Equal(t, "ben", s.topics["public_topic"].rateVisitor.user.Name)
|
||||
require.Equal(t, "ben", s.topics["announcements"].rateVisitor.user.Name)
|
||||
}
|
||||
|
||||
func TestServer_SubscriberRateLimiting_ProtectedTopics_WithDefaultReadWrite(t *testing.T) {
|
||||
c := newTestConfigWithAuthFile(t)
|
||||
c.AuthDefault = user.PermissionReadWrite
|
||||
@@ -2664,6 +2625,188 @@ func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||
"X-Message": "${foo}",
|
||||
"X-Title": "${nested.title}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "bar", m.Message)
|
||||
require.Equal(t, "here", m.Title)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_RepeatPlaceholder(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||
"Message": "${foo} is ${foo}",
|
||||
"Title": "${nested.title} is ${nested.title}",
|
||||
"Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "bar is bar", m.Message)
|
||||
require.Equal(t, "here is here", m.Title)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_JSONBody(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"}}"}`
|
||||
response := request(t, s, "PUT", "/", body, map[string]string{
|
||||
"m": "${foo}",
|
||||
"t": "${nested.title}",
|
||||
"tpl": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "bar", m.Message)
|
||||
require.Equal(t, "here", m.Title)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_MalformedJSONBody(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
body := `{"topic": "mytopic", "message": "{\"foo\":\"bar\",\"nested\":{\"title\":\"here\"INVALID"}`
|
||||
response := request(t, s, "PUT", "/", body, map[string]string{
|
||||
"X-Message": "${foo}",
|
||||
"X-Title": "${nested.title}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40042, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_PlaceholderTypo(t *testing.T) {
|
||||
t.Parallel()
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||
"X-Message": "${food}",
|
||||
"X-Title": "${nested.titl}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "${food}", m.Message)
|
||||
require.Equal(t, "${nested.titl}", m.Title)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_MultiplePlaceholders(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||
"X-Message": "${foo} is ${nested.title}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "bar is here", m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_NestedPlaceholders(t *testing.T) {
|
||||
// not intended to work recursively for now
|
||||
// i.e., ${${nested.bar}} should NOT evaluate to ${foo} and then to "bar"
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}}`, map[string]string{
|
||||
"X-Message": "${${nested.bar}}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "${${nested.bar}}", m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_NestedPlaceholdersFunky(t *testing.T) {
|
||||
// The above example can technically work
|
||||
// ${${nested.bar}} would be interpreted as a nested GJSON path with key "${nested" then key "bar"
|
||||
// so you would probably expect the output to be "works!", BUT the second } in the placeholder is not
|
||||
// included by the regex, so it is still there after replacing the placeholder, thus giving you "works!}"
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here","bar":"foo"}, "${nested":{"bar":"works!"}}`, map[string]string{
|
||||
"X-Message": "${${nested.bar}}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "works!}", m.Message)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_FancyGJSON(t *testing.T) {
|
||||
s := newTestServer(t, newTestConfig(t))
|
||||
jsonBody := `{"foo": "bar", "errors": [{"level": "severe", "url": "https://severe1.com"},{"level": "warning", "url": "https://warning.com"},{"level": "severe", "url": "https://severe2.com"}]}`
|
||||
response := request(t, s, "PUT", "/mytopic", jsonBody, map[string]string{
|
||||
"X-Message": `${errors.#(level=="severe")#.url}`,
|
||||
"X-Title": `${errors.#(level=="severe")#|#} Severe Errors`,
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, `["https://severe1.com","https://severe2.com"]`, m.Message)
|
||||
require.Equal(t, `2 Severe Errors`, m.Title)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageOK(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
c.MessageSizeLimit = 25 // 25 < len(HTTP body) < 32k, and len(m.Message) < 25
|
||||
s := newTestServer(t, c)
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"bar", "nested":{"title":"here"}}`, map[string]string{
|
||||
"X-Message": "${foo}",
|
||||
"X-Title": "${nested.title}",
|
||||
"X-Template": "yes",
|
||||
})
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "bar", m.Message)
|
||||
require.Equal(t, "here", m.Title)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_ExceedMessageSize_TemplatedMessageTooLong(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
c.MessageSizeLimit = 21 // 21 < len(HTTP body) < 32k, but !len(m.Message) < 21
|
||||
s := newTestServer(t, c)
|
||||
response := request(t, s, "PUT", "/mytopic", `{"foo":"This is a long message"}`, map[string]string{
|
||||
"X-Message": "${foo}",
|
||||
"X-Template": "1",
|
||||
})
|
||||
|
||||
require.Equal(t, 400, response.Code)
|
||||
require.Equal(t, 40041, toHTTPError(t, response.Body.String()).Code)
|
||||
}
|
||||
|
||||
func TestServer_MessageTemplate_Grafana(t *testing.T) {
|
||||
c := newTestConfig(t)
|
||||
s := newTestServer(t, c)
|
||||
body := `{"receiver":"ntfy\\.example\\.com/alerts","status":"resolved","alerts":[{"status":"resolved","labels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"annotations":{"summary":"15m load average too high"},"startsAt":"2024-03-15T02:28:00Z","endsAt":"2024-03-15T02:42:00Z","generatorURL":"localhost:3000/alerting/grafana/NW9oDw-4z/view","fingerprint":"becbfb94bd81ef48","silenceURL":"localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter","dashboardURL":"","panelURL":"","values":{"B":18.98211314475876,"C":0},"valueString":"[ var='B' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=18.98211314475876 ], [ var='C' labels={__name__=node_load15, instance=10.108.0.2:9100, job=node-exporter} value=0 ]"}],"groupLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts"},"commonLabels":{"alertname":"Load avg 15m too high","grafana_folder":"Node alerts","instance":"10.108.0.2:9100","job":"node-exporter"},"commonAnnotations":{"summary":"15m load average too high"},"externalURL":"localhost:3000/","version":"1","groupKey":"{}:{alertname=\"Load avg 15m too high\", grafana_folder=\"Node alerts\"}","truncatedAlerts":0,"orgId":1,"title":"[RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)","state":"ok","message":"**Resolved**\n\nValue: B=18.98211314475876, C=0\nLabels:\n - alertname = Load avg 15m too high\n - grafana_folder = Node alerts\n - instance = 10.108.0.2:9100\n - job = node-exporter\nAnnotations:\n - summary = 15m load average too high\nSource: localhost:3000/alerting/grafana/NW9oDw-4z/view\nSilence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter\n"}`
|
||||
response := request(t, s, "PUT", "/mytopic?tpl=yes&title=Grafana+alert:+${title}&message=${message}", body, nil)
|
||||
|
||||
require.Equal(t, 200, response.Code)
|
||||
m := toMessage(t, response.Body.String())
|
||||
require.Equal(t, "Grafana alert: [RESOLVED] Load avg 15m too high Node alerts (10.108.0.2:9100 node-exporter)", m.Title)
|
||||
require.Equal(t, `**Resolved**
|
||||
|
||||
Value: B=18.98211314475876, C=0
|
||||
Labels:
|
||||
- alertname = Load avg 15m too high
|
||||
- grafana_folder = Node alerts
|
||||
- instance = 10.108.0.2:9100
|
||||
- job = node-exporter
|
||||
Annotations:
|
||||
- summary = 15m load average too high
|
||||
Source: localhost:3000/alerting/grafana/NW9oDw-4z/view
|
||||
Silence: localhost:3000/alerting/silence/new?alertmanager=grafana&matcher=alertname%3DLoad+avg+15m+too+high&matcher=grafana_folder%3DNode+alerts&matcher=instance%3D10.108.0.2%3A9100&matcher=job%3Dnode-exporter
|
||||
`, m.Message)
|
||||
}
|
||||
|
||||
func newTestConfig(t *testing.T) *Config {
|
||||
conf := NewConfig()
|
||||
conf.BaseURL = "http://127.0.0.1:12345"
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
@@ -2,8 +2,8 @@ package server
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/SherClockHolmes/webpush-go"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
type mailer interface {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/emersion/go-smtp"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -27,6 +29,11 @@ var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
)
|
||||
|
||||
var (
|
||||
onlySpacesRegex = regexp.MustCompile(`(?m)^\s+$`)
|
||||
consecutiveNewLinesRegex = regexp.MustCompile(`\n{3,}`)
|
||||
)
|
||||
|
||||
const (
|
||||
maxMultipartDepth = 2
|
||||
)
|
||||
@@ -143,8 +150,8 @@ func (s *smtpSession) Data(r io.Reader) error {
|
||||
return err
|
||||
}
|
||||
body = strings.TrimSpace(body)
|
||||
if len(body) > conf.MessageLimit {
|
||||
body = body[:conf.MessageLimit]
|
||||
if len(body) > conf.MessageSizeLimit {
|
||||
body = body[:conf.MessageSizeLimit]
|
||||
}
|
||||
m := newDefaultMessage(s.topic, body)
|
||||
subject := strings.TrimSpace(msg.Header.Get("Subject"))
|
||||
@@ -232,37 +239,66 @@ func readMailBody(body io.Reader, header mail.Header) (string, error) {
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if strings.ToLower(contentType) == "text/plain" {
|
||||
return readPlainTextMailBody(body, header.Get("Content-Transfer-Encoding"))
|
||||
} else if strings.HasPrefix(strings.ToLower(contentType), "multipart/") {
|
||||
return readMultipartMailBody(body, params, 0)
|
||||
canonicalContentType := strings.ToLower(contentType)
|
||||
if canonicalContentType == "text/plain" || canonicalContentType == "text/html" {
|
||||
return readTextMailBody(body, canonicalContentType, header.Get("Content-Transfer-Encoding"))
|
||||
} else if strings.HasPrefix(canonicalContentType, "multipart/") {
|
||||
return readMultipartMailBody(body, params)
|
||||
}
|
||||
return "", errUnsupportedContentType
|
||||
}
|
||||
|
||||
func readMultipartMailBody(body io.Reader, params map[string]string, depth int) (string, error) {
|
||||
func readMultipartMailBody(body io.Reader, params map[string]string) (string, error) {
|
||||
parts := make(map[string]string)
|
||||
if err := readMultipartMailBodyParts(body, params, 0, parts); err != nil && err != io.EOF {
|
||||
return "", err
|
||||
} else if s, ok := parts["text/plain"]; ok {
|
||||
return s, nil
|
||||
} else if s, ok := parts["text/html"]; ok {
|
||||
return s, nil
|
||||
}
|
||||
return "", io.EOF
|
||||
}
|
||||
|
||||
func readMultipartMailBodyParts(body io.Reader, params map[string]string, depth int, parts map[string]string) error {
|
||||
if depth >= maxMultipartDepth {
|
||||
return "", errMultipartNestedTooDeep
|
||||
return errMultipartNestedTooDeep
|
||||
}
|
||||
mr := multipart.NewReader(body, params["boundary"])
|
||||
for {
|
||||
part, err := mr.NextPart()
|
||||
if err != nil { // may be io.EOF
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
partContentType, partParams, err := mime.ParseMediaType(part.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
return err
|
||||
}
|
||||
if strings.ToLower(partContentType) == "text/plain" {
|
||||
return readPlainTextMailBody(part, part.Header.Get("Content-Transfer-Encoding"))
|
||||
canonicalPartContentType := strings.ToLower(partContentType)
|
||||
if canonicalPartContentType == "text/plain" || canonicalPartContentType == "text/html" {
|
||||
s, err := readTextMailBody(part, canonicalPartContentType, part.Header.Get("Content-Transfer-Encoding"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts[canonicalPartContentType] = s
|
||||
} else if strings.HasPrefix(strings.ToLower(partContentType), "multipart/") {
|
||||
return readMultipartMailBody(part, partParams, depth+1)
|
||||
if err := readMultipartMailBodyParts(part, partParams, depth+1, parts); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// Continue with next part
|
||||
}
|
||||
}
|
||||
|
||||
func readTextMailBody(reader io.Reader, contentType, transferEncoding string) (string, error) {
|
||||
if contentType == "text/plain" {
|
||||
return readPlainTextMailBody(reader, transferEncoding)
|
||||
} else if contentType == "text/html" {
|
||||
return readHTMLMailBody(reader, transferEncoding)
|
||||
}
|
||||
return "", fmt.Errorf("unsupported content type: %s", contentType)
|
||||
}
|
||||
|
||||
func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, error) {
|
||||
if strings.ToLower(transferEncoding) == "base64" {
|
||||
reader = base64.NewDecoder(base64.StdEncoding, reader)
|
||||
@@ -275,3 +311,21 @@ func readPlainTextMailBody(reader io.Reader, transferEncoding string) (string, e
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
func readHTMLMailBody(reader io.Reader, transferEncoding string) (string, error) {
|
||||
body, err := readPlainTextMailBody(reader, transferEncoding)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
stripped := bluemonday.
|
||||
StrictPolicy().
|
||||
AddSpaceWhenStrippingTag(true).
|
||||
Sanitize(body)
|
||||
return removeExtraEmptyLines(stripped), nil
|
||||
}
|
||||
|
||||
func removeExtraEmptyLines(s string) string {
|
||||
s = onlySpacesRegex.ReplaceAllString(s, "")
|
||||
s = consecutiveNewLinesRegex.ReplaceAllString(s, "\n\n")
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -568,6 +568,803 @@ L0VOIj4KClRoaXMgaXMgYSB0ZXN0IG1lc3NhZ2UgZnJvbSBUcnVlTkFTIENPUkUuCg==
|
||||
writeAndReadUntilLine(t, email, c, scanner, "554 5.0.0 Error: transaction failed, blame it on the weather: multipart message nested too deep")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_HTMLEmail(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: test@mydomain.me
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
Message-Id: <51610934ss4.mmailer@fritz.box>
|
||||
From: <email@email.com>
|
||||
To: <email@email.com>,
|
||||
<ntfy-subjectatntfy@ntfy.sh>
|
||||
Date: Thu, 30 Mar 2023 02:56:53 +0000
|
||||
Subject: A HTML email
|
||||
Mime-Version: 1.0
|
||||
Content-Type: text/html;
|
||||
charset="utf-8"
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<=21DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Alerttitle</title>
|
||||
<meta http-equiv=3D"content-type" content=3D"text/html;charset=3Dutf-8"/>
|
||||
</head>
|
||||
<body style=3D"color: =23000000; background-color: =23f0eee6;">
|
||||
<table width=3D"100%" align=3D"center" style=3D"border:solid 2px =23eeeeee=
|
||||
; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td>
|
||||
<table style=3D"border-collapse: collapse;">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td style=3D"background: =23FFFFFF;">
|
||||
<table style=3D"color: =23FFFFFF; background-color: =23006EC0; border-coll=
|
||||
apse: collapse;">
|
||||
<tr>
|
||||
<td style=3D"width: 1000px; text-align: center; font-size: 18pt; font-fami=
|
||||
ly: Arial, Helvetica, sans-serif; padding: 10px;">
|
||||
|
||||
|
||||
headertext of table
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td style=3D"padding: 10px 20px; background: =23FFFFFF;">
|
||||
<table style=3D"border-collapse: collapse;">
|
||||
<tr>
|
||||
<td style=3D"width: 940px; font-size: 13pt; font-family: Arial, Helvetica,=
|
||||
sans-serif; text-align: left;">
|
||||
" Very important information about a change in your
|
||||
home automation setup
|
||||
|
||||
Now the light is on
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
|
||||
<tr>
|
||||
<td style=3D"padding: 10px 20px; background: =23FFFFFF;">
|
||||
<table>
|
||||
<tr>
|
||||
<td style=3D"width: 960px; font-size: 10pt; font-family: Arial, Helvetica,=
|
||||
sans-serif; text-align: left;">
|
||||
<hr />
|
||||
If you don't want to receive this message anymore, stop the push
|
||||
services in your <a href=3D"https:fritzbox" target=3D"_=
|
||||
blank">FRITZ=21Box</a>=2E<br />
|
||||
Here you can see the active push services: "System > Push Service"=2E
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<table style=3D"color: =23FFFFFF; background-color: =23006EC0;">
|
||||
<tr>
|
||||
<td style=3D"width: 1000px; font-size: 10pt; font-family: Arial, Helvetica=
|
||||
, sans-serif; text-align: center; padding: 10px;">
|
||||
This mail has ben sent by your <a style=3D"color: =23FFFFFF;" href=3D"https:=
|
||||
//fritzbox" target=3D"_blank">FRITZ=21Box</a=
|
||||
> automatically=2E
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
.
|
||||
`
|
||||
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "A HTML email", r.Header.Get("Title"))
|
||||
expected := `headertext of table
|
||||
|
||||
" Very important information about a change in your
|
||||
home automation setup
|
||||
|
||||
Now the light is on
|
||||
|
||||
If you don't want to receive this message anymore, stop the push
|
||||
services in your FRITZ!Box .
|
||||
Here you can see the active push services: "System > Push Service".
|
||||
|
||||
This mail has ben sent by your FRITZ!Box automatically.`
|
||||
require.Equal(t, expected, readAll(t, r.Body))
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
const spamEmail = `
|
||||
EHLO example.com
|
||||
MAIL FROM: test@mydomain.me
|
||||
RCPT TO: ntfy-mytopic@ntfy.sh
|
||||
DATA
|
||||
Delivered-To: somebody@gmail.com
|
||||
Received: by 2002:a05:651c:1248:b0:2bf:c263:285 with SMTP id h8csp1096496ljh;
|
||||
Mon, 30 Oct 2023 06:23:08 -0700 (PDT)
|
||||
X-Google-Smtp-Source: AGHT+IFsB3WqbwbeefbeefbeefbeefbeefiXRNDHnIy2xBeaYHZCM3EC8DfPv55qDtgq9djTeBCF
|
||||
X-Received: by 2002:a05:6808:147:b0:3af:66e5:5d3c with SMTP id h7-20020a056808014700b003af66e55d3cmr11662458oie.26.1698672188132;
|
||||
Mon, 30 Oct 2023 06:23:08 -0700 (PDT)
|
||||
ARC-Seal: i=1; a=rsa-sha256; t=1698672188; cv=none;
|
||||
d=google.com; s=arc-20160816;
|
||||
b=XM96KvnTbr4h6bqrTPTuuDNXmFCr9Be/HvVhu+UsSQjP9RxPk0wDTPUPZ/HWIJs52y
|
||||
beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef
|
||||
BUmQ==
|
||||
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
|
||||
h=list-unsubscribe-post:list-unsubscribe:mime-version:subject:to
|
||||
:reply-to:from:date:message-id:dkim-signature:dkim-signature;
|
||||
bh=BERwBIp6fBgrZePFKQjyNMmgPkcnq1Zy1jPO8M0T4Ok=;
|
||||
fh=+kTCcNpX22TOI/SVSLygnrDqWeUt4zW7QKiv0TOVSGs=;
|
||||
b=lyIBRuOxPOTY2s36OqP7M7awlBKd4t5PX9mJOEJB0eTnTZqML+cplrXUIg2ZTlAAi9
|
||||
beeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef
|
||||
tgVQ==
|
||||
ARC-Authentication-Results: i=1; mx.google.com;
|
||||
dkim=pass header.i=@spamspam.com header.s=2020294246 header.b=G8y6xmtK;
|
||||
dkim=pass header.i=@auth.ccsend.com header.s=1000073432 header.b=ht8IksVK;
|
||||
spf=pass (google.com: domain of aigxeklyirlg+dvwkrmsgua==_1133104752381_suqcukvbeeynm/owplvdba==@in.constantcontact.com designates 208.75.123.226 as permitted sender) smtp.mailfrom="AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com";
|
||||
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=spamspam.com
|
||||
Return-Path: <AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com>
|
||||
Received: from ccm30.constantcontact.com (ccm30.constantcontact.com. [208.75.123.226])
|
||||
by mx.google.com with ESMTPS id h2-20020a05620a21c200b0076eeed38118si5450962qka.131.2023.10.30.06.23.07
|
||||
for <somebody@gmail.com>
|
||||
(version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);
|
||||
Mon, 30 Oct 2023 06:23:08 -0700 (PDT)
|
||||
Received-SPF: pass (google.com: domain of aigxeklyirlg+dvwkrmsgua==_1133104752381_suqcukvbeeynm/owplvdba==@in.constantcontact.com designates 208.75.123.226 as permitted sender) client-ip=208.75.123.226;
|
||||
Authentication-Results: mx.google.com;
|
||||
dkim=pass header.i=@spamspam.com header.s=2020294246 header.b=G8y6xmtK;
|
||||
dkim=pass header.i=@auth.ccsend.com header.s=1000073432 header.b=ht8IksVK;
|
||||
spf=pass (google.com: domain of aigxeklyirlg+dvwkrmsgua==_1133104752381_suqcukvbeeynm/owplvdba==@in.constantcontact.com designates 208.75.123.226 as permitted sender) smtp.mailfrom="AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com";
|
||||
dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=spamspam.com
|
||||
Return-Path: <AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com>
|
||||
Received: from [10.252.0.3] ([10.252.0.3:53254] helo=p2-jbemailsyndicator12.ctct.net) by 10.249.225.20 (envelope-from <AigXeKlyIRLG+DvWkRMsGUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com>) (ecelerity 4.3.1.999 r(:)) with ESMTP id A4/82-60517-B3EAF356; Mon, 30 Oct 2023 09:23:07 -0400
|
||||
DKIM-Signature: v=1; q=dns/txt; a=rsa-sha256; c=relaxed/relaxed; s=2020294246; d=spamspam.com; h=date:mime-version:subject:X-Feedback-ID:X-250ok-CID:message-id:from:reply-to:list-unsubscribe:list-unsubscribe-post:to; bh=BERwBIp6fBgrZePFKQjyNMmgPkcnq1Zy1jPO8M0T4Ok=; b=G8y6xmtKv8asfEXA9o8dP+6foQjclo6j5sFREYVIJBbj5YJ5tqoiv5B04/qoRkoTBFDhmjt+BUua7AqDgPSnwbP2iPSA4fTJehnHhut1PyVUp/9vqSYlhxQehfdhma8tPg8ArKfYIKmfKJwKRaQBU0JHCaB1m+5LNQQX3UjkxAg=
|
||||
DKIM-Signature: v=1; q=dns/txt; a=rsa-sha256; c=relaxed/relaxed; s=1000073432; d=auth.ccsend.com; h=date:mime-version:subject:X-Feedback-ID:X-250ok-CID:message-id:from:reply-to:list-unsubscribe:list-unsubscribe-post:to; bh=BERwBIp6fBgrZePFKQjyNMmgPkcnq1Zy1jPO8M0T4Ok=; b=ht8IksVKYY/Kb3dUERWoeW4eVdYjKL6F4PEoIZOhfFXor6XAIbPnd3A/CPmbmoqFZjnKh5OdcUy1N5qEoj8w1Q3TmN8/ySQkqrlrmSDSZIHZMY7Qp9/TJrqUe4RMFOO1KKIN6Y0vGP1+dWe98msMAHwvi2qMjG9aEKLfFr2JUTQ=
|
||||
Message-ID: <1140728754828.1133104752381.1941549819.0.260913JL.2002@synd.ccsend.com>
|
||||
Date: Mon, 30 Oct 2023 09:23:07 -0400 (EDT)
|
||||
From: spamspam Loan Servicing <marklake@spamspam.com>
|
||||
Reply-To: marklake@spamspam.com
|
||||
To: somebody@gmail.com
|
||||
Subject: Buying a home? You deserve the confidence of Pre-Approval
|
||||
MIME-Version: 1.0
|
||||
Content-Type: multipart/alternative; boundary="----=_Part_75055660_144854819.1698672187348"
|
||||
List-Unsubscribe: <https://visitor.constantcontact.com/do?p=un&m=beefbeefbeef>
|
||||
List-Unsubscribe-Post: List-Unsubscribe=One-Click
|
||||
X-Campaign-Activity-ID: 8a05de2a-5c88-44b1-be0e-f5a444cb0650
|
||||
X-250ok-CID: 8a05de2a-5c88-44b1-be0e-f5a444cb0650
|
||||
X-Channel-ID: b1441c50-a541-11ec-a79b-fa163e5bc304
|
||||
X-Return-Path-Hint: AbeefbeefbeefbeefbeefUA==_1133104752381_sUQcUKVBEeynm/oWPlvDBA==@in.constantcontact.com
|
||||
X-Roving-Campaignid: 1140728754811
|
||||
X-Roving-Id: 1133104752381.1111111111
|
||||
X-Feedback-ID: b1441c50-a541-11ec-beef-beefbeefbeefbeef5de2a-5c88-44b1-be0e-f5a444cb0650:1133104752381:CTCT
|
||||
X-CTCT-ID: b13a9586-a541-11ec-beef-beefbeefbeef
|
||||
|
||||
------=_Part_75055660_144854819.1698672187348
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
When you're buying a home, Pre-Approval gives you confidence you're in the =
|
||||
right price range and shows sellers you mean business. xxxxxxxxx SELLING or=
|
||||
BUYING? Call: 844-590-2275 Get Your Homebuying PRE-APPROVAL IN 24-HOURS* G=
|
||||
et Pre-Approved When you're buying a home, Pre-Approval gives you confidenc=
|
||||
e you're in the right price range and shows sellers you mean business. xxx=
|
||||
xxxxxxGet Pre-Approved today! Click or Call to Get Pre-Approved 844-590-227=
|
||||
5 Get Pre-Approved nmlsconsumeraccess.org/ *The 24 hour timeframe is for mo=
|
||||
st approvals, however if additional information is needed or a request is o=
|
||||
n a holiday, the time for preapproval may be greater than 24 hours. This em=
|
||||
ail is for informational purposes only and is not an offer, loan approval o=
|
||||
r loan commitment. Mortgage rates are subject to change without notice. Som=
|
||||
e terms and restrictions may apply to certain loan programs. Refinancing ex=
|
||||
isting loans may result in total finance charges being higher over the life=
|
||||
of the loan, reduction in payments may partially reflect a longer loan ter=
|
||||
m. This information is provided as guidance and illustrative purposes only =
|
||||
and does not constitute legal or financial advice. We are not liable or bou=
|
||||
nd legally for any answers provided to any user for our process or position=
|
||||
on an issue. This information may change from time to time and at any time=
|
||||
without notification. The most current information will be updated periodi=
|
||||
cally and posted in the online forum. spamspam Loan Servicing, LLC. NMLS#39=
|
||||
1521. nmlsconsumeraccess.org. You are receiving this information as a curre=
|
||||
nt loan customer with spamspam Loan Servicing, LLC. Not licensed for lendin=
|
||||
g activities in any of the U.S. territories. Not authorized to originate lo=
|
||||
ans in the State of New York. Licensed by the Dept. of Financial Protection=
|
||||
and Innovation under the California Residential Mortgage .Lending Act #413=
|
||||
1216. This email was sent to somebody@gmail.com Version 103023PCHPrAp=
|
||||
9 xxxxxxxxx spamspam Loan Servicing | 4425 Ponce de Leon Blvd 5-251, Coral =
|
||||
Gables, FL 33146-1837 Unsubscribe somebody@gmail.com Update Profile |=
|
||||
Our Privacy Policy | Constant Contact Data Notice Sent by marklake@spamspa=
|
||||
m.com
|
||||
------=_Part_75055660_144854819.1698672187348
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: quoted-printable
|
||||
|
||||
<!DOCTYPE HTML>
|
||||
<html lang=3D"en-US"> <head> <meta http-equiv=3D"Content-Type" content=3D"=
|
||||
text/html; charset=3Dutf-8"> <meta name=3D"viewport" content=3D"width=3Ddev=
|
||||
ice-width, initial-scale=3D1, maximum-scale=3D1"> <style type=3D"text/css=
|
||||
" data-premailer=3D"ignore">=20
|
||||
@media only screen and (max-width:480px) { .footer-main-width { width: 100%=
|
||||
!important; } .footer-mobile-hidden { display: none !important; } .foote=
|
||||
r-mobile-hidden { display: none !important; } .footer-column { display: bl=
|
||||
ock !important; } .footer-mobile-stack { display: block !important; } .fo=
|
||||
oter-mobile-stack-padding { padding-top: 3px; } }=20
|
||||
/* IE: correctly scale images with w/h attbs */ img { -ms-interpolation-mod=
|
||||
e: bicubic; }=20
|
||||
.layout { min-width: 100%; }=20
|
||||
table { table-layout: fixed; } .shell_outer-row { table-layout: auto; }=20
|
||||
/* Gmail/Web viewport fix */ u + .body .shell_outer-row { width: 620px; }=
|
||||
=20
|
||||
/* LIST AND p STYLE OVERRIDES */ .text .text_content-cell p { margin: 0; pa=
|
||||
dding: 0; margin-bottom: 0; } .text .text_content-cell ul, .text .text_cont=
|
||||
ent-cell ol { padding: 0; margin: 0 0 0 40px; } .text .text_content-cell li=
|
||||
{ padding: 0; margin: 0; /* line-height: 1.2; Remove after testing */ } /*=
|
||||
Text Link Style Reset */ a { text-decoration: underline; } /* iOS: Autolin=
|
||||
k styles inherited */ a[x-apple-data-detectors] { text-decoration: underlin=
|
||||
e !important; font-size: inherit !important; font-family: inherit !importan=
|
||||
t; font-weight: inherit !important; line-height: inherit !important; color:=
|
||||
inherit !important; } /* FF/Chrome: Smooth font rendering */ .text .text_c=
|
||||
ontent-cell { -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing:=
|
||||
grayscale; }=20
|
||||
</style> <!--[if gte mso 9]> <style id=3D"ol-styles">=20
|
||||
/* OUTLOOK-SPECIFIC STYLES */ li { text-indent: -1em; padding: 0; margin: 0=
|
||||
; /* line-height: 1.2; Remove after testing */ } ul, ol { padding: 0; margi=
|
||||
n: 0 0 0 40px; } p { margin: 0; padding: 0; margin-bottom: 0; }=20
|
||||
</style> <![endif]--> <style>@media only screen and (max-width:480px) {
|
||||
.button_content-cell {
|
||||
padding-top: 10px !important; padding-right: 20px !important; padding-botto=
|
||||
m: 10px !important; padding-left: 20px !important;
|
||||
}
|
||||
.button_border-row .button_content-cell {
|
||||
padding-top: 10px !important; padding-right: 20px !important; padding-botto=
|
||||
m: 10px !important; padding-left: 20px !important;
|
||||
}
|
||||
.column .content-padding-horizontal {
|
||||
padding-left: 20px !important; padding-right: 20px !important;
|
||||
}
|
||||
.layout .column .content-padding-horizontal .content-padding-horizontal {
|
||||
padding-left: 0px !important; padding-right: 0px !important;
|
||||
}
|
||||
.layout .column .content-padding-horizontal .block-wrapper_border-row .cont=
|
||||
ent-padding-horizontal {
|
||||
padding-left: 20px !important; padding-right: 20px !important;
|
||||
}
|
||||
.dataTable {
|
||||
overflow: auto !important;
|
||||
}
|
||||
.dataTable .dataTable_content {
|
||||
width: auto !important;
|
||||
}
|
||||
.image--mobile-scale .image_container img {
|
||||
width: auto !important;
|
||||
}
|
||||
.image--mobile-center .image_container img {
|
||||
margin-left: auto !important; margin-right: auto !important;
|
||||
}
|
||||
.layout-margin .layout-margin_cell {
|
||||
padding: 0px 20px !important;
|
||||
}
|
||||
.layout-margin--uniform .layout-margin_cell {
|
||||
padding: 20px 20px !important;
|
||||
}
|
||||
.scale {
|
||||
width: 100% !important;
|
||||
}
|
||||
.stack {
|
||||
display: block !important; box-sizing: border-box;
|
||||
}
|
||||
.hide {
|
||||
display: none !important;
|
||||
}
|
||||
u + .body .shell_outer-row {
|
||||
width: 100% !important;
|
||||
}
|
||||
.socialFollow_container {
|
||||
text-align: center !important;
|
||||
}
|
||||
.text .text_content-cell {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
.text .text_content-cell h1 {
|
||||
font-size: 24px !important;
|
||||
}
|
||||
.text .text_content-cell h2 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text .text_content-cell h3 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text--sectionHeading .text_content-cell {
|
||||
font-size: 26px !important;
|
||||
}
|
||||
.text--heading .text_content-cell {
|
||||
font-size: 26px !important;
|
||||
}
|
||||
.text--feature .text_content-cell h2 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text--articleHeading .text_content-cell {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text--article .text_content-cell h3 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text--featureHeading .text_content-cell {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text--feature .text_content-cell h3 {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
.text--dataTable .text_content-cell .dataTable .dataTable_content-cell {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
.text--dataTable .text_content-cell .dataTable th.dataTable_content-cell {
|
||||
font-size: px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head> <body class=3D"body template template--en-US" data-template-version=
|
||||
=3D"1.38.0" data-canonical-name=3D"CPE10001" lang=3D"en-US" align=3D"center=
|
||||
" style=3D"-ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; min-=
|
||||
width: 100%; width: 100%; margin: 0px; padding: 0px;"> <div id=3D"preheader=
|
||||
" style=3D"color: transparent; display: none; font-size: 1px; line-height: =
|
||||
1px; max-height: 0px; max-width: 0px; opacity: 0; overflow: hidden;"><span =
|
||||
data-entity-ref=3D"preheader">When you're buying a home, Pre-Approval =
|
||||
gives you confidence you're in the right price range and shows sellers=
|
||||
you mean business. </span></div> <div id=3D"tracking-image" style=3D"color=
|
||||
: transparent; display: none; font-size: 1px; line-height: 1px; max-height:=
|
||||
0px; max-width: 0px; opacity: 0; overflow: hidden;"><img src=3D"https://r2=
|
||||
0.rs6.net/on.jsp?ca=beefbeefbe-beef-44b1-be0e-f5a444cb0650&a=3D113310475238=
|
||||
1&c=3Db13a9586-a541-11ec-a79b-fa163e5bc304&ch=3Db1441c50-a541-11ec-a79b-fa1=
|
||||
63e5bc304" / alt=3D""></div> <div class=3D"shell" lang=3D"en-US" style=3D"b=
|
||||
ackground-color: #015288;"> <table class=3D"shell_panel-row" width=3D"100%=
|
||||
" border=3D"0" cellpadding=3D"0" cellspacing=3D"0" style=3D"background-colo=
|
||||
r: #015288;" bgcolor=3D"#015288"> <tr class=3D""> <td class=3D"shell_panel-=
|
||||
cell" style=3D"" align=3D"center" valign=3D"top"> <table class=3D"shell_wid=
|
||||
th-row scale" style=3D"width: 620px;" align=3D"center" border=3D"0" cellpad=
|
||||
ding=3D"0" cellspacing=3D"0"> <tr> <td class=3D"shell_width-cell" style=3D"=
|
||||
padding: 15px 10px;" align=3D"center" valign=3D"top"> <table class=3D"shell=
|
||||
_content-row" width=3D"100%" align=3D"center" border=3D"0" cellpadding=3D"0=
|
||||
" cellspacing=3D"0"> <tr> <td class=3D"shell_content-cell" style=3D"border-=
|
||||
radius: 0px; background-color: #FFFFFF; padding: 0; border: 0px solid #0096=
|
||||
d6;" align=3D"center" valign=3D"top" bgcolor=3D"#FFFFFF"> <table class=3D"l=
|
||||
ayout layout--1-column" style=3D"table-layout: fixed;" width=3D"100%" borde=
|
||||
r=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column colum=
|
||||
n--1 scale stack" style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"divider" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0"=
|
||||
border=3D"0"> <tr> <td class=3D"divider_container" style=3D"padding-top: 0=
|
||||
px; padding-bottom: 10px;" width=3D"100%" align=3D"center" valign=3D"top"> =
|
||||
<table class=3D"divider_content-row" style=3D"width: 100%; height: 1px;" ce=
|
||||
llpadding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td class=3D"divider_c=
|
||||
ontent-cell" style=3D"padding-bottom: 5px; height: 1px; line-height: 1px; b=
|
||||
ackground-color: #0096D6; border-bottom-width: 0px;" height=3D"1" align=3D"=
|
||||
center" bgcolor=3D"#0096D6"> <img alt=3D"" width=3D"5" height=3D"1" border=
|
||||
=3D"0" hspace=3D"0" vspace=3D"0" src=3D"https://imgssl.constantcontact.com/=
|
||||
letters/images/1101116784221/S.gif" style=3D"display: block; height: 1px; w=
|
||||
idth: 5px;"> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table>=
|
||||
<table class=3D"layout layout--1-column" style=3D"table-layout: fixed;" wi=
|
||||
dth=3D"100%" border=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr> <td cla=
|
||||
ss=3D"column column--1 scale stack" style=3D"width: 100%;" align=3D"center"=
|
||||
valign=3D"top"><div class=3D"spacer" style=3D"line-height: 10px; height: 1=
|
||||
0px;"> </div></td> </tr> </table> <table class=3D"layout layout--1-c=
|
||||
olumn" style=3D"table-layout: fixed;" width=3D"100%" border=3D"0" cellpaddi=
|
||||
ng=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column column--1 scale stack"=
|
||||
style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"image image--padding-vertical image--mobile-scale image--mo=
|
||||
bile-center" width=3D"100%" border=3D"0" cellpadding=3D"0" cellspacing=3D"0=
|
||||
"> <tr> <td class=3D"image_container" align=3D"center" valign=3D"top" style=
|
||||
=3D"padding-top: 10px; padding-bottom: 10px;"> <a href=3D"https://r20.rs6.n=
|
||||
et/tn.jsp?f=3D001YKO1VR2jLW0SuSLZLfN7qCP9AwEGO0v-Vy-0SCUlMWvTEiCsv-QEMhmJe9=
|
||||
ch=3DHu9wLy0fth6D8jxFBWPA_NhdnWcZZPivk0KUTgRJoVIo_si10jiydw=3D=3D" data-tra=
|
||||
ckable=3D"true"><img data-image-content class=3D"image_content" width=3D"26=
|
||||
2" src=3D"https://files.constantcontact.com/beefbeefbee/057bff2a-bdba-4165-=
|
||||
b108-a7baa91c42c6.jpg" alt=3D"" style=3D"display: block; height: auto; max-=
|
||||
width: 100%;"></a> </td> </tr> </table> </td> </tr> </table> <table class=
|
||||
=3D"layout layout--heading layout--1-column" style=3D"background-color: #00=
|
||||
527e; table-layout: fixed;" width=3D"100%" border=3D"0" cellpadding=3D"0" c=
|
||||
ellspacing=3D"0" bgcolor=3D"#00527e"> <tr> <td class=3D"column column--1 sc=
|
||||
ale stack" style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"text text--padding-vertical" width=3D"100%" border=3D"0" ce=
|
||||
llpadding=3D"0" cellspacing=3D"0" style=3D"table-layout: fixed;"> <tr> <td =
|
||||
class=3D"text_content-cell content-padding-horizontal" style=3D"text-align:=
|
||||
center; font-family: Arial,Verdana,Helvetica,sans-serif; color: #000000; f=
|
||||
ont-size: 14px; line-height: 1.2; display: block; word-wrap: break-word; pa=
|
||||
dding: 10px 20px;" align=3D"center" valign=3D"top">
|
||||
<h1 style=3D"font-family: Arial,Verdana,Helvetica,sans-serif; color: #606d7=
|
||||
8; font-size: 26px; font-weight: bold; margin: 0;"><span style=3D"color: rg=
|
||||
b(0, 150, 214);">SELLING or BUYING?</span></h1>
|
||||
<p style=3D"margin: 0;"><span style=3D"font-size: 16px; color: rgb(255, 255=
|
||||
, 255); font-weight: bold;">Call: 844-590-2275</span></p>
|
||||
</td> </tr> </table> </td> </tr> </table> <table class=3D"layout layout--ar=
|
||||
ticle layout--1-column" style=3D"table-layout: fixed;" width=3D"100%" borde=
|
||||
r=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column colum=
|
||||
n--1 scale stack" style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"text text--heading text--padding-vertical" width=3D"100%" b=
|
||||
order=3D"0" cellpadding=3D"0" cellspacing=3D"0" style=3D"table-layout: fixe=
|
||||
d;"> <tr> <td class=3D"text_content-cell content-padding-horizontal" style=
|
||||
=3D"text-align: center; font-family: Arial,Verdana,Helvetica,sans-serif; co=
|
||||
lor: #606d78; font-size: 26px; line-height: 1.2; display: block; word-wrap:=
|
||||
break-word; font-weight: bold; padding: 10px 20px;" align=3D"center" valig=
|
||||
n=3D"top">
|
||||
<p style=3D"margin: 0;"><span style=3D"font-size: 30px; color: rgb(0, 150, =
|
||||
214);">Get Your Homebuying</span></p>
|
||||
<p style=3D"margin: 0;"><span style=3D"font-size: 30px; color: rgb(0, 82, 1=
|
||||
26);">PRE-APPROVAL IN 24-HOURS</span><span style=3D"font-size: 30px; color:=
|
||||
rgb(0, 82, 126); font-weight: normal;">*</span></p>
|
||||
</td> </tr> </table> <table class=3D"image image--padding-vertical image--m=
|
||||
obile-scale image--mobile-center" width=3D"100%" border=3D"0" cellpadding=
|
||||
=3D"0" cellspacing=3D"0"> <tr> <td class=3D"image_container content-padding=
|
||||
-horizontal" align=3D"center" valign=3D"top" style=3D"padding: 10px 20px;">=
|
||||
<img data-image-content class=3D"image_content" width=3D"548" src=3D"https=
|
||||
://files.constantcontact.com/df66e42d701/2092a2d7-0bda-4289-910b-bf50a2398d=
|
||||
60.jpg" alt=3D"" style=3D"display: block; height: auto; max-width: 100%;"> =
|
||||
</td> </tr> </table> <table class=3D"button button--padding-vertical" widt=
|
||||
h=3D"100%" border=3D"0" cellpadding=3D"0" cellspacing=3D"0" style=3D"table-=
|
||||
layout: fixed;"> <tr> <td class=3D"button_container content-padding-horizon=
|
||||
tal" align=3D"center" style=3D"padding: 10px 20px;"> <table class=3D"but=
|
||||
ton_content-row" style=3D"width: inherit; border-radius: 3px; border-spacin=
|
||||
g: 0; background-color: #0096D6; border: none;" border=3D"0" cellpadding=3D=
|
||||
"0" cellspacing=3D"0" bgcolor=3D"#0096D6"> <tr> <td class=3D"button_content=
|
||||
-cell" style=3D"padding: 10px 40px;" align=3D"center"> <a class=3D"button_l=
|
||||
ink" href=3D"https://r20.rs6.net/tn.jsp?f=3D001YKO1VR2jLW0SuSLZLfN7qCP9AwEG=
|
||||
O0v-Vy-0SCUlMWvTEiCsv-QEMuu9ZVVi6WGHhCias4f7-QkeggQvxIvbs-6TTaZHHhXLKf88NID=
|
||||
dci4Ge7aYN-QihEgqblie1-DQ2Fa1BKLbT3AM8rtrgeYQgVxJ6cG8POsvFzv7JstrGkCkg3a3AE=
|
||||
633LfQpAddyVLFkTv6oyS4T2j_YjYIPKDOZktqK_5rOR-Fh8cWGtUD8YPpPNnZ037z6_t9Nkemu=
|
||||
hxG&c=3DA65qX-dQJPS0J4afCS7H0Je5N-_6Q8Nh2fNHkb5-5biUYd5B9SY3zA=3D=3D&ch=3DH=
|
||||
u9wLy0fth6D8jxFBWPA_NhdnWcZZPivk0KUTgRJoVIo_si10jiydw=3D=3D" data-trackable=
|
||||
=3D"true" style=3D"font-size: 16px; font-weight: bold; color: #FFFFFF; font=
|
||||
-family: Helvetica,Arial,sans-serif; word-wrap: break-word; text-decoration=
|
||||
: none;">Get Pre-Approved</a> </td> </tr> </table> </td> </tr> </table> =
|
||||
<table class=3D"text text--padding-vertical" width=3D"100%" border=3D"0" =
|
||||
cellpadding=3D"0" cellspacing=3D"0" style=3D"table-layout: fixed;"> <tr> <t=
|
||||
d class=3D"text_content-cell content-padding-horizontal" style=3D"line-heig=
|
||||
ht: 1; text-align: center; font-family: Arial,Verdana,Helvetica,sans-serif;=
|
||||
color: #000000; font-size: 14px; display: block; word-wrap: break-word; pa=
|
||||
dding: 10px 20px;" align=3D"center" valign=3D"top">
|
||||
<p style=3D"text-align: left; margin: 0;" align=3D"left"><br></p>
|
||||
<p style=3D"margin: 0;"><span style=3D"font-size: 19px;">When you're buying=
|
||||
a home, Pre-Approval gives you confidence you're in the right price range =
|
||||
and shows sellers you mean business. </span></p>
|
||||
<p style=3D"margin: 0;"><span style=3D"font-size: 19px;">Get Pre-Ap=
|
||||
proved today!</span></p>
|
||||
</td> </tr> </table> </td> </tr> </table> <table class=3D"layout layout--1-=
|
||||
column" style=3D"table-layout: fixed;" width=3D"100%" border=3D"0" cellpadd=
|
||||
ing=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column column--1 scale stack=
|
||||
" style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"text text--padding-vertical" width=3D"100%" border=3D"0" ce=
|
||||
llpadding=3D"0" cellspacing=3D"0" style=3D"table-layout: fixed;"> <tr> <td =
|
||||
class=3D"text_content-cell content-padding-horizontal" style=3D"text-align:=
|
||||
left; font-family: Arial,Verdana,Helvetica,sans-serif; color: #000000; fon=
|
||||
t-size: 14px; line-height: 1.2; display: block; word-wrap: break-word; padd=
|
||||
ing: 10px 20px;" align=3D"left" valign=3D"top">
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><br></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 23px; color: rgb(0, 82, 126); font-weight: bold; font-family: A=
|
||||
rial, Verdana, Helvetica, sans-serif;">Click or Call to Get Pre-Approved </=
|
||||
span></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 28px; color: rgb(0, 150, 214); font-weight: bold;">844-590-2275=
|
||||
</span></p>
|
||||
</td> </tr> </table> </td> </tr> </table> <table class=3D"layout layout--1-=
|
||||
column" style=3D"table-layout: fixed;" width=3D"100%" border=3D"0" cellpadd=
|
||||
ing=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column column--1 scale stack=
|
||||
" style=3D"width: 100%;" align=3D"center" valign=3D"top"> <table class=3D"b=
|
||||
utton button--padding-vertical" width=3D"100%" border=3D"0" cellpadding=3D"=
|
||||
0" cellspacing=3D"0" style=3D"table-layout: fixed;"> <tr> <td class=3D"butt=
|
||||
on_container content-padding-horizontal" align=3D"center" style=3D"padding:=
|
||||
10px 20px;"> <table class=3D"button_content-row" style=3D"background-co=
|
||||
lor: #0096D6; width: inherit; border-radius: 3px; border-spacing: 0; border=
|
||||
: none;" border=3D"0" cellpadding=3D"0" cellspacing=3D"0" bgcolor=3D"#0096D=
|
||||
6"> <tr> <td class=3D"button_content-cell" style=3D"padding: 10px 40px;" al=
|
||||
ign=3D"center"> <a class=3D"button_link" href=3D"https://r20.rs6.net/tn.jsp=
|
||||
?f=3D001thisisfakethisisfakethisisfakev-Vy-0SCUlMWvTEiCsv-QEMuu9ZVVi6WGHhCi=
|
||||
oVIo_si10jiydw=3D=3D" data-trackable=3D"true" style=3D"font-size: 16px; fon=
|
||||
t-weight: bold; color: #FFFFFF; font-family: Helvetica,Arial,sans-serif; wo=
|
||||
rd-wrap: break-word; text-decoration: none;">Get Pre-Approved</a> </td> </t=
|
||||
r> </table> </td> </tr> </table> </td> </tr> </table> <table class=3D"=
|
||||
layout layout--1-column" style=3D"table-layout: fixed;" width=3D"100%" bord=
|
||||
er=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column colu=
|
||||
mn--1 scale stack" style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"image image--padding-vertical image--mobile-scale image--mo=
|
||||
bile-center" width=3D"100%" border=3D"0" cellpadding=3D"0" cellspacing=3D"0=
|
||||
"> <tr> <td class=3D"image_container" align=3D"center" valign=3D"top" style=
|
||||
=3D"padding-top: 10px; padding-bottom: 10px;"> <img data-image-content clas=
|
||||
s=3D"image_content" width=3D"87" src=3D"https://files.constantcontact.com/d=
|
||||
f66e42d701/beefbeef-beef-beef-9a13-2779ab497b8d.png" alt=3D"" style=3D"disp=
|
||||
lay: block; height: auto; max-width: 100%;"> </td> </tr> </table> </td> </t=
|
||||
r> </table> <table class=3D"layout layout--1-column" style=3D"table-layout:=
|
||||
fixed;" width=3D"100%" border=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <=
|
||||
tr> <td class=3D"column column--1 scale stack" style=3D"width: 100%;" align=
|
||||
=3D"center" valign=3D"top">
|
||||
<table class=3D"text text--padding-vertical" width=3D"100%" border=3D"0" ce=
|
||||
llpadding=3D"0" cellspacing=3D"0" style=3D"table-layout: fixed;"> <tr> <td =
|
||||
class=3D"text_content-cell content-padding-horizontal" style=3D"text-align:=
|
||||
left; font-family: Arial,Verdana,Helvetica,sans-serif; color: #000000; fon=
|
||||
t-size: 14px; line-height: 1.2; display: block; word-wrap: break-word; padd=
|
||||
ing: 10px 20px;" align=3D"left" valign=3D"top">
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><br></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><a href=3D"htt=
|
||||
ps://r20.rs6.net/tn.jsp?f=3D001YKO1VR2jLW0SuSLZLfN7qCP9AwEGO0v-Vy-0SCUlMWvT=
|
||||
EiCsv-QEMgYju54LKeEV1_a2OCyOAfG7VhZpxtOW89WM-s6S5iiXcmnbK-Z6XDc9LL569h6DE4L=
|
||||
IRMWiBWHOlFB9TZWQVuX6Ycz3505y1keCrca4QArp&c=3DA65qX-dQJPS0J4afCS7H0Je5N-_6Q=
|
||||
8Nh2fNHkb5-5biUYd5B9SY3zA=3D=3D&ch=3DHu9wLy0fth6D8jxFBWPA_NhdnWcZZPivk0KUTg=
|
||||
RJoVIo_si10jiydw=3D=3D" target=3D"_blank" style=3D"font-size: 11px; color: =
|
||||
rgb(153, 153, 153); text-decoration: underline; font-weight: normal; font-s=
|
||||
tyle: normal;">nmlsconsumeraccess.org/</a></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 11px; color: rgb(153, 153, 153);">*The 24 hour timeframe is for=
|
||||
most approvals, however if additional information is needed or a request i=
|
||||
s on a holiday, the time for preapproval may be greater than 24 hours.</spa=
|
||||
n></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 11px; color: rgb(153, 153, 153); background-color: rgb(255, 255=
|
||||
, 255);">This email is for informational purposes only and is not an offer,=
|
||||
loan approval or loan commitment. Mortgage rates are subject to change wit=
|
||||
hout notice. Some terms and restrictions may apply to certain loan programs=
|
||||
. Refinancing existing loans may result in total finance charges being high=
|
||||
er over the life of the loan, reduction in payments may partially reflect a=
|
||||
longer loan term. This information is provided as guidance and illustrativ=
|
||||
e purposes only and does not constitute legal or financial advice. We are n=
|
||||
ot liable or bound legally for any answers provided to any user for our pro=
|
||||
cess or position on an issue. This information may change from time to time=
|
||||
and at any time without notification. The most current information will be=
|
||||
updated periodically and posted in the online forum.</span></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 11px; color: rgb(153, 153, 153); background-color: rgb(255, 255=
|
||||
, 255);">spamspam Loan Servicing, LLC. NMLS#391521. nmlsconsumeraccess.org.=
|
||||
You are receiving this information as a current loan customer with spamspa=
|
||||
m Loan Servicing, LLC. Not licensed for lending activities in any of the U.=
|
||||
S. territories. Not authorized to originate loans in the State of New York.=
|
||||
Licensed by the Dept. of Financial Protection and Innovation under the Cal=
|
||||
ifornia Residential Mortgage .Lending Act #4131216.</span></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><br></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 11px; color: rgb(153, 153, 153);">This email was sent to <span =
|
||||
data-id=3D"emailAddress">somebody@gmail.com</span></span></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 11px; color: rgb(153, 153, 153);">Version 103023PCHPrAp9 </span=
|
||||
></p>
|
||||
<p style=3D"text-align: center; margin: 0;" align=3D"center"><span style=3D=
|
||||
"font-size: 11px; color: rgb(162, 162, 162);"></span></p>
|
||||
</td> </tr> </table> </td> </tr> </table> <table class=3D"layout layout--1-=
|
||||
column" style=3D"table-layout: fixed;" width=3D"100%" border=3D"0" cellpadd=
|
||||
ing=3D"0" cellspacing=3D"0"> <tr> <td class=3D"column column--1 scale stack=
|
||||
" style=3D"width: 100%;" align=3D"center" valign=3D"top">
|
||||
<table class=3D"divider" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0"=
|
||||
border=3D"0"> <tr> <td class=3D"divider_container" style=3D"padding-top: 1=
|
||||
0px; padding-bottom: 0px;" width=3D"100%" align=3D"center" valign=3D"top"> =
|
||||
<table class=3D"divider_content-row" style=3D"width: 100%; height: 1px;" ce=
|
||||
llpadding=3D"0" cellspacing=3D"0" border=3D"0"> <tr> <td class=3D"divider_c=
|
||||
ontent-cell" style=3D"padding-bottom: 2px; height: 1px; line-height: 1px; b=
|
||||
ackground-color: #0096D6; border-bottom-width: 0px;" height=3D"1" align=3D"=
|
||||
center" bgcolor=3D"#0096D6"> <img alt=3D"" width=3D"5" height=3D"1" border=
|
||||
=3D"0" hspace=3D"0" vspace=3D"0" src=3D"https://imgssl.constantcontact.com/=
|
||||
letters/images/1111111111111/S.gif" style=3D"display: block; height: 1px; w=
|
||||
idth: 5px;"> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table>=
|
||||
</td> </tr> </table> </td> </tr> </table> </td> </tr> <tr> <td class=3D"s=
|
||||
hell_panel-cell shell_panel-cell--systemFooter" style=3D"" align=3D"center"=
|
||||
valign=3D"top"> <table class=3D"shell_width-row scale" style=3D"width: 100=
|
||||
%;" align=3D"center" border=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr>=
|
||||
<td class=3D"shell_width-cell" style=3D"padding: 0px;" align=3D"center" va=
|
||||
lign=3D"top"> <table class=3D"shell_content-row" width=3D"100%" align=3D"ce=
|
||||
nter" border=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr> <td class=3D"s=
|
||||
hell_content-cell" style=3D"background-color: #FFFFFF; padding: 0; border: =
|
||||
0 solid #0096d6;" align=3D"center" valign=3D"top" bgcolor=3D"#FFFFFF"> <tab=
|
||||
le class=3D"layout layout--1-column" style=3D"table-layout: fixed;" width=
|
||||
=3D"100%" border=3D"0" cellpadding=3D"0" cellspacing=3D"0"> <tr> <td class=
|
||||
=3D"column column--1 scale stack" style=3D"width: 100%;" align=3D"center" v=
|
||||
align=3D"top"> <table class=3D"footer" width=3D"100%" border=3D"0" cellpadd=
|
||||
ing=3D"0" cellspacing=3D"0" style=3D"font-family: Verdana,Geneva,sans-serif=
|
||||
; color: #5d5d5d; font-size: 12px;"> <tr> <td class=3D"footer_container" al=
|
||||
ign=3D"center"> <table class=3D"footer-container" width=3D"100%" cellpaddin=
|
||||
g=3D"0" cellspacing=3D"0" border=3D"0" style=3D"background-color: #ffffff; =
|
||||
margin-left: auto; margin-right: auto; table-layout: auto !important;" bgco=
|
||||
lor=3D"#ffffff">
|
||||
<tr>
|
||||
<td width=3D"100%" align=3D"center" valign=3D"top" style=3D"width: 100%;">
|
||||
<div class=3D"footer-max-main-width" align=3D"center" style=3D"margin-left:=
|
||||
auto; margin-right: auto; max-width: 100%;">
|
||||
<table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td class=3D"footer-layout" align=3D"center" valign=3D"top" style=3D"paddin=
|
||||
g: 16px 0px;">
|
||||
<table class=3D"footer-main-width" style=3D"width: 580px;" border=3D"0" cel=
|
||||
lpadding=3D"0" cellspacing=3D"0">
|
||||
<tr>
|
||||
<td class=3D"footer-text" align=3D"center" valign=3D"top" style=3D"color: #=
|
||||
5d5d5d; font-family: Verdana,Geneva,sans-serif; font-size: 12px; padding: 4=
|
||||
px 0px;">
|
||||
<span class=3D"footer-column">spamspam Loan Servicing<span class=3D"footer-=
|
||||
mobile-hidden"> | </span></span><span class=3D"footer-column">4425 Ponce de=
|
||||
Leon Blvd 5-251<span class=3D"footer-mobile-hidden">, </span></span><span =
|
||||
class=3D"footer-column"></span><span class=3D"footer-column"></span><span c=
|
||||
lass=3D"footer-column">Coral Gables, FL 33146-1837</span><span class=3D"foo=
|
||||
ter-column"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class=3D"footer-row" align=3D"center" valign=3D"top" style=3D"padding: =
|
||||
10px 0px;">
|
||||
<table cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td class=3D"footer-text" align=3D"center" valign=3D"top" style=3D"color: #=
|
||||
5d5d5d; font-family: Verdana,Geneva,sans-serif; font-size: 12px; padding: 4=
|
||||
px 0px;">
|
||||
<a href=3D"https://visitor.constantcontact.com/do?p=3Dun&m=3D001g3dtlqhzM3v=
|
||||
-44b1-be0e-f5a444cb0650" data-track=3D"false" style=3D"color: #5d5d5d;">Uns=
|
||||
ubscribe somebody@gmail.com<span class=3D"partnerOptOut"></span></a>
|
||||
<span class=3D"partnerOptOut"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class=3D"footer-text" align=3D"center" valign=3D"top" style=3D"color: #=
|
||||
5d5d5d; font-family: Verdana,Geneva,sans-serif; font-size: 12px; padding: 4=
|
||||
px 0px;">
|
||||
<a href=3D"https://visitor.constantcontact.com/do?p=3Doo&m=3D001g3dtlqhzM3v=
|
||||
-44b1-be0e-f5a444cb0650" data-track=3D"false" style=3D"color: #5d5d5d;">Upd=
|
||||
ate Profile</a> |
|
||||
<a href=3D"https://spamspam.com/privacy-notice/" data-track=3D"false" style=
|
||||
=3D"color: #5d5d5d;">Our Privacy Policy</a><span class=3D"footer-mobile-hid=
|
||||
den"> |</span>
|
||||
<a class=3D"footer-about-provider footer-mobile-stack footer-mobile-stack-p=
|
||||
adding" href=3D"http://www.constantcontact.com/legal/about-constant-contact=
|
||||
" data-track=3D"false" style=3D"color: #5d5d5d;">Constant Contact Data Noti=
|
||||
ce</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class=3D"footer-text" align=3D"center" valign=3D"top" style=3D"color: #=
|
||||
5d5d5d; font-family: Verdana,Geneva,sans-serif; font-size: 12px; padding: 4=
|
||||
px 0px;">
|
||||
Sent by
|
||||
<a href=3D"mailto:marklake@spamspam.com" style=3D"color: #5d5d5d; text-deco=
|
||||
ration: none;">marklake@spamspam.com</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class=3D"footer-text" align=3D"center" valign=3D"top" style=3D"color: #=
|
||||
5d5d5d; font-family: Verdana,Geneva,sans-serif; font-size: 12px; padding: 4=
|
||||
px 0px;">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table> </td> </tr> </table> </td> </tr> </table> </td> </tr> </table> =
|
||||
</td> </tr> </table> </td> </tr> </table> </div> </body> </html>
|
||||
|
||||
------=_Part_75055660_144854819.1698672187348--
|
||||
.
|
||||
`
|
||||
|
||||
func TestSmtpBackend_Spam_Text(t *testing.T) {
|
||||
email := spamEmail
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Buying a home? You deserve the confidence of Pre-Approval", r.Header.Get("Title"))
|
||||
actual := readAll(t, r.Body)
|
||||
expected := "When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business. xxxxxxxxx SELLING or BUYING? Call: 844-590-2275 Get Your Homebuying PRE-APPROVAL IN 24-HOURS* Get Pre-Approved When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business. xxxxxxxxxGet Pre-Approved today! Click or Call to Get Pre-Approved 844-590-2275 Get Pre-Approved nmlsconsumeraccess.org/ *The 24 hour timeframe is for most approvals, however if additional information is needed or a request is on a holiday, the time for preapproval may be greater than 24 hours. This email is for informational purposes only and is not an offer, loan approval or loan commitment. Mortgage rates are subject to change without notice. Some terms and restrictions may apply to certain loan programs. Refinancing existing loans may result in total finance charges being higher over the life of the loan, reduction in payments may partially reflect a longer loan term. This information is provided as guidance and illustrative purposes only and does not constitute legal or financial advice. We are not liable or bound legally for any answers provided to any user for our process or position on an issue. This information may change from time to time and at any time without notification. The most current information will be updated periodically and posted in the online forum. spamspam Loan Servicing, LLC. NMLS#391521. nmlsconsumeraccess.org. You are receiving this information as a current loan customer with spamspam Loan Servicing, LLC. Not licensed for lending activities in any of the U.S. territories. Not authorized to originate loans in the State of New York. Licensed by the Dept. of Financial Protection and Innovation under the California Residential Mortgage .Lending Act #4131216. This email was sent to somebody@gmail.com Version 103023PCHPrAp9 xxxxxxxxx spamspam Loan Servicing | 4425 Ponce de Leon Blvd 5-251, Coral Gables, FL 33146-1837 Unsubscribe somebody@gmail.com Update Profile | Our Privacy Policy | Constant Contact Data Notice Sent by marklake@spamspam.com"
|
||||
require.Equal(t, expected, actual)
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_Spam_HTML(t *testing.T) {
|
||||
email := strings.ReplaceAll(spamEmail, "text/plain", "text/not-plain-anymore") // We artificially force HTML parsing here
|
||||
s, c, _, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/mytopic", r.URL.Path)
|
||||
require.Equal(t, "Buying a home? You deserve the confidence of Pre-Approval", r.Header.Get("Title"))
|
||||
actual := readAll(t, r.Body)
|
||||
expected := `When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business.
|
||||
` + "\u200a" + `
|
||||
|
||||
SELLING or BUYING?
|
||||
Call: 844-590-2275
|
||||
|
||||
Get Your Homebuying
|
||||
PRE-APPROVAL IN 24-HOURS *
|
||||
Get Pre-Approved
|
||||
|
||||
When you're buying a home, Pre-Approval gives you confidence you're in the right price range and shows sellers you mean business.
|
||||
` + "\ufeff" + `Get Pre-Approved today!
|
||||
|
||||
Click or Call to Get Pre-Approved
|
||||
844-590-2275
|
||||
Get Pre-Approved
|
||||
|
||||
nmlsconsumeraccess.org/
|
||||
*The 24 hour timeframe is for most approvals, however if additional information is needed or a request is on a holiday, the time for preapproval may be greater than 24 hours.
|
||||
This email is for informational purposes only and is not an offer, loan approval or loan commitment. Mortgage rates are subject to change without notice. Some terms and restrictions may apply to certain loan programs Refinancing existing loans may result in total finance charges being higher over the life of the loan, reduction in payments may partially reflect a longer loan term. This information is provided as guidance and illustrative purposes only and does not constitute legal or financial advice. We are not liable or bound legally for any answers provided to any user for our process or position on an issue. This information may change from time to time and at any time without notification. The most current information will be updated periodically and posted in the online forum.
|
||||
spamspam Loan Servicing, LLC. NMLS#391521. nmlsconsumeraccess.org. You are receiving this information as a current loan customer with spamspam Loan Servicing, LLC. Not licensed for lending activities in any of the U.S. territories. Not authorized to originate loans in the State of New York. Licensed by the Dept. of Financial Protection and Innovation under the California Residential Mortgage .Lending Act #4131216.
|
||||
|
||||
This email was sent to somebody@gmail.com
|
||||
Version 103023PCHPrAp9
|
||||
` + "\ufeff" + `
|
||||
|
||||
spamspam Loan Servicing | 4425 Ponce de Leon Blvd 5-251 , Coral Gables, FL 33146-1837
|
||||
|
||||
Unsubscribe somebody@gmail.com
|
||||
|
||||
Update Profile |
|
||||
Our Privacy Policy |
|
||||
Constant Contact Data Notice
|
||||
|
||||
Sent by
|
||||
marklake@spamspam.com`
|
||||
require.Equal(t, expected, actual)
|
||||
})
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_HTMLOnly_FromDiskStation(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: synology@mydomain.me
|
||||
RCPT TO: synology@mydomain.me
|
||||
DATA
|
||||
From: "=?UTF-8?B?Um9iYmll?=" <synology@mydomain.me>
|
||||
To: <synology@mydomain.me>
|
||||
Message-Id: <640e6f562895d.6c9584bcfa491ac9c546b480b32ffc1d@mydomain.me>
|
||||
MIME-Version: 1.0
|
||||
Subject: =?UTF-8?B?W1N5bm9sb2d5IE5BU10gVGVzdCBNZXNzYWdlIGZyb20gTGl0dHNfTkFT?=
|
||||
Content-Type: text/html; charset=utf-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
Congratulations! You have successfully set up the email notification on Synology_NAS.<BR>For further system configurations, please visit http://192.168.1.28:5000/, http://172.16.60.5:5000/.<BR>(If you cannot connect to the server, please contact the administrator.)<BR><BR>From Synology_NAS<BR><BR><BR>
|
||||
.
|
||||
`
|
||||
s, c, conf, scanner := newTestSMTPServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Equal(t, "/synology", r.URL.Path)
|
||||
require.Equal(t, "[Synology NAS] Test Message from Litts_NAS", r.Header.Get("Title"))
|
||||
actual := readAll(t, r.Body)
|
||||
expected := `Congratulations! You have successfully set up the email notification on Synology_NAS. For further system configurations, please visit http://192.168.1.28:5000/, http://172.16.60.5:5000/. (If you cannot connect to the server, please contact the administrator.) From Synology_NAS`
|
||||
require.Equal(t, expected, actual)
|
||||
})
|
||||
conf.SMTPServerDomain = "mydomain.me"
|
||||
conf.SMTPServerAddrPrefix = ""
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
writeAndReadUntilLine(t, email, c, scanner, "250 2.0.0 OK: queued")
|
||||
}
|
||||
|
||||
func TestSmtpBackend_PlaintextWithToken(t *testing.T) {
|
||||
email := `EHLO example.com
|
||||
MAIL FROM: phil@example.com
|
||||
@@ -639,7 +1436,6 @@ func readUntilLine(t *testing.T, conn net.Conn, scanner *bufio.Scanner, expected
|
||||
return
|
||||
}
|
||||
output += text + "\n"
|
||||
//fmt.Println(text)
|
||||
}
|
||||
t.Fatalf("Expected line '%s' not found in output:\n%s", expectedLine, output)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestTopic_Subscribe_DuplicateID(t *testing.T) {
|
||||
t.Parallel()
|
||||
to := newTopic("mytopic")
|
||||
|
||||
// Fix random seed to force same number generation
|
||||
//lint:ignore SA1019 Fix random seed to force same number generation
|
||||
rand.Seed(1)
|
||||
a := rand.Int()
|
||||
to.subscribers[a] = &topicSubscriber{
|
||||
@@ -82,7 +82,7 @@ func TestTopic_Subscribe_DuplicateID(t *testing.T) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Force rand.Int to generate the same id once more
|
||||
//lint:ignore SA1019 Force rand.Int to generate the same id once more
|
||||
rand.Seed(1)
|
||||
id := to.Subscribe(subFn, "b", func() {})
|
||||
res := to.subscribers[id]
|
||||
|
||||
@@ -5,10 +5,10 @@ import (
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
// List of possible events
|
||||
|
||||
@@ -2,16 +2,21 @@ package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"io"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var mimeDecoder mime.WordDecoder
|
||||
var (
|
||||
mimeDecoder mime.WordDecoder
|
||||
priorityHeaderIgnoreRegex = regexp.MustCompile(`^u=\d,\s*(i|\d)$|^u=\d$`)
|
||||
)
|
||||
|
||||
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
|
||||
value := strings.ToLower(readParam(r, names...))
|
||||
@@ -50,9 +55,9 @@ func readParam(r *http.Request, names ...string) string {
|
||||
|
||||
func readHeaderParam(r *http.Request, names ...string) string {
|
||||
for _, name := range names {
|
||||
value := maybeDecodeHeader(r.Header.Get(name))
|
||||
value := strings.TrimSpace(maybeDecodeHeader(name, r.Header.Get(name)))
|
||||
if value != "" {
|
||||
return strings.TrimSpace(value)
|
||||
return value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
@@ -100,9 +105,9 @@ func extractIPAddress(r *http.Request, behindProxy bool) netip.Addr {
|
||||
|
||||
func readJSONWithLimit[T any](r io.ReadCloser, limit int, allowEmpty bool) (*T, error) {
|
||||
obj, err := util.UnmarshalJSONWithLimit[T](r, limit, allowEmpty)
|
||||
if err == util.ErrUnmarshalJSON {
|
||||
if errors.Is(err, util.ErrUnmarshalJSON) {
|
||||
return nil, errHTTPBadRequestJSONInvalid
|
||||
} else if err == util.ErrTooLargeJSON {
|
||||
} else if errors.Is(err, util.ErrTooLargeJSON) {
|
||||
return nil, errHTTPEntityTooLargeJSONBody
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
@@ -126,10 +131,26 @@ func fromContext[T any](r *http.Request, key contextKey) (T, error) {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func maybeDecodeHeader(header string) string {
|
||||
decoded, err := mimeDecoder.DecodeHeader(header)
|
||||
// maybeDecodeHeader decodes the given header value if it is MIME encoded, e.g. "=?utf-8?q?Hello_World?=",
|
||||
// or returns the original header value if it is not MIME encoded. It also calls maybeIgnoreSpecialHeader
|
||||
// to ignore new HTTP "Priority" header.
|
||||
func maybeDecodeHeader(name, value string) string {
|
||||
decoded, err := mimeDecoder.DecodeHeader(value)
|
||||
if err != nil {
|
||||
return header
|
||||
return maybeIgnoreSpecialHeader(name, value)
|
||||
}
|
||||
return decoded
|
||||
return maybeIgnoreSpecialHeader(name, decoded)
|
||||
}
|
||||
|
||||
// maybeIgnoreSpecialHeader ignores new HTTP "Priority" header (see https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-priority)
|
||||
//
|
||||
// Cloudflare (and potentially other providers) add this to requests when forwarding to the backend (ntfy),
|
||||
// so we just ignore it. If the "Priority" header is set to "u=*, i" or "u=*" (by Cloudflare), the header will be ignored.
|
||||
// Returning an empty string will allow the rest of the logic to continue searching for another header (x-priority, prio, p),
|
||||
// or in the Query parameters.
|
||||
func maybeIgnoreSpecialHeader(name, value string) string {
|
||||
if strings.ToLower(name) == "priority" && priorityHeaderIgnoreRegex.MatchString(strings.TrimSpace(value)) {
|
||||
return ""
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -75,3 +75,16 @@ Accept: */*
|
||||
(peeked bytes not UTF-8, peek limit of 4096 bytes reached, hex: ` + fmt.Sprintf("%x", body[:4096]) + ` ...)`
|
||||
require.Equal(t, expected, renderHTTPRequest(r))
|
||||
}
|
||||
|
||||
func TestMaybeIgnoreSpecialHeader(t *testing.T) {
|
||||
require.Empty(t, maybeIgnoreSpecialHeader("priority", "u=1"))
|
||||
require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1"))
|
||||
require.Empty(t, maybeIgnoreSpecialHeader("Priority", "u=1, i"))
|
||||
}
|
||||
|
||||
func TestMaybeDecodeHeaders(t *testing.T) {
|
||||
r, _ := http.NewRequest("GET", "http://ntfy.sh/mytopic/json?since=all", nil)
|
||||
r.Header.Set("Priority", "u=1") // Cloudflare priority header
|
||||
r.Header.Set("X-Priority", "5") // ntfy priority header
|
||||
require.Equal(t, "5", readHeaderParam(r, "x-priority", "priority", "p"))
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@ package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/user"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/user"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -30,10 +30,10 @@ const (
|
||||
visitorDefaultCallsLimit = int64(0)
|
||||
)
|
||||
|
||||
// Constants used to convert a tier-user's MessageLimit (see user.Tier) into adequate request limiter
|
||||
// Constants used to convert a tier-user's MessageSizeLimit (see user.Tier) into adequate request limiter
|
||||
// values (token bucket). This is only used to increase the values in server.yml, never decrease them.
|
||||
//
|
||||
// Example: Assuming a user.Tier's MessageLimit is 10,000:
|
||||
// Example: Assuming a user.Tier's MessageSizeLimit is 10,000:
|
||||
// - the allowed burst is 500 (= 10,000 * 5%), which is < 1000 (the max)
|
||||
// - the replenish rate is 2 * 10,000 / 24 hours
|
||||
const (
|
||||
|
||||
@@ -3,7 +3,7 @@ package server
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/netip"
|
||||
"time"
|
||||
|
||||
|
||||
@@ -2,18 +2,13 @@ package test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"heckel.io/ntfy/server"
|
||||
"heckel.io/ntfy/v2/server"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
// StartServer starts a server.Server with a random port and waits for the server to be up
|
||||
func StartServer(t *testing.T) (*server.Server, int) {
|
||||
return StartServerWithConfig(t, server.NewConfig())
|
||||
@@ -21,7 +16,7 @@ func StartServer(t *testing.T) (*server.Server, int) {
|
||||
|
||||
// StartServerWithConfig starts a server.Server with a random port and waits for the server to be up
|
||||
func StartServerWithConfig(t *testing.T, conf *server.Config) (*server.Server, int) {
|
||||
port := 10000 + rand.Intn(20000)
|
||||
port := 10000 + rand.Intn(30000)
|
||||
conf.ListenHTTP = fmt.Sprintf(":%d", port)
|
||||
conf.AttachmentCacheDir = t.TempDir()
|
||||
conf.CacheFile = filepath.Join(t.TempDir(), "cache.db")
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"github.com/mattn/go-sqlite3"
|
||||
"github.com/stripe/stripe-go/v74"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"heckel.io/ntfy/log"
|
||||
"heckel.io/ntfy/util"
|
||||
"heckel.io/ntfy/v2/log"
|
||||
"heckel.io/ntfy/v2/util"
|
||||
"net/netip"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -161,7 +161,7 @@ const (
|
||||
FROM user_access a
|
||||
JOIN user u ON u.id = a.user_id
|
||||
WHERE (u.user = ? OR u.user = ?) AND ? LIKE a.topic ESCAPE '\'
|
||||
ORDER BY u.user DESC
|
||||
ORDER BY u.user DESC, LENGTH(a.topic) DESC, a.write DESC
|
||||
`
|
||||
|
||||
insertUserQuery = `
|
||||
@@ -197,13 +197,13 @@ const (
|
||||
selectUserAllAccessQuery = `
|
||||
SELECT user_id, topic, read, write
|
||||
FROM user_access
|
||||
ORDER BY write DESC, read DESC, topic
|
||||
ORDER BY LENGTH(topic) DESC, write DESC, read DESC, topic
|
||||
`
|
||||
selectUserAccessQuery = `
|
||||
SELECT topic, read, write
|
||||
FROM user_access
|
||||
WHERE user_id = (SELECT id FROM user WHERE user = ?)
|
||||
ORDER BY write DESC, read DESC, topic
|
||||
ORDER BY LENGTH(topic) DESC, write DESC, read DESC, topic
|
||||
`
|
||||
selectUserReservationsQuery = `
|
||||
SELECT a_user.topic, a_user.read, a_user.write, a_everyone.read AS everyone_read, a_everyone.write AS everyone_write
|
||||
@@ -833,8 +833,10 @@ func (a *Manager) Authorize(user *User, topic string, perm Permission) error {
|
||||
if user != nil {
|
||||
username = user.Name
|
||||
}
|
||||
// Select the read/write permissions for this user/topic combo. The query may return two
|
||||
// rows (one for everyone, and one for the user), but prioritizes the user.
|
||||
// Select the read/write permissions for this user/topic combo.
|
||||
// - The query may return two rows (one for everyone, and one for the user), but prioritizes the user.
|
||||
// - Furthermore, the query prioritizes more specific permissions (longer!) over more generic ones, e.g. "test*" > "*"
|
||||
// - It also prioritizes write permissions over read permissions
|
||||
rows, err := a.db.Query(selectTopicPermsQuery, Everyone, username, topic)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user