Compare commits

...

6 Commits

Author SHA1 Message Date
Philipp Heckel
0e9fa1c4dc Fix raw endpoint 2021-11-02 14:10:56 -04:00
Philipp Heckel
b775e6dfce Limits 2021-11-01 16:39:40 -04:00
Philipp Heckel
fa7a45902f Subscription limit 2021-11-01 15:21:38 -04:00
Philipp Heckel
5f2bb4f876 Readme 2021-10-30 23:46:50 -04:00
Philipp Heckel
91541f9c69 Add logo and color 2021-10-30 23:46:08 -04:00
Philipp Heckel
9a91312392 Only send Firebase data messages 2021-10-29 15:47:46 -04:00
9 changed files with 238 additions and 74 deletions

View File

@@ -64,13 +64,13 @@ sudo apt install ntfy
**Debian/Ubuntu** (*manual install*)**:** **Debian/Ubuntu** (*manual install*)**:**
```bash ```bash
sudo apt install tmux sudo apt install tmux
wget https://github.com/binwiederhier/ntfy/releases/download/v1.0.0/ntfy_1.0.0_amd64.deb wget https://github.com/binwiederhier/ntfy/releases/download/v1.1.2/ntfy_1.1.2_amd64.deb
dpkg -i ntfy_1.0.0_amd64.deb dpkg -i ntfy_1.1.2_amd64.deb
``` ```
**Fedora/RHEL/CentOS:** **Fedora/RHEL/CentOS:**
```bash ```bash
rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.0.0/ntfy_1.0.0_amd64.rpm rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.1.2/ntfy_1.1.2_amd64.rpm
``` ```
**Docker:** **Docker:**
@@ -85,8 +85,8 @@ go get -u heckel.io/ntfy
**Manual install** (*any x86_64-based Linux*)**:** **Manual install** (*any x86_64-based Linux*)**:**
```bash ```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.0.0/ntfy_1.0.0_linux_x86_64.tar.gz wget https://github.com/binwiederhier/ntfy/releases/download/v1.1.2/ntfy_1.1.2_linux_x86_64.tar.gz
sudo tar -C /usr/bin -zxf ntfy_1.0.0_linux_x86_64.tar.gz ntfy sudo tar -C /usr/bin -zxf ntfy_1.1.2_linux_x86_64.tar.gz ntfy
./ntfy ./ntfy
``` ```

View File

@@ -14,33 +14,41 @@ const (
DefaultManagerInterval = time.Minute DefaultManagerInterval = time.Minute
) )
// Defines the max number of requests, here: // Defines all the limits
// 50 requests bucket, replenished at a rate of 1 per second // - request limit: max number of PUT/GET/.. requests (here: 50 requests bucket, replenished at a rate of 1 per second)
// - global topic limit: max number of topics overall
// - subscription limit: max number of subscriptions (active HTTP connections) per per-visitor/IP
var ( var (
defaultLimit = rate.Every(time.Second) defaultGlobalTopicLimit = 5000
defaultLimitBurst = 50 defaultVisitorRequestLimit = rate.Every(time.Second)
defaultVisitorRequestLimitBurst = 50
defaultVisitorSubscriptionLimit = 30
) )
// Config is the main config struct for the application. Use New to instantiate a default config struct. // Config is the main config struct for the application. Use New to instantiate a default config struct.
type Config struct { type Config struct {
ListenHTTP string ListenHTTP string
FirebaseKeyFile string FirebaseKeyFile string
MessageBufferDuration time.Duration MessageBufferDuration time.Duration
KeepaliveInterval time.Duration KeepaliveInterval time.Duration
ManagerInterval time.Duration ManagerInterval time.Duration
Limit rate.Limit GlobalTopicLimit int
LimitBurst int VisitorRequestLimit rate.Limit
VisitorRequestLimitBurst int
VisitorSubscriptionLimit int
} }
// New instantiates a default new config // New instantiates a default new config
func New(listenHTTP string) *Config { func New(listenHTTP string) *Config {
return &Config{ return &Config{
ListenHTTP: listenHTTP, ListenHTTP: listenHTTP,
FirebaseKeyFile: "", FirebaseKeyFile: "",
MessageBufferDuration: DefaultMessageBufferDuration, MessageBufferDuration: DefaultMessageBufferDuration,
KeepaliveInterval: DefaultKeepaliveInterval, KeepaliveInterval: DefaultKeepaliveInterval,
ManagerInterval: DefaultManagerInterval, ManagerInterval: DefaultManagerInterval,
Limit: defaultLimit, GlobalTopicLimit: defaultGlobalTopicLimit,
LimitBurst: defaultLimitBurst, VisitorRequestLimit: defaultVisitorRequestLimit,
VisitorRequestLimitBurst: defaultVisitorRequestLimitBurst,
VisitorSubscriptionLimit: defaultVisitorSubscriptionLimit,
} }
} }

View File

@@ -25,12 +25,12 @@
<meta property="og:site_name" content="ntfy.sh" /> <meta property="og:site_name" content="ntfy.sh" />
<meta property="og:title" content="ntfy.sh | simple HTTP-based pub-sub" /> <meta property="og:title" content="ntfy.sh | simple HTTP-based pub-sub" />
<meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." /> <meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
<meta property="og:image" content="/static/img/favicon.png" /> <meta property="og:image" content="/static/img/ntfy.png" />
<meta property="og:url" content="https://ntfy.sh" /> <meta property="og:url" content="https://ntfy.sh" />
</head> </head>
<body> <body>
<div id="main"> <div id="main">
<h1>ntfy.sh - simple HTTP-based pub-sub</h1> <h1><img src="static/img/ntfy.png" alt="ntfy"/><br/>ntfy.sh - simple HTTP-based pub-sub</h1>
<p> <p>
<b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">pub-sub</a> notification service. <b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based <a href="https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern">pub-sub</a> notification service.
It allows you to send <b>desktop notifications via scripts from any computer</b>, entirely <b>without signup or cost</b>. It allows you to send <b>desktop notifications via scripts from any computer</b>, entirely <b>without signup or cost</b>.
@@ -81,6 +81,12 @@
<ul id="topicsList"></ul> <ul id="topicsList"></ul>
<audio id="notifySound" src="static/sound/mixkit-message-pop-alert-2354.mp3"></audio> <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> <h3>Subscribe via your app, or via the CLI</h3>
<p class="smallMarginBottom"> <p class="smallMarginBottom">
Using <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventSource">EventSource</a> in JS, you can consume 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/> $ curl -s "ntfy.sh/mytopic/json?poll=1&since=10m"<br/>
# Returns messages from up to 10 minutes ago and ends the connection # Returns messages from up to 10 minutes ago and ends the connection
</code> </code>
<h2>FAQ</h2> <h2>FAQ</h2>
<p> <p>
<b>Isn't this like ...?</b><br/> <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. 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>
<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> <center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
</div> </div>
<script src="static/js/app.js"></script> <script src="static/js/app.js"></script>

View File

@@ -9,7 +9,6 @@ import (
firebase "firebase.google.com/go" firebase "firebase.google.com/go"
"firebase.google.com/go/messaging" "firebase.google.com/go/messaging"
"fmt" "fmt"
"golang.org/x/time/rate"
"google.golang.org/api/option" "google.golang.org/api/option"
"heckel.io/ntfy/config" "heckel.io/ntfy/config"
"io" "io"
@@ -23,9 +22,9 @@ import (
"time" "time"
) )
// TODO add "max connections open" limit
// TODO add "max messages in a topic" limit // TODO add "max messages in a topic" limit
// TODO add "max topics" limit // TODO implement persistence
// TODO implement "since=<ID>"
// Server is the main server // Server is the main server
type Server struct { type Server struct {
@@ -37,12 +36,6 @@ type Server struct {
mu sync.Mutex mu sync.Mutex
} }
// visitor represents an API user, and its associated rate.Limiter used for rate limiting
type visitor struct {
limiter *rate.Limiter
seen time.Time
}
// errHTTP is a generic HTTP error for any non-200 HTTP error // errHTTP is a generic HTTP error for any non-200 HTTP error
type errHTTP struct { type errHTTP struct {
Code int Code int
@@ -54,8 +47,7 @@ func (e errHTTP) Error() string {
} }
const ( const (
messageLimit = 1024 messageLimit = 1024
visitorExpungeAfter = 30 * time.Minute
) )
var ( var (
@@ -105,19 +97,14 @@ func createFirebaseSubscriber(conf *config.Config) (subscriber, error) {
} }
return func(m *message) error { return func(m *message) error {
_, err := msg.Send(context.Background(), &messaging.Message{ _, err := msg.Send(context.Background(), &messaging.Message{
Topic: m.Topic,
Data: map[string]string{ Data: map[string]string{
"id": m.ID, "id": m.ID,
"time": fmt.Sprintf("%d", m.Time), "time": fmt.Sprintf("%d", m.Time),
"event": m.Event, "event": m.Event,
"topic": m.Topic, "topic": m.Topic,
"message": m.Message, "message": m.Message,
}, },
Notification: &messaging.Notification{
Title: m.Topic, // FIXME convert to ntfy.sh/$topic instead
Body: m.Message,
ImageURL: "",
},
Topic: m.Topic,
}) })
return err return err
}, nil }, nil
@@ -152,21 +139,21 @@ func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error {
v := s.visitor(r.RemoteAddr) v := s.visitor(r.RemoteAddr)
if !v.limiter.Allow() { if err := v.RequestAllowed(); err != nil {
return errHTTPTooManyRequests return err
} }
if r.Method == http.MethodGet && r.URL.Path == "/" { if r.Method == http.MethodGet && r.URL.Path == "/" {
return s.handleHome(w, r) return s.handleHome(w, r)
} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) {
return s.handleStatic(w, r) return s.handleStatic(w, r)
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicRegex.MatchString(r.URL.Path) { } 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) { } else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) {
return s.handleSubscribeJSON(w, r) return s.handleSubscribeJSON(w, r, v)
} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) {
return s.handleSubscribeSSE(w, r) return s.handleSubscribeSSE(w, r, v)
} else if r.Method == http.MethodGet && rawRegex.MatchString(r.URL.Path) { } else if r.Method == http.MethodGet && rawRegex.MatchString(r.URL.Path) {
return s.handleSubscribeRaw(w, r) return s.handleSubscribeRaw(w, r, v)
} else if r.Method == http.MethodOptions { } else if r.Method == http.MethodOptions {
return s.handleOptions(w, r) return s.handleOptions(w, r)
} }
@@ -183,8 +170,11 @@ func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request) error { func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request, v *visitor) error {
t := s.createTopic(r.URL.Path[1:]) t, err := s.topic(r.URL.Path[1:])
if err != nil {
return err
}
reader := io.LimitReader(r.Body, messageLimit) reader := io.LimitReader(r.Body, messageLimit)
b, err := io.ReadAll(reader) b, err := io.ReadAll(reader)
if err != nil { if err != nil {
@@ -200,7 +190,7 @@ func (s *Server) handlePublish(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request) error { func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request, v *visitor) error {
encoder := func(msg *message) (string, error) { encoder := func(msg *message) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&msg); err != nil { if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
@@ -208,10 +198,10 @@ func (s *Server) handleSubscribeJSON(w http.ResponseWriter, r *http.Request) err
} }
return buf.String(), nil return buf.String(), nil
} }
return s.handleSubscribe(w, r, "json", "application/stream+json", encoder) return s.handleSubscribe(w, r, v, "json", "application/stream+json", encoder)
} }
func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request) error { func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request, v *visitor) error {
encoder := func(msg *message) (string, error) { encoder := func(msg *message) (string, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(&msg); err != nil { if err := json.NewEncoder(&buf).Encode(&msg); err != nil {
@@ -222,21 +212,28 @@ func (s *Server) handleSubscribeSSE(w http.ResponseWriter, r *http.Request) erro
} }
return fmt.Sprintf("data: %s\n", buf.String()), nil return fmt.Sprintf("data: %s\n", buf.String()), nil
} }
return s.handleSubscribe(w, r, "sse", "text/event-stream", encoder) return s.handleSubscribe(w, r, v, "sse", "text/event-stream", encoder)
} }
func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request) error { func (s *Server) handleSubscribeRaw(w http.ResponseWriter, r *http.Request, v *visitor) error {
encoder := func(msg *message) (string, error) { encoder := func(msg *message) (string, error) {
if msg.Event == "" { // only handle default events if msg.Event == messageEvent { // only handle default events
return strings.ReplaceAll(msg.Message, "\n", " ") + "\n", nil return strings.ReplaceAll(msg.Message, "\n", " ") + "\n", nil
} }
return "\n", nil // "keepalive" and "open" events just send an empty line return "\n", nil // "keepalive" and "open" events just send an empty line
} }
return s.handleSubscribe(w, r, "raw", "text/plain", encoder) return s.handleSubscribe(w, r, v, "raw", "text/plain", encoder)
} }
func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, format string, contentType string, encoder messageEncoder) error { func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, v *visitor, format string, contentType string, encoder messageEncoder) error {
t := s.createTopic(strings.TrimSuffix(r.URL.Path[1:], "/"+format)) // Hack if err := v.AddSubscription(); err != nil {
return errHTTPTooManyRequests
}
defer v.RemoveSubscription()
t, err := s.topic(strings.TrimSuffix(r.URL.Path[1:], "/"+format)) // Hack
if err != nil {
return err
}
since, err := parseSince(r) since, err := parseSince(r)
if err != nil { if err != nil {
return err return err
@@ -275,6 +272,7 @@ func (s *Server) handleSubscribe(w http.ResponseWriter, r *http.Request, format
case <-r.Context().Done(): case <-r.Context().Done():
return nil return nil
case <-time.After(s.config.KeepaliveInterval): case <-time.After(s.config.KeepaliveInterval):
v.Keepalive()
if err := sub(newKeepaliveMessage(t.id)); err != nil { // Send keepalive message if err := sub(newKeepaliveMessage(t.id)); err != nil { // Send keepalive message
return err return err
} }
@@ -313,16 +311,19 @@ func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request) error {
return nil return nil
} }
func (s *Server) createTopic(id string) *topic { func (s *Server) topic(id string) (*topic, error) {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
if _, ok := s.topics[id]; !ok { if _, ok := s.topics[id]; !ok {
if len(s.topics) >= s.config.GlobalTopicLimit {
return nil, errHTTPTooManyRequests
}
s.topics[id] = newTopic(id) s.topics[id] = newTopic(id)
if s.firebase != nil { if s.firebase != nil {
s.topics[id].Subscribe(s.firebase) s.topics[id].Subscribe(s.firebase)
} }
} }
return s.topics[id] return s.topics[id], nil
} }
func (s *Server) updateStatsAndExpire() { func (s *Server) updateStatsAndExpire() {
@@ -331,16 +332,16 @@ func (s *Server) updateStatsAndExpire() {
// Expire visitors from rate visitors map // Expire visitors from rate visitors map
for ip, v := range s.visitors { for ip, v := range s.visitors {
if time.Since(v.seen) > visitorExpungeAfter { if v.Stale() {
delete(s.visitors, ip) delete(s.visitors, ip)
} }
} }
// Prune old messages, remove topics without subscribers // Prune old messages, remove subscriptions without subscribers
for _, t := range s.topics { for _, t := range s.topics {
t.Prune(s.config.MessageBufferDuration) t.Prune(s.config.MessageBufferDuration)
subs, msgs := t.Stats() 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) delete(s.topics, t.id)
} }
} }
@@ -367,12 +368,8 @@ func (s *Server) visitor(remoteAddr string) *visitor {
} }
v, exists := s.visitors[ip] v, exists := s.visitors[ip]
if !exists { if !exists {
v = &visitor{ s.visitors[ip] = newVisitor(s.config)
rate.NewLimiter(s.config.Limit, s.config.LimitBurst), return s.visitors[ip]
time.Now(),
}
s.visitors[ip] = v
return v
} }
v.seen = time.Now() v.seen = time.Now()
return v return v

View File

@@ -6,12 +6,13 @@ html, body {
font-size: 1.1em; font-size: 1.1em;
} }
a { a, a:visited {
color: #39005a; color: #3a9784;
} }
a:hover { a:hover {
text-decoration: none; text-decoration: none;
color: #317f6f;
} }
h1 { h1 {
@@ -20,7 +21,6 @@ h1 {
font-size: 2.5em; font-size: 2.5em;
} }
h2 { h2 {
margin-top: 20px; margin-top: 20px;
margin-bottom: 5px; margin-bottom: 5px;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
server/static/img/ntfy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

65
server/visitor.go Normal file
View File

@@ -0,0 +1,65 @@
package server
import (
"golang.org/x/time/rate"
"heckel.io/ntfy/config"
"heckel.io/ntfy/util"
"sync"
"time"
)
const (
visitorExpungeAfter = 30 * time.Minute
)
// visitor represents an API user, and its associated rate.Limiter used for rate limiting
type visitor struct {
config *config.Config
limiter *rate.Limiter
subscriptions *util.Limiter
seen time.Time
mu sync.Mutex
}
func newVisitor(conf *config.Config) *visitor {
return &visitor{
config: conf,
limiter: rate.NewLimiter(conf.VisitorRequestLimit, conf.VisitorRequestLimitBurst),
subscriptions: util.NewLimiter(int64(conf.VisitorSubscriptionLimit)),
seen: time.Now(),
}
}
func (v *visitor) RequestAllowed() error {
if !v.limiter.Allow() {
return errHTTPTooManyRequests
}
return nil
}
func (v *visitor) AddSubscription() error {
v.mu.Lock()
defer v.mu.Unlock()
if err := v.subscriptions.Add(1); err != nil {
return errHTTPTooManyRequests
}
return nil
}
func (v *visitor) RemoveSubscription() {
v.mu.Lock()
defer v.mu.Unlock()
v.subscriptions.Sub(1)
}
func (v *visitor) Keepalive() {
v.mu.Lock()
defer v.mu.Unlock()
v.seen = time.Now()
}
func (v *visitor) Stale() bool {
v.mu.Lock()
defer v.mu.Unlock()
return time.Since(v.seen) > visitorExpungeAfter
}

65
util/limit.go Normal file
View File

@@ -0,0 +1,65 @@
package util
import (
"errors"
"sync"
)
// ErrLimitReached is the error returned by the Limiter and LimitWriter when the predefined limit has been reached
var ErrLimitReached = errors.New("limit reached")
// Limiter is a helper that allows adding values up to a well-defined limit. Once the limit is reached
// ErrLimitReached will be returned. Limiter may be used by multiple goroutines.
type Limiter struct {
value int64
limit int64
mu sync.Mutex
}
// NewLimiter creates a new Limiter
func NewLimiter(limit int64) *Limiter {
return &Limiter{
limit: limit,
}
}
// Add adds n to the limiters internal value, but only if the limit has not been reached. If the limit would be
// exceeded after adding n, ErrLimitReached is returned.
func (l *Limiter) Add(n int64) error {
l.mu.Lock()
defer l.mu.Unlock()
if l.limit == 0 {
l.value += n
return nil
} else if l.value+n <= l.limit {
l.value += n
return nil
} else {
return ErrLimitReached
}
}
// Sub subtracts a value from the limiters internal value
func (l *Limiter) Sub(n int64) {
l.Add(-n)
}
// Set sets the value of the limiter to n. This function ignores the limit. It is meant to set the value
// based on reality.
func (l *Limiter) Set(n int64) {
l.mu.Lock()
l.value = n
l.mu.Unlock()
}
// Value returns the internal value of the limiter
func (l *Limiter) Value() int64 {
l.mu.Lock()
defer l.mu.Unlock()
return l.value
}
// Limit returns the defined limit
func (l *Limiter) Limit() int64 {
return l.limit
}