-
-
Notifications
You must be signed in to change notification settings - Fork 891
Rough draft of reciever rate limiting for discussion #584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
| // what if someone unsubscribed and DOESNT want their sub to count against them anymore, maybe the app server lost sync and will keep on sending stuff | ||
| // I guess they suffer for unifiedPushSubscriptionDuration? | ||
|
|
||
| // if lastunsub v exists, and the time since it was unsubbed is longer than our limit, it should not exist | ||
| if t.lastUnsub.v != nil && time.Since(t.lastUnsub.unsubTime) > unifiedPushSubscriptionDuration { | ||
| t.lastUnsub.v = nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That. How do we know whether the last subscriber wants to cache messages (gone temporarily) or not (unsubscribed permanently)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what's happening here. Sorry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Messages are being billed to the last visitor (either someone currently subscribed or the last person to unsubscribe). This bill-the-last-unsusbscriber behavior remains for unifiedPushSubscriptionDuration.
Let's say user:KM is subscribed to ntfy.sh/upABCD, and unifiedPushSubscriptionDuration := 10 * time.Hour
Scenario 1:
06:00 user:KM's internet went out and they unsubscribed from ntfy.sh/upABCD
06:15: ntfy.sh receives a message at upABCD. It is stored and billed to KM
06:30 user:KM is online again. The missed message is delivered and everyone's happy.
Scenario 2:
06:00 user:KM's internet went out and they unsubscribed from ntfy.sh/upABCD
06:15: ntfy.sh receives a message at upABCD. It is stored and billed to KM
15:59: ntfy.sh receives a message at upABCD. It is stored and billed to KM
16:01: ntfy.sh receives a message at upABCD. t.lastUnsub.v is set to nil. The message is rejected (412 or whatever else).
23:59 user:KM is online again. They missed some messages, but it's not that big of a deal.
Scenario 3:
06:00 user:KM's intentionally unsubscribed from ntfy.sh/upABCD. They don't want upABCD messages anymore, but the app server keeps sending them.
06:15: ntfy.sh receives a message at upABCD. It is stored and billed to KM
15:59: ntfy.sh receives a message at upABCD. It is stored and billed to KM
16:01: ntfy.sh receives a message at upABCD. t.lastUnsub.v is set to nil. The message is rejected (412 or whatever else).
user:KM is angry that 2 (or 200 or 2000) messages from their rate limit were eaten up by this random app server.
| var v_billing *visitor | ||
| if strings.HasPrefix(t.ID, unifiedpushTopicPrefix) { | ||
| v_billing := t.getBillee() | ||
| if v_billing != nil { | ||
| // instant reject and won't even store it if there's no one registered for a UP topic in the past some time | ||
| // need to find error code for device not available try again later | ||
| return nil, errHTTPInternalError | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there is no last subscriber, or there was a subscriber very long ago, what should ntfy do?
- Cache it anyway but bill it to the app server - inconsistent
- Reject it. Which status code? 500?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*should be == not !=
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is kind of up to the UP spec, no? "What should the push server do if there is no one listening" seems like a spec question.
Obviously the easiest thing would be to reject it. Everything else would cause lots of headscratching.
The response code may be https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point
| } | ||
| } | ||
|
|
||
| // need a better name for bill? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the term "Bill" fine?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(apparently Billee is not a real word)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Invoiced? Billed visitor/account? Charged? Accounted?
Other "Bill" synonyms: cost, expense, tally
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t.SubscriberVisitor()?
| var v_billing *visitor | ||
| if strings.HasPrefix(t.ID, unifiedpushTopicPrefix) { | ||
| v_billing := t.getBillee() | ||
| if v_billing != nil { | ||
| // instant reject and won't even store it if there's no one registered for a UP topic in the past some time | ||
| // need to find error code for device not available try again later | ||
| return nil, errHTTPInternalError | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is kind of up to the UP spec, no? "What should the push server do if there is no one listening" seems like a spec question.
Obviously the easiest thing would be to reject it. Everything else would cause lots of headscratching.
The response code may be https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/412?
| v_billing.IncrementMessages() | ||
| } else { | ||
| v.IncrementMessages() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would generally opt for replacing v early in this function, instead of adding if-then-elses everywhere.
Case and point: There's a v.MessageAllowed() at the top which would be run against the IP-visitor and not the subscriber visitor.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but in that case, logs and email-allowed need to be if-then-elsed (which makes more sense I guess, since there's fewer of those things). I'll figure out a way to minimize complications.
| } | ||
| } | ||
|
|
||
| // need a better name for bill? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
t.SubscriberVisitor()?
| // what if someone unsubscribed and DOESNT want their sub to count against them anymore, maybe the app server lost sync and will keep on sending stuff | ||
| // I guess they suffer for unifiedPushSubscriptionDuration? | ||
|
|
||
| // if lastunsub v exists, and the time since it was unsubbed is longer than our limit, it should not exist | ||
| if t.lastUnsub.v != nil && time.Since(t.lastUnsub.unsubTime) > unifiedPushSubscriptionDuration { | ||
| t.lastUnsub.v = nil | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't understand what's happening here. Sorry.
| @@ -544,6 +546,17 @@ func (s *Server) handlePublishWithoutResponse(r *http.Request, v *visitor) (*mes | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
|
|
|||
| var v_billing *visitor | |||
| if strings.HasPrefix(t.ID, unifiedpushTopicPrefix) { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an idea: instead of using the prefix, could we use the presence of the ?up=1 query param? If the qurey param is present, bill differently? I vaguely remember this being discussed before, but I do not recall what the outcome was
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From the UP dev chat
up=1 is sent by the server, whereas these messages are being billed to the subscriber. I thought about that too, but then came up with this:
- I am subscribed to ntfy.sh/videogameupdates, an open channel for anyone to post
- A malicious actor comes along and sends 10000 messages to ntfy.sh/videogameupdates?up=1
- Me, poor subscriber, is now rate-limited
So, we need a way in which the subscriber knows that they are subscribing to a subscriber-billed topic instead of trusting the sender to inform us. The easiest way is the topic.
|
Closing in favor of a new PR (coming soon) |
This is the original proposal from the Matrix room
me:
binwiederhier:
me: