This commit is contained in:
Philipp Heckel 2021-11-01 16:39:40 -04:00
parent fa7a45902f
commit b775e6dfce
5 changed files with 146 additions and 37 deletions

View file

@ -81,6 +81,12 @@
<ul id="topicsList"></ul>
<audio id="notifySound" src="static/sound/mixkit-message-pop-alert-2354.mp3"></audio>
<h3>Subscribe via phone</h3>
<p>
Once it's approved, you can use the <b>Ntfy Android App</b> to receive notifications directly on your phone. Just like
the server, this app is also <a href="https://github.com/binwiederhier/ntfy-android">open source</a>.
</p>
<h3>Subscribe via your app, or via the CLI</h3>
<p class="smallMarginBottom">
Using <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a> in JS, you can consume
@ -142,6 +148,7 @@
$ curl -s "ntfy.sh/mytopic/json?poll=1&since=10m"<br/>
# Returns messages from up to 10 minutes ago and ends the connection
</code>
<h2>FAQ</h2>
<p>
<b>Isn't this like ...?</b><br/>
@ -165,6 +172,28 @@
That said, the logs do not contain any topic names or other details about you. Check the code if you don't believe me.
</p>
<p>
<b>Why is Firebase used?</b><br/>
In addition to caching messages locally and delivering them to long-polling subscribers, all messages are also
published to Firebase Cloud Messaging (FCM) (if <tt>FirebaseKeyFile</tt> is set, which it is on ntfy.sh). This
is to facilitate instant notifications on Android. I tried really, really hard to avoid using FCM, but newer
versions of Android made it impossible to implement <a href="https://developer.android.com/guide/background">background services</a>>.
I'm sorry.
</p>
<h2>Privacy policy</h2>
<p>
Neither the server nor the app record any personal information, or share any of the messages and topics with
any outside service. All data is exclusively used to make the service function properly. The notable exception
is the Firebase Cloud Messaging (FCM) service, which is required to provide instant Android notifications (see
FAQ for details).
</p>
<p>
The web server does not log or otherwise store request paths, remote IP addresses or even topics or messages,
aside from a short on-disk cache (up to a day) to support the <tt>since=</tt> feature and service restarts.
</p>
<center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
</div>
<script src="static/js/app.js"></script>

View file

@ -24,6 +24,7 @@ import (
// TODO add "max messages in a topic" limit
// TODO implement persistence
// TODO implement "since=<ID>"
// Server is the main server
type Server struct {
@ -146,7 +147,7 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
return s.handleStatic(w, r)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) {
return s.handlePublish(w, r)
return s.handlePublish(w, r, v)
} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
return s.handleSubscribeJSON(w, r, v)
} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) {
@ -169,8 +170,11 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request) error {
t := s.createTopic(r.URL.Path[1:])
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
t, err := s.topic(r.URL.Path[1:])
if err != nil {
return err
}
reader := io.LimitReader(r.Body, messageLimit)
b, err := io.ReadAll(reader)
if err != nil {
@ -223,10 +227,13 @@ func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request, v *v
func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visitor, format string, contentType string, encoder messageEncoder) error {
if err := v.AddSubscription(); err != nil {
return err
return errHTTPTooManyRequests
}
defer v.RemoveSubscription()
t := s.createTopic(strings.TrimSuffix(r.URL.Path[1:], "/"+format)) // Hack
t, err := s.topic(strings.TrimSuffix(r.URL.Path[1:], "/"+format)) // Hack
if err != nil {
return err
}
since, err := parseSince(r)
if err != nil {
return err
@ -304,16 +311,19 @@ func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request) error {
return nil
}
func (s *Server) createTopic(id string) *topic {
func (s *Server) topic(id string) (*topic, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.topics[id]; !ok {
if len(s.topics) >= s.config.GlobalTopicLimit {
return nil, errHTTPTooManyRequests
}
s.topics[id] = newTopic(id)
if s.firebase != nil {
s.topics[id].Subscribe(s.firebase)
}
}
return s.topics[id]
return s.topics[id], nil
}
func (s *Server) updateStatsAndExpire() {
@ -331,7 +341,7 @@ func (s *Server) updateStatsAndExpire() {
for _, t := range s.topics {
t.Prune(s.config.MessageBufferDuration)
subs, msgs := t.Stats()
if msgs == 0 && (subs == 0 || (s.firebase != nil && subs == 1)) {
if msgs == 0 && (subs == 0 || (s.firebase != nil && subs == 1)) { // Firebase is a subscriber!
delete(s.topics, t.id)
}
}

View file

@ -3,6 +3,7 @@ package server
import (
"golang.org/x/time/rate"
"heckel.io/ntfy/config"
"heckel.io/ntfy/util"
"sync"
"time"
)
@ -15,16 +16,17 @@ const (
type visitor struct {
config *config.Config
limiter *rate.Limiter
subscriptions int
subscriptions *util.Limiter
seen time.Time
mu sync.Mutex
}
func newVisitor(conf *config.Config) *visitor {
return &visitor{
config: conf,
limiter: rate.NewLimiter(conf.RequestLimit, conf.RequestLimitBurst),
seen: time.Now(),
config: conf,
limiter: rate.NewLimiter(conf.VisitorRequestLimit, conf.VisitorRequestLimitBurst),
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
seen: time.Now(),
}
}
@ -38,17 +40,16 @@ func (v *visitor) RequestAllowed() error {
func (v *visitor) AddSubscription() error {
v.mu.Lock()
defer v.mu.Unlock()
if v.subscriptions >= v.config.SubscriptionLimit {
if err := v.subscriptions.Add(1); err != nil {
return errHTTPTooManyRequests
}
v.subscriptions++
return nil
}
func (v *visitor) RemoveSubscription() {
v.mu.Lock()
defer v.mu.Unlock()
v.subscriptions--
v.subscriptions.Sub(1)
}
func (v *visitor) Keepalive() {
@ -60,6 +61,5 @@ func (v *visitor) Keepalive() {
func (v *visitor) Stale() bool {
v.mu.Lock()
defer v.mu.Unlock()
v.seen = time.Now()
return time.Since(v.seen) > visitorExpungeAfter
}