342 lines
8.8 KiB
Go
342 lines
8.8 KiB
Go
package v1
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
"zxq.co/ripple/rippleapi/common"
|
|
"zxq.co/ripple/rippleapi/limit"
|
|
"zxq.co/ripple/schiavolib"
|
|
)
|
|
|
|
type tokenNewInData struct {
|
|
// either username or userid must be given in the request.
|
|
// if none is given, the request is trashed.
|
|
Username string `json:"username"`
|
|
UserID int `json:"id"`
|
|
Password string `json:"password"`
|
|
Privileges int `json:"privileges"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
type tokenNewResponse struct {
|
|
common.ResponseBase
|
|
Username string `json:"username"`
|
|
ID int `json:"id"`
|
|
Privileges int `json:"privileges"`
|
|
Token string `json:"token,omitempty"`
|
|
Banned bool `json:"banned"`
|
|
}
|
|
|
|
// TokenNewPOST is the handler for POST /token/new.
|
|
func TokenNewPOST(md common.MethodData) common.CodeMessager {
|
|
var r tokenNewResponse
|
|
data := tokenNewInData{}
|
|
err := md.Unmarshal(&data)
|
|
if err != nil {
|
|
return ErrBadJSON
|
|
}
|
|
|
|
md.Doggo.Incr("tokens.new", nil, 1)
|
|
|
|
var miss []string
|
|
if data.Username == "" && data.UserID == 0 {
|
|
miss = append(miss, "username|id")
|
|
}
|
|
if data.Password == "" {
|
|
miss = append(miss, "password")
|
|
}
|
|
if len(miss) != 0 {
|
|
return ErrMissingField(miss...)
|
|
}
|
|
|
|
var q *sql.Row
|
|
const base = "SELECT id, username, privileges, password_md5, password_version, privileges FROM users "
|
|
if data.UserID != 0 {
|
|
q = md.DB.QueryRow(base+"WHERE id = ? LIMIT 1", data.UserID)
|
|
} else {
|
|
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", common.SafeUsername(data.Username))
|
|
}
|
|
|
|
var (
|
|
rank int
|
|
pw string
|
|
pwVersion int
|
|
privilegesRaw uint64
|
|
)
|
|
|
|
err = q.Scan(&r.ID, &r.Username, &rank, &pw, &pwVersion, &privilegesRaw)
|
|
switch {
|
|
case err == sql.ErrNoRows:
|
|
return common.SimpleResponse(404, "No user with that username/id was found.")
|
|
case err != nil:
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
privileges := common.UserPrivileges(privilegesRaw)
|
|
|
|
if !limit.NonBlockingRequest(fmt.Sprintf("loginattempt:%d:%s", r.ID, md.ClientIP()), 5) {
|
|
return common.SimpleResponse(429, "You've made too many login attempts. Try again later.")
|
|
}
|
|
|
|
if pwVersion == 1 {
|
|
return common.SimpleResponse(418, "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website.")
|
|
}
|
|
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil {
|
|
if err == bcrypt.ErrMismatchedHashAndPassword {
|
|
return common.SimpleResponse(403, "That password doesn't match!")
|
|
}
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
const want = (common.UserPrivilegePublic | common.UserPrivilegeNormal)
|
|
if (privileges & want) != want {
|
|
r.Code = 402
|
|
r.Message = "That user is banned."
|
|
r.Banned = true
|
|
return r
|
|
}
|
|
r.Privileges = int(common.Privileges(data.Privileges).CanOnly(privileges))
|
|
|
|
var (
|
|
tokenStr string
|
|
tokenMD5 string
|
|
)
|
|
for {
|
|
tokenStr = common.RandomString(32)
|
|
tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
|
|
r.Token = tokenStr
|
|
|
|
var id int
|
|
err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=? LIMIT 1", tokenMD5).Scan(&id)
|
|
if err == sql.ErrNoRows {
|
|
break
|
|
}
|
|
if err != nil {
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
}
|
|
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token, private, last_updated) VALUES (?, ?, ?, ?, '0', ?)",
|
|
r.ID, r.Privileges, data.Description, tokenMD5, time.Now().Unix())
|
|
if err != nil {
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
|
|
r.Code = 200
|
|
return r
|
|
}
|
|
|
|
// TokenSelfDeletePOST deletes the token the user is connecting with.
|
|
func TokenSelfDeletePOST(md common.MethodData) common.CodeMessager {
|
|
if md.ID() == 0 {
|
|
return common.SimpleResponse(400, "How should we delete your token if you haven't even given us one?!")
|
|
}
|
|
var err error
|
|
if md.IsBearer() {
|
|
_, err = md.DB.Exec("DELETE FROM osin_access WHERE access_token = ? LIMIT 1",
|
|
fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value))))
|
|
} else {
|
|
_, err = md.DB.Exec("DELETE FROM tokens WHERE token = ? LIMIT 1",
|
|
fmt.Sprintf("%x", md5.Sum([]byte(md.User.Value))))
|
|
}
|
|
if err != nil {
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
return common.SimpleResponse(200, "Bye!")
|
|
}
|
|
|
|
type token struct {
|
|
ID int `json:"id"`
|
|
Privileges uint64 `json:"privileges"`
|
|
Description string `json:"description"`
|
|
LastUpdated common.UnixTimestamp `json:"last_updated"`
|
|
}
|
|
type tokenResponse struct {
|
|
common.ResponseBase
|
|
Tokens []token `json:"tokens"`
|
|
}
|
|
|
|
// TokenGET retrieves a list listing all the user's public tokens.
|
|
func TokenGET(md common.MethodData) common.CodeMessager {
|
|
wc := common.Where("user = ? AND private = 0", strconv.Itoa(md.ID()))
|
|
if md.Query("id") != "" {
|
|
wc.Where("id = ?", md.Query("id"))
|
|
}
|
|
rows, err := md.DB.Query("SELECT id, privileges, description, last_updated FROM tokens "+
|
|
wc.Clause+common.Paginate(md.Query("p"), md.Query("l"), 50), wc.Params...)
|
|
|
|
if err != nil {
|
|
return Err500
|
|
}
|
|
var r tokenResponse
|
|
for rows.Next() {
|
|
var t token
|
|
err = rows.Scan(&t.ID, &t.Privileges, &t.Description, &t.LastUpdated)
|
|
if err != nil {
|
|
md.Err(err)
|
|
continue
|
|
}
|
|
r.Tokens = append(r.Tokens, t)
|
|
}
|
|
r.Code = 200
|
|
return r
|
|
}
|
|
|
|
type oauthClient struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
OwnerID int `json:"owner_id"`
|
|
Avatar string `json:"avatar"`
|
|
}
|
|
|
|
// Scan scans the extra in the mysql table into Name, OwnerID and Avatar.
|
|
func (o *oauthClient) Scan(src interface{}) error {
|
|
var s []byte
|
|
switch x := src.(type) {
|
|
case string:
|
|
s = []byte(x)
|
|
case []byte:
|
|
s = x
|
|
default:
|
|
return errors.New("Can't scan non-string")
|
|
}
|
|
|
|
var vals [3]string
|
|
err := json.Unmarshal(s, &vals)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.Name = vals[0]
|
|
o.OwnerID, _ = strconv.Atoi(vals[1])
|
|
o.Avatar = vals[2]
|
|
|
|
return nil
|
|
}
|
|
|
|
type bearerToken struct {
|
|
Client oauthClient `json:"client"`
|
|
Scope string `json:"scope"`
|
|
Privileges common.Privileges `json:"privileges"`
|
|
Created time.Time `json:"created"`
|
|
}
|
|
|
|
type tokenSingleResponse struct {
|
|
common.ResponseBase
|
|
token
|
|
}
|
|
|
|
type bearerTokenSingleResponse struct {
|
|
common.ResponseBase
|
|
bearerToken
|
|
}
|
|
|
|
// TokenSelfGET retrieves information about the token the user is connecting with.
|
|
func TokenSelfGET(md common.MethodData) common.CodeMessager {
|
|
if md.ID() == 0 {
|
|
return common.SimpleResponse(404, "How are we supposed to find the token you're using if you ain't even using one?!")
|
|
}
|
|
if md.IsBearer() {
|
|
return getBearerToken(md)
|
|
}
|
|
var r tokenSingleResponse
|
|
// md.User.ID = token id, userid would have been md.User.UserID. what a clusterfuck
|
|
err := md.DB.QueryRow("SELECT id, privileges, description, last_updated FROM tokens WHERE id = ?", md.User.ID).Scan(
|
|
&r.ID, &r.Privileges, &r.Description, &r.LastUpdated,
|
|
)
|
|
if err != nil {
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
r.Code = 200
|
|
return r
|
|
}
|
|
|
|
func getBearerToken(md common.MethodData) common.CodeMessager {
|
|
var b bearerTokenSingleResponse
|
|
err := md.DB.
|
|
QueryRow(`
|
|
SELECT t.scope, t.created_at, c.id, c.extra
|
|
FROM osin_access t INNER JOIN osin_client c
|
|
WHERE t.access_token = ?
|
|
`, fmt.Sprintf("%x", sha256.Sum256([]byte(md.User.Value)))).Scan(
|
|
&b.Scope, &b.Created, &b.Client.ID, &b.Client,
|
|
)
|
|
if err != nil {
|
|
md.Err(err)
|
|
return Err500
|
|
}
|
|
b.Code = 200
|
|
b.Privileges = md.User.TokenPrivileges
|
|
return b
|
|
}
|
|
|
|
// TokenFixPrivilegesPOST fixes the privileges on the token of the given user,
|
|
// or of all the users if no user is given.
|
|
func TokenFixPrivilegesPOST(md common.MethodData) common.CodeMessager {
|
|
id := common.Int(md.Query("id"))
|
|
if md.Query("id") == "self" {
|
|
id = md.ID()
|
|
}
|
|
go fixPrivileges(id, md.DB)
|
|
return common.SimpleResponse(200, "Privilege fixing started!")
|
|
}
|
|
|
|
func fixPrivileges(user int, db *sqlx.DB) {
|
|
var wc string
|
|
var params = make([]interface{}, 0, 1)
|
|
if user != 0 {
|
|
// dirty, but who gives a shit
|
|
wc = "WHERE user = ?"
|
|
params = append(params, user)
|
|
}
|
|
rows, err := db.Query(`
|
|
SELECT
|
|
tokens.id, tokens.privileges, users.privileges
|
|
FROM tokens
|
|
LEFT JOIN users ON users.id = tokens.user
|
|
`+wc, params...)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
schiavo.Bunker.Send(err.Error())
|
|
return
|
|
}
|
|
for rows.Next() {
|
|
var (
|
|
id int
|
|
privsRaw uint64
|
|
privs common.Privileges
|
|
newPrivs common.Privileges
|
|
privilegesRaw uint64
|
|
)
|
|
err := rows.Scan(&id, &privsRaw, &privilegesRaw)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
continue
|
|
}
|
|
privileges := common.UserPrivileges(privilegesRaw)
|
|
privs = common.Privileges(privsRaw)
|
|
newPrivs = privs.CanOnly(privileges)
|
|
if newPrivs != privs {
|
|
_, err := db.Exec("UPDATE tokens SET privileges = ? WHERE id = ? LIMIT 1", uint64(newPrivs), id)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
schiavo.Bunker.Send(err.Error())
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|