diff --git a/app/tokens.go b/app/tokens.go index c2142db..9b07a7c 100644 --- a/app/tokens.go +++ b/app/tokens.go @@ -5,7 +5,6 @@ import ( "crypto/sha256" "database/sql" "fmt" - "strings" "time" "github.com/jmoiron/sqlx" @@ -87,7 +86,7 @@ func tokenUpdater(db *sqlx.DB) { } } -// BearerToken parses a Token guiven in the Authorization header, with the +// BearerToken parses a Token given in the Authorization header, with the // Bearer prefix. func BearerToken(token string, db *sqlx.DB) (common.Token, bool) { var x struct { @@ -107,20 +106,7 @@ func BearerToken(token string, db *sqlx.DB) (common.Token, bool) { t.UserID = x.Extra t.Value = token t.UserPrivileges = common.UserPrivileges(privs) - t.TokenPrivileges = oauthPrivileges(x.Scope).CanOnly(t.UserPrivileges) + t.TokenPrivileges = common.OAuthPrivileges(x.Scope).CanOnly(t.UserPrivileges) return t, true } - -var privilegeMap = map[string]common.Privileges{ - "read_confidential": common.PrivilegeReadConfidential, - "write": common.PrivilegeWrite, -} - -func oauthPrivileges(scopes string) common.Privileges { - var p common.Privileges - for _, x := range strings.Split(scopes, " ") { - p |= privilegeMap[x] - } - return p -} diff --git a/app/websockets/identify.go b/app/websockets/identify.go new file mode 100644 index 0000000..fb174c7 --- /dev/null +++ b/app/websockets/identify.go @@ -0,0 +1,101 @@ +package websockets + +import ( + "crypto/md5" + "crypto/sha256" + "encoding/json" + "fmt" + + "database/sql" + + "zxq.co/ripple/rippleapi/common" +) + +type websocketUser struct { + ID int `json:"id"` + Username string `json:"username"` + UserPrivileges uint64 `json:"user_privileges"` + TokenPrivileges uint64 `json:"token_privileges"` + ApplicationID *string `json:"application_id"` +} + +type identifyMessage struct { + Token string `json:"token"` + IsBearer bool `json:"is_bearer"` +} + +// Identify sets the identity of the user. +func Identify(c *conn, message incomingMessage) { + var idMsg identifyMessage + err := json.Unmarshal(message.Data, &idMsg) + if err != nil { + c.WriteJSON(TypeInvalidMessage, err.Error()) + return + } + + var wsu websocketUser + if idMsg.IsBearer { + err = getBearerToken(idMsg.Token, &wsu) + } else { + err = db.Get(&wsu, ` +SELECT + t.user as id, t.privileges as token_privileges, + u.username, u.privileges as user_privileges +FROM tokens t +INNER JOIN users u ON t.user = u.id +WHERE t.token = ?`, fmt.Sprintf("%x", md5.Sum([]byte(idMsg.Token)))) + } + + switch err { + case nil: + break + case sql.ErrNoRows: + c.WriteJSON(TypeNotFound, nil) + return + default: + common.WSErr(err) + c.WriteJSON(TypeUnexpectedError, nil) + return + } + + wsu.TokenPrivileges = uint64( + common.Privileges(wsu.TokenPrivileges).CanOnly( + common.UserPrivileges(wsu.UserPrivileges), + ), + ) + + c.Mtx.Lock() + c.User = &wsu + c.Mtx.Unlock() + + c.WriteJSON(TypeIdentified, wsu) +} + +func getBearerToken(token string, wsu *websocketUser) error { + var x struct { + Client string + Scope string + Extra int + } + err := db.Get(&x, "SELECT client, scope, extra FROM osin_access WHERE access_token = ? LIMIT 1", fmt.Sprintf("%x", sha256.Sum256([]byte(token)))) + if err != nil { + return err + } + + var userInfo struct { + Username string + Privileges uint64 + } + err = db.Get(&userInfo, "SELECT username, privileges FROM users WHERE id = ? LIMIT 1", x.Extra) + if err != nil { + return err + } + + wsu.ApplicationID = &x.Client + wsu.ID = x.Extra + wsu.Username = userInfo.Username + wsu.UserPrivileges = userInfo.Privileges + wsu.TokenPrivileges = uint64(common.OAuthPrivileges(x.Scope)) + + return nil +} diff --git a/app/websockets/main_handler.go b/app/websockets/main_handler.go index 3d69785..9118d14 100644 --- a/app/websockets/main_handler.go +++ b/app/websockets/main_handler.go @@ -26,6 +26,8 @@ func handler(rawConn *websocket.Conn) { rawConn, sync.Mutex{}, step | uint64(time.Now().UnixNano()<<10), + false, + nil, } c.WriteJSON(TypeConnected, nil) @@ -54,9 +56,11 @@ func handler(rawConn *websocket.Conn) { } type conn struct { - Conn *websocket.Conn - Mtx sync.Mutex - ID uint64 + Conn *websocket.Conn + Mtx sync.Mutex + ID uint64 + RestrictedVisible bool + User *websocketUser } func (c *conn) WriteJSON(t string, data interface{}) error { @@ -67,23 +71,31 @@ func (c *conn) WriteJSON(t string, data interface{}) error { } var messageHandler = map[string]func(c *conn, message incomingMessage){ - TypeSubscribeScores: SubscribeScores, - TypePing: pingHandler, + TypeSubscribeScores: SubscribeScores, + TypeSetRestrictedVisibility: SetRestrictedVisibility, + TypeIdentify: Identify, + TypePing: pingHandler, } // Server Message Types const ( - TypeConnected = "connected" - TypeInvalidMessage = "invalid_message_type" - TypeSubscribedToScores = "subscribed_to_scores" - TypeNewScore = "new_score" - TypePong = "pong" + TypeConnected = "connected" + TypeInvalidMessage = "invalid_message_type" + TypeUnexpectedError = "unexpected_error" + TypeNotFound = "not_found" + TypeSubscribedToScores = "subscribed_to_scores" + TypeNewScore = "new_score" + TypeIdentified = "identified" + TypeRestrictedVisibilitySet = "restricted_visibility_set" + TypePong = "pong" ) // Client Message Types const ( - TypeSubscribeScores = "subscribe_scores" - TypePing = "ping" + TypeSubscribeScores = "subscribe_scores" + TypeIdentify = "identify" + TypeSetRestrictedVisibility = "set_restricted_visibility" + TypePing = "ping" ) func pingHandler(c *conn, message incomingMessage) { diff --git a/app/websockets/restricted_visibility.go b/app/websockets/restricted_visibility.go new file mode 100644 index 0000000..91f6429 --- /dev/null +++ b/app/websockets/restricted_visibility.go @@ -0,0 +1,31 @@ +package websockets + +import ( + "encoding/json" + + "zxq.co/ripple/rippleapi/common" +) + +// SetRestrictedVisibility sets whether the information of restricted users +// can be seen. +func SetRestrictedVisibility(c *conn, message incomingMessage) { + var visibility bool + + err := json.Unmarshal(message.Data, &visibility) + if err != nil { + c.WriteJSON(TypeInvalidMessage, err.Error()) + return + } + + var userIsManager bool + if c.User != nil && (c.User.UserPrivileges&uint64(common.AdminPrivilegeManageUsers) > 0) { + userIsManager = true + } + + c.Mtx.Lock() + visibility = visibility && userIsManager + c.RestrictedVisible = visibility + c.Mtx.Unlock() + + c.WriteJSON(TypeRestrictedVisibilitySet, visibility) +} diff --git a/app/websockets/scores.go b/app/websockets/scores.go index 910c250..0c5fcbd 100644 --- a/app/websockets/scores.go +++ b/app/websockets/scores.go @@ -8,6 +8,7 @@ import ( "gopkg.in/thehowl/go-osuapi.v1" "zxq.co/ripple/rippleapi/app/v1" + "zxq.co/ripple/rippleapi/common" "zxq.co/x/getrank" ) @@ -69,9 +70,21 @@ func scoreRetriever() { } } +type scoreUser struct { + UserID int `json:"id"` + Username string `json:"username"` + Privileges uint64 `json:"privileges"` +} + type score struct { v1.Score - UserID int `json:"user_id"` + scoreUser +} + +type scoreJSON struct { + v1.Score + UserID int `json:"user_id"` + User scoreUser `json:"user"` } func handleNewScore(id string) { @@ -79,10 +92,13 @@ func handleNewScore(id string) { var s score err := db.Get(&s, ` SELECT - id, beatmap_md5, score, max_combo, full_combo, mods, - 300_count, 100_count, 50_count, gekis_count, katus_count, misses_count, - time, play_mode, accuracy, pp, completed, userid AS user_id -FROM scores WHERE id = ?`, id) + s.id, s.beatmap_md5, s.score, s.max_combo, s.full_combo, s.mods, + s.300_count, s.100_count, s.50_count, s.gekis_count, s.katus_count, s.misses_count, + s.time, s.play_mode, s.accuracy, s.pp, s.completed, s.userid AS user_id, + u.username, u.privileges +FROM scores s +INNER JOIN users u ON s.userid = u.id +WHERE s.id = ?`, id) if err != nil { fmt.Println(err) return @@ -96,21 +112,32 @@ FROM scores WHERE id = ?`, id) s.Count50, s.CountMiss, )) + + sj := scoreJSON{ + Score: s.Score, + UserID: s.UserID, + User: s.scoreUser, + } + scoreSubscriptionsMtx.RLock() cp := make([]scoreSubscription, len(scoreSubscriptions)) copy(cp, scoreSubscriptions) scoreSubscriptionsMtx.RUnlock() for _, el := range cp { - if len(el.Users) > 0 && !scoreUserValid(el.Users, s) { + if len(el.Users) > 0 && !scoreUserValid(el.Users, sj) { continue } - el.Conn.WriteJSON(TypeNewScore, s) + if sj.User.Privileges&3 != 3 && !el.Conn.RestrictedVisible { + continue + } + + el.Conn.WriteJSON(TypeNewScore, sj) } } -func scoreUserValid(users []subscribeScoresUser, s score) bool { +func scoreUserValid(users []subscribeScoresUser, s scoreJSON) bool { for _, u := range users { if u.User == s.UserID { if len(u.Modes) > 0 { @@ -136,7 +163,11 @@ func inModes(modes []int, i int) bool { func catchPanic() { r := recover() if r != nil { - fmt.Println(r) - // TODO: sentry + switch r := r.(type) { + case error: + common.WSErr(r) + default: + fmt.Println("PANIC", r) + } } } diff --git a/common/method_data.go b/common/method_data.go index 4c093c3..6e0e171 100644 --- a/common/method_data.go +++ b/common/method_data.go @@ -70,6 +70,13 @@ func Err(c *fasthttp.RequestCtx, err error) { _err(err, tags, nil, c) } +// WSErr is the error function for errors happening in the websockets. +func WSErr(err error) { + _err(err, map[string]string{ + "endpoint": "/api/v1/ws", + }, nil, nil) +} + func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.RequestCtx) { if RavenClient == nil { fmt.Println("ERROR!!!!") @@ -93,6 +100,10 @@ func _err(err error, tags map[string]string, user *raven.User, c *fasthttp.Reque } func generateRavenHTTP(ctx *fasthttp.RequestCtx) *raven.Http { + if ctx == nil { + return nil + } + // build uri uri := ctx.URI() // safe to use b2s because a new string gets allocated eventually for diff --git a/common/privileges.go b/common/privileges.go index e5bc3f0..46048d1 100644 --- a/common/privileges.go +++ b/common/privileges.go @@ -77,3 +77,18 @@ func (p Privileges) CanOnly(userPrivs UserPrivileges) Privileges { } return Privileges(newPrivilege) } + +var privilegeMap = map[string]Privileges{ + "read_confidential": PrivilegeReadConfidential, + "write": PrivilegeWrite, +} + +// OAuthPrivileges returns the equivalent in Privileges of a space-separated +// list of scopes. +func OAuthPrivileges(scopes string) Privileges { + var p Privileges + for _, x := range strings.Split(scopes, " ") { + p |= privilegeMap[x] + } + return p +}