diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c3079bdbf8069e9e4f333c01f07acf5eaf5955a..ee4149fabd4c4e62d4c3922d648e9b768abaad4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,18 @@ All notable changes to this project are documented below. The format is based on [keep a changelog](http://keepachangelog.com) and this project uses [semantic versioning](http://semver.org). ## [Unreleased] +### Added +- Handle thrown JS runtime custom exceptions containing a message and a grpc code to be returned in the server response. + ### Changed - Size limit for status messages increased from 128 to 2048 characters. - Improve unfiltered group listings responses. - Improve error when attempting to create a group with the system user. +- Add userId field for permission validation in JS/Lua runtimes group update functions. + +### Fixed +- Fix creator id being read from the wrong argument in JS runtime group update function. +- Fix max count being incorrectly validated in group update JS runtime function. ## [3.4.0] - 2021-07-08 ### Added diff --git a/server/runtime_javascript.go b/server/runtime_javascript.go index ab38346a9b83b72ef7af8bc4d6a79f73c23719ec..966724fdfb7a85e434b02797c0040f4f42d17b62 100644 --- a/server/runtime_javascript.go +++ b/server/runtime_javascript.go @@ -72,26 +72,10 @@ func (r *RuntimeJS) GetCallback(e RuntimeExecutionMode, key string) string { return "" } -type JsErrorType int - -func (e JsErrorType) String() string { - switch e { - case JsErrorException: - return "exception" - default: - return "" - } -} - -const ( - JsErrorException JsErrorType = iota - JsErrorRuntime -) - type jsError struct { - StackTrace string - Type string - Message string `json:",omitempty"` + StackTrace string `json:"stackTrace,omitempty"` + Type string `json:"type,omitempty"` + custom bool error error } @@ -99,19 +83,22 @@ func (e *jsError) Error() string { return e.error.Error() } -func newJsExceptionError(t JsErrorType, error, st string) *jsError { - return &jsError{ - StackTrace: st, - Type: t.String(), - error: errors.New(error), +func newJsUncaughtExceptionError(error error, st string, custom bool) *jsError { + jsErr := &jsError{ + Type: "uncaughtExceptionError", + error: error, + custom: custom, } + if !custom { + jsErr.StackTrace = st + } + return jsErr } -func newJsError(t JsErrorType, err error) *jsError { +func newJsRuntimeError(err error) *jsError { return &jsError{ - Message: err.Error(), - Type: t.String(), - error: err, + Type: "runtimeError", + error: err, } } @@ -240,7 +227,11 @@ func (rp *RuntimeProviderJS) BeforeRt(ctx context.Context, id string, logger *za rp.Put(r) if fnErr != nil { - logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if jsErr, ok := fnErr.(*jsError); ok { + if !jsErr.custom { + logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(fnErr)) + } + } return nil, fnErr } @@ -301,7 +292,11 @@ func (rp *RuntimeProviderJS) AfterRt(ctx context.Context, id string, logger *zap rp.Put(r) if fnErr != nil { - logger.Error("Runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if jsErr, ok := fnErr.(*jsError); ok { + if !jsErr.custom { + logger.Error("Runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) + } + } return fnErr } @@ -358,7 +353,11 @@ func (rp *RuntimeProviderJS) BeforeReq(ctx context.Context, id string, logger *z rp.Put(r) if fnErr != nil { - logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(err)) + if jsErr, ok := fnErr.(*jsError); ok { + if !jsErr.custom { + logger.Error("Runtime Before function caused an error.", zap.String("id", id), zap.Error(err)) + } + } return nil, fnErr, code } @@ -453,7 +452,11 @@ func (rp *RuntimeProviderJS) AfterReq(ctx context.Context, id string, logger *za rp.Put(r) if fnErr != nil { - logger.Error("JavaScript runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) + if jsErr, ok := fnErr.(*jsError); ok { + if !jsErr.custom { + logger.Error("JavaScript runtime After function caused an error.", zap.String("id", id), zap.Error(fnErr)) + } + } return fnErr } @@ -487,11 +490,32 @@ func (r *RuntimeJS) invokeFunction(execMode RuntimeExecutionMode, id string, fn retVal, err := fn(goja.Null(), args...) if err != nil { if exErr, ok := err.(*goja.Exception); ok { - r.logger.Error("JavaScript runtime function raised an uncaught exception", zap.String("mode", execMode.String()), zap.String("id", id), zap.Error(err)) - return nil, newJsExceptionError(JsErrorException, exErr.Error(), exErr.String()), codes.Internal + errMsg := exErr.Error() + errCode := codes.Internal + custom := false + if errMap, ok := exErr.Value().Export().(map[string]interface{}); ok { + // Custom exception with message and code + if msg, ok := errMap["message"]; ok { + if msgStr, ok := msg.(string); ok { + errMsg = msgStr + custom = true + } + } + if code, ok := errMap["code"]; ok { + if codeInt, ok := code.(int64); ok { + errCode = codes.Code(codeInt) + custom = true + } + } + } + + if !custom { + r.logger.Error("JavaScript runtime function raised an uncaught exception", zap.String("mode", execMode.String()), zap.String("id", id), zap.Error(err)) + } + return nil, newJsUncaughtExceptionError(errors.New(errMsg), exErr.String(), custom), errCode } r.logger.Error("JavaScript runtime function caused an error", zap.String("mode", execMode.String()), zap.String("id", id), zap.Error(err)) - return nil, newJsError(JsErrorRuntime, err), codes.Internal + return nil, newJsRuntimeError(err), codes.Internal } if retVal == nil || retVal == goja.Undefined() || retVal == goja.Null() { return nil, nil, codes.OK @@ -1613,6 +1637,9 @@ func (rp *RuntimeProviderJS) MatchmakerMatched(ctx context.Context, entries []*M retValue, err, _ := r.InvokeFunction(RuntimeExecutionModeMatchmaker, "matchmakerMatched", fn, jsLogger, nil, "", "", nil, 0, "", "", "", "", r.vm.ToValue(entriesSlice)) rp.Put(r) + if err != nil { + return "", false, fmt.Errorf("Error running runtime Matchmaker Matched hook: %v", err.Error()) + } if retValue == nil { // No return value or hook decided not to return an authoritative match ID. diff --git a/server/runtime_javascript_context.go b/server/runtime_javascript_context.go index 0003eae50503e3fdc5e06bba252d1aba2f007487..2fd95032ccbb8ce2ab87d282b092d81ea07fcb32 100644 --- a/server/runtime_javascript_context.go +++ b/server/runtime_javascript_context.go @@ -43,7 +43,7 @@ func NewRuntimeJsContext(r *goja.Runtime, node string, env goja.Value, mode Runt ctxObj := r.NewObject() ctxObj.Set(__RUNTIME_JAVASCRIPT_CTX_NODE, node) ctxObj.Set(__RUNTIME_JAVASCRIPT_CTX_ENV, env) - ctxObj.Set(__RUNTIME_JAVASCRIPT_CTX_MODE, mode) + ctxObj.Set(__RUNTIME_JAVASCRIPT_CTX_MODE, mode.String()) if queryParams != nil { ctxObj.Set(__RUNTIME_JAVASCRIPT_CTX_QUERY_PARAMS, queryParams) } diff --git a/server/runtime_javascript_nakama.go b/server/runtime_javascript_nakama.go index cac26a5b59c097a6d71400a20a3d0b91ce94124c..ef323d13a9eb4b758c79ceb9693aa64d94d92d91 100644 --- a/server/runtime_javascript_nakama.go +++ b/server/runtime_javascript_nakama.go @@ -5172,8 +5172,8 @@ func (n *runtimeJavascriptNakamaModule) groupCreate(r *goja.Runtime) func(goja.F } creatorIDString := uuid.Nil.String() - if f.Argument(3) != goja.Undefined() && f.Argument(3) != goja.Null() { - creatorIDString = getJsString(r, f.Argument(3)) + if f.Argument(2) != goja.Undefined() && f.Argument(2) != goja.Null() { + creatorIDString = getJsString(r, f.Argument(2)) } creatorID, err := uuid.FromString(creatorIDString) if err != nil { @@ -5259,15 +5259,24 @@ func (n *runtimeJavascriptNakamaModule) groupUpdate(r *goja.Runtime) func(goja.F panic(r.NewTypeError("expects group ID to be a valid identifier")) } - var name *wrapperspb.StringValue + userId := uuid.Nil if f.Argument(1) != goja.Undefined() && f.Argument(1) != goja.Null() { - nameStr := getJsString(r, f.Argument(1)) + userIdStr := getJsString(r, f.Argument(1)) + userId, err = uuid.FromString(userIdStr) + if err != nil { + panic(r.NewTypeError("expects user ID to be a valid identifier")) + } + } + + var name *wrapperspb.StringValue + if f.Argument(2) != goja.Undefined() && f.Argument(2) != goja.Null() { + nameStr := getJsString(r, f.Argument(2)) name = &wrapperspb.StringValue{Value: nameStr} } creatorID := uuid.Nil - if f.Argument(2) != goja.Undefined() && f.Argument(2) != goja.Null() { - creatorIDStr := getJsString(r, f.Argument(2)) + if f.Argument(3) != goja.Undefined() && f.Argument(3) != goja.Null() { + creatorIDStr := getJsString(r, f.Argument(3)) creatorID, err = uuid.FromString(creatorIDStr) if err != nil { panic(r.NewTypeError("expects creator ID to be a valid identifier")) @@ -5275,30 +5284,30 @@ func (n *runtimeJavascriptNakamaModule) groupUpdate(r *goja.Runtime) func(goja.F } var lang *wrapperspb.StringValue - if f.Argument(3) != goja.Undefined() && f.Argument(3) != goja.Null() { - langStr := getJsString(r, f.Argument(3)) + if f.Argument(4) != goja.Undefined() && f.Argument(4) != goja.Null() { + langStr := getJsString(r, f.Argument(4)) lang = &wrapperspb.StringValue{Value: langStr} } var desc *wrapperspb.StringValue - if f.Argument(4) != goja.Undefined() && f.Argument(4) != goja.Null() { - descStr := getJsString(r, f.Argument(4)) + if f.Argument(5) != goja.Undefined() && f.Argument(5) != goja.Null() { + descStr := getJsString(r, f.Argument(5)) desc = &wrapperspb.StringValue{Value: descStr} } var avatarURL *wrapperspb.StringValue - if f.Argument(5) != goja.Undefined() && f.Argument(5) != goja.Null() { - avatarStr := getJsString(r, f.Argument(5)) + if f.Argument(6) != goja.Undefined() && f.Argument(6) != goja.Null() { + avatarStr := getJsString(r, f.Argument(6)) avatarURL = &wrapperspb.StringValue{Value: avatarStr} } var open *wrapperspb.BoolValue - if f.Argument(6) != goja.Undefined() && f.Argument(6) != goja.Null() { - open = &wrapperspb.BoolValue{Value: getJsBool(r, f.Argument(6))} + if f.Argument(7) != goja.Undefined() && f.Argument(7) != goja.Null() { + open = &wrapperspb.BoolValue{Value: getJsBool(r, f.Argument(7))} } var metadata *wrapperspb.StringValue - metadataIn := f.Argument(7) + metadataIn := f.Argument(8) if metadataIn != goja.Undefined() && metadataIn != goja.Null() { metadataMap, ok := metadataIn.Export().(map[string]interface{}) if !ok { @@ -5312,16 +5321,16 @@ func (n *runtimeJavascriptNakamaModule) groupUpdate(r *goja.Runtime) func(goja.F } maxCount := 0 - if f.Argument(8) != goja.Undefined() && f.Argument(8) != goja.Null() { - maxCountIn := int(getJsInt(r, f.Argument(8))) - if maxCount > 0 && maxCount <= 100 { + if f.Argument(9) != goja.Undefined() && f.Argument(9) != goja.Null() { + maxCountIn := int(getJsInt(r, f.Argument(9))) + if maxCountIn > 0 && maxCountIn <= 100 { maxCount = maxCountIn } else { panic(r.NewTypeError("max count must be 1-100")) } } - if err = UpdateGroup(context.Background(), n.logger, n.db, groupID, uuid.Nil, creatorID, name, lang, desc, avatarURL, metadata, open, maxCount); err != nil { + if err = UpdateGroup(context.Background(), n.logger, n.db, groupID, userId, creatorID, name, lang, desc, avatarURL, metadata, open, maxCount); err != nil { panic(r.NewGoError(fmt.Errorf("error while trying to update group: %v", err.Error()))) } diff --git a/server/runtime_lua_nakama.go b/server/runtime_lua_nakama.go index 92a7d462f17935aa53b94183fd6cfe39500de806..ef80df3e1026596eb40b10e9f6204a654fc9ddd9 100644 --- a/server/runtime_lua_nakama.go +++ b/server/runtime_lua_nakama.go @@ -6764,13 +6764,19 @@ func (n *RuntimeLuaNakamaModule) groupUpdate(l *lua.LState) int { return 0 } - nameStr := l.OptString(2, "") + userID, err := uuid.FromString(l.OptString(2, uuid.Nil.String())) + if err != nil { + l.ArgError(1, "expects user ID to be a valid identifier") + return 0 + } + + nameStr := l.OptString(3, "") var name *wrapperspb.StringValue if nameStr != "" { name = &wrapperspb.StringValue{Value: nameStr} } - creatorIDStr := l.OptString(3, "") + creatorIDStr := l.OptString(4, "") creatorID := uuid.Nil if creatorIDStr != "" { var err error @@ -6781,31 +6787,31 @@ func (n *RuntimeLuaNakamaModule) groupUpdate(l *lua.LState) int { } } - langStr := l.OptString(4, "") + langStr := l.OptString(5, "") var lang *wrapperspb.StringValue if langStr != "" { lang = &wrapperspb.StringValue{Value: langStr} } - descStr := l.OptString(5, "") + descStr := l.OptString(6, "") var desc *wrapperspb.StringValue if descStr != "" { desc = &wrapperspb.StringValue{Value: descStr} } - avatarURLStr := l.OptString(6, "") + avatarURLStr := l.OptString(7, "") var avatarURL *wrapperspb.StringValue if avatarURLStr != "" { avatarURL = &wrapperspb.StringValue{Value: avatarURLStr} } - openV := l.Get(7) + openV := l.Get(8) var open *wrapperspb.BoolValue if openV != lua.LNil { open = &wrapperspb.BoolValue{Value: l.OptBool(7, false)} } - metadataTable := l.OptTable(8, nil) + metadataTable := l.OptTable(9, nil) var metadata *wrapperspb.StringValue if metadataTable != nil { metadataMap := RuntimeLuaConvertLuaTable(metadataTable) @@ -6817,13 +6823,13 @@ func (n *RuntimeLuaNakamaModule) groupUpdate(l *lua.LState) int { metadata = &wrapperspb.StringValue{Value: string(metadataBytes)} } - maxCountInt := l.OptInt(9, 0) + maxCountInt := l.OptInt(10, 0) maxCount := 0 if maxCountInt > 0 && maxCountInt <= 100 { maxCount = maxCountInt } - if err = UpdateGroup(l.Context(), n.logger, n.db, groupID, uuid.Nil, creatorID, name, lang, desc, avatarURL, metadata, open, maxCount); err != nil { + if err = UpdateGroup(l.Context(), n.logger, n.db, groupID, userID, creatorID, name, lang, desc, avatarURL, metadata, open, maxCount); err != nil { l.RaiseError("error while trying to update group: %v", err.Error()) return 0 }