mirror of
https://github.com/binwiederhier/ntfy.git
synced 2024-11-25 04:40:02 +01:00
parent
4b86085a8c
commit
9684629549
7 changed files with 65 additions and 21 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
||||||
dist/
|
dist/
|
||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
|
*.swp
|
||||||
server/docs/
|
server/docs/
|
||||||
server/site/
|
server/site/
|
||||||
tools/fbsend/fbsend
|
tools/fbsend/fbsend
|
||||||
|
|
|
@ -3,15 +3,16 @@ package cmd
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/urfave/cli/v2"
|
|
||||||
"github.com/urfave/cli/v2/altsrc"
|
|
||||||
"heckel.io/ntfy/server"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"log"
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/urfave/cli/v2"
|
||||||
|
"github.com/urfave/cli/v2/altsrc"
|
||||||
|
"heckel.io/ntfy/server"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -146,8 +147,10 @@ func execServe(c *cli.Context) error {
|
||||||
return errors.New("if set, web-root must be 'home' or 'app'")
|
return errors.New("if set, web-root must be 'home' or 'app'")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default auth permissions
|
|
||||||
webRootIsApp := webRoot == "app"
|
webRootIsApp := webRoot == "app"
|
||||||
|
enableWeb := webRoot != "disable"
|
||||||
|
|
||||||
|
// Default auth permissions
|
||||||
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
|
authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
|
||||||
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
|
authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
|
||||||
|
|
||||||
|
@ -227,6 +230,7 @@ func execServe(c *cli.Context) error {
|
||||||
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
|
||||||
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
|
||||||
conf.BehindProxy = behindProxy
|
conf.BehindProxy = behindProxy
|
||||||
|
conf.EnableWeb = enableWeb
|
||||||
s, err := server.New(conf)
|
s, err := server.New(conf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
|
|
|
@ -802,7 +802,7 @@ variable before running the `ntfy` command (e.g. `export NTFY_LISTEN_HTTP=:80`).
|
||||||
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
| `smtp-server-addr-prefix` | `NTFY_SMTP_SERVER_ADDR_PREFIX` | `[ip]:port` | - | Optional prefix for the e-mail addresses to prevent spam, e.g. `ntfy-` |
|
||||||
| `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. |
|
| `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. |
|
| `manager-interval` | `$NTFY_MANAGER_INTERVAL` | *duration* | 1m | Interval in which the manager prunes old messages, deletes topics and prints the stats. |
|
||||||
| `web-root` | `NTFY_WEB_ROOT` | `app` or `home` | `app` | Sets web root to landing page (home) or web app (app) |
|
| `web-root` | `NTFY_WEB_ROOT` | `app` or `home` | `app` | Sets web root to landing page (home), web app (app) or (disable) for no WebUI. |
|
||||||
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
|
| `global-topic-limit` | `NTFY_GLOBAL_TOPIC_LIMIT` | *number* | 15,000 | Rate limiting: Total number of topics before the server rejects new topics. |
|
||||||
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
| `visitor-subscription-limit` | `NTFY_VISITOR_SUBSCRIPTION_LIMIT` | *number* | 30 | Rate limiting: Number of subscriptions per visitor (IP address) |
|
||||||
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
|
| `visitor-attachment-total-size-limit` | `NTFY_VISITOR_ATTACHMENT_TOTAL_SIZE_LIMIT` | *size* | 100M | Rate limiting: Total storage limit used for attachments per visitor, for all attachments combined. Storage is freed after attachments expire. See `attachment-expiry-duration`. |
|
||||||
|
|
|
@ -88,6 +88,7 @@ type Config struct {
|
||||||
VisitorEmailLimitBurst int
|
VisitorEmailLimitBurst int
|
||||||
VisitorEmailLimitReplenish time.Duration
|
VisitorEmailLimitReplenish time.Duration
|
||||||
BehindProxy bool
|
BehindProxy bool
|
||||||
|
EnableWeb bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewConfig instantiates a default new server config
|
// NewConfig instantiates a default new server config
|
||||||
|
@ -126,5 +127,6 @@ func NewConfig() *Config {
|
||||||
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
VisitorEmailLimitBurst: DefaultVisitorEmailLimitBurst,
|
||||||
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
VisitorEmailLimitReplenish: DefaultVisitorEmailLimitReplenish,
|
||||||
BehindProxy: false,
|
BehindProxy: false,
|
||||||
|
EnableWeb: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,11 +8,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/emersion/go-smtp"
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"golang.org/x/sync/errgroup"
|
|
||||||
"heckel.io/ntfy/auth"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -28,6 +23,12 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/emersion/go-smtp"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
"heckel.io/ntfy/auth"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Server is the main server, providing the UI and API for ntfy
|
// Server is the main server, providing the UI and API for ntfy
|
||||||
|
@ -262,19 +263,19 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
|
||||||
if r.Method == http.MethodGet && r.URL.Path == "/" {
|
if r.Method == http.MethodGet && r.URL.Path == "/" && s.config.EnableWeb {
|
||||||
return s.handleHome(w, r)
|
return s.handleHome(w, r)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
|
} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" && s.config.EnableWeb {
|
||||||
return s.handleExample(w, r)
|
return s.handleExample(w, r)
|
||||||
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
} else if r.Method == http.MethodHead && r.URL.Path == "/" {
|
||||||
return s.handleEmpty(w, r, v)
|
return s.handleEmpty(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath && s.config.EnableWeb {
|
||||||
return s.handleWebConfig(w, r)
|
return s.handleWebConfig(w, r)
|
||||||
} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
|
} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
|
||||||
return s.handleUserStats(w, r, v)
|
return s.handleUserStats(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) && s.config.EnableWeb {
|
||||||
return s.handleStatic(w, r)
|
return s.handleStatic(w, r)
|
||||||
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) && s.config.EnableWeb {
|
||||||
return s.handleDocs(w, r)
|
return s.handleDocs(w, r)
|
||||||
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
|
} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
|
||||||
return s.limitRequests(s.handleFile)(w, r, v)
|
return s.limitRequests(s.handleFile)(w, r, v)
|
||||||
|
|
|
@ -127,7 +127,8 @@
|
||||||
# manager-interval: "1m"
|
# manager-interval: "1m"
|
||||||
|
|
||||||
# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
|
# Defines if the root route (/) is pointing to the landing page (as on ntfy.sh) or the
|
||||||
# web app. If you self-host, you don't want to change this. Can be "app" (default) or "home".
|
# web app. If you self-host, you don't want to change this.
|
||||||
|
# Can be "app" (default), "home" or "disable" to disable the WebUI.
|
||||||
#
|
#
|
||||||
# web-root: app
|
# web-root: app
|
||||||
|
|
||||||
|
|
|
@ -6,9 +6,6 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"heckel.io/ntfy/auth"
|
|
||||||
"heckel.io/ntfy/util"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
@ -18,6 +15,10 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"heckel.io/ntfy/auth"
|
||||||
|
"heckel.io/ntfy/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestServer_PublishAndPoll(t *testing.T) {
|
func TestServer_PublishAndPoll(t *testing.T) {
|
||||||
|
@ -162,6 +163,40 @@ func TestServer_StaticSites(t *testing.T) {
|
||||||
require.Contains(t, rr.Body.String(), "</html>")
|
require.Contains(t, rr.Body.String(), "</html>")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServer_WebEnabled(t *testing.T) {
|
||||||
|
conf := newTestConfig(t)
|
||||||
|
conf.EnableWeb = false
|
||||||
|
s := newTestServer(t, conf)
|
||||||
|
|
||||||
|
rr := request(t, s, "GET", "/", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/example.html", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/config.js", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s, "GET", "/static/css/home.css", "", nil)
|
||||||
|
require.Equal(t, 404, rr.Code)
|
||||||
|
|
||||||
|
conf2 := newTestConfig(t)
|
||||||
|
conf2.EnableWeb = true
|
||||||
|
s2 := newTestServer(t, conf2)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/example.html", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/config.js", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
|
||||||
|
rr = request(t, s2, "GET", "/static/css/home.css", "", nil)
|
||||||
|
require.Equal(t, 200, rr.Code)
|
||||||
|
}
|
||||||
|
|
||||||
func TestServer_PublishLargeMessage(t *testing.T) {
|
func TestServer_PublishLargeMessage(t *testing.T) {
|
||||||
c := newTestConfig(t)
|
c := newTestConfig(t)
|
||||||
c.AttachmentCacheDir = "" // Disable attachments
|
c.AttachmentCacheDir = "" // Disable attachments
|
||||||
|
@ -1303,7 +1338,7 @@ func firebaseServiceAccountFile(t *testing.T) string {
|
||||||
return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE")
|
return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE")
|
||||||
} else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" {
|
} else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" {
|
||||||
filename := filepath.Join(t.TempDir(), "firebase.json")
|
filename := filepath.Join(t.TempDir(), "firebase.json")
|
||||||
require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0600))
|
require.NotNil(t, os.WriteFile(filename, []byte(os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT")), 0o600))
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
t.SkipNow()
|
t.SkipNow()
|
||||||
|
|
Loading…
Reference in a new issue