Просмотр исходного кода

Merge branch 'main' into macos-server

Philipp Heckel 3 лет назад
Родитель
Сommit
891257cce8
9 измененных файлов с 90 добавлено и 36 удалено
  1. 1 0
      .gitignore
  2. 11 7
      cmd/serve.go
  3. 1 1
      docs/config.md
  4. 1 0
      docs/releases.md
  5. 2 0
      server/config.go
  6. 32 22
      server/server.go
  7. 2 1
      server/server.yml
  8. 39 4
      server/server_test.go
  9. 1 1
      web/src/components/Preferences.js

+ 1 - 0
.gitignore

@@ -1,6 +1,7 @@
 dist/
 build/
 .idea/
+*.swp
 server/docs/
 server/site/
 tools/fbsend/fbsend

+ 11 - 7
cmd/serve.go

@@ -5,15 +5,16 @@ package cmd
 import (
 	"errors"
 	"fmt"
-	"github.com/urfave/cli/v2"
-	"github.com/urfave/cli/v2/altsrc"
-	"heckel.io/ntfy/server"
-	"heckel.io/ntfy/util"
 	"log"
 	"math"
 	"net"
 	"strings"
 	"time"
+
+	"github.com/urfave/cli/v2"
+	"github.com/urfave/cli/v2/altsrc"
+	"heckel.io/ntfy/server"
+	"heckel.io/ntfy/util"
 )
 
 func init() {
@@ -39,7 +40,7 @@ var flagsServe = []cli.Flag{
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "attachment-expiry-duration", Aliases: []string{"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{"k"}, EnvVars: []string{"NTFY_KEEPALIVE_INTERVAL"}, Value: server.DefaultKeepaliveInterval, Usage: "interval of keepalive messages"}),
 	altsrc.NewDurationFlag(&cli.DurationFlag{Name: "manager-interval", Aliases: []string{"m"}, EnvVars: []string{"NTFY_MANAGER_INTERVAL"}, Value: server.DefaultManagerInterval, Usage: "interval of for message pruning and stats printing"}),
-	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home) or web app (app)"}),
+	altsrc.NewStringFlag(&cli.StringFlag{Name: "web-root", EnvVars: []string{"NTFY_WEB_ROOT"}, Value: "app", Usage: "sets web root to landing page (home), web app (app) or disabled (disable)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-addr", EnvVars: []string{"NTFY_SMTP_SENDER_ADDR"}, Usage: "SMTP server address (host:port) for outgoing emails"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-user", EnvVars: []string{"NTFY_SMTP_SENDER_USER"}, Usage: "SMTP user (if e-mail sending is enabled)"}),
 	altsrc.NewStringFlag(&cli.StringFlag{Name: "smtp-sender-pass", EnvVars: []string{"NTFY_SMTP_SENDER_PASS"}, Usage: "SMTP password (if e-mail sending is enabled)"}),
@@ -144,12 +145,14 @@ func execServe(c *cli.Context) error {
 		return errors.New("if set, base-url must start with http:// or https://")
 	} else if !util.InStringList([]string{"read-write", "read-only", "write-only", "deny-all"}, authDefaultAccess) {
 		return errors.New("if set, auth-default-access must start set to 'read-write', 'read-only', 'write-only' or 'deny-all'")
-	} else if !util.InStringList([]string{"app", "home"}, webRoot) {
+	} else if !util.InStringList([]string{"app", "home", "disable"}, webRoot) {
 		return errors.New("if set, web-root must be 'home' or 'app'")
 	}
 
-	// Default auth permissions
 	webRootIsApp := webRoot == "app"
+	enableWeb := webRoot != "disable"
+
+	// Default auth permissions
 	authDefaultRead := authDefaultAccess == "read-write" || authDefaultAccess == "read-only"
 	authDefaultWrite := authDefaultAccess == "read-write" || authDefaultAccess == "write-only"
 
@@ -229,6 +232,7 @@ func execServe(c *cli.Context) error {
 	conf.VisitorEmailLimitBurst = visitorEmailLimitBurst
 	conf.VisitorEmailLimitReplenish = visitorEmailLimitReplenish
 	conf.BehindProxy = behindProxy
+	conf.EnableWeb = enableWeb
 	s, err := server.New(conf)
 	if err != nil {
 		log.Fatalln(err)

+ 1 - 1
docs/config.md

@@ -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-`                                                                                                                                                          |
 | `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.                                                                                                                                         |
-| `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`, `home` or `disable`                          | `app`        | Sets web root to landing page (home), web app (app) or disables the web app entirely (disable)                                                                                                                                  |
 | `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-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`.                                                 |

+ 1 - 0
docs/releases.md

@@ -9,6 +9,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
 **Features:**
 
 * [Windows](https://ntfy.sh/docs/install/#windows) and [macOS](https://ntfy.sh/docs/install/#macos) builds for the [ntfy CLI](https://ntfy.sh/docs/subscribe/cli/) ([#112](https://github.com/binwiederhier/ntfy/issues/112))
+* Ability to disable the web app entirely ([#238](https://github.com/binwiederhier/ntfy/issues/238)/[#249](https://github.com/binwiederhier/ntfy/pull/249), thanks to [@Curid](https://github.com/Curid))
 
 **Additional translations:**
 

+ 2 - 0
server/config.go

@@ -88,6 +88,7 @@ type Config struct {
 	VisitorEmailLimitBurst               int
 	VisitorEmailLimitReplenish           time.Duration
 	BehindProxy                          bool
+	EnableWeb                            bool
 }
 
 // NewConfig instantiates a default new server config
@@ -126,5 +127,6 @@ func NewConfig() *Config {
 		VisitorEmailLimitBurst:               DefaultVisitorEmailLimitBurst,
 		VisitorEmailLimitReplenish:           DefaultVisitorEmailLimitReplenish,
 		BehindProxy:                          false,
+		EnableWeb:                            true,
 	}
 }

+ 32 - 22
server/server.go

@@ -8,11 +8,6 @@ import (
 	"encoding/json"
 	"errors"
 	"fmt"
-	"github.com/emersion/go-smtp"
-	"github.com/gorilla/websocket"
-	"golang.org/x/sync/errgroup"
-	"heckel.io/ntfy/auth"
-	"heckel.io/ntfy/util"
 	"io"
 	"log"
 	"net"
@@ -28,6 +23,12 @@ import (
 	"sync"
 	"time"
 	"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
@@ -263,23 +264,23 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
 
 func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	if r.Method == http.MethodGet && r.URL.Path == "/" {
-		return s.handleHome(w, r)
+		return s.ensureWebEnabled(s.handleHome)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == "/example.html" {
-		return s.handleExample(w, r)
+		return s.ensureWebEnabled(s.handleExample)(w, r, v)
 	} else if r.Method == http.MethodHead && r.URL.Path == "/" {
-		return s.handleEmpty(w, r, v)
+		return s.ensureWebEnabled(s.handleEmpty)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == webConfigPath {
-		return s.handleWebConfig(w, r)
+		return s.ensureWebEnabled(s.handleWebConfig)(w, r, v)
 	} else if r.Method == http.MethodGet && r.URL.Path == userStatsPath {
 		return s.handleUserStats(w, r, v)
 	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
-		return s.handleStatic(w, r)
+		return s.ensureWebEnabled(s.handleStatic)(w, r, v)
 	} else if r.Method == http.MethodGet && docsRegex.MatchString(r.URL.Path) {
-		return s.handleDocs(w, r)
+		return s.ensureWebEnabled(s.handleDocs)(w, r, v)
 	} else if r.Method == http.MethodGet && fileRegex.MatchString(r.URL.Path) && s.config.AttachmentCacheDir != "" {
 		return s.limitRequests(s.handleFile)(w, r, v)
 	} else if r.Method == http.MethodOptions {
-		return s.handleOptions(w, r)
+		return s.ensureWebEnabled(s.handleOptions)(w, r, v)
 	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && r.URL.Path == "/" {
 		return s.limitRequests(s.transformBodyJSON(s.authWrite(s.handlePublish)))(w, r, v)
 	} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
@@ -297,21 +298,21 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
 	} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
 		return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
 	} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || externalTopicPathRegex.MatchString(r.URL.Path)) {
-		return s.handleTopic(w, r)
+		return s.ensureWebEnabled(s.handleTopic)(w, r, v)
 	}
 	return errHTTPNotFound
 }
 
-func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleHome(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	if s.config.WebRootIsApp {
 		r.URL.Path = webAppIndex
 	} else {
 		r.URL.Path = webHomeIndex
 	}
-	return s.handleStatic(w, r)
+	return s.handleStatic(w, r, v)
 }
 
-func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request, v *visitor) error {
 	unifiedpush := readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see PUT/POST too!
 	if unifiedpush {
 		w.Header().Set("Content-Type", "application/json")
@@ -320,7 +321,7 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
 		return err
 	}
 	r.URL.Path = webAppIndex
-	return s.handleStatic(w, r)
+	return s.handleStatic(w, r, v)
 }
 
 func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
@@ -334,12 +335,12 @@ func (s *Server) handleTopicAuth(w http.ResponseWriter, _ *http.Request, _ *visi
 	return err
 }
 
-func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
+func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
 	_, err := io.WriteString(w, exampleSource)
 	return err
 }
 
-func (s *Server) handleWebConfig(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleWebConfig(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
 	appRoot := "/"
 	if !s.config.WebRootIsApp {
 		appRoot = "/app"
@@ -367,13 +368,13 @@ func (s *Server) handleUserStats(w http.ResponseWriter, r *http.Request, v *visi
 	return nil
 }
 
-func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request, _ *visitor) error {
 	r.URL.Path = webSiteDir + r.URL.Path
 	util.Gzip(http.FileServer(http.FS(webFsCached))).ServeHTTP(w, r)
 	return nil
 }
 
-func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request) error {
+func (s *Server) handleDocs(w http.ResponseWriter, r *http.Request, _ *visitor) error {
 	util.Gzip(http.FileServer(http.FS(docsStaticCached))).ServeHTTP(w, r)
 	return nil
 }
@@ -904,7 +905,7 @@ func parseSince(r *http.Request, poll bool) (sinceMarker, error) {
 	return sinceNoMessages, errHTTPBadRequestSinceInvalid
 }
 
-func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request) error {
+func (s *Server) handleOptions(w http.ResponseWriter, _ *http.Request, _ *visitor) error {
 	w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, POST")
 	w.Header().Set("Access-Control-Allow-Origin", "*")  // CORS, allow cross-origin requests
 	w.Header().Set("Access-Control-Allow-Headers", "*") // CORS, allow auth via JS // FIXME is this terrible?
@@ -1118,6 +1119,15 @@ func (s *Server) limitRequests(next handleFunc) handleFunc {
 	}
 }
 
+func (s *Server) ensureWebEnabled(next handleFunc) handleFunc {
+	return func(w http.ResponseWriter, r *http.Request, v *visitor) error {
+		if !s.config.EnableWeb {
+			return errHTTPNotFound
+		}
+		return next(w, r, v)
+	}
+}
+
 // transformBodyJSON peeks the request body, reads the JSON, and converts it to headers
 // 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 {

+ 2 - 1
server/server.yml

@@ -127,7 +127,8 @@
 # manager-interval: "1m"
 
 # 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 web app entirely.
 #
 # web-root: app
 

+ 39 - 4
server/server_test.go

@@ -6,9 +6,6 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
-	"github.com/stretchr/testify/require"
-	"heckel.io/ntfy/auth"
-	"heckel.io/ntfy/util"
 	"math/rand"
 	"net/http"
 	"net/http/httptest"
@@ -18,6 +15,10 @@ import (
 	"sync"
 	"testing"
 	"time"
+
+	"github.com/stretchr/testify/require"
+	"heckel.io/ntfy/auth"
+	"heckel.io/ntfy/util"
 )
 
 func TestServer_PublishAndPoll(t *testing.T) {
@@ -162,6 +163,40 @@ func TestServer_StaticSites(t *testing.T) {
 	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) {
 	c := newTestConfig(t)
 	c.AttachmentCacheDir = "" // Disable attachments
@@ -1303,7 +1338,7 @@ func firebaseServiceAccountFile(t *testing.T) string {
 		return os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT_FILE")
 	} else if os.Getenv("NTFY_TEST_FIREBASE_SERVICE_ACCOUNT") != "" {
 		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
 	}
 	t.SkipNow()

+ 1 - 1
web/src/components/Preferences.js

@@ -458,7 +458,7 @@ const Language = () => {
                     <MenuItem value="id">Bahasa Indonesia</MenuItem>
                     <MenuItem value="ja">日本語</MenuItem>
                     <MenuItem value="nb_NO">Norsk bokmål</MenuItem>
-                    <MenuItem value="pt_BR">Português</MenuItem>
+                    <MenuItem value="pt_BR">Português (Brasil)</MenuItem>
                     <MenuItem value="ru">Русский</MenuItem>
                     <MenuItem value="tr">Türkçe</MenuItem>
                 </Select>