-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Implement strict decoding for JetStream API requests #5858
base: main
Are you sure you want to change the base?
Changes from 2 commits
50f27e4
b2553c0
7bc2273
4ec16f2
f50cf20
42abb6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ import ( | |
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"math/rand" | ||
"os" | ||
"path/filepath" | ||
|
@@ -1066,6 +1067,35 @@ func (s *Server) getRequestInfo(c *client, raw []byte) (pci *ClientInfo, acc *Ac | |
return &ci, acc, hdr, msg, nil | ||
} | ||
|
||
func (s *Server) unmarshalRequest(c *client, acc *Account, subject string, msg []byte, v interface{}) error { | ||
decoder := json.NewDecoder(bytes.NewReader(msg)) | ||
decoder.DisallowUnknownFields() | ||
|
||
for { | ||
err := decoder.Decode(v) | ||
|
||
if err == io.EOF { | ||
return nil | ||
} | ||
|
||
if err != nil { | ||
var syntaxErr *json.SyntaxError | ||
if errors.As(err, &syntaxErr) { | ||
err = fmt.Errorf("%w at offset %d", err, syntaxErr.Offset) | ||
} | ||
|
||
c.RateLimitWarnf("Invalid JetStream request '%s > %s': %s", acc, subject, err) | ||
|
||
var config = s.JetStreamConfig() | ||
if config.Strict { | ||
return err | ||
caspervonb marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return json.Unmarshal(msg, v) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not convinced by this block, in that if strict mode is disabled, we can in effect unmarshal twice. (Unmarshalling JSON is actually quite expensive on CPU so need to be careful on hot paths.) Why this vs just having one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My first pass did just that, but was discussed offline with @ripienaar that we want to log marshaling failures even if strict is set to false to have a softer error path but keep the current behavior for existing deployments where it would at-least surface in the logs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A misbehaving client is going to a) spam log lines and b) unmarshal twice for each request. I don't think that's wise, needs further discussion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Derek suggested that for 2.11 while we are in soft mode we should still report the problem so client authors can discover and remediate issues so he suggested this approach. It's nice a nice assist before we enable this by default to reject requests but I agree with you it will spam logs until things are fixed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think client authors could enable this as an opt-in too, in order to avoid spamming there is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah we discussed maybe a header on specific requests - but we have no api behaviours on headers so I am a bit reluctant. Other option is something at the request level like we have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. rateLimitFormatWarnf is a good idea though for sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that any opt-in for the client misses the point that Derek had - it's about detecting problems with non-tier-1 and older clients, and those will not have such opt-in implemented. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed the log lines to use rate limited warnings. |
||
} | ||
} | ||
} | ||
|
||
func (a *Account) trackAPI() { | ||
a.mu.RLock() | ||
jsa := a.js | ||
|
@@ -1195,7 +1225,7 @@ func (s *Server) jsTemplateCreateRequest(sub *subscription, c *client, _ *Accoun | |
} | ||
|
||
var cfg StreamTemplateConfig | ||
if err := json.Unmarshal(msg, &cfg); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &cfg); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -1252,7 +1282,7 @@ func (s *Server) jsTemplateNamesRequest(sub *subscription, c *client, _ *Account | |
var offset int | ||
if !isEmptyRequest(msg) { | ||
var req JSApiStreamTemplatesRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -1435,7 +1465,7 @@ func (s *Server) jsStreamCreateRequest(sub *subscription, c *client, _ *Account, | |
} | ||
|
||
var cfg StreamConfigRequest | ||
if err := json.Unmarshal(msg, &cfg); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &cfg); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -1546,7 +1576,7 @@ func (s *Server) jsStreamUpdateRequest(sub *subscription, c *client, _ *Account, | |
return | ||
} | ||
var ncfg StreamConfigRequest | ||
if err := json.Unmarshal(msg, &ncfg); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &ncfg); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -1645,7 +1675,7 @@ func (s *Server) jsStreamNamesRequest(sub *subscription, c *client, _ *Account, | |
|
||
if !isEmptyRequest(msg) { | ||
var req JSApiStreamNamesRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -1775,7 +1805,7 @@ func (s *Server) jsStreamListRequest(sub *subscription, c *client, _ *Account, s | |
|
||
if !isEmptyRequest(msg) { | ||
var req JSApiStreamListRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -1945,7 +1975,7 @@ func (s *Server) jsStreamInfoRequest(sub *subscription, c *client, a *Account, s | |
var offset int | ||
if !isEmptyRequest(msg) { | ||
var req JSApiStreamInfoRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -2302,7 +2332,7 @@ func (s *Server) jsStreamRemovePeerRequest(sub *subscription, c *client, _ *Acco | |
} | ||
|
||
var req JSApiStreamRemovePeerRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -2382,7 +2412,7 @@ func (s *Server) jsLeaderServerRemoveRequest(sub *subscription, c *client, _ *Ac | |
} | ||
|
||
var req JSApiMetaServerRemoveRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -2485,7 +2515,7 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _ | |
var resp = JSApiStreamUpdateResponse{ApiResponse: ApiResponse{Type: JSApiStreamUpdateResponseType}} | ||
|
||
var req JSApiMetaServerStreamMoveRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -2512,7 +2542,7 @@ func (s *Server) jsLeaderServerStreamMoveRequest(sub *subscription, c *client, _ | |
if ok { | ||
sa, ok := streams[streamName] | ||
if ok { | ||
cfg = *sa.Config.clone() | ||
cfg = *sa.Config | ||
streamFound = true | ||
currPeers = sa.Group.Peers | ||
currCluster = sa.Group.Cluster | ||
|
@@ -2654,7 +2684,7 @@ func (s *Server) jsLeaderServerStreamCancelMoveRequest(sub *subscription, c *cli | |
if ok { | ||
sa, ok := streams[streamName] | ||
if ok { | ||
cfg = *sa.Config.clone() | ||
cfg = *sa.Config | ||
streamFound = true | ||
currPeers = sa.Group.Peers | ||
} | ||
|
@@ -2829,7 +2859,7 @@ func (s *Server) jsLeaderStepDownRequest(sub *subscription, c *client, _ *Accoun | |
|
||
if !isEmptyRequest(msg) { | ||
var req JSApiLeaderStepdownRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -3035,7 +3065,7 @@ func (s *Server) jsMsgDeleteRequest(sub *subscription, c *client, _ *Account, su | |
return | ||
} | ||
var req JSApiMsgDeleteRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -3154,7 +3184,7 @@ func (s *Server) jsMsgGetRequest(sub *subscription, c *client, _ *Account, subje | |
return | ||
} | ||
var req JSApiMsgGetRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -3297,7 +3327,7 @@ func (s *Server) jsStreamPurgeRequest(sub *subscription, c *client, _ *Account, | |
var purgeRequest *JSApiStreamPurgeRequest | ||
if !isEmptyRequest(msg) { | ||
var req JSApiStreamPurgeRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -3387,7 +3417,7 @@ func (s *Server) jsStreamRestoreRequest(sub *subscription, c *client, _ *Account | |
} | ||
|
||
var req JSApiStreamRestoreRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -3690,7 +3720,7 @@ func (s *Server) jsStreamSnapshotRequest(sub *subscription, c *client, _ *Accoun | |
} | ||
|
||
var req JSApiStreamSnapshotRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, smsg, s.jsonResponse(&resp)) | ||
return | ||
|
@@ -3888,7 +3918,7 @@ func (s *Server) jsConsumerCreateRequest(sub *subscription, c *client, a *Accoun | |
var resp = JSApiConsumerCreateResponse{ApiResponse: ApiResponse{Type: JSApiConsumerCreateResponseType}} | ||
|
||
var req CreateConsumerRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -4130,7 +4160,7 @@ func (s *Server) jsConsumerNamesRequest(sub *subscription, c *client, _ *Account | |
var offset int | ||
if !isEmptyRequest(msg) { | ||
var req JSApiConsumersRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -4252,7 +4282,7 @@ func (s *Server) jsConsumerListRequest(sub *subscription, c *client, _ *Account, | |
var offset int | ||
if !isEmptyRequest(msg) { | ||
var req JSApiConsumersRequest | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
@@ -4563,7 +4593,7 @@ func (s *Server) jsConsumerPauseRequest(sub *subscription, c *client, _ *Account | |
var resp = JSApiConsumerPauseResponse{ApiResponse: ApiResponse{Type: JSApiConsumerPauseResponseType}} | ||
|
||
if !isEmptyRequest(msg) { | ||
if err := json.Unmarshal(msg, &req); err != nil { | ||
if err := s.unmarshalRequest(c, acc, subject, msg, &req); err != nil { | ||
resp.Error = NewJSInvalidJSONError(err) | ||
s.sendAPIErrResponse(ci, acc, subject, reply, string(msg), s.jsonResponse(&resp)) | ||
return | ||
|
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 we can remove these from banner
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'd vote to keep it, it's a sanity notice that you got the configuration you expected?
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.
Let's only print it if
cfg.Strict == true
.